- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict"; -const Homey = require("homey"); -const Logger = require("./Logger"); - -async function formulaAutocompleteHelper(query, args) { - const device = args.device; - if (!device || typeof device.getFormulas !== "function") { - console.warn( - `formulaAutocompleteHelper: Invalid device or missing getFormulas in args for autocomplete.`, - ); - return []; - } - try { - const formulas = device.getFormulas(); - if (!Array.isArray(formulas)) { - console.warn( - `getFormulas didn't return array for ${device.getName()}.`, - ); - return []; - } - const lowerQuery = query ? query.toLowerCase() : ""; - return formulas.filter( - (f) => - f && - f.name && - (!query || f.name.toLowerCase().includes(lowerQuery)), - ); - } catch (e) { - console.error( - `Error in formulaAutocompleteHelper for ${device.getName()}`, - e, - ); - return []; - } -} - -async function inputAutocompleteHelper(query, args) { - const device = args.device; - if (!device || typeof device.getInputOptions !== "function") { - console.warn( - `inputAutocompleteHelper: Invalid device or missing getInputOptions in args for autocomplete.`, - ); - return []; - } - try { - const inputs = device.getInputOptions(args); - if (!Array.isArray(inputs)) { - console.warn( - `getInputOptions didn't return array for ${device.getName()}.`, - ); - return []; - } - const lowerQuery = query ? query.toLowerCase() : ""; - return inputs.filter( - (i) => - i && - i.name && - (!query || i.name.toLowerCase().includes(lowerQuery)), - ); - } catch (e) { - console.error( - `Error in inputAutocompleteHelper for ${device.getName()}`, - e, - ); - return []; - } -} -// --- End Autocomplete Helper Functions --- - -class BaseLogicDriver extends Homey.Driver { - static logicUnitCardsRegistered = false; - - async onInit() { - const driverName = `Driver: ${this.id}`; - this.logger = new Logger(this, driverName); - - // Set numInputs based on driver ID - const driverId = this.id; - let numInputsParsed = 2; - try { - const parts = driverId.split("-"); - const numStr = parts[parts.length - 1]; - const num = parseInt(numStr); - if (!isNaN(num) && num > 0) { - numInputsParsed = num; - } else if (driverId !== "logic-device") { - this.logger.warn( - `Could not parse number of inputs from driver ID '${driverId}'. Using default: ${numInputsParsed}.`, - {}, - ); - } - } catch (e) { - this.logger.error( - `Error parsing numInputs from driver ID '${driverId}'. Using default: ${numInputsParsed}.`, - e, - ); - } - this.numInputs = numInputsParsed; - - this.logger.info("driver.ready", { - driverId: this.id, - numInputs: this.numInputs, - }); - - if (!BaseLogicDriver.logicUnitCardsRegistered) { - this.logger.info( - "First Logic Unit driver initializing. Registering shared flow cards...", - ); - await this.registerFlowCards(); - BaseLogicDriver.logicUnitCardsRegistered = true; - } else { - this.logger.debug( - "Shared flow cards already registered by another Logic Unit driver.", - ); - } - } - - async registerFlowCards() { - this.logger.debug("driver.registering_flow_cards"); - - // --- Triggers --- - const triggerCards = [ - { - id: "formula_changed_to_false_lu", - }, - { - id: "formula_changed_to_true_lu", - }, - { - id: "formula_timeout_lu", - }, - ]; - triggerCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getTriggerCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - return ( - args && - args.device && - args.device.driver && - args.device.driver.id && - args.device.driver.id.startsWith("logic-unit-") - ); - }); - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - this.logger.debug( - ` -> OK: TRIGGER card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering TRIGGER card '${cardInfo.id}'`, - e, - ); - } - }); - - // --- Actions --- - const actionCards = [ - { - id: "set_input_value_lu", - handler: "onFlowActionSetInput", - }, - { - id: "evaluate_formula_lu", - handler: "onFlowActionEvaluateFormula", - }, - { - id: "evaluate_all_formulas_lu", - handler: "onFlowActionReEvaluateAll", - }, - { - id: "clear_error_state_lu", - handler: "onFlowActionClearError", - }, - { - id: "set_all_inputs_lu", - handler: "setAllInputsFromFlow", - }, - { - id: "set_input_lu", - handler: "setInputForAllFormulasFromFlow", - }, - ]; - actionCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getActionCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - const device = args.device; - if (!device) - throw new Error( - this.homey.__("errors.invalid_device_instance"), - ); - const methodName = cardInfo.handler; - if (typeof device[methodName] !== "function") - throw new Error( - this.homey.__("errors.method_missing_on_device", { - methodName, - }), - ); - this.logger.flow( - `Executing ACTION '${cardInfo.id}' on device ${device.getName()}`, - ); - return await device[methodName](args, state); - }); - - // --- Autocomplete --- - if ( - [ - "set_input_value_lu", - "evaluate_formula_lu", - "clear_error_state_lu", - ].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - } - if ( - ["set_input_value_lu", "set_input_lu"].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "input", - inputAutocompleteHelper, - ); - } - this.logger.debug( - ` -> OK: ACTION card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering ACTION card '${cardInfo.id}'`, - e, - ); - } - }); - - // --- Conditions --- - const conditionCards = [ - { - id: "formula_has_timed_out_lu", - checkType: "timeout", - }, - { - id: "formula_result_is_lu", - checkTypeFromArg: "what_is", - }, - { - id: "has_any_error_lu", - checkType: "has_error", - }, - ]; - conditionCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getConditionCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - const device = args.device; - if (!device) return false; - if (typeof device.onFlowCondition !== "function") - return false; - - let checkType; - if (cardInfo.checkType) { - checkType = cardInfo.checkType; - } else if (cardInfo.checkTypeFromArg) { - checkType = args[cardInfo.checkTypeFromArg] === "true"; - } - - this.logger.flow( - `Executing CONDITION '${cardInfo.id}' on device ${device.getName()}`, - ); - return await device.onFlowCondition(args, state, checkType); - }); - - if ( - [ - "formula_has_timed_out_lu", - "formula_result_is_lu", - ].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - } - this.logger.debug( - ` -> OK: CONDITION card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering CONDITION card '${cardInfo.id}'`, - e, - ); - } - }); - } - - // --- Helper for Autocomplete Registration --- - registerAutocomplete(card, argName, helperFn) { - try { - if (typeof helperFn !== "function") { - throw new Error( - this.homey.__("errors.helper_not_function", { argName }), - ); - } - card.registerArgumentAutocompleteListener( - argName, - async (query, args) => { - const device = args?.device; - - if (argName === "formula" || argName === "input") { - if (!device) { - this.logger.warn( - `Autocomplete for device arg '${argName}' on card '${card.id}' called without device context.`, - ); - return []; - } - - let requiredMethod = ""; - if (argName === "formula") - requiredMethod = "getFormulas"; - if (argName === "input") - requiredMethod = "getInputOptions"; - - if (typeof device[requiredMethod] !== "function") { - this.logger.warn( - `Autocomplete for device arg '${argName}' on card '${card.id}': Device ${device.getName()} missing method ${requiredMethod}.`, - ); - return []; - } - } - - try { - return await helperFn(query, args); - } catch (autocompleteError) { - this.logger.error( - `Error during autocomplete for ${argName} on card '${card.id}'`, - autocompleteError, - ); - return []; - } - }, - ); - this.logger.debug( - `Registered ${argName.toUpperCase()} autocomplete for card '${card.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED to register autocomplete for '${argName}' on card '${card.id}'`, - e, - ); - } - } - - async ensureUniqueDeviceName(name) { - try { - if ( - !this.homey.app || - !this.homey.app.api || - !this.homey.app.api.devices || - typeof this.homey.app.api.devices.getDevices !== "function" - ) { - this.logger.warn( - "ensureUniqueDeviceName: Homey API not ready, returning original name.", - {}, - ); - return String(name || ""); - } - const all = await this.homey.app.api.devices.getDevices(); - if (!all || typeof all !== "object") { - this.logger.error( - "ensureUniqueDeviceName: Unexpected response from getDevices:", - all, - ); - return String(name || ""); - } - const existing = new Set( - Object.values(all) - .map((d) => - d && typeof d === "object" - ? String(d.name || "").trim() - : "", - ) - .filter(Boolean), - ); - const nameStr = String(name || ""); - if (!existing.has(nameStr)) return nameStr; - const base = nameStr.replace(/\s+\d+$/, "").trim(); - const m = nameStr.match(/\s+(\d+)$/); - let n = m ? parseInt(m[1], 10) + 1 : 2; - if (isNaN(n)) n = 2; - let candidate = `${base} ${n}`; - let safetyCounter = 0; - while (existing.has(candidate) && safetyCounter < 100) { - n++; - candidate = `${base} ${n}`; - safetyCounter++; - } - if (safetyCounter >= 100) { - this.logger.error( - "ensureUniqueDeviceName: Could not find unique name after 100 attempts, returning original + timestamp.", - {}, - ); - return `${nameStr}-${Date.now()}`; - } - return candidate; - } catch (e) { - this.logger.error("driver.ensure_unique_failed", e); - return String(name || ""); - } - } - - async onPair(session) { - this.logger.info("pair.session_started", {}); - session.setHandler("list_devices", async () => { - try { - this.logger.debug("Pair handler: list_devices called", {}); - let localeString; - try { - localeString = this.homey.__("device.logic_unit_name"); - } catch (e) { - this.logger.error( - "Error getting localization for 'device.logic_unit_name'", - e, - ); - localeString = this.homey.__( - "device.logic_unit_name_fallback", - ); - } - const baseName = String(localeString || "").replace( - "{count}", - this.numInputs, - ); - const name = await this.ensureUniqueDeviceName(baseName); - const defaultFormulaName = this.homey.__( - "pair.default_formula_name", - ); - const deviceData = { - name: name, - data: { - id: `${this.id}-${Math.random().toString(16).slice(2)}`, - numInputs: this.numInputs, - }, - settings: { - formulas: `[{"id":"f1","name":"${defaultFormulaName}","expression":"${this.getDefaultExpression()}","enabled":true,"timeout":0,"firstImpression":false}]`, - }, - }; - this.logger.debug( - "Pair handler: list_devices returning:", - deviceData, - ); - return [deviceData]; - } catch (e) { - this.logger.error("Error in list_devices pair handler", e); - throw e; - } - }); - this.logger.debug("Pair handler: list_devices registered", {}); - } - - getDefaultExpression() { - const letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; - const inputsToUse = Math.min(this.numInputs, letters.length); - if (inputsToUse <= 0) return "true"; - return letters.slice(0, inputsToUse).join(" AND "); - } -} - -module.exports = BaseLogicDriver; -module.exports.formulaAutocompleteHelper = formulaAutocompleteHelper; -module.exports.inputAutocompleteHelper = inputAutocompleteHelper; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -897 -898 -899 -900 -901 -902 -903 -904 -905 -906 -907 -908 -909 -910 -911 -912 -913 -914 -915 -916 -917 -918 -919 -920 -921 -922 -923 -924 -925 -926 -927 -928 -929 -930 -931 -932 -933 -934 -935 -936 -937 -938 -939 -940 -941 -942 -943 -944 -945 -946 -947 -948 -949 -950 -951 -952 -953 -954 -955 -956 -957 -958 -959 -960 -961 -962 -963 -964 -965 -966 -967 -968 -969 -970 -971 -972 -973 -974 -975 -976 -977 -978 -979 -980 -981 -982 -983 -984 -985 -986 -987 -988 -989 -990 -991 -992 -993 -994 -995 -996 -997 -998 -999 -1000 -1001 -1002 -1003 -1004 -1005 -1006 -1007 -1008 -1009 -1010 -1011 -1012 -1013 -1014 -1015 -1016 -1017 -1018 -1019 -1020 -1021 -1022 -1023 -1024 -1025 -1026 -1027 -1028 -1029 -1030 -1031 -1032 -1033 -1034 -1035 -1036 -1037 -1038 -1039 -1040 -1041 -1042 -1043 -1044 -1045 -1046 -1047 -1048 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict"; -const Homey = require("homey"); -const Logger = require("./Logger"); - -module.exports = class BaseLogicUnit extends Homey.Device { - async safeSetCapabilityValue(cap, value) { - if (this._isDeleting) return; - try { - if (!this.hasCapability(cap)) { - return; - } - await this.setCapabilityValue(cap, value); - } catch (e) { - const msg = e?.message || String(e); - if (e?.statusCode === 404 || /not\s*found/i.test(msg)) { - this.logger.debug("device.capability_skip_deleted", { - capability: cap, - }); - } else { - this.logger.error("device.capability_update_failed", { - capability: cap, - message: msg, - }); - } - } - } - - async onInit() { - const driverName = `Device: ${this.driver ? this.driver.id : "unknown-driver"}`; - this.logger = new Logger(this, driverName); - this.logger.device("device.initializing", { - name: this.getName(), - }); - - if (!this.hasCapability("onoff")) { - await this.addCapability("onoff").catch((e) => - this.logger.error("Failed to add 'onoff' capability", e), - ); - } - if (!this.hasCapability("alarm_generic")) { - await this.addCapability("alarm_generic").catch((e) => - this.logger.error("Failed to add 'alarm_generic' capability", e), - ); - } - - this.numInputs = this.getData().numInputs ?? 2; - this.availableInputs = this.getAvailableInputIds(); - this.logger.debug( - `Device initialized with ${this.numInputs} inputs: ${this.availableInputs.join(", ")}`, - {}, - ); - - await this.initializeFormulas(); - await this.evaluateAllFormulasInitial(); - this.startTimeoutChecks(); - - // REGISTRATION IS REMOVED FROM HERE - this.logger.info("device.initialized", { - name: this.getName(), - count: this.numInputs, - }); - } - - async onFlowCondition(args, state, checkType) { - this.logger.flow(`onFlowCondition called for checkType: '${checkType}'`, { - formulaId: args.formula?.id, - }); - - if (checkType === "has_error") { - const hasTimeout = this.formulas.some((f) => f.timedOut === true); - this.logger.flow(`Condition 'has_any_error' check result: ${hasTimeout}`); - return hasTimeout; - } - - const cardIdFromArgs = args.cardId; - if ( - (cardIdFromArgs === "formula_result_is_ld" || - cardIdFromArgs === "formula_has_timed_out_ld") && - !args.formula - ) { - this.logger.warn( - "onFlowCondition: _ld card called without formula on LU device. Checking first formula as fallback.", - ); - if (this.formulas.length > 0) args.formula = this.formulas[0]; - else return false; - } - - if (!args.formula || !args.formula.id) { - this.logger.warn( - "onFlowCondition: No formula specified in args for formula-specific check.", - { - checkType, - }, - ); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - - if (!formula) { - this.logger.warn("onFlowCondition: Invalid formula ID received.", { - formulaId, - }); - return false; - } - - if (checkType === "timeout") { - const isTimedOut = formula.timedOut === true; - this.logger.flow( - `Condition 'formula_has_timed_out' check for '${formula.name}': Result=${isTimedOut}`, - ); - return isTimedOut; - } else if (typeof checkType === "boolean") { - const currentResult = formula.result; - const desiredResult = checkType; - const evaluatedResult = currentResult === true; - const conditionMet = evaluatedResult === desiredResult; - this.logger.flow( - `Condition 'formula_result_is' check for '${formula.name}': Current=${currentResult}, Desired=${desiredResult}, Match=${conditionMet}`, - ); - return conditionMet; - } else { - this.logger.error( - `onFlowCondition: Unknown checkType received: '${checkType}'`, - ); - return false; - } - } - - async onFlowActionSetInput(args, state) { - if (!args.formula || !args.input || !args.formula.id || !args.input.id) { - this.logger.warn("onFlowActionSetInput: Missing formula or input ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const inputId = args.input.id; - const value = args.value === "true"; - this.logger.flow("onFlowActionSetInput: Setting input", { - formula: formulaId, - input: inputId, - value: value, - }); - try { - await this.setInputForFormula(formulaId, inputId, value); - return true; - } catch (e) { - this.logger.error( - `Error during onFlowActionSetInput for ${formulaId}/${inputId}`, - e, - ); - return false; - } - } - - async onFlowActionEvaluateFormula(args, state) { - const cardIdFromArgs = args.cardId; - if ( - (!args.formula || !args.formula.id) && - cardIdFromArgs === "evaluate_formula_ld" - ) { - this.logger.warn( - `onFlowActionEvaluateFormula (LD) called on LU device ${this.getName()}. Falling back to re-evaluate all.`, - ); - return this.onFlowActionReEvaluateAll(args, state); - } - if (!args.formula || !args.formula.id) { - this.logger.warn("onFlowActionEvaluateFormula: Missing formula ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("onFlowActionEvaluateFormula: Invalid formula ID.", { - formulaId, - }); - return false; - } - this.logger.flow( - `onFlowActionEvaluateFormula: Evaluating '${formula.name}' (Resetting locks)`, - ); - try { - await this.evaluateFormula(formulaId, true); - return true; - } catch (e) { - this.logger.error( - `Error during onFlowActionEvaluateFormula for ${formulaId}`, - e, - ); - return false; - } - } - - async onFlowActionClearError(args, state) { - const cardIdFromArgs = args.cardId; - if ( - (!args.formula || !args.formula.id) && - cardIdFromArgs === "clear_error_ld" - ) { - this.logger.warn( - `onFlowActionClearError (LD) called on LU device ${this.getName()}. Clearing all formula timeouts.`, - ); - this.formulas.forEach((f) => { - f.timedOut = false; - }); - this.logger.info("notifications.error_cleared_all", {}); - return true; - } - if (!args.formula || !args.formula.id) { - this.logger.warn("onFlowActionClearError: Missing formula ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("onFlowActionClearError: Invalid formula ID.", { - formulaId, - }); - return false; - } - this.logger.flow( - `onFlowActionClearError: Clearing timeout state for '${formula.name}'`, - ); - formula.timedOut = false; - this.logger.info("notifications.error_cleared", { - formulaName: formula.name, - }); - return true; - } - - async onFlowActionReEvaluateAll(args, state) { - this.logger.flow( - "onFlowActionReEvaluateAll: Re-evaluating all formulas", - {}, - ); - try { - await this.evaluateAllFormulas(); - return true; - } catch (e) { - this.logger.error("Error during onFlowActionReEvaluateAll", e); - return false; - } - } - - async setAllInputsFromFlow(args, state) { - const device = this; - const valuesJson = args.values; - this.logger.flow( - `Executing Action 'set_all_inputs' on device '${device.getName()}'`, - ); - if (typeof valuesJson !== "string" || valuesJson.trim() === "") { - this.logger.warn( - `RunListener set_all_inputs: Empty or invalid JSON provided.`, - ); - return false; - } - try { - const values = JSON.parse(valuesJson); - if (typeof values !== "object" || values === null) { - throw new Error("Parsed JSON is not an object."); - } - const promises = []; - const inputs = device.getAvailableInputIds - ? device.getAvailableInputIds() - : []; - const formulas = Array.isArray(device.formulas) ? device.formulas : []; - - for (const key in values) { - if (!inputs.includes(key.toLowerCase())) { - this.logger.warn( - `setAllInputsFromFlow: JSON contains invalid input key '${key}' for this device.`, - ); - } - } - - for (const inputId of inputs) { - const key = inputId.toUpperCase(); - if (values.hasOwnProperty(key)) { - formulas.forEach((f) => { - if ( - f && - f.enabled && - typeof device.setInputForFormula === "function" - ) { - const boolValue = - values[key] === true || - String(values[key]).toLowerCase() === "true"; - promises.push( - device.setInputForFormula(f.id, inputId, boolValue), - ); - } - }); - } - } - await Promise.all(promises); - this.logger.info(`set_all_inputs OK for ${device.getName()}`, {}); - return true; - } catch (e) { - this.logger.error(`Error set_all_inputs for ${device.getName()}`, e); - return false; - } - } - - async setInputForAllFormulasFromFlow(args, state) { - const device = this; - const inputId = args.input?.id; - const value = args.value === "true"; - - this.logger.flow( - `Executing Action 'set_input' on device '${device.getName()}'`, - { - input: inputId, - value, - }, - ); - - if (!inputId || !this.availableInputs.includes(inputId)) { - this.logger.error( - `RunListener set_input: Invalid or missing input ID: ${inputId}. Available: ${this.availableInputs.join(",")}`, - ); - - return false; - } - - const promises = []; - const formulas = Array.isArray(device.formulas) ? device.formulas : []; - try { - formulas.forEach((f) => { - if (f && f.enabled && typeof device.setInputForFormula === "function") { - promises.push(device.setInputForFormula(f.id, inputId, value)); - } - }); - await Promise.all(promises); - this.logger.info(`set_input OK for ${device.getName()}`, {}); - return true; - } catch (e) { - this.logger.error(`Error set_input for ${device.getName()}`, e); - return false; - } - } - - getAvailableInputIds() { - const allInputs = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; - const count = Math.max( - 0, - Math.min(allInputs.length, Number(this.numInputs) || 0), - ); - return allInputs.slice(0, count); - } - - getAvailableInputsUppercase() { - return this.availableInputs.map((i) => i.toUpperCase()); - } - - initializeFormulas() { - this.logger.debug("Initializing formulas from settings...", {}); - const settings = this.getSettings(); - try { - const formulasData = settings.formulas - ? JSON.parse(settings.formulas) - : []; - if (!Array.isArray(formulasData)) { - this.logger.error("errors.invalid_formula", { - message: "Formulas setting is not an array.", - }); - this.formulas = []; - } else { - this.formulas = formulasData.map((f) => ({ - id: f.id || `formula_${Math.random().toString(16).slice(2)}`, - name: f.name || this.homey.__("formula.unnamed_formula"), - expression: f.expression || "", - enabled: f.enabled !== false, - timeout: Number(f.timeout) || 0, - firstImpression: !!f.firstImpression, - inputStates: {}, - lockedInputs: {}, - lastInputTime: null, - result: null, - timedOut: false, - })); - } - - this.formulas.forEach((formula) => { - this.availableInputs.forEach((id) => { - formula.inputStates[id] = "undefined"; - formula.lockedInputs[id] = false; - }); - }); - } catch (e) { - this.logger.error("errors.invalid_formula", e); - this.formulas = []; - } - - if (this.formulas.length === 0) { - this.logger.warn( - "No valid formulas found in settings, creating default.", - {}, - ); - const defaultFormula = { - id: "formula_1", - name: this.homey.__ - ? this.homey.__("formula.default_name_alt") - : this.homey.__("formula.default_name_alt_fallback"), - expression: this.getDefaultExpression(), - enabled: true, - timeout: 0, - firstImpression: false, - inputStates: {}, - lockedInputs: {}, - lastInputTime: null, - result: null, - timedOut: false, - }; - - this.availableInputs.forEach((id) => { - defaultFormula.inputStates[id] = "undefined"; - defaultFormula.lockedInputs[id] = false; - }); - this.formulas = [defaultFormula]; - } - this.logger.info("formula.initialized", { - count: this.formulas.length, - }); - this.formulas.forEach((f) => - this.logger.debug("formula.details", { - name: f.name, - expression: f.expression, - enabled: f.enabled, - }), - ); - } - - getDefaultExpression() { - const inputs = this.getAvailableInputsUppercase(); - return inputs.length > 0 ? inputs.join(" AND ") : "true"; - } - - getFormulas() { - return this.formulas - .filter((f) => f.enabled) - .map((f) => ({ - id: f.id, - name: f.name, - description: - f.expression || - (this.homey.__ - ? this.homey.__("formula.no_expression") - : "(no expression)"), - })); - } - - getInputOptions() { - return this.getAvailableInputsUppercase().map((input) => ({ - id: input.toLowerCase(), - name: input, - })); - } - - validateExpression(expression) { - this.logger.debug(`Validating expression: "${expression || ""}"`, {}); - if ( - !expression || - typeof expression !== "string" || - expression.trim() === "" - ) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.expression_empty") - : "Expression empty", - }; - } - const upper = expression.toUpperCase(); - const inputs = this.getAvailableInputsUppercase(); - if (!inputs.length && upper.match(/[A-J]/)) { - return { - valid: false, - error: this.homey.__("formula.error_inputs_used_but_none_configured"), - }; - } - let validationInputs = inputs.length > 0 ? inputs : ["TEMP_VALIDATION_VAR"]; - - const tokenRe = new RegExp( - `\\b(?:AND|OR|XOR|NOT)\\b|&&|\\|\\||\\^|!=|\\*|\\+|!|\\(|\\)|\\b(?:${validationInputs.join("|")})\\b|\\bTRUE\\b|\\bFALSE\\b`, - "gi", - ); - const stripped = upper.replace(tokenRe, "").replace(/\s+/g, ""); - if (stripped.length > 0) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.invalid_tokens", { - tokens: stripped, - }) - : `Invalid tokens: ${stripped}`, - }; - } - - let depth = 0; - for (const ch of upper) { - if (ch === "(") depth++; - else if (ch === ")") depth--; - if (depth < 0) - return { - valid: false, - error: this.homey.__("formula.error_unbalanced_parentheses"), - }; - } - if (depth !== 0) - return { - valid: false, - error: "Unbalanced parentheses", - }; - - // Attempt to evaluate with dummy values to catch syntax errors - let testExpr = upper - .replace(/\bAND\b|&|\*/gi, "&&") // Normalize AND operators - .replace(/\bOR\b|\||\+/gi, "||") // Normalize OR operators - .replace(/\bXOR\b|\^|!=/gi, "!=") // Normalize XOR operators (use JS !=) - .replace(/\bNOT\b|!/gi, "!"); // Normalize NOT operator - - // Replace input letters and TRUE/FALSE with boolean literals - for (const key of validationInputs) { - const re = new RegExp(`\\b${key}\\b`, "g"); - testExpr = testExpr.replace(re, "true"); - } - testExpr = testExpr - .replace(/\bTRUE\b/g, "true") - .replace(/\bFALSE\b/g, "false"); - - // Try creating a function to catch final syntax errors - try { - const fn = new Function(`return ${testExpr};`); - fn(); // Execute once to be sure - return { - valid: true, - }; - } catch (e) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.invalid_syntax", { - message: e.message, - }) - : `Syntax error: ${e.message}`, - }; - } - } - - parseExpression(expression) { - if (!expression || typeof expression !== "string") return []; - const inputs = this.getAvailableInputsUppercase(); - if (!inputs.length) return []; - const varRe = new RegExp(`\\b(${inputs.join("|")})\\b`, "gi"); - const matches = expression.match(varRe); - return matches ? [...new Set(matches.map((c) => c.toUpperCase()))] : []; - } - - async setInputForFormula(formulaId, inputId, value) { - if (this._isDeleting) return null; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("errors.invalid_formula", { - formulaId, - }); - return null; - } - if (!this.availableInputs.includes(inputId)) { - this.logger.warn( - `setInputForFormula: Invalid inputId '${inputId}' for device with ${this.numInputs} inputs.`, - ); - return null; - } - - if ( - formula.firstImpression === true && - formula.lockedInputs[inputId] === true - ) { - this.logger.debug("inputs.locked", { - input: inputId.toUpperCase(), - formula: formula.name, - }); - return formula.result; - } - - const oldValue = formula.inputStates[inputId]; - formula.inputStates[inputId] = - value === true || value === false ? value : "undefined"; - formula.timedOut = false; - - if ( - formula.firstImpression === true && - formula.inputStates[inputId] !== "undefined" && - formula.lockedInputs[inputId] !== true - ) { - formula.lockedInputs[inputId] = true; - this.logger.debug("inputs.locked_at_value", { - input: inputId.toUpperCase(), - value: formula.inputStates[inputId], - formula: formula.name, - }); - } - - if (formula.inputStates[inputId] !== "undefined") { - formula.lastInputTime = Date.now(); - } - - return await this.evaluateFormula(formulaId, false); - } - - async evaluateFormula(formulaId, resetLocks = false) { - if (this._isDeleting) return null; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula || !formula.enabled) { - this.logger.debug("errors.invalid_formula", { - formulaId, - }); - return null; - } - - if (resetLocks === true && formula.firstImpression === true) { - this.availableInputs.forEach((id) => { - formula.lockedInputs[id] = false; - }); - this.logger.debug("inputs.unlocked", { - formula: formula.name, - }); - } - - const expression = formula.expression; - if (!expression) { - this.logger.debug("formula.invalid", { - formula: formula.name, - reason: "No expression", - }); - formula.result = null; - return null; - } - - const requiredInputs = this.parseExpression(expression); - const allDefined = requiredInputs.every( - (inputIdUpper) => - formula.inputStates[inputIdUpper.toLowerCase()] !== "undefined", - ); - - if (!allDefined && requiredInputs.length > 0) { - const missing = requiredInputs.filter( - (id) => formula.inputStates[id.toLowerCase()] === "undefined", - ); - this.logger.debug("inputs.waiting", { - formula: formula.name, - missing: missing.join(", "), - }); - return formula.result; - } - - const values = {}; - this.availableInputs.forEach((id) => { - values[id.toUpperCase()] = formula.inputStates[id]; - }); - this.logger.debug("formula.evaluating", { - formula: formula.name, - }); - - let evalExpression = expression - .replace(/\bAND\b|&|\*/gi, "&&") - .replace(/\bOR\b|\||\+/gi, "||") - .replace(/\bXOR\b|\^|!=/gi, "!=") - .replace(/\bNOT\b|!/gi, "!"); - - for (const key in values) { - if (requiredInputs.includes(key) && values[key] !== "undefined") { - const re = new RegExp(`\\b${key}\\b`, "gi"); - evalExpression = evalExpression.replace( - re, - String(values[key] === true), - ); - } - } - evalExpression = evalExpression - .replace(/\bTRUE\b/gi, "true") - .replace(/\bFALSE\b/gi, "false"); - this.logger.formula("formula.evaluating_expression", { - expression: expression, - evalExpression: evalExpression, - }); - - try { - const evaluateFn = new Function(`return ${evalExpression};`); - const result = !!evaluateFn(); - this.logger.debug("formula.evaluated", { - formula: formula.name, - result: result, - }); - - const previous = formula.result; - formula.result = result; - formula.timedOut = false; - - const overallDeviceState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - const currentOnOffState = this.getCapabilityValue("onoff"); - - if (currentOnOffState !== overallDeviceState) { - await this.safeSetCapabilityValue("onoff", overallDeviceState); - await this.safeSetCapabilityValue("alarm_generic", overallDeviceState); - this.logger.info( - `Overall device capability updated: ${overallDeviceState}`, - ); - } - - if (result !== previous && previous !== null) { - const triggerData = { - formula: { - id: formula.id, - name: formula.name, - }, - }; - const state = { - formulaId: formula.id, - }; - const triggerCardId = result - ? "formula_changed_to_true" - : "formula_changed_to_false"; - try { - this.logger.flow( - `Triggering flow '${triggerCardId}' for '${formula.name}'`, - ); - const card = this.homey.flow.getDeviceTriggerCard(triggerCardId); - await card.trigger(this, triggerData, state); - } catch (e) { - if (e.message && e.message.includes("Invalid Flow Card ID")) { - this.logger.error( - `FATAL: Trigger card '${triggerCardId}' not found. Check app.json/compose flow definitions.`, - e, - ); - } else { - this.logger.error("flow.trigger_error", e); - } - } - } - return result; - } catch (e) { - this.logger.error("errors.evaluation_failed", e); - formula.result = null; - return null; - } - } - - async evaluateAllFormulas() { - this.logger.info("notifications.reevaluating", {}); - const results = []; - for (const formula of this.formulas) { - if (formula.enabled) { - if (formula.firstImpression) { - this.availableInputs.forEach((id) => { - formula.lockedInputs[id] = false; - }); - this.logger.debug("inputs.unlocked", { - formula: formula.name, - }); - } - const result = await this.evaluateFormula(formula.id, false); - results.push({ - id: formula.id, - name: formula.name, - result, - }); - } else { - formula.result = null; - } - } - const overallDeviceState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - if (this.getCapabilityValue("onoff") !== overallDeviceState) { - await this.safeSetCapabilityValue("onoff", overallDeviceState); - await this.safeSetCapabilityValue("alarm_generic", overallDeviceState); - this.logger.info( - `Overall device capability updated after re-evaluating all: ${overallDeviceState}`, - ); - } - this.logger.debug("formula.evaluated_count", { - count: results.length, - }); - return results; - } - - async evaluateAllFormulasInitial() { - this.logger.info("evaluation.initial_complete", {}); - let anyEvaluated = false; - - for (const formula of this.formulas) { - if (!formula.enabled) { - formula.result = null; - continue; - } - const expr = formula.expression; - if (!expr) { - formula.result = null; - continue; - } - const required = this.parseExpression(expr); - this.logger.debug("debug.checking_formula", { - name: formula.name, - expression: expr, - }); - if (required.length > 0) { - this.logger.debug("debug.required_inputs", { - inputs: required.join(", "), - }); - } - - const allDefined = required.every( - (id) => formula.inputStates[id.toLowerCase()] !== "undefined", - ); - - if (allDefined || required.length === 0) { - this.logger.debug("formula.all_inputs_defined", { - name: formula.name, - }); - const result = await this.evaluateFormula(formula.id); - if (result !== null) { - anyEvaluated = true; - } - } else { - const missing = required.filter( - (id) => formula.inputStates[id.toLowerCase()] === "undefined", - ); - this.logger.debug("inputs.waiting", { - formula: formula.name, - missing: missing.join(", "), - }); - formula.result = null; - } - } - - const finalState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - - this.logger.info( - `Initial evaluation complete. Final state: ${finalState} (anyEvaluated=${anyEvaluated})`, - ); - - await this.safeSetCapabilityValue("onoff", finalState); - await this.safeSetCapabilityValue("alarm_generic", finalState); - - if ( - !anyEvaluated && - this.formulas.some( - (f) => - f.enabled && - f.expression && - this.parseExpression(f.expression).length > 0, - ) - ) { - this.logger.warn("evaluation.no_formulas_ready", {}); - } - } - - getFormulaResult(formulaId) { - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("errors.invalid_formula", { - formulaId, - }); - return null; - } - this.logger.debug("formula.result_debug", { - name: formula.name, - id: formulaId, - result: formula.result, - type: typeof formula.result, - }); - return formula.result === true; - } - - hasFormulaTimedOut(formulaId) { - const formula = this.formulas.find((f) => f.id === formulaId); - return !!(formula && formula.timedOut); - } - - startTimeoutChecks() { - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - } - this.timeoutInterval = setInterval(() => { - this.checkTimeouts(); - }, 1000); - this.logger.debug("Started timeout check interval.", {}); - } - - checkTimeouts() { - const now = Date.now(); - this.formulas.forEach((formula) => { - if ( - !formula.enabled || - formula.timedOut || - !formula.timeout || - formula.timeout <= 0 - ) - return; - - const requiredInputs = this.parseExpression(formula.expression); - if ( - requiredInputs.length === 0 || - requiredInputs.every( - (id) => formula.inputStates[id.toLowerCase()] !== "undefined", - ) - ) { - return; - } - - if (!formula.lastInputTime) return; - - const timeoutMs = formula.timeout * 1000; - const elapsed = now - formula.lastInputTime; - - if (elapsed >= timeoutMs) { - this.logger.info("formula.timed_out", { - name: formula.name, - timeout: formula.timeout, - }); - formula.timedOut = true; - - const triggerData = { - formula: { - id: formula.id, - name: formula.name, - }, - }; - const state = { - formulaId: formula.id, - }; - try { - const card = this.homey.flow.getDeviceTriggerCard("formula_timeout"); - card - .trigger(this, triggerData, state) - .catch((err) => this.logger.error("timeout.error", err)); - } catch (e) { - if (e.message && e.message.includes("Invalid Flow Card ID")) { - this.logger.error( - `FATAL: Trigger card 'formula_timeout' not found. Check app.json/compose flow definitions.`, - e, - ); - } else { - this.logger.error( - this.homey.__("errors.trigger_timeout_card_failed"), - e, - ); - } - } - } - }); - } - - async onSettings({ oldSettings, newSettings, changedKeys }) { - this.logger.info("settings.changed", { - keys: changedKeys.join(", "), - }); - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - this.timeoutInterval = null; - } - - await this.initializeFormulas(); - - if (changedKeys.includes("formulas")) { - for (const formula of this.formulas) { - const validation = this.validateExpression(formula.expression); - if (!validation.valid) { - this.homey.notifications - .createNotification({ - excerpt: this.homey.__("notifications.invalid_formula_config", { - formulaName: formula.name, - error: validation.error, - }), - }) - .catch((e) => - this.logger.error( - this.homey.__("errors.notification_failed_invalid_formula"), - e, - ), - ); - this.logger.error( - `Invalid formula configuration saved for '${formula.name}'`, - { - error: validation.error, - }, - ); - } - } - } - - await this.evaluateAllFormulasInitial(); - - this.startTimeoutChecks(); - this.logger.info("settings.applied", {}); - - const formatSettings = {}; - let needsFormat = false; - if (changedKeys.includes("formulas")) { - try { - const parsed = JSON.parse(newSettings.formulas); - const formatted = JSON.stringify(parsed, null, 2); - if (formatted !== newSettings.formulas) { - formatSettings.formulas = formatted; - needsFormat = true; - this.logger.debug("settings.formatting", { - type: "formulas", - }); - } - } catch (e) {} - } - if (needsFormat) { - setTimeout(async () => { - try { - this.logger.debug("settings.applying_formatted", {}); - await this.setSettings(formatSettings); - this.logger.info("settings.auto_formatted", {}); - } catch (e) { - this.logger.error("settings.format_failed", e); - } - }, 500); - } - } - - async onDeleted() { - this._isDeleting = true; - this.logger.device("device.deleted_cleanup", { - name: this.getName(), - }); - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - this.timeoutInterval = null; - } - this.logger.info("device.cleanup_complete", { - name: this.getName(), - }); - } -}; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 | - - - - - - - - - - - - - - - - - - - -52x -52x - - - - - - - - -108x -6x - - -102x -102x -102x - -102x - -630x -242x -242x - - - -388x -25x -25x -25x - -363x -24x -24x -24x - - - -339x -263x -263x -2x -2x -2x - -261x -2x -2x -2x - -259x -2x -2x -2x - - - - -333x -3x -3x -3x - -330x -3x -3x -3x - -327x -3x -3x -3x - -324x -3x -3x -3x - -321x -2x -2x -2x - -319x -2x -2x -2x - - - -317x -302x -302x -526x -526x - - -302x - - -302x -72x -230x -16x -214x -2x -212x -12x -200x -6x -194x -4x -190x - -187x - -3x - -299x - - - -15x -11x -11x -3x -8x -6x - -2x - -9x -9x - - - -4x - - -93x - - - - - - - - -71x -71x - -71x -1x - - -70x - -64x -3x - - -61x - - - - - - -94x - -88x -20x -20x -20x - - - - - - - -88x - - - - - - -114x - -108x -3x -3x -3x - - - - - - - -108x - - - - - - -117x - -113x -71x -71x -69x - - - - - - - -111x - - - - - - -200x -12x -12x -12x - - - - - - -188x - - - - - - -188x -2x - - -186x - - -186x -24x -24x -24x -3x - -21x -21x - - - -162x -12x -12x - - - - - - -150x -149x -149x - - - - - -1x - - - - - - - - - -270x - - - -270x - -14x - - -153x -3x - -150x - - -11x -11x - - - - -92x -92x - -90x - -62x - -21x - -7x - - - - - - - - - - - - - - - - -67x -60x -54x - - - -1x | /**
- * AST-Based Formula Evaluator
- *
- * Securely evaluates boolean formulas without using eval() or new Function()
- * by parsing expressions into an Abstract Syntax Tree (AST) and evaluating it.
- *
- * Supported operators:
- * - AND: AND, &&, *, &
- * - OR: OR, ||, +, |
- * - XOR: XOR, ^, !=
- * - NOT: NOT, !
- *
- * Supported values:
- * - Variables: A-J (case-insensitive)
- * - Literals: TRUE, FALSE, true, false, 1, 0
- * - Parentheses: ( )
- */
-
-class FormulaEvaluator {
- constructor() {
- this.tokens = [];
- this.position = 0;
- }
-
- /**
- * Tokenize the expression into tokens
- * @param {string} expression - The formula expression to tokenize
- * @returns {Array} Array of tokens
- */
- tokenize(expression) {
- if (!expression || typeof expression !== 'string') {
- throw new Error('Expression must be a non-empty string');
- }
-
- const tokens = [];
- const expr = expression.trim();
- let i = 0;
-
- while (i < expr.length) {
- // Skip whitespace
- if (/\s/.test(expr[i])) {
- i++;
- continue;
- }
-
- // Parentheses
- if (expr[i] === '(') {
- tokens.push({ type: 'LPAREN', value: '(' });
- i++;
- continue;
- }
- if (expr[i] === ')') {
- tokens.push({ type: 'RPAREN', value: ')' });
- i++;
- continue;
- }
-
- // Two-character operators
- if (i < expr.length - 1) {
- const twoChar = expr.substring(i, i + 2);
- if (twoChar === '&&') {
- tokens.push({ type: 'AND', value: '&&' });
- i += 2;
- continue;
- }
- if (twoChar === '||') {
- tokens.push({ type: 'OR', value: '||' });
- i += 2;
- continue;
- }
- if (twoChar === '!=') {
- tokens.push({ type: 'XOR', value: '!=' });
- i += 2;
- continue;
- }
- }
-
- // Single-character operators
- if (expr[i] === '&') {
- tokens.push({ type: 'AND', value: '&' });
- i++;
- continue;
- }
- if (expr[i] === '*') {
- tokens.push({ type: 'AND', value: '*' });
- i++;
- continue;
- }
- if (expr[i] === '|') {
- tokens.push({ type: 'OR', value: '|' });
- i++;
- continue;
- }
- if (expr[i] === '+') {
- tokens.push({ type: 'OR', value: '+' });
- i++;
- continue;
- }
- if (expr[i] === '^') {
- tokens.push({ type: 'XOR', value: '^' });
- i++;
- continue;
- }
- if (expr[i] === '!') {
- tokens.push({ type: 'NOT', value: '!' });
- i++;
- continue;
- }
-
- // Keywords and variables
- if (/[a-zA-Z]/.test(expr[i])) {
- let word = '';
- while (i < expr.length && /[a-zA-Z0-9]/.test(expr[i])) {
- word += expr[i];
- i++;
- }
-
- const upper = word.toUpperCase();
-
- // Keywords
- if (upper === 'AND') {
- tokens.push({ type: 'AND', value: 'AND' });
- } else if (upper === 'OR') {
- tokens.push({ type: 'OR', value: 'OR' });
- } else if (upper === 'XOR') {
- tokens.push({ type: 'XOR', value: 'XOR' });
- } else if (upper === 'NOT') {
- tokens.push({ type: 'NOT', value: 'NOT' });
- } else if (upper === 'TRUE') {
- tokens.push({ type: 'LITERAL', value: true });
- } else if (upper === 'FALSE') {
- tokens.push({ type: 'LITERAL', value: false });
- } else if (/^[A-J]$/.test(upper)) {
- // Variables A-J
- tokens.push({ type: 'VARIABLE', value: upper });
- } else {
- throw new Error(`Invalid identifier: ${word}`);
- }
- continue;
- }
-
- // Numbers (0 or 1 as boolean literals)
- if (/[0-9]/.test(expr[i])) {
- const num = expr[i];
- if (num === '0') {
- tokens.push({ type: 'LITERAL', value: false });
- } else if (num === '1') {
- tokens.push({ type: 'LITERAL', value: true });
- } else {
- throw new Error(`Invalid number: ${num}. Only 0 and 1 are allowed.`);
- }
- i++;
- continue;
- }
-
- // Unknown character
- throw new Error(`Unexpected character: ${expr[i]}`);
- }
-
- return tokens;
- }
-
- /**
- * Parse tokens into an Abstract Syntax Tree (AST)
- * @param {Array} tokens - Array of tokens from tokenize()
- * @returns {Object} AST root node
- */
- parse(tokens) {
- this.tokens = tokens;
- this.position = 0;
-
- if (tokens.length === 0) {
- throw new Error('Empty expression');
- }
-
- const ast = this.parseOr();
-
- if (this.position < this.tokens.length) {
- throw new Error(`Unexpected token: ${this.tokens[this.position].value}`);
- }
-
- return ast;
- }
-
- /**
- * Parse OR expressions (lowest precedence)
- */
- parseOr() {
- let left = this.parseXor();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'OR') {
- this.position++;
- const right = this.parseXor();
- left = {
- type: 'BinaryOp',
- operator: 'OR',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse XOR expressions
- */
- parseXor() {
- let left = this.parseAnd();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'XOR') {
- this.position++;
- const right = this.parseAnd();
- left = {
- type: 'BinaryOp',
- operator: 'XOR',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse AND expressions
- */
- parseAnd() {
- let left = this.parseUnary();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'AND') {
- this.position++;
- const right = this.parseUnary();
- left = {
- type: 'BinaryOp',
- operator: 'AND',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse unary expressions (NOT)
- */
- parseUnary() {
- if (this.position < this.tokens.length && this.tokens[this.position].type === 'NOT') {
- this.position++;
- const operand = this.parseUnary(); // Allow multiple NOTs
- return {
- type: 'UnaryOp',
- operator: 'NOT',
- operand: operand
- };
- }
-
- return this.parsePrimary();
- }
-
- /**
- * Parse primary expressions (literals, variables, parentheses)
- */
- parsePrimary() {
- if (this.position >= this.tokens.length) {
- throw new Error('Unexpected end of expression');
- }
-
- const token = this.tokens[this.position];
-
- // Parentheses
- if (token.type === 'LPAREN') {
- this.position++;
- const expr = this.parseOr();
- if (this.position >= this.tokens.length || this.tokens[this.position].type !== 'RPAREN') {
- throw new Error('Unbalanced parentheses');
- }
- this.position++;
- return expr;
- }
-
- // Literals
- if (token.type === 'LITERAL') {
- this.position++;
- return {
- type: 'Literal',
- value: token.value
- };
- }
-
- // Variables
- if (token.type === 'VARIABLE') {
- this.position++;
- return {
- type: 'Variable',
- name: token.value
- };
- }
-
- throw new Error(`Unexpected token: ${token.value}`);
- }
-
- /**
- * Evaluate an AST with given variable values
- * @param {Object} ast - The AST to evaluate
- * @param {Object} variables - Object mapping variable names to boolean values
- * @returns {boolean} The result of the evaluation
- */
- evaluateAST(ast, variables = {}) {
- Iif (!ast) {
- throw new Error('Invalid AST');
- }
-
- switch (ast.type) {
- case 'Literal':
- return !!ast.value;
-
- case 'Variable':
- if (!(ast.name in variables)) {
- throw new Error(`Undefined variable: ${ast.name}`);
- }
- return !!variables[ast.name];
-
- case 'UnaryOp':
- Eif (ast.operator === 'NOT') {
- return !this.evaluateAST(ast.operand, variables);
- }
- throw new Error(`Unknown unary operator: ${ast.operator}`);
-
- case 'BinaryOp':
- const left = this.evaluateAST(ast.left, variables);
- const right = this.evaluateAST(ast.right, variables);
-
- switch (ast.operator) {
- case 'AND':
- return left && right;
- case 'OR':
- return left || right;
- case 'XOR':
- return left !== right;
- default:
- throw new Error(`Unknown binary operator: ${ast.operator}`);
- }
-
- default:
- throw new Error(`Unknown AST node type: ${ast.type}`);
- }
- }
-
- /**
- * Validate and evaluate an expression
- * @param {string} expression - The formula expression
- * @param {Object} variables - Object mapping variable names to boolean values
- * @returns {boolean} The result of the evaluation
- */
- evaluate(expression, variables = {}) {
- const tokens = this.tokenize(expression);
- const ast = this.parse(tokens);
- return this.evaluateAST(ast, variables);
- }
-}
-
-module.exports = FormulaEvaluator; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict";
-
-/**
- * Advanced Logger for Homey Apps
- *
- * NEW: Supports global configuration via loggerConfig.js
- * FIX: Manually performs variable substitution as this.homey.__() fails to do so.
- * FIX (User): Added log level (e.g., [INFO ], [DEBUG]) to the console output.
- * FIX (User): Moved log level to the front of the output.
- * FIX (User): Fixed 'minLevel = 0 || ...' bug.
- * FIX (User): Added config loaded logging and static cache.
- * FIX (User): Moved config log to appear *after* the banner.
- * FIX (User): Fixed bug in _getDefaultConfig catch block return shape.
- * FIX (User): Fixed null-logging in banner().
- * FIX (User): Reverted info() to log as INFO. Code should be explicit.
- */
-
-class Logger {
- static LEVELS = {
- DEBUG: 0,
- INFO: 1,
- WARN: 2,
- ERROR: 3,
- NONE: 4,
- };
-
- static SYMBOLS = {
- DEBUG: "š",
- INFO: "ā
",
- WARN: "ā ļø",
- ERROR: "ā",
- TIMER: "ā±ļø",
- FORMULA: "š",
- INPUT: "š„",
- OUTPUT: "š¤",
- DEVICE: "š",
- API: "š",
- FLOW: "š",
- };
-
- static _globalConfig = null;
-
- /**
- * Get default configuration
- * Tries to load from loggerConfig.js, falls back to defaults
- * @private
- */
- static _getDefaultConfig() {
- // Use cache if available
- if (Logger._globalConfig) {
- return Logger._globalConfig;
- }
-
- try {
- const config = require("./loggerConfig");
- Logger._globalConfig = { config: config, source: "custom" };
- return Logger._globalConfig;
- } catch (e) {
- const defaultConfig = {
- defaultLevel: "DEBUG", // ENDRET: Fallback til DEBUG
- categoryLevels: {},
- options: {},
- };
- // --- FIKS: Returner samme objektform som i 'try' ---
- Logger._globalConfig = { config: defaultConfig, source: "default" };
- return Logger._globalConfig;
- // --- SLUTT FIKS ---
- }
- }
-
- constructor(context, category = "App", options = {}) {
- // --- FIKS I KONSTRUKTĆR ---
- // Finner det *ekte* homey-objektet uansett om 'context' er App eller Device
- if (context && context.homey) {
- this.homey = context.homey;
- } else if (context && context.app && context.app.homey) {
- // Fallback
- this.homey = context.app.homey;
- } else {
- console.error(
- `Logger: Could not find 'homey' object on context for category '${category}'.`,
- );
- // Oppretter en dummy for Ƅ unngƄ krasj
- this.homey = {
- app: { log: console.log, error: console.error },
- __: (s) => s, // Returnerer bare nĆøkkelen
- };
- }
- // --- SLUTT FIKS ---
-
- this.context = context;
- this.category = category;
-
- // Load global config
- const { config: globalConfig, source: configSource } =
- Logger._getDefaultConfig();
- this.configSource = configSource; // Lagrer kilden for bruk i banner()
-
- // Determine log level (priority: options > categoryLevels > defaultLevel)
- let level = options.level;
- if (!level && globalConfig.categoryLevels[category]) {
- level = globalConfig.categoryLevels[category];
- }
- if (!level) {
- level = globalConfig.defaultLevel || "DEBUG"; // ENDRET: Fallback til DEBUG
- }
-
- this.options = {
- level: level.toUpperCase(),
- timestamps:
- options.timestamps || globalConfig.options.timestamps || false,
- colors: options.colors || globalConfig.options.colors || false,
- }; // --- FIKS: Manglende } lagt til ---
-
- // --- FIKS: Korrekt sjekk for 0 (DEBUG) ---
- if (Logger.LEVELS[this.options.level] !== undefined) {
- this.minLevel = Logger.LEVELS[this.options.level];
- } else {
- this.minLevel = Logger.LEVELS.DEBUG; // Fallback
- }
- // --- SLUTT FIKS ---
-
- this.timers = new Map();
- }
-
- setLevel(level) {
- const upperLevel = level.toUpperCase();
- if (Logger.LEVELS[upperLevel] !== undefined) {
- this.minLevel = Logger.LEVELS[upperLevel];
- this.options.level = upperLevel;
- }
- }
-
- getLevel() {
- return this.options.level;
- }
-
- _shouldLog(level) {
- return Logger.LEVELS[level] >= this.minLevel;
- }
-
- _getPrefix(symbol) {
- return `${symbol} [${this.category}]`;
- }
-
- _formatMessage(keyOrMessage, data) {
- let message = keyOrMessage; // Start with the original key/message
-
- // 1. Attempt Translation IF it looks like a key and __ exists
- if (
- typeof keyOrMessage === "string" &&
- keyOrMessage.includes(".") &&
- this.homey &&
- typeof this.homey.__ === "function"
- ) {
- try {
- const translated = this.homey.__(keyOrMessage, data);
- // Use translation ONLY if it's a non-empty string and DIFFERENT from the key
- if (
- typeof translated === "string" &&
- translated.trim() !== "" &&
- translated !== keyOrMessage
- ) {
- message = translated;
- }
- // If translated is null, undefined, empty, or same as key, 'message' remains keyOrMessage
- } catch (e) {
- // If translation throws error, 'message' remains keyOrMessage
- // console.error(`Logger: Error during this.homey.__ for key '${keyOrMessage}'. Using key.`, e);
- }
- }
-
- // 2. Ensure 'message' is now DEFINITELY a string (fallback if translation failed badly)
- let messageStr = String(message ?? keyOrMessage); // Use keyOrMessage if message became null/undefined
-
- // 3. Perform Manual Substitution on the resulting string
- if (data && typeof data === "object" && data !== null) {
- try {
- messageStr = messageStr.replace(/\{([^{}]+)\}/g, (match, key) => {
- // Use value from data if exists and is not null/undefined, otherwise keep placeholder
- return data.hasOwnProperty(key) &&
- data[key] !== null &&
- data[key] !== undefined
- ? String(data[key]) // Ensure replacement is a string
- : match;
- });
- } catch (e) {
- console.error(
- `Logger: Manual substitution failed for: "${messageStr}"`,
- e,
- );
- // messageStr remains as it was before substitution attempt
- }
- }
-
- // Return the final string
- return messageStr;
- }
-
- _log(level, symbol, keyOrMessage, data) {
- if (!this._shouldLog(level)) {
- return;
- }
-
- // --- FIKS: Log-nivƄ lagt til ---
- const levelString = `[${level.padEnd(5)}]`;
- const prefix = this._getPrefix(symbol);
- const formattedMessage = this._formatMessage(keyOrMessage, data);
-
- try {
- // Bruk ALLTID this.homey.app.log
- // --- FIKS: Flyttet levelString FĆRST ---
- this.homey.app.log(levelString, prefix, formattedMessage);
- } catch (error) {
- console.error("Logger internal error:", error);
- console.log(levelString, prefix, formattedMessage); // Fallback til console.log
- }
- }
-
- _logError(keyOrMessage, error) {
- if (!this._shouldLog("ERROR")) {
- return;
- }
-
- // --- FIKS: Log-nivƄ lagt til ---
- const levelString = `[${"ERROR".padEnd(5)}]`;
-
- const prefix = this._getPrefix(Logger.SYMBOLS.ERROR);
-
- // Data-objektet kan vƦre gjemt i 'error' hvis det ikke er en ekte Error
- let data = error instanceof Error ? null : error;
- const formattedMessage = this._formatMessage(keyOrMessage, data);
-
- try {
- // --- FIKS: Flyttet levelString FĆRST ---
- this.homey.app.error(levelString, prefix, formattedMessage);
- if (error instanceof Error) {
- this.homey.app.error(error); // Logg stack trace hvis det er en Error
- } else if (data) {
- // Hvis 'error' bare var data, er den allerede i formattedMessage
- }
- } catch (err) {
- console.error("Logger internal error (error):", err);
- console.error(levelString, prefix, formattedMessage);
- if (error) {
- console.error(error);
- }
- }
- }
-
- debug(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, message, data);
- }
-
- // --- FIKS: info() logger nƄ som INFO ---
- info(message, data) {
- this._log("INFO", Logger.SYMBOLS.INFO, message, data);
- }
-
- warn(message, data) {
- this._log("WARN", Logger.SYMBOLS.WARN, message, data);
- }
-
- error(message, error) {
- this._logError(message, error);
- }
-
- formula(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.FORMULA, message, data);
- }
-
- input(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.INPUT, message, data);
- }
-
- output(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.OUTPUT, message, data);
- }
-
- // --- FIKS: device() logger nƄ som DEBUG ---
- device(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.DEVICE, message, data);
- }
-
- api(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.API, message, data);
- }
-
- // --- FIKS: flow() logger nƄ som DEBUG ---
- flow(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.FLOW, message, data);
- }
-
- timeStart(label) {
- this.timers.set(label, Date.now());
- if (this._shouldLog("DEBUG")) {
- this._log("DEBUG", Logger.SYMBOLS.TIMER, `Timer started: ${label}`);
- }
- }
-
- timeEnd(label) {
- const startTime = this.timers.get(label);
- if (!startTime) {
- this.warn(`Timer '${label}' was not started`);
- return 0;
- }
-
- const duration = Date.now() - startTime;
- this.timers.delete(label);
-
- if (this._shouldLog("DEBUG")) {
- this._log(
- "DEBUG",
- Logger.SYMBOLS.TIMER,
- `Timer '${label}': ${duration}ms`,
- );
- }
-
- return duration;
- }
-
- dump(label, object) {
- if (!this._shouldLog("DEBUG")) {
- return;
- }
-
- try {
- const formatted = JSON.stringify(object, null, 2);
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, `${label}:\n${formatted}`);
- } catch (error) {
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, `${label}:`, object);
- }
- }
-
- once(key, level, message, data) {
- if (!this._onceKeys) {
- this._onceKeys = new Set();
- }
-
- if (this._onceKeys.has(key)) {
- return;
- }
-
- this._onceKeys.add(key);
- const method = level.toLowerCase();
- if (typeof this[method] === "function") {
- this[method](message, data);
- } else {
- console.warn(
- `Logger: Invalid level "${level}" provided to 'once' method.`,
- );
- }
- }
-
- child(subCategory) {
- return new Logger(
- this.context,
- `${this.category}:${subCategory}`,
- this.options,
- );
- }
-
- separator() {
- if (this._shouldLog("DEBUG")) {
- this._log("DEBUG", "", "ā".repeat(50));
- }
- }
-
- banner(message) {
- if (this._shouldLog("INFO")) {
- const line = "ā".repeat(message.length + 4);
- // Banner kaller logg-funksjonen direkte for Ƅ unngƄ prefiks
- try {
- this.homey.app.log(line);
- this.homey.app.log(` ${message} `);
- this.homey.app.log(line);
-
- // --- FIKS: Bruker this.homey.app.log direkte ---
- // _log() feilet fordi this.homey.__() ikke var klar for strenger
- // som ikke var locale-nøkler pÄ dette tidlige stadiet.
- if (this.configSource) {
- const configMessage =
- this.configSource === "custom"
- ? "Loaded custom configuration from ./loggerConfig.js"
- : "Using default log settings (./loggerConfig.js not found)";
-
- // Logger med [INFO] og š-symbolet, som du foreslo
- // Bruker this.homey.app.log direkte for Ƅ unngƄ _formatMessage-feil
- this.homey.app.log(
- `[INFO ] ${Logger.SYMBOLS.DEBUG} [${this.category}]`,
- configMessage,
- );
-
- // Nullstill, sƄ den ikke logger dette igjen hvis banner() kalles pƄ nytt
- this.configSource = null;
- }
- // --- SLUTT FIKS ---
- } catch (e) {
- console.log(line);
- console.log(` ${message} `);
- console.log(line);
- }
- }
- }
-}
-
-module.exports = Logger; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| BaseLogicDriver.js | -
-
- |
- 0% | -0/178 | -0% | -0/107 | -0% | -0/19 | -0% | -0/174 | -
| BaseLogicUnit.js | -
-
- |
- 0% | -0/459 | -0% | -0/283 | -0% | -0/67 | -0% | -0/438 | -
| FormulaEvaluator.js | -
-
- |
- 97.5% | -156/160 | -96.03% | -97/101 | -100% | -10/10 | -97.5% | -156/160 | -
| Logger.js | -
-
- |
- 0% | -0/131 | -0% | -0/88 | -0% | -0/28 | -0% | -0/131 | -
| loggerConfig.js | -
-
- |
- 0% | -0/1 | -100% | -0/0 | -100% | -0/0 | -0% | -0/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict"; -const Homey = require("homey"); -const Logger = require("./Logger"); - -async function formulaAutocompleteHelper(query, args) { - const device = args.device; - if (!device || typeof device.getFormulas !== "function") { - console.warn( - `formulaAutocompleteHelper: Invalid device or missing getFormulas in args for autocomplete.`, - ); - return []; - } - try { - const formulas = device.getFormulas(); - if (!Array.isArray(formulas)) { - console.warn( - `getFormulas didn't return array for ${device.getName()}.`, - ); - return []; - } - const lowerQuery = query ? query.toLowerCase() : ""; - return formulas.filter( - (f) => - f && - f.name && - (!query || f.name.toLowerCase().includes(lowerQuery)), - ); - } catch (e) { - console.error( - `Error in formulaAutocompleteHelper for ${device.getName()}`, - e, - ); - return []; - } -} - -async function inputAutocompleteHelper(query, args) { - const device = args.device; - if (!device || typeof device.getInputOptions !== "function") { - console.warn( - `inputAutocompleteHelper: Invalid device or missing getInputOptions in args for autocomplete.`, - ); - return []; - } - try { - const inputs = device.getInputOptions(args); - if (!Array.isArray(inputs)) { - console.warn( - `getInputOptions didn't return array for ${device.getName()}.`, - ); - return []; - } - const lowerQuery = query ? query.toLowerCase() : ""; - return inputs.filter( - (i) => - i && - i.name && - (!query || i.name.toLowerCase().includes(lowerQuery)), - ); - } catch (e) { - console.error( - `Error in inputAutocompleteHelper for ${device.getName()}`, - e, - ); - return []; - } -} -// --- End Autocomplete Helper Functions --- - -class BaseLogicDriver extends Homey.Driver { - static logicUnitCardsRegistered = false; - - async onInit() { - const driverName = `Driver: ${this.id}`; - this.logger = new Logger(this, driverName); - - // Set numInputs based on driver ID - const driverId = this.id; - let numInputsParsed = 2; - try { - const parts = driverId.split("-"); - const numStr = parts[parts.length - 1]; - const num = parseInt(numStr); - if (!isNaN(num) && num > 0) { - numInputsParsed = num; - } else if (driverId !== "logic-device") { - this.logger.warn( - `Could not parse number of inputs from driver ID '${driverId}'. Using default: ${numInputsParsed}.`, - {}, - ); - } - } catch (e) { - this.logger.error( - `Error parsing numInputs from driver ID '${driverId}'. Using default: ${numInputsParsed}.`, - e, - ); - } - this.numInputs = numInputsParsed; - - this.logger.info("driver.ready", { - driverId: this.id, - numInputs: this.numInputs, - }); - - if (!BaseLogicDriver.logicUnitCardsRegistered) { - this.logger.info( - "First Logic Unit driver initializing. Registering shared flow cards...", - ); - await this.registerFlowCards(); - BaseLogicDriver.logicUnitCardsRegistered = true; - } else { - this.logger.debug( - "Shared flow cards already registered by another Logic Unit driver.", - ); - } - } - - async registerFlowCards() { - this.logger.debug("driver.registering_flow_cards"); - - // --- Triggers --- - const triggerCards = [ - { - id: "formula_changed_to_false_lu", - }, - { - id: "formula_changed_to_true_lu", - }, - { - id: "formula_timeout_lu", - }, - ]; - triggerCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getTriggerCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - return ( - args && - args.device && - args.device.driver && - args.device.driver.id && - args.device.driver.id.startsWith("logic-unit-") - ); - }); - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - this.logger.debug( - ` -> OK: TRIGGER card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering TRIGGER card '${cardInfo.id}'`, - e, - ); - } - }); - - // --- Actions --- - const actionCards = [ - { - id: "set_input_value_lu", - handler: "onFlowActionSetInput", - }, - { - id: "evaluate_formula_lu", - handler: "onFlowActionEvaluateFormula", - }, - { - id: "evaluate_all_formulas_lu", - handler: "onFlowActionReEvaluateAll", - }, - { - id: "clear_error_state_lu", - handler: "onFlowActionClearError", - }, - { - id: "set_all_inputs_lu", - handler: "setAllInputsFromFlow", - }, - { - id: "set_input_lu", - handler: "setInputForAllFormulasFromFlow", - }, - ]; - actionCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getActionCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - const device = args.device; - if (!device) - throw new Error( - this.homey.__("errors.invalid_device_instance"), - ); - const methodName = cardInfo.handler; - if (typeof device[methodName] !== "function") - throw new Error( - this.homey.__("errors.method_missing_on_device", { - methodName, - }), - ); - this.logger.flow( - `Executing ACTION '${cardInfo.id}' on device ${device.getName()}`, - ); - return await device[methodName](args, state); - }); - - // --- Autocomplete --- - if ( - [ - "set_input_value_lu", - "evaluate_formula_lu", - "clear_error_state_lu", - ].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - } - if ( - ["set_input_value_lu", "set_input_lu"].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "input", - inputAutocompleteHelper, - ); - } - this.logger.debug( - ` -> OK: ACTION card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering ACTION card '${cardInfo.id}'`, - e, - ); - } - }); - - // --- Conditions --- - const conditionCards = [ - { - id: "formula_has_timed_out_lu", - checkType: "timeout", - }, - { - id: "formula_result_is_lu", - checkTypeFromArg: "what_is", - }, - { - id: "has_any_error_lu", - checkType: "has_error", - }, - ]; - conditionCards.forEach((cardInfo) => { - try { - const card = this.homey.flow.getConditionCard(cardInfo.id); - card.registerRunListener(async (args, state) => { - const device = args.device; - if (!device) return false; - if (typeof device.onFlowCondition !== "function") - return false; - - let checkType; - if (cardInfo.checkType) { - checkType = cardInfo.checkType; - } else if (cardInfo.checkTypeFromArg) { - checkType = args[cardInfo.checkTypeFromArg] === "true"; - } - - this.logger.flow( - `Executing CONDITION '${cardInfo.id}' on device ${device.getName()}`, - ); - return await device.onFlowCondition(args, state, checkType); - }); - - if ( - [ - "formula_has_timed_out_lu", - "formula_result_is_lu", - ].includes(cardInfo.id) - ) { - this.registerAutocomplete( - card, - "formula", - formulaAutocompleteHelper, - ); - } - this.logger.debug( - ` -> OK: CONDITION card registered: '${cardInfo.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED: Registering CONDITION card '${cardInfo.id}'`, - e, - ); - } - }); - } - - // --- Helper for Autocomplete Registration --- - registerAutocomplete(card, argName, helperFn) { - try { - if (typeof helperFn !== "function") { - throw new Error( - this.homey.__("errors.helper_not_function", { argName }), - ); - } - card.registerArgumentAutocompleteListener( - argName, - async (query, args) => { - const device = args?.device; - - if (argName === "formula" || argName === "input") { - if (!device) { - this.logger.warn( - `Autocomplete for device arg '${argName}' on card '${card.id}' called without device context.`, - ); - return []; - } - - let requiredMethod = ""; - if (argName === "formula") - requiredMethod = "getFormulas"; - if (argName === "input") - requiredMethod = "getInputOptions"; - - if (typeof device[requiredMethod] !== "function") { - this.logger.warn( - `Autocomplete for device arg '${argName}' on card '${card.id}': Device ${device.getName()} missing method ${requiredMethod}.`, - ); - return []; - } - } - - try { - return await helperFn(query, args); - } catch (autocompleteError) { - this.logger.error( - `Error during autocomplete for ${argName} on card '${card.id}'`, - autocompleteError, - ); - return []; - } - }, - ); - this.logger.debug( - `Registered ${argName.toUpperCase()} autocomplete for card '${card.id}'`, - ); - } catch (e) { - this.logger.error( - ` -> FAILED to register autocomplete for '${argName}' on card '${card.id}'`, - e, - ); - } - } - - async ensureUniqueDeviceName(name) { - try { - if ( - !this.homey.app || - !this.homey.app.api || - !this.homey.app.api.devices || - typeof this.homey.app.api.devices.getDevices !== "function" - ) { - this.logger.warn( - "ensureUniqueDeviceName: Homey API not ready, returning original name.", - {}, - ); - return String(name || ""); - } - const all = await this.homey.app.api.devices.getDevices(); - if (!all || typeof all !== "object") { - this.logger.error( - "ensureUniqueDeviceName: Unexpected response from getDevices:", - all, - ); - return String(name || ""); - } - const existing = new Set( - Object.values(all) - .map((d) => - d && typeof d === "object" - ? String(d.name || "").trim() - : "", - ) - .filter(Boolean), - ); - const nameStr = String(name || ""); - if (!existing.has(nameStr)) return nameStr; - const base = nameStr.replace(/\s+\d+$/, "").trim(); - const m = nameStr.match(/\s+(\d+)$/); - let n = m ? parseInt(m[1], 10) + 1 : 2; - if (isNaN(n)) n = 2; - let candidate = `${base} ${n}`; - let safetyCounter = 0; - while (existing.has(candidate) && safetyCounter < 100) { - n++; - candidate = `${base} ${n}`; - safetyCounter++; - } - if (safetyCounter >= 100) { - this.logger.error( - "ensureUniqueDeviceName: Could not find unique name after 100 attempts, returning original + timestamp.", - {}, - ); - return `${nameStr}-${Date.now()}`; - } - return candidate; - } catch (e) { - this.logger.error("driver.ensure_unique_failed", e); - return String(name || ""); - } - } - - async onPair(session) { - this.logger.info("pair.session_started", {}); - session.setHandler("list_devices", async () => { - try { - this.logger.debug("Pair handler: list_devices called", {}); - let localeString; - try { - localeString = this.homey.__("device.logic_unit_name"); - } catch (e) { - this.logger.error( - "Error getting localization for 'device.logic_unit_name'", - e, - ); - localeString = this.homey.__( - "device.logic_unit_name_fallback", - ); - } - const baseName = String(localeString || "").replace( - "{count}", - this.numInputs, - ); - const name = await this.ensureUniqueDeviceName(baseName); - const defaultFormulaName = this.homey.__( - "pair.default_formula_name", - ); - const deviceData = { - name: name, - data: { - id: `${this.id}-${Math.random().toString(16).slice(2)}`, - numInputs: this.numInputs, - }, - settings: { - formulas: `[{"id":"f1","name":"${defaultFormulaName}","expression":"${this.getDefaultExpression()}","enabled":true,"timeout":0,"firstImpression":false}]`, - }, - }; - this.logger.debug( - "Pair handler: list_devices returning:", - deviceData, - ); - return [deviceData]; - } catch (e) { - this.logger.error("Error in list_devices pair handler", e); - throw e; - } - }); - this.logger.debug("Pair handler: list_devices registered", {}); - } - - getDefaultExpression() { - const letters = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]; - const inputsToUse = Math.min(this.numInputs, letters.length); - if (inputsToUse <= 0) return "true"; - return letters.slice(0, inputsToUse).join(" AND "); - } -} - -module.exports = BaseLogicDriver; -module.exports.formulaAutocompleteHelper = formulaAutocompleteHelper; -module.exports.inputAutocompleteHelper = inputAutocompleteHelper; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 -408 -409 -410 -411 -412 -413 -414 -415 -416 -417 -418 -419 -420 -421 -422 -423 -424 -425 -426 -427 -428 -429 -430 -431 -432 -433 -434 -435 -436 -437 -438 -439 -440 -441 -442 -443 -444 -445 -446 -447 -448 -449 -450 -451 -452 -453 -454 -455 -456 -457 -458 -459 -460 -461 -462 -463 -464 -465 -466 -467 -468 -469 -470 -471 -472 -473 -474 -475 -476 -477 -478 -479 -480 -481 -482 -483 -484 -485 -486 -487 -488 -489 -490 -491 -492 -493 -494 -495 -496 -497 -498 -499 -500 -501 -502 -503 -504 -505 -506 -507 -508 -509 -510 -511 -512 -513 -514 -515 -516 -517 -518 -519 -520 -521 -522 -523 -524 -525 -526 -527 -528 -529 -530 -531 -532 -533 -534 -535 -536 -537 -538 -539 -540 -541 -542 -543 -544 -545 -546 -547 -548 -549 -550 -551 -552 -553 -554 -555 -556 -557 -558 -559 -560 -561 -562 -563 -564 -565 -566 -567 -568 -569 -570 -571 -572 -573 -574 -575 -576 -577 -578 -579 -580 -581 -582 -583 -584 -585 -586 -587 -588 -589 -590 -591 -592 -593 -594 -595 -596 -597 -598 -599 -600 -601 -602 -603 -604 -605 -606 -607 -608 -609 -610 -611 -612 -613 -614 -615 -616 -617 -618 -619 -620 -621 -622 -623 -624 -625 -626 -627 -628 -629 -630 -631 -632 -633 -634 -635 -636 -637 -638 -639 -640 -641 -642 -643 -644 -645 -646 -647 -648 -649 -650 -651 -652 -653 -654 -655 -656 -657 -658 -659 -660 -661 -662 -663 -664 -665 -666 -667 -668 -669 -670 -671 -672 -673 -674 -675 -676 -677 -678 -679 -680 -681 -682 -683 -684 -685 -686 -687 -688 -689 -690 -691 -692 -693 -694 -695 -696 -697 -698 -699 -700 -701 -702 -703 -704 -705 -706 -707 -708 -709 -710 -711 -712 -713 -714 -715 -716 -717 -718 -719 -720 -721 -722 -723 -724 -725 -726 -727 -728 -729 -730 -731 -732 -733 -734 -735 -736 -737 -738 -739 -740 -741 -742 -743 -744 -745 -746 -747 -748 -749 -750 -751 -752 -753 -754 -755 -756 -757 -758 -759 -760 -761 -762 -763 -764 -765 -766 -767 -768 -769 -770 -771 -772 -773 -774 -775 -776 -777 -778 -779 -780 -781 -782 -783 -784 -785 -786 -787 -788 -789 -790 -791 -792 -793 -794 -795 -796 -797 -798 -799 -800 -801 -802 -803 -804 -805 -806 -807 -808 -809 -810 -811 -812 -813 -814 -815 -816 -817 -818 -819 -820 -821 -822 -823 -824 -825 -826 -827 -828 -829 -830 -831 -832 -833 -834 -835 -836 -837 -838 -839 -840 -841 -842 -843 -844 -845 -846 -847 -848 -849 -850 -851 -852 -853 -854 -855 -856 -857 -858 -859 -860 -861 -862 -863 -864 -865 -866 -867 -868 -869 -870 -871 -872 -873 -874 -875 -876 -877 -878 -879 -880 -881 -882 -883 -884 -885 -886 -887 -888 -889 -890 -891 -892 -893 -894 -895 -896 -897 -898 -899 -900 -901 -902 -903 -904 -905 -906 -907 -908 -909 -910 -911 -912 -913 -914 -915 -916 -917 -918 -919 -920 -921 -922 -923 -924 -925 -926 -927 -928 -929 -930 -931 -932 -933 -934 -935 -936 -937 -938 -939 -940 -941 -942 -943 -944 -945 -946 -947 -948 -949 -950 -951 -952 -953 -954 -955 -956 -957 -958 -959 -960 -961 -962 -963 -964 -965 -966 -967 -968 -969 -970 -971 -972 -973 -974 -975 -976 -977 -978 -979 -980 -981 -982 -983 -984 -985 -986 -987 -988 -989 -990 -991 -992 -993 -994 -995 -996 -997 -998 -999 -1000 -1001 -1002 -1003 -1004 -1005 -1006 -1007 -1008 -1009 -1010 -1011 -1012 -1013 -1014 -1015 -1016 -1017 -1018 -1019 -1020 -1021 -1022 -1023 -1024 -1025 -1026 -1027 -1028 -1029 -1030 -1031 -1032 -1033 -1034 -1035 -1036 -1037 -1038 -1039 -1040 -1041 -1042 -1043 -1044 -1045 -1046 -1047 -1048 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict"; -const Homey = require("homey"); -const Logger = require("./Logger"); - -module.exports = class BaseLogicUnit extends Homey.Device { - async safeSetCapabilityValue(cap, value) { - if (this._isDeleting) return; - try { - if (!this.hasCapability(cap)) { - return; - } - await this.setCapabilityValue(cap, value); - } catch (e) { - const msg = e?.message || String(e); - if (e?.statusCode === 404 || /not\s*found/i.test(msg)) { - this.logger.debug("device.capability_skip_deleted", { - capability: cap, - }); - } else { - this.logger.error("device.capability_update_failed", { - capability: cap, - message: msg, - }); - } - } - } - - async onInit() { - const driverName = `Device: ${this.driver ? this.driver.id : "unknown-driver"}`; - this.logger = new Logger(this, driverName); - this.logger.device("device.initializing", { - name: this.getName(), - }); - - if (!this.hasCapability("onoff")) { - await this.addCapability("onoff").catch((e) => - this.logger.error("Failed to add 'onoff' capability", e), - ); - } - if (!this.hasCapability("alarm_generic")) { - await this.addCapability("alarm_generic").catch((e) => - this.logger.error("Failed to add 'alarm_generic' capability", e), - ); - } - - this.numInputs = this.getData().numInputs ?? 2; - this.availableInputs = this.getAvailableInputIds(); - this.logger.debug( - `Device initialized with ${this.numInputs} inputs: ${this.availableInputs.join(", ")}`, - {}, - ); - - await this.initializeFormulas(); - await this.evaluateAllFormulasInitial(); - this.startTimeoutChecks(); - - // REGISTRATION IS REMOVED FROM HERE - this.logger.info("device.initialized", { - name: this.getName(), - count: this.numInputs, - }); - } - - async onFlowCondition(args, state, checkType) { - this.logger.flow(`onFlowCondition called for checkType: '${checkType}'`, { - formulaId: args.formula?.id, - }); - - if (checkType === "has_error") { - const hasTimeout = this.formulas.some((f) => f.timedOut === true); - this.logger.flow(`Condition 'has_any_error' check result: ${hasTimeout}`); - return hasTimeout; - } - - const cardIdFromArgs = args.cardId; - if ( - (cardIdFromArgs === "formula_result_is_ld" || - cardIdFromArgs === "formula_has_timed_out_ld") && - !args.formula - ) { - this.logger.warn( - "onFlowCondition: _ld card called without formula on LU device. Checking first formula as fallback.", - ); - if (this.formulas.length > 0) args.formula = this.formulas[0]; - else return false; - } - - if (!args.formula || !args.formula.id) { - this.logger.warn( - "onFlowCondition: No formula specified in args for formula-specific check.", - { - checkType, - }, - ); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - - if (!formula) { - this.logger.warn("onFlowCondition: Invalid formula ID received.", { - formulaId, - }); - return false; - } - - if (checkType === "timeout") { - const isTimedOut = formula.timedOut === true; - this.logger.flow( - `Condition 'formula_has_timed_out' check for '${formula.name}': Result=${isTimedOut}`, - ); - return isTimedOut; - } else if (typeof checkType === "boolean") { - const currentResult = formula.result; - const desiredResult = checkType; - const evaluatedResult = currentResult === true; - const conditionMet = evaluatedResult === desiredResult; - this.logger.flow( - `Condition 'formula_result_is' check for '${formula.name}': Current=${currentResult}, Desired=${desiredResult}, Match=${conditionMet}`, - ); - return conditionMet; - } else { - this.logger.error( - `onFlowCondition: Unknown checkType received: '${checkType}'`, - ); - return false; - } - } - - async onFlowActionSetInput(args, state) { - if (!args.formula || !args.input || !args.formula.id || !args.input.id) { - this.logger.warn("onFlowActionSetInput: Missing formula or input ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const inputId = args.input.id; - const value = args.value === "true"; - this.logger.flow("onFlowActionSetInput: Setting input", { - formula: formulaId, - input: inputId, - value: value, - }); - try { - await this.setInputForFormula(formulaId, inputId, value); - return true; - } catch (e) { - this.logger.error( - `Error during onFlowActionSetInput for ${formulaId}/${inputId}`, - e, - ); - return false; - } - } - - async onFlowActionEvaluateFormula(args, state) { - const cardIdFromArgs = args.cardId; - if ( - (!args.formula || !args.formula.id) && - cardIdFromArgs === "evaluate_formula_ld" - ) { - this.logger.warn( - `onFlowActionEvaluateFormula (LD) called on LU device ${this.getName()}. Falling back to re-evaluate all.`, - ); - return this.onFlowActionReEvaluateAll(args, state); - } - if (!args.formula || !args.formula.id) { - this.logger.warn("onFlowActionEvaluateFormula: Missing formula ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("onFlowActionEvaluateFormula: Invalid formula ID.", { - formulaId, - }); - return false; - } - this.logger.flow( - `onFlowActionEvaluateFormula: Evaluating '${formula.name}' (Resetting locks)`, - ); - try { - await this.evaluateFormula(formulaId, true); - return true; - } catch (e) { - this.logger.error( - `Error during onFlowActionEvaluateFormula for ${formulaId}`, - e, - ); - return false; - } - } - - async onFlowActionClearError(args, state) { - const cardIdFromArgs = args.cardId; - if ( - (!args.formula || !args.formula.id) && - cardIdFromArgs === "clear_error_ld" - ) { - this.logger.warn( - `onFlowActionClearError (LD) called on LU device ${this.getName()}. Clearing all formula timeouts.`, - ); - this.formulas.forEach((f) => { - f.timedOut = false; - }); - this.logger.info("notifications.error_cleared_all", {}); - return true; - } - if (!args.formula || !args.formula.id) { - this.logger.warn("onFlowActionClearError: Missing formula ID.", { - args, - }); - return false; - } - const formulaId = args.formula.id; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("onFlowActionClearError: Invalid formula ID.", { - formulaId, - }); - return false; - } - this.logger.flow( - `onFlowActionClearError: Clearing timeout state for '${formula.name}'`, - ); - formula.timedOut = false; - this.logger.info("notifications.error_cleared", { - formulaName: formula.name, - }); - return true; - } - - async onFlowActionReEvaluateAll(args, state) { - this.logger.flow( - "onFlowActionReEvaluateAll: Re-evaluating all formulas", - {}, - ); - try { - await this.evaluateAllFormulas(); - return true; - } catch (e) { - this.logger.error("Error during onFlowActionReEvaluateAll", e); - return false; - } - } - - async setAllInputsFromFlow(args, state) { - const device = this; - const valuesJson = args.values; - this.logger.flow( - `Executing Action 'set_all_inputs' on device '${device.getName()}'`, - ); - if (typeof valuesJson !== "string" || valuesJson.trim() === "") { - this.logger.warn( - `RunListener set_all_inputs: Empty or invalid JSON provided.`, - ); - return false; - } - try { - const values = JSON.parse(valuesJson); - if (typeof values !== "object" || values === null) { - throw new Error("Parsed JSON is not an object."); - } - const promises = []; - const inputs = device.getAvailableInputIds - ? device.getAvailableInputIds() - : []; - const formulas = Array.isArray(device.formulas) ? device.formulas : []; - - for (const key in values) { - if (!inputs.includes(key.toLowerCase())) { - this.logger.warn( - `setAllInputsFromFlow: JSON contains invalid input key '${key}' for this device.`, - ); - } - } - - for (const inputId of inputs) { - const key = inputId.toUpperCase(); - if (values.hasOwnProperty(key)) { - formulas.forEach((f) => { - if ( - f && - f.enabled && - typeof device.setInputForFormula === "function" - ) { - const boolValue = - values[key] === true || - String(values[key]).toLowerCase() === "true"; - promises.push( - device.setInputForFormula(f.id, inputId, boolValue), - ); - } - }); - } - } - await Promise.all(promises); - this.logger.info(`set_all_inputs OK for ${device.getName()}`, {}); - return true; - } catch (e) { - this.logger.error(`Error set_all_inputs for ${device.getName()}`, e); - return false; - } - } - - async setInputForAllFormulasFromFlow(args, state) { - const device = this; - const inputId = args.input?.id; - const value = args.value === "true"; - - this.logger.flow( - `Executing Action 'set_input' on device '${device.getName()}'`, - { - input: inputId, - value, - }, - ); - - if (!inputId || !this.availableInputs.includes(inputId)) { - this.logger.error( - `RunListener set_input: Invalid or missing input ID: ${inputId}. Available: ${this.availableInputs.join(",")}`, - ); - - return false; - } - - const promises = []; - const formulas = Array.isArray(device.formulas) ? device.formulas : []; - try { - formulas.forEach((f) => { - if (f && f.enabled && typeof device.setInputForFormula === "function") { - promises.push(device.setInputForFormula(f.id, inputId, value)); - } - }); - await Promise.all(promises); - this.logger.info(`set_input OK for ${device.getName()}`, {}); - return true; - } catch (e) { - this.logger.error(`Error set_input for ${device.getName()}`, e); - return false; - } - } - - getAvailableInputIds() { - const allInputs = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]; - const count = Math.max( - 0, - Math.min(allInputs.length, Number(this.numInputs) || 0), - ); - return allInputs.slice(0, count); - } - - getAvailableInputsUppercase() { - return this.availableInputs.map((i) => i.toUpperCase()); - } - - initializeFormulas() { - this.logger.debug("Initializing formulas from settings...", {}); - const settings = this.getSettings(); - try { - const formulasData = settings.formulas - ? JSON.parse(settings.formulas) - : []; - if (!Array.isArray(formulasData)) { - this.logger.error("errors.invalid_formula", { - message: "Formulas setting is not an array.", - }); - this.formulas = []; - } else { - this.formulas = formulasData.map((f) => ({ - id: f.id || `formula_${Math.random().toString(16).slice(2)}`, - name: f.name || this.homey.__("formula.unnamed_formula"), - expression: f.expression || "", - enabled: f.enabled !== false, - timeout: Number(f.timeout) || 0, - firstImpression: !!f.firstImpression, - inputStates: {}, - lockedInputs: {}, - lastInputTime: null, - result: null, - timedOut: false, - })); - } - - this.formulas.forEach((formula) => { - this.availableInputs.forEach((id) => { - formula.inputStates[id] = "undefined"; - formula.lockedInputs[id] = false; - }); - }); - } catch (e) { - this.logger.error("errors.invalid_formula", e); - this.formulas = []; - } - - if (this.formulas.length === 0) { - this.logger.warn( - "No valid formulas found in settings, creating default.", - {}, - ); - const defaultFormula = { - id: "formula_1", - name: this.homey.__ - ? this.homey.__("formula.default_name_alt") - : this.homey.__("formula.default_name_alt_fallback"), - expression: this.getDefaultExpression(), - enabled: true, - timeout: 0, - firstImpression: false, - inputStates: {}, - lockedInputs: {}, - lastInputTime: null, - result: null, - timedOut: false, - }; - - this.availableInputs.forEach((id) => { - defaultFormula.inputStates[id] = "undefined"; - defaultFormula.lockedInputs[id] = false; - }); - this.formulas = [defaultFormula]; - } - this.logger.info("formula.initialized", { - count: this.formulas.length, - }); - this.formulas.forEach((f) => - this.logger.debug("formula.details", { - name: f.name, - expression: f.expression, - enabled: f.enabled, - }), - ); - } - - getDefaultExpression() { - const inputs = this.getAvailableInputsUppercase(); - return inputs.length > 0 ? inputs.join(" AND ") : "true"; - } - - getFormulas() { - return this.formulas - .filter((f) => f.enabled) - .map((f) => ({ - id: f.id, - name: f.name, - description: - f.expression || - (this.homey.__ - ? this.homey.__("formula.no_expression") - : "(no expression)"), - })); - } - - getInputOptions() { - return this.getAvailableInputsUppercase().map((input) => ({ - id: input.toLowerCase(), - name: input, - })); - } - - validateExpression(expression) { - this.logger.debug(`Validating expression: "${expression || ""}"`, {}); - if ( - !expression || - typeof expression !== "string" || - expression.trim() === "" - ) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.expression_empty") - : "Expression empty", - }; - } - const upper = expression.toUpperCase(); - const inputs = this.getAvailableInputsUppercase(); - if (!inputs.length && upper.match(/[A-J]/)) { - return { - valid: false, - error: this.homey.__("formula.error_inputs_used_but_none_configured"), - }; - } - let validationInputs = inputs.length > 0 ? inputs : ["TEMP_VALIDATION_VAR"]; - - const tokenRe = new RegExp( - `\\b(?:AND|OR|XOR|NOT)\\b|&&|\\|\\||\\^|!=|\\*|\\+|!|\\(|\\)|\\b(?:${validationInputs.join("|")})\\b|\\bTRUE\\b|\\bFALSE\\b`, - "gi", - ); - const stripped = upper.replace(tokenRe, "").replace(/\s+/g, ""); - if (stripped.length > 0) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.invalid_tokens", { - tokens: stripped, - }) - : `Invalid tokens: ${stripped}`, - }; - } - - let depth = 0; - for (const ch of upper) { - if (ch === "(") depth++; - else if (ch === ")") depth--; - if (depth < 0) - return { - valid: false, - error: this.homey.__("formula.error_unbalanced_parentheses"), - }; - } - if (depth !== 0) - return { - valid: false, - error: "Unbalanced parentheses", - }; - - // Attempt to evaluate with dummy values to catch syntax errors - let testExpr = upper - .replace(/\bAND\b|&|\*/gi, "&&") // Normalize AND operators - .replace(/\bOR\b|\||\+/gi, "||") // Normalize OR operators - .replace(/\bXOR\b|\^|!=/gi, "!=") // Normalize XOR operators (use JS !=) - .replace(/\bNOT\b|!/gi, "!"); // Normalize NOT operator - - // Replace input letters and TRUE/FALSE with boolean literals - for (const key of validationInputs) { - const re = new RegExp(`\\b${key}\\b`, "g"); - testExpr = testExpr.replace(re, "true"); - } - testExpr = testExpr - .replace(/\bTRUE\b/g, "true") - .replace(/\bFALSE\b/g, "false"); - - // Try creating a function to catch final syntax errors - try { - const fn = new Function(`return ${testExpr};`); - fn(); // Execute once to be sure - return { - valid: true, - }; - } catch (e) { - return { - valid: false, - error: this.homey.__ - ? this.homey.__("formula.invalid_syntax", { - message: e.message, - }) - : `Syntax error: ${e.message}`, - }; - } - } - - parseExpression(expression) { - if (!expression || typeof expression !== "string") return []; - const inputs = this.getAvailableInputsUppercase(); - if (!inputs.length) return []; - const varRe = new RegExp(`\\b(${inputs.join("|")})\\b`, "gi"); - const matches = expression.match(varRe); - return matches ? [...new Set(matches.map((c) => c.toUpperCase()))] : []; - } - - async setInputForFormula(formulaId, inputId, value) { - if (this._isDeleting) return null; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("errors.invalid_formula", { - formulaId, - }); - return null; - } - if (!this.availableInputs.includes(inputId)) { - this.logger.warn( - `setInputForFormula: Invalid inputId '${inputId}' for device with ${this.numInputs} inputs.`, - ); - return null; - } - - if ( - formula.firstImpression === true && - formula.lockedInputs[inputId] === true - ) { - this.logger.debug("inputs.locked", { - input: inputId.toUpperCase(), - formula: formula.name, - }); - return formula.result; - } - - const oldValue = formula.inputStates[inputId]; - formula.inputStates[inputId] = - value === true || value === false ? value : "undefined"; - formula.timedOut = false; - - if ( - formula.firstImpression === true && - formula.inputStates[inputId] !== "undefined" && - formula.lockedInputs[inputId] !== true - ) { - formula.lockedInputs[inputId] = true; - this.logger.debug("inputs.locked_at_value", { - input: inputId.toUpperCase(), - value: formula.inputStates[inputId], - formula: formula.name, - }); - } - - if (formula.inputStates[inputId] !== "undefined") { - formula.lastInputTime = Date.now(); - } - - return await this.evaluateFormula(formulaId, false); - } - - async evaluateFormula(formulaId, resetLocks = false) { - if (this._isDeleting) return null; - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula || !formula.enabled) { - this.logger.debug("errors.invalid_formula", { - formulaId, - }); - return null; - } - - if (resetLocks === true && formula.firstImpression === true) { - this.availableInputs.forEach((id) => { - formula.lockedInputs[id] = false; - }); - this.logger.debug("inputs.unlocked", { - formula: formula.name, - }); - } - - const expression = formula.expression; - if (!expression) { - this.logger.debug("formula.invalid", { - formula: formula.name, - reason: "No expression", - }); - formula.result = null; - return null; - } - - const requiredInputs = this.parseExpression(expression); - const allDefined = requiredInputs.every( - (inputIdUpper) => - formula.inputStates[inputIdUpper.toLowerCase()] !== "undefined", - ); - - if (!allDefined && requiredInputs.length > 0) { - const missing = requiredInputs.filter( - (id) => formula.inputStates[id.toLowerCase()] === "undefined", - ); - this.logger.debug("inputs.waiting", { - formula: formula.name, - missing: missing.join(", "), - }); - return formula.result; - } - - const values = {}; - this.availableInputs.forEach((id) => { - values[id.toUpperCase()] = formula.inputStates[id]; - }); - this.logger.debug("formula.evaluating", { - formula: formula.name, - }); - - let evalExpression = expression - .replace(/\bAND\b|&|\*/gi, "&&") - .replace(/\bOR\b|\||\+/gi, "||") - .replace(/\bXOR\b|\^|!=/gi, "!=") - .replace(/\bNOT\b|!/gi, "!"); - - for (const key in values) { - if (requiredInputs.includes(key) && values[key] !== "undefined") { - const re = new RegExp(`\\b${key}\\b`, "gi"); - evalExpression = evalExpression.replace( - re, - String(values[key] === true), - ); - } - } - evalExpression = evalExpression - .replace(/\bTRUE\b/gi, "true") - .replace(/\bFALSE\b/gi, "false"); - this.logger.formula("formula.evaluating_expression", { - expression: expression, - evalExpression: evalExpression, - }); - - try { - const evaluateFn = new Function(`return ${evalExpression};`); - const result = !!evaluateFn(); - this.logger.debug("formula.evaluated", { - formula: formula.name, - result: result, - }); - - const previous = formula.result; - formula.result = result; - formula.timedOut = false; - - const overallDeviceState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - const currentOnOffState = this.getCapabilityValue("onoff"); - - if (currentOnOffState !== overallDeviceState) { - await this.safeSetCapabilityValue("onoff", overallDeviceState); - await this.safeSetCapabilityValue("alarm_generic", overallDeviceState); - this.logger.info( - `Overall device capability updated: ${overallDeviceState}`, - ); - } - - if (result !== previous && previous !== null) { - const triggerData = { - formula: { - id: formula.id, - name: formula.name, - }, - }; - const state = { - formulaId: formula.id, - }; - const triggerCardId = result - ? "formula_changed_to_true" - : "formula_changed_to_false"; - try { - this.logger.flow( - `Triggering flow '${triggerCardId}' for '${formula.name}'`, - ); - const card = this.homey.flow.getDeviceTriggerCard(triggerCardId); - await card.trigger(this, triggerData, state); - } catch (e) { - if (e.message && e.message.includes("Invalid Flow Card ID")) { - this.logger.error( - `FATAL: Trigger card '${triggerCardId}' not found. Check app.json/compose flow definitions.`, - e, - ); - } else { - this.logger.error("flow.trigger_error", e); - } - } - } - return result; - } catch (e) { - this.logger.error("errors.evaluation_failed", e); - formula.result = null; - return null; - } - } - - async evaluateAllFormulas() { - this.logger.info("notifications.reevaluating", {}); - const results = []; - for (const formula of this.formulas) { - if (formula.enabled) { - if (formula.firstImpression) { - this.availableInputs.forEach((id) => { - formula.lockedInputs[id] = false; - }); - this.logger.debug("inputs.unlocked", { - formula: formula.name, - }); - } - const result = await this.evaluateFormula(formula.id, false); - results.push({ - id: formula.id, - name: formula.name, - result, - }); - } else { - formula.result = null; - } - } - const overallDeviceState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - if (this.getCapabilityValue("onoff") !== overallDeviceState) { - await this.safeSetCapabilityValue("onoff", overallDeviceState); - await this.safeSetCapabilityValue("alarm_generic", overallDeviceState); - this.logger.info( - `Overall device capability updated after re-evaluating all: ${overallDeviceState}`, - ); - } - this.logger.debug("formula.evaluated_count", { - count: results.length, - }); - return results; - } - - async evaluateAllFormulasInitial() { - this.logger.info("evaluation.initial_complete", {}); - let anyEvaluated = false; - - for (const formula of this.formulas) { - if (!formula.enabled) { - formula.result = null; - continue; - } - const expr = formula.expression; - if (!expr) { - formula.result = null; - continue; - } - const required = this.parseExpression(expr); - this.logger.debug("debug.checking_formula", { - name: formula.name, - expression: expr, - }); - if (required.length > 0) { - this.logger.debug("debug.required_inputs", { - inputs: required.join(", "), - }); - } - - const allDefined = required.every( - (id) => formula.inputStates[id.toLowerCase()] !== "undefined", - ); - - if (allDefined || required.length === 0) { - this.logger.debug("formula.all_inputs_defined", { - name: formula.name, - }); - const result = await this.evaluateFormula(formula.id); - if (result !== null) { - anyEvaluated = true; - } - } else { - const missing = required.filter( - (id) => formula.inputStates[id.toLowerCase()] === "undefined", - ); - this.logger.debug("inputs.waiting", { - formula: formula.name, - missing: missing.join(", "), - }); - formula.result = null; - } - } - - const finalState = this.formulas.some( - (f) => f.enabled && f.result === true, - ); - - this.logger.info( - `Initial evaluation complete. Final state: ${finalState} (anyEvaluated=${anyEvaluated})`, - ); - - await this.safeSetCapabilityValue("onoff", finalState); - await this.safeSetCapabilityValue("alarm_generic", finalState); - - if ( - !anyEvaluated && - this.formulas.some( - (f) => - f.enabled && - f.expression && - this.parseExpression(f.expression).length > 0, - ) - ) { - this.logger.warn("evaluation.no_formulas_ready", {}); - } - } - - getFormulaResult(formulaId) { - const formula = this.formulas.find((f) => f.id === formulaId); - if (!formula) { - this.logger.warn("errors.invalid_formula", { - formulaId, - }); - return null; - } - this.logger.debug("formula.result_debug", { - name: formula.name, - id: formulaId, - result: formula.result, - type: typeof formula.result, - }); - return formula.result === true; - } - - hasFormulaTimedOut(formulaId) { - const formula = this.formulas.find((f) => f.id === formulaId); - return !!(formula && formula.timedOut); - } - - startTimeoutChecks() { - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - } - this.timeoutInterval = setInterval(() => { - this.checkTimeouts(); - }, 1000); - this.logger.debug("Started timeout check interval.", {}); - } - - checkTimeouts() { - const now = Date.now(); - this.formulas.forEach((formula) => { - if ( - !formula.enabled || - formula.timedOut || - !formula.timeout || - formula.timeout <= 0 - ) - return; - - const requiredInputs = this.parseExpression(formula.expression); - if ( - requiredInputs.length === 0 || - requiredInputs.every( - (id) => formula.inputStates[id.toLowerCase()] !== "undefined", - ) - ) { - return; - } - - if (!formula.lastInputTime) return; - - const timeoutMs = formula.timeout * 1000; - const elapsed = now - formula.lastInputTime; - - if (elapsed >= timeoutMs) { - this.logger.info("formula.timed_out", { - name: formula.name, - timeout: formula.timeout, - }); - formula.timedOut = true; - - const triggerData = { - formula: { - id: formula.id, - name: formula.name, - }, - }; - const state = { - formulaId: formula.id, - }; - try { - const card = this.homey.flow.getDeviceTriggerCard("formula_timeout"); - card - .trigger(this, triggerData, state) - .catch((err) => this.logger.error("timeout.error", err)); - } catch (e) { - if (e.message && e.message.includes("Invalid Flow Card ID")) { - this.logger.error( - `FATAL: Trigger card 'formula_timeout' not found. Check app.json/compose flow definitions.`, - e, - ); - } else { - this.logger.error( - this.homey.__("errors.trigger_timeout_card_failed"), - e, - ); - } - } - } - }); - } - - async onSettings({ oldSettings, newSettings, changedKeys }) { - this.logger.info("settings.changed", { - keys: changedKeys.join(", "), - }); - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - this.timeoutInterval = null; - } - - await this.initializeFormulas(); - - if (changedKeys.includes("formulas")) { - for (const formula of this.formulas) { - const validation = this.validateExpression(formula.expression); - if (!validation.valid) { - this.homey.notifications - .createNotification({ - excerpt: this.homey.__("notifications.invalid_formula_config", { - formulaName: formula.name, - error: validation.error, - }), - }) - .catch((e) => - this.logger.error( - this.homey.__("errors.notification_failed_invalid_formula"), - e, - ), - ); - this.logger.error( - `Invalid formula configuration saved for '${formula.name}'`, - { - error: validation.error, - }, - ); - } - } - } - - await this.evaluateAllFormulasInitial(); - - this.startTimeoutChecks(); - this.logger.info("settings.applied", {}); - - const formatSettings = {}; - let needsFormat = false; - if (changedKeys.includes("formulas")) { - try { - const parsed = JSON.parse(newSettings.formulas); - const formatted = JSON.stringify(parsed, null, 2); - if (formatted !== newSettings.formulas) { - formatSettings.formulas = formatted; - needsFormat = true; - this.logger.debug("settings.formatting", { - type: "formulas", - }); - } - } catch (e) {} - } - if (needsFormat) { - setTimeout(async () => { - try { - this.logger.debug("settings.applying_formatted", {}); - await this.setSettings(formatSettings); - this.logger.info("settings.auto_formatted", {}); - } catch (e) { - this.logger.error("settings.format_failed", e); - } - }, 500); - } - } - - async onDeleted() { - this._isDeleting = true; - this.logger.device("device.deleted_cleanup", { - name: this.getName(), - }); - if (this.timeoutInterval) { - clearInterval(this.timeoutInterval); - this.timeoutInterval = null; - } - this.logger.info("device.cleanup_complete", { - name: this.getName(), - }); - } -}; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 | - - - - - - - - - - - - - - - - - - - -52x -52x - - - - - - - - -108x -6x - - -102x -102x -102x - -102x - -630x -242x -242x - - - -388x -25x -25x -25x - -363x -24x -24x -24x - - - -339x -263x -263x -2x -2x -2x - -261x -2x -2x -2x - -259x -2x -2x -2x - - - - -333x -3x -3x -3x - -330x -3x -3x -3x - -327x -3x -3x -3x - -324x -3x -3x -3x - -321x -2x -2x -2x - -319x -2x -2x -2x - - - -317x -302x -302x -526x -526x - - -302x - - -302x -72x -230x -16x -214x -2x -212x -12x -200x -6x -194x -4x -190x - -187x - -3x - -299x - - - -15x -11x -11x -3x -8x -6x - -2x - -9x -9x - - - -4x - - -93x - - - - - - - - -71x -71x - -71x -1x - - -70x - -64x -3x - - -61x - - - - - - -94x - -88x -20x -20x -20x - - - - - - - -88x - - - - - - -114x - -108x -3x -3x -3x - - - - - - - -108x - - - - - - -117x - -113x -71x -71x -69x - - - - - - - -111x - - - - - - -200x -12x -12x -12x - - - - - - -188x - - - - - - -188x -2x - - -186x - - -186x -24x -24x -24x -3x - -21x -21x - - - -162x -12x -12x - - - - - - -150x -149x -149x - - - - - -1x - - - - - - - - - -270x - - - -270x - -14x - - -153x -3x - -150x - - -11x -11x - - - - -92x -92x - -90x - -62x - -21x - -7x - - - - - - - - - - - - - - - - -67x -60x -54x - - - -1x | /**
- * AST-Based Formula Evaluator
- *
- * Securely evaluates boolean formulas without using eval() or new Function()
- * by parsing expressions into an Abstract Syntax Tree (AST) and evaluating it.
- *
- * Supported operators:
- * - AND: AND, &&, *, &
- * - OR: OR, ||, +, |
- * - XOR: XOR, ^, !=
- * - NOT: NOT, !
- *
- * Supported values:
- * - Variables: A-J (case-insensitive)
- * - Literals: TRUE, FALSE, true, false, 1, 0
- * - Parentheses: ( )
- */
-
-class FormulaEvaluator {
- constructor() {
- this.tokens = [];
- this.position = 0;
- }
-
- /**
- * Tokenize the expression into tokens
- * @param {string} expression - The formula expression to tokenize
- * @returns {Array} Array of tokens
- */
- tokenize(expression) {
- if (!expression || typeof expression !== 'string') {
- throw new Error('Expression must be a non-empty string');
- }
-
- const tokens = [];
- const expr = expression.trim();
- let i = 0;
-
- while (i < expr.length) {
- // Skip whitespace
- if (/\s/.test(expr[i])) {
- i++;
- continue;
- }
-
- // Parentheses
- if (expr[i] === '(') {
- tokens.push({ type: 'LPAREN', value: '(' });
- i++;
- continue;
- }
- if (expr[i] === ')') {
- tokens.push({ type: 'RPAREN', value: ')' });
- i++;
- continue;
- }
-
- // Two-character operators
- if (i < expr.length - 1) {
- const twoChar = expr.substring(i, i + 2);
- if (twoChar === '&&') {
- tokens.push({ type: 'AND', value: '&&' });
- i += 2;
- continue;
- }
- if (twoChar === '||') {
- tokens.push({ type: 'OR', value: '||' });
- i += 2;
- continue;
- }
- if (twoChar === '!=') {
- tokens.push({ type: 'XOR', value: '!=' });
- i += 2;
- continue;
- }
- }
-
- // Single-character operators
- if (expr[i] === '&') {
- tokens.push({ type: 'AND', value: '&' });
- i++;
- continue;
- }
- if (expr[i] === '*') {
- tokens.push({ type: 'AND', value: '*' });
- i++;
- continue;
- }
- if (expr[i] === '|') {
- tokens.push({ type: 'OR', value: '|' });
- i++;
- continue;
- }
- if (expr[i] === '+') {
- tokens.push({ type: 'OR', value: '+' });
- i++;
- continue;
- }
- if (expr[i] === '^') {
- tokens.push({ type: 'XOR', value: '^' });
- i++;
- continue;
- }
- if (expr[i] === '!') {
- tokens.push({ type: 'NOT', value: '!' });
- i++;
- continue;
- }
-
- // Keywords and variables
- if (/[a-zA-Z]/.test(expr[i])) {
- let word = '';
- while (i < expr.length && /[a-zA-Z0-9]/.test(expr[i])) {
- word += expr[i];
- i++;
- }
-
- const upper = word.toUpperCase();
-
- // Keywords
- if (upper === 'AND') {
- tokens.push({ type: 'AND', value: 'AND' });
- } else if (upper === 'OR') {
- tokens.push({ type: 'OR', value: 'OR' });
- } else if (upper === 'XOR') {
- tokens.push({ type: 'XOR', value: 'XOR' });
- } else if (upper === 'NOT') {
- tokens.push({ type: 'NOT', value: 'NOT' });
- } else if (upper === 'TRUE') {
- tokens.push({ type: 'LITERAL', value: true });
- } else if (upper === 'FALSE') {
- tokens.push({ type: 'LITERAL', value: false });
- } else if (/^[A-J]$/.test(upper)) {
- // Variables A-J
- tokens.push({ type: 'VARIABLE', value: upper });
- } else {
- throw new Error(`Invalid identifier: ${word}`);
- }
- continue;
- }
-
- // Numbers (0 or 1 as boolean literals)
- if (/[0-9]/.test(expr[i])) {
- const num = expr[i];
- if (num === '0') {
- tokens.push({ type: 'LITERAL', value: false });
- } else if (num === '1') {
- tokens.push({ type: 'LITERAL', value: true });
- } else {
- throw new Error(`Invalid number: ${num}. Only 0 and 1 are allowed.`);
- }
- i++;
- continue;
- }
-
- // Unknown character
- throw new Error(`Unexpected character: ${expr[i]}`);
- }
-
- return tokens;
- }
-
- /**
- * Parse tokens into an Abstract Syntax Tree (AST)
- * @param {Array} tokens - Array of tokens from tokenize()
- * @returns {Object} AST root node
- */
- parse(tokens) {
- this.tokens = tokens;
- this.position = 0;
-
- if (tokens.length === 0) {
- throw new Error('Empty expression');
- }
-
- const ast = this.parseOr();
-
- if (this.position < this.tokens.length) {
- throw new Error(`Unexpected token: ${this.tokens[this.position].value}`);
- }
-
- return ast;
- }
-
- /**
- * Parse OR expressions (lowest precedence)
- */
- parseOr() {
- let left = this.parseXor();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'OR') {
- this.position++;
- const right = this.parseXor();
- left = {
- type: 'BinaryOp',
- operator: 'OR',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse XOR expressions
- */
- parseXor() {
- let left = this.parseAnd();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'XOR') {
- this.position++;
- const right = this.parseAnd();
- left = {
- type: 'BinaryOp',
- operator: 'XOR',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse AND expressions
- */
- parseAnd() {
- let left = this.parseUnary();
-
- while (this.position < this.tokens.length && this.tokens[this.position].type === 'AND') {
- this.position++;
- const right = this.parseUnary();
- left = {
- type: 'BinaryOp',
- operator: 'AND',
- left: left,
- right: right
- };
- }
-
- return left;
- }
-
- /**
- * Parse unary expressions (NOT)
- */
- parseUnary() {
- if (this.position < this.tokens.length && this.tokens[this.position].type === 'NOT') {
- this.position++;
- const operand = this.parseUnary(); // Allow multiple NOTs
- return {
- type: 'UnaryOp',
- operator: 'NOT',
- operand: operand
- };
- }
-
- return this.parsePrimary();
- }
-
- /**
- * Parse primary expressions (literals, variables, parentheses)
- */
- parsePrimary() {
- if (this.position >= this.tokens.length) {
- throw new Error('Unexpected end of expression');
- }
-
- const token = this.tokens[this.position];
-
- // Parentheses
- if (token.type === 'LPAREN') {
- this.position++;
- const expr = this.parseOr();
- if (this.position >= this.tokens.length || this.tokens[this.position].type !== 'RPAREN') {
- throw new Error('Unbalanced parentheses');
- }
- this.position++;
- return expr;
- }
-
- // Literals
- if (token.type === 'LITERAL') {
- this.position++;
- return {
- type: 'Literal',
- value: token.value
- };
- }
-
- // Variables
- if (token.type === 'VARIABLE') {
- this.position++;
- return {
- type: 'Variable',
- name: token.value
- };
- }
-
- throw new Error(`Unexpected token: ${token.value}`);
- }
-
- /**
- * Evaluate an AST with given variable values
- * @param {Object} ast - The AST to evaluate
- * @param {Object} variables - Object mapping variable names to boolean values
- * @returns {boolean} The result of the evaluation
- */
- evaluateAST(ast, variables = {}) {
- Iif (!ast) {
- throw new Error('Invalid AST');
- }
-
- switch (ast.type) {
- case 'Literal':
- return !!ast.value;
-
- case 'Variable':
- if (!(ast.name in variables)) {
- throw new Error(`Undefined variable: ${ast.name}`);
- }
- return !!variables[ast.name];
-
- case 'UnaryOp':
- Eif (ast.operator === 'NOT') {
- return !this.evaluateAST(ast.operand, variables);
- }
- throw new Error(`Unknown unary operator: ${ast.operator}`);
-
- case 'BinaryOp':
- const left = this.evaluateAST(ast.left, variables);
- const right = this.evaluateAST(ast.right, variables);
-
- switch (ast.operator) {
- case 'AND':
- return left && right;
- case 'OR':
- return left || right;
- case 'XOR':
- return left !== right;
- default:
- throw new Error(`Unknown binary operator: ${ast.operator}`);
- }
-
- default:
- throw new Error(`Unknown AST node type: ${ast.type}`);
- }
- }
-
- /**
- * Validate and evaluate an expression
- * @param {string} expression - The formula expression
- * @param {Object} variables - Object mapping variable names to boolean values
- * @returns {boolean} The result of the evaluation
- */
- evaluate(expression, variables = {}) {
- const tokens = this.tokenize(expression);
- const ast = this.parse(tokens);
- return this.evaluateAST(ast, variables);
- }
-}
-
-module.exports = FormulaEvaluator; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310 -311 -312 -313 -314 -315 -316 -317 -318 -319 -320 -321 -322 -323 -324 -325 -326 -327 -328 -329 -330 -331 -332 -333 -334 -335 -336 -337 -338 -339 -340 -341 -342 -343 -344 -345 -346 -347 -348 -349 -350 -351 -352 -353 -354 -355 -356 -357 -358 -359 -360 -361 -362 -363 -364 -365 -366 -367 -368 -369 -370 -371 -372 -373 -374 -375 -376 -377 -378 -379 -380 -381 -382 -383 -384 -385 -386 -387 -388 -389 -390 -391 -392 -393 -394 -395 -396 -397 -398 -399 -400 -401 -402 -403 -404 -405 -406 -407 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | "use strict";
-
-/**
- * Advanced Logger for Homey Apps
- *
- * NEW: Supports global configuration via loggerConfig.js
- * FIX: Manually performs variable substitution as this.homey.__() fails to do so.
- * FIX (User): Added log level (e.g., [INFO ], [DEBUG]) to the console output.
- * FIX (User): Moved log level to the front of the output.
- * FIX (User): Fixed 'minLevel = 0 || ...' bug.
- * FIX (User): Added config loaded logging and static cache.
- * FIX (User): Moved config log to appear *after* the banner.
- * FIX (User): Fixed bug in _getDefaultConfig catch block return shape.
- * FIX (User): Fixed null-logging in banner().
- * FIX (User): Reverted info() to log as INFO. Code should be explicit.
- */
-
-class Logger {
- static LEVELS = {
- DEBUG: 0,
- INFO: 1,
- WARN: 2,
- ERROR: 3,
- NONE: 4,
- };
-
- static SYMBOLS = {
- DEBUG: "š",
- INFO: "ā
",
- WARN: "ā ļø",
- ERROR: "ā",
- TIMER: "ā±ļø",
- FORMULA: "š",
- INPUT: "š„",
- OUTPUT: "š¤",
- DEVICE: "š",
- API: "š",
- FLOW: "š",
- };
-
- static _globalConfig = null;
-
- /**
- * Get default configuration
- * Tries to load from loggerConfig.js, falls back to defaults
- * @private
- */
- static _getDefaultConfig() {
- // Use cache if available
- if (Logger._globalConfig) {
- return Logger._globalConfig;
- }
-
- try {
- const config = require("./loggerConfig");
- Logger._globalConfig = { config: config, source: "custom" };
- return Logger._globalConfig;
- } catch (e) {
- const defaultConfig = {
- defaultLevel: "DEBUG", // ENDRET: Fallback til DEBUG
- categoryLevels: {},
- options: {},
- };
- // --- FIKS: Returner samme objektform som i 'try' ---
- Logger._globalConfig = { config: defaultConfig, source: "default" };
- return Logger._globalConfig;
- // --- SLUTT FIKS ---
- }
- }
-
- constructor(context, category = "App", options = {}) {
- // --- FIKS I KONSTRUKTĆR ---
- // Finner det *ekte* homey-objektet uansett om 'context' er App eller Device
- if (context && context.homey) {
- this.homey = context.homey;
- } else if (context && context.app && context.app.homey) {
- // Fallback
- this.homey = context.app.homey;
- } else {
- console.error(
- `Logger: Could not find 'homey' object on context for category '${category}'.`,
- );
- // Oppretter en dummy for Ƅ unngƄ krasj
- this.homey = {
- app: { log: console.log, error: console.error },
- __: (s) => s, // Returnerer bare nĆøkkelen
- };
- }
- // --- SLUTT FIKS ---
-
- this.context = context;
- this.category = category;
-
- // Load global config
- const { config: globalConfig, source: configSource } =
- Logger._getDefaultConfig();
- this.configSource = configSource; // Lagrer kilden for bruk i banner()
-
- // Determine log level (priority: options > categoryLevels > defaultLevel)
- let level = options.level;
- if (!level && globalConfig.categoryLevels[category]) {
- level = globalConfig.categoryLevels[category];
- }
- if (!level) {
- level = globalConfig.defaultLevel || "DEBUG"; // ENDRET: Fallback til DEBUG
- }
-
- this.options = {
- level: level.toUpperCase(),
- timestamps:
- options.timestamps || globalConfig.options.timestamps || false,
- colors: options.colors || globalConfig.options.colors || false,
- }; // --- FIKS: Manglende } lagt til ---
-
- // --- FIKS: Korrekt sjekk for 0 (DEBUG) ---
- if (Logger.LEVELS[this.options.level] !== undefined) {
- this.minLevel = Logger.LEVELS[this.options.level];
- } else {
- this.minLevel = Logger.LEVELS.DEBUG; // Fallback
- }
- // --- SLUTT FIKS ---
-
- this.timers = new Map();
- }
-
- setLevel(level) {
- const upperLevel = level.toUpperCase();
- if (Logger.LEVELS[upperLevel] !== undefined) {
- this.minLevel = Logger.LEVELS[upperLevel];
- this.options.level = upperLevel;
- }
- }
-
- getLevel() {
- return this.options.level;
- }
-
- _shouldLog(level) {
- return Logger.LEVELS[level] >= this.minLevel;
- }
-
- _getPrefix(symbol) {
- return `${symbol} [${this.category}]`;
- }
-
- _formatMessage(keyOrMessage, data) {
- let message = keyOrMessage; // Start with the original key/message
-
- // 1. Attempt Translation IF it looks like a key and __ exists
- if (
- typeof keyOrMessage === "string" &&
- keyOrMessage.includes(".") &&
- this.homey &&
- typeof this.homey.__ === "function"
- ) {
- try {
- const translated = this.homey.__(keyOrMessage, data);
- // Use translation ONLY if it's a non-empty string and DIFFERENT from the key
- if (
- typeof translated === "string" &&
- translated.trim() !== "" &&
- translated !== keyOrMessage
- ) {
- message = translated;
- }
- // If translated is null, undefined, empty, or same as key, 'message' remains keyOrMessage
- } catch (e) {
- // If translation throws error, 'message' remains keyOrMessage
- // console.error(`Logger: Error during this.homey.__ for key '${keyOrMessage}'. Using key.`, e);
- }
- }
-
- // 2. Ensure 'message' is now DEFINITELY a string (fallback if translation failed badly)
- let messageStr = String(message ?? keyOrMessage); // Use keyOrMessage if message became null/undefined
-
- // 3. Perform Manual Substitution on the resulting string
- if (data && typeof data === "object" && data !== null) {
- try {
- messageStr = messageStr.replace(/\{([^{}]+)\}/g, (match, key) => {
- // Use value from data if exists and is not null/undefined, otherwise keep placeholder
- return data.hasOwnProperty(key) &&
- data[key] !== null &&
- data[key] !== undefined
- ? String(data[key]) // Ensure replacement is a string
- : match;
- });
- } catch (e) {
- console.error(
- `Logger: Manual substitution failed for: "${messageStr}"`,
- e,
- );
- // messageStr remains as it was before substitution attempt
- }
- }
-
- // Return the final string
- return messageStr;
- }
-
- _log(level, symbol, keyOrMessage, data) {
- if (!this._shouldLog(level)) {
- return;
- }
-
- // --- FIKS: Log-nivƄ lagt til ---
- const levelString = `[${level.padEnd(5)}]`;
- const prefix = this._getPrefix(symbol);
- const formattedMessage = this._formatMessage(keyOrMessage, data);
-
- try {
- // Bruk ALLTID this.homey.app.log
- // --- FIKS: Flyttet levelString FĆRST ---
- this.homey.app.log(levelString, prefix, formattedMessage);
- } catch (error) {
- console.error("Logger internal error:", error);
- console.log(levelString, prefix, formattedMessage); // Fallback til console.log
- }
- }
-
- _logError(keyOrMessage, error) {
- if (!this._shouldLog("ERROR")) {
- return;
- }
-
- // --- FIKS: Log-nivƄ lagt til ---
- const levelString = `[${"ERROR".padEnd(5)}]`;
-
- const prefix = this._getPrefix(Logger.SYMBOLS.ERROR);
-
- // Data-objektet kan vƦre gjemt i 'error' hvis det ikke er en ekte Error
- let data = error instanceof Error ? null : error;
- const formattedMessage = this._formatMessage(keyOrMessage, data);
-
- try {
- // --- FIKS: Flyttet levelString FĆRST ---
- this.homey.app.error(levelString, prefix, formattedMessage);
- if (error instanceof Error) {
- this.homey.app.error(error); // Logg stack trace hvis det er en Error
- } else if (data) {
- // Hvis 'error' bare var data, er den allerede i formattedMessage
- }
- } catch (err) {
- console.error("Logger internal error (error):", err);
- console.error(levelString, prefix, formattedMessage);
- if (error) {
- console.error(error);
- }
- }
- }
-
- debug(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, message, data);
- }
-
- // --- FIKS: info() logger nƄ som INFO ---
- info(message, data) {
- this._log("INFO", Logger.SYMBOLS.INFO, message, data);
- }
-
- warn(message, data) {
- this._log("WARN", Logger.SYMBOLS.WARN, message, data);
- }
-
- error(message, error) {
- this._logError(message, error);
- }
-
- formula(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.FORMULA, message, data);
- }
-
- input(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.INPUT, message, data);
- }
-
- output(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.OUTPUT, message, data);
- }
-
- // --- FIKS: device() logger nƄ som DEBUG ---
- device(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.DEVICE, message, data);
- }
-
- api(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.API, message, data);
- }
-
- // --- FIKS: flow() logger nƄ som DEBUG ---
- flow(message, data) {
- this._log("DEBUG", Logger.SYMBOLS.FLOW, message, data);
- }
-
- timeStart(label) {
- this.timers.set(label, Date.now());
- if (this._shouldLog("DEBUG")) {
- this._log("DEBUG", Logger.SYMBOLS.TIMER, `Timer started: ${label}`);
- }
- }
-
- timeEnd(label) {
- const startTime = this.timers.get(label);
- if (!startTime) {
- this.warn(`Timer '${label}' was not started`);
- return 0;
- }
-
- const duration = Date.now() - startTime;
- this.timers.delete(label);
-
- if (this._shouldLog("DEBUG")) {
- this._log(
- "DEBUG",
- Logger.SYMBOLS.TIMER,
- `Timer '${label}': ${duration}ms`,
- );
- }
-
- return duration;
- }
-
- dump(label, object) {
- if (!this._shouldLog("DEBUG")) {
- return;
- }
-
- try {
- const formatted = JSON.stringify(object, null, 2);
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, `${label}:\n${formatted}`);
- } catch (error) {
- this._log("DEBUG", Logger.SYMBOLS.DEBUG, `${label}:`, object);
- }
- }
-
- once(key, level, message, data) {
- if (!this._onceKeys) {
- this._onceKeys = new Set();
- }
-
- if (this._onceKeys.has(key)) {
- return;
- }
-
- this._onceKeys.add(key);
- const method = level.toLowerCase();
- if (typeof this[method] === "function") {
- this[method](message, data);
- } else {
- console.warn(
- `Logger: Invalid level "${level}" provided to 'once' method.`,
- );
- }
- }
-
- child(subCategory) {
- return new Logger(
- this.context,
- `${this.category}:${subCategory}`,
- this.options,
- );
- }
-
- separator() {
- if (this._shouldLog("DEBUG")) {
- this._log("DEBUG", "", "ā".repeat(50));
- }
- }
-
- banner(message) {
- if (this._shouldLog("INFO")) {
- const line = "ā".repeat(message.length + 4);
- // Banner kaller logg-funksjonen direkte for Ƅ unngƄ prefiks
- try {
- this.homey.app.log(line);
- this.homey.app.log(` ${message} `);
- this.homey.app.log(line);
-
- // --- FIKS: Bruker this.homey.app.log direkte ---
- // _log() feilet fordi this.homey.__() ikke var klar for strenger
- // som ikke var locale-nøkler pÄ dette tidlige stadiet.
- if (this.configSource) {
- const configMessage =
- this.configSource === "custom"
- ? "Loaded custom configuration from ./loggerConfig.js"
- : "Using default log settings (./loggerConfig.js not found)";
-
- // Logger med [INFO] og š-symbolet, som du foreslo
- // Bruker this.homey.app.log direkte for Ƅ unngƄ _formatMessage-feil
- this.homey.app.log(
- `[INFO ] ${Logger.SYMBOLS.DEBUG} [${this.category}]`,
- configMessage,
- );
-
- // Nullstill, sƄ den ikke logger dette igjen hvis banner() kalles pƄ nytt
- this.configSource = null;
- }
- // --- SLUTT FIKS ---
- } catch (e) {
- console.log(line);
- console.log(` ${message} `);
- console.log(line);
- }
- }
- }
-}
-
-module.exports = Logger; |
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| File | -- | Statements | -- | Branches | -- | Functions | -- | Lines | -- |
|---|---|---|---|---|---|---|---|---|---|
| BaseLogicDriver.js | -
-
- |
- 0% | -0/178 | -0% | -0/107 | -0% | -0/19 | -0% | -0/174 | -
| BaseLogicUnit.js | -
-
- |
- 0% | -0/459 | -0% | -0/283 | -0% | -0/67 | -0% | -0/438 | -
| FormulaEvaluator.js | -
-
- |
- 97.5% | -156/160 | -96.03% | -97/101 | -100% | -10/10 | -97.5% | -156/160 | -
| Logger.js | -
-
- |
- 0% | -0/131 | -0% | -0/88 | -0% | -0/28 | -0% | -0/131 | -
| loggerConfig.js | -
-
- |
- 0% | -0/1 | -100% | -0/0 | -100% | -0/0 | -0% | -0/1 | -
- Press n or j to go to the next uncovered block, b, p or k for the previous block. -
- -| 1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 | - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - | 'use strict';
-
-/**
- * Global Logger Configuration for Boolean Toolbox
- *
- * This is the central configuration for all logging in the app.
- * Place this file in: lib/loggerConfig.js
- *
- * USAGE:
- * - Adjust `defaultLevel` to change the standard log level for all modules.
- * - Add specific class names to `categoryLevels` for finer control over individual modules.
- * - The Logger will automatically pick up this file if it exists.
- */
-module.exports = {
- // Default log level for any logger not specified below.
- // Options: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
- defaultLevel: 'debug',
-
- // Override log levels for specific categories (class names).
- // This helps in reducing noise or enabling detailed logs for specific parts of the app.
- categoryLevels: {
- // Example: Show detailed logging for a specific driver during debugging.
- // 'MyDeviceName': 'DEBUG',
-
- // Reduce noise from base classes which are generally stable.
- // 'BaseLogicDriver': 'INFO',
- // 'BaseLogicUnit': 'INFO',
-
- // Always show important app-level messages.
- // 'App': 'INFO',
- },
-
- // Global options (rarely needed).
- options: {
- timestamps: false, // Homey already adds timestamps to its logs.
- colors: false, // Not supported in the Homey development console.
- },
-};
- |