UNPKG

homebridge-pentair-intellicenter-ai

Version:

Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)

526 lines 20.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.mergeResponse = exports.mergeResponseArray = exports.isObject = exports.getIntelliBriteColor = exports.celsiusToFahrenheit = exports.fahrenheitToCelsius = exports.transformPanels = exports.findBodyCircuit = exports.updatePump = exports.updateBody = exports.updateCircuit = void 0; const types_1 = require("./types"); function isIntelliCenterObject(obj) { return (typeof obj === 'object' && obj !== null && typeof obj[constants_1.OBJ_ID_KEY] === 'string' && typeof obj[constants_1.PARAMS_KEY] === 'object'); } /** * Safe property access helpers to prevent unsafe dynamic property access */ const safeGetStringProperty = (obj, key, defaultValue = '') => { const value = obj[key]; return typeof value === 'string' ? value : defaultValue; }; const safeGetStringPropertyOptional = (obj, key) => { const value = obj[key]; return typeof value === 'string' && value !== '' ? value : undefined; }; const safeGetNumberProperty = (obj, key, defaultValue = 0) => { const value = obj[key]; if (typeof value === 'number') { return value; } if (typeof value === 'string') { const parsed = Number(value); return isNaN(parsed) ? defaultValue : parsed; } return defaultValue; }; const safeGetObjectProperty = (obj, key) => { const value = obj[key]; return value && typeof value === 'object' && !Array.isArray(value) ? value : null; }; const safeGetArrayProperty = (obj, key) => { const value = obj[key]; return Array.isArray(value) ? value : []; }; const safeGetParams = (obj) => { return safeGetObjectProperty(obj, constants_1.PARAMS_KEY) || {}; }; const constants_1 = require("./constants"); const transformHeaters = (heaters) => { if (!heaters) { return []; } return heaters .filter(obj => { if (!isIntelliCenterObject(obj)) { return false; } const params = safeGetParams(obj); return safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Heater; }) .map(heaterObj => { const obj = heaterObj; const params = safeGetParams(obj); const bodyIdsString = safeGetStringProperty(params, types_1.ObjectType.Body); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); const coolValue = safeGetStringPropertyOptional(params, constants_1.COOL_KEY); return { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), objectType: types_1.ObjectType.Heater, type: subtype ? subtype.toUpperCase() : undefined, bodyIds: bodyIdsString ? bodyIdsString.split(' ') : [], coolingEnabled: coolValue !== undefined && coolValue !== 'OFF', }; }); }; const circuitParams = new Map([['status', 'STATUS']]); const bodyParams = new Map([ ['temperature', constants_1.LAST_TEMP_KEY], ['highTemperature', 'HITMP'], ['lowTemperature', 'LOTMP'], ['heaterId', 'HTSRC'], ['heatMode', constants_1.HTMODE_KEY], ]); const pumpParams = new Map([ ['speedType', constants_1.SELECT_KEY], ['speed', constants_1.SPEED_KEY], ['rpm', constants_1.RPM_KEY], ['gpm', constants_1.GPM_KEY], ['watts', constants_1.WATTS_KEY], ]); const updateCircuit = (circuit, params) => { circuitParams.forEach((value, key) => { if (params[value]) { circuit[key] = params[value]; } }); }; exports.updateCircuit = updateCircuit; const updateBody = (body, params) => { bodyParams.forEach((value, key) => { if (params[value]) { // Convert numeric parameters to actual numbers if (key === 'heatMode') { const paramValue = params[value]; if (typeof paramValue === 'string') { const numValue = Number(paramValue); body[key] = isNaN(numValue) ? undefined : numValue; } else if (typeof paramValue === 'number') { body[key] = paramValue; } else { body[key] = undefined; } } else { body[key] = params[value]; } } }); }; exports.updateBody = updateBody; const updatePump = (pump, params) => { pumpParams.forEach((value, key) => { if (params[value]) { pump[key] = params[value]; } }); }; exports.updatePump = updatePump; const transformBodies = (circuits) => { if (!circuits) { return []; } return circuits .filter(obj => { if (!isIntelliCenterObject(obj)) { return false; } const params = safeGetParams(obj); return safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Body; }) .map(bodyObj => { const obj = bodyObj; const params = safeGetParams(obj); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); const body = { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), objectType: types_1.ObjectType.Body, type: subtype ? subtype.toUpperCase() : undefined, }; (0, exports.updateBody)(body, params); return { ...body, circuit: (0, exports.findBodyCircuit)(body, circuits), }; }); }; const findBodyCircuit = (body, circuits) => { for (const circuit of circuits) { if (!isIntelliCenterObject(circuit)) { continue; } const params = safeGetParams(circuit); const circuitType = safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY); const circuitSubType = safeGetStringProperty(params, constants_1.OBJ_SUBTYPE_KEY); const circuitName = safeGetStringProperty(params, constants_1.OBJ_NAME_KEY); if (circuitType === types_1.ObjectType.Circuit && body.type === circuitSubType && body.name === circuitName) { return { id: safeGetStringProperty(circuit, constants_1.OBJ_ID_KEY), name: circuitName, objectType: types_1.ObjectType.Circuit, }; } } return undefined; }; exports.findBodyCircuit = findBodyCircuit; const isValidFeatureObject = (featureObj) => { if (!isIntelliCenterObject(featureObj)) { return false; } const params = safeGetParams(featureObj); return !!params; }; const getFeatureProperties = (featureObj) => { const params = safeGetParams(featureObj); return { subtype: safeGetStringProperty(params, constants_1.OBJ_SUBTYPE_KEY).toUpperCase(), objId: safeGetStringProperty(featureObj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), isCircuit: safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Circuit, featrValue: safeGetStringProperty(params, 'FEATR'), }; }; const shouldIncludeFeature = (props, includeAllCircuits) => { const { subtype, isCircuit, featrValue, objId } = props; if (!isCircuit || subtype === 'LEGACY') { return false; } return includeAllCircuits || isFeatureEnabled(featrValue, includeAllCircuits) || isIntelliBriteCircuit(subtype) || isCircuitGroup(objId); }; const isCircuitGroup = (objId) => { return objId.startsWith('GRP'); }; const isFeatureEnabled = (featrValue, includeAllCircuits) => { return featrValue === 'ON' || (includeAllCircuits && featrValue === 'FEATR'); }; const isIntelliBriteCircuit = (subtype) => { return subtype === types_1.CircuitType.IntelliBrite; }; const logFilteredFeature = (props, includeAllCircuits, shouldInclude, logger) => { if (!logger || !props.isCircuit || props.subtype === 'LEGACY') { return; } const { subtype, objId, name, featrValue } = props; if (!shouldInclude) { logFilteredOutCircuit(objId, name, subtype, featrValue, logger); } else if (shouldLogIncludedCircuit(includeAllCircuits, featrValue, subtype)) { logIncludedNonFeatureCircuit(objId, name, logger); } }; const logFilteredOutCircuit = (objId, name, subtype, featrValue, logger) => { logger.debug(`Circuit filtered out - ID: ${objId}, Name: ${name}, ` + `SubType: ${subtype}, Feature: ${featrValue}, Will send updates but not be accessible in HomeKit`); }; const logIncludedNonFeatureCircuit = (objId, name, logger) => { logger.info(`Non-feature circuit included due to includeAllCircuits - ID: ${objId}, Name: ${name}`); }; const shouldLogIncludedCircuit = (includeAllCircuits, featrValue, subtype) => { const isFeature = featrValue === 'ON' || featrValue === 'FEATR'; const isIntelliBrite = subtype === types_1.CircuitType.IntelliBrite; return includeAllCircuits && !isFeature && !isIntelliBrite; }; const transformFeatures = (circuits, includeAllCircuits = false, logger) => { if (!circuits) { return []; } return circuits .filter(featureObj => processFeatureFilter(featureObj, includeAllCircuits, logger)) .map(featureObj => transformFeatureToCircuit(featureObj)); }; const processFeatureFilter = (featureObj, includeAllCircuits, logger) => { if (!isValidFeatureObject(featureObj)) { return false; } const props = getFeatureProperties(featureObj); const shouldInclude = shouldIncludeFeature(props, includeAllCircuits); logFilteredFeature(props, includeAllCircuits, shouldInclude, logger); return shouldInclude; }; const transformFeatureToCircuit = (obj) => { const params = safeGetParams(obj); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); const circuit = { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), objectType: types_1.ObjectType.Circuit, type: subtype ? subtype.toUpperCase() : undefined, }; (0, exports.updateCircuit)(circuit, params); return circuit; }; const transformPumps = (pumps, logger) => { if (!pumps) { return []; } return pumps.filter(pumpObj => isValidPumpObject(pumpObj, logger)).map(pumpObj => transformPumpObject(pumpObj)); }; const isValidPumpObject = (pumpObj, logger) => { if (!isIntelliCenterObject(pumpObj)) { return false; } const params = safeGetParams(pumpObj); if (!params) { return false; } const pumpValidation = validatePumpObjectType(pumpObj, params); if (pumpValidation.isPump && logger) { logPumpDiscovery(pumpObj, pumpValidation, logger); } return pumpValidation.isPump && pumpValidation.isVariableSpeed; }; const validatePumpObjectType = (pumpObj, params) => { const objType = safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY); const subType = safeGetStringProperty(params, constants_1.OBJ_SUBTYPE_KEY).toUpperCase(); const isPump = objType === types_1.ObjectType.Pump; const isVariableSpeed = constants_1.VARIABLE_SPEED_PUMP_SUBTYPES.has(subType); return { isPump, isVariableSpeed, objType, subType }; }; const logPumpDiscovery = (pumpObj, validation, logger) => { const objId = safeGetStringProperty(pumpObj, constants_1.OBJ_ID_KEY); const params = safeGetParams(pumpObj); const pumpName = safeGetStringProperty(params, constants_1.OBJ_NAME_KEY); if (validation.isVariableSpeed) { logger.debug(`Variable speed pump discovered - ID: ${objId}, SubType: ${validation.subType}, Name: ${pumpName}`); } else { logger.debug(`Pump filtered out - ID: ${objId}, SubType: ${validation.subType}, Name: ${pumpName} ` + `(not in VARIABLE_SPEED_PUMP_SUBTYPES: ${Array.from(constants_1.VARIABLE_SPEED_PUMP_SUBTYPES).join(', ')})`); } }; const transformPumpObject = (obj) => { const params = safeGetParams(obj); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); const pump = createPumpFromParams(obj, params, subtype); const objList = safeGetArrayProperty(params, constants_1.OBJ_LIST_KEY); const circuits = objList.length > 0 ? transformPumpCircuits(pump, objList) : []; return { ...pump, circuits: circuits, }; }; const createPumpFromParams = (obj, params, subtype) => { return { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), objectType: types_1.ObjectType.Pump, type: subtype ? subtype.toUpperCase() : undefined, minRpm: safeGetNumberProperty(params, constants_1.OBJ_MIN_KEY), maxRpm: safeGetNumberProperty(params, constants_1.OBJ_MAX_KEY), minFlow: safeGetNumberProperty(params, constants_1.OBJ_MIN_FLOW_KEY), maxFlow: safeGetNumberProperty(params, constants_1.OBJ_MAX_FLOW_KEY), }; }; const transformTempSensors = (sensors) => { if (!sensors) { return []; } return sensors .filter(sensorObj => { if (!isIntelliCenterObject(sensorObj)) { return false; } const params = safeGetParams(sensorObj); return (params && safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Sensor && Object.values(types_1.TemperatureSensorType).includes(safeGetStringProperty(params, constants_1.OBJ_SUBTYPE_KEY))); }) .map(sensorObj => { const obj = sensorObj; const params = safeGetParams(obj); const probeValue = safeGetNumberProperty(params, 'PROBE'); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); return { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), name: safeGetStringProperty(params, constants_1.OBJ_NAME_KEY), objectType: types_1.ObjectType.Sensor, type: subtype ? subtype.toUpperCase() : undefined, probe: probeValue, }; }); }; const transformPumpCircuits = (pump, pumpObjList) => { if (!pumpObjList) { return []; } return pumpObjList.filter(isIntelliCenterObject).map(pumpObj => { const params = safeGetParams(pumpObj); const speedType = safeGetStringPropertyOptional(params, constants_1.SELECT_KEY); return { id: safeGetStringProperty(pumpObj, constants_1.OBJ_ID_KEY), pump: pump, circuitId: safeGetStringProperty(params, constants_1.CIRCUIT_KEY), speed: safeGetNumberProperty(params, constants_1.SPEED_KEY), speedType: speedType ? speedType.toUpperCase() : undefined, }; }); }; const transformModules = (modules, includeAllCircuits = false, logger) => { if (!modules) { return []; } return modules .filter(obj => { if (!isIntelliCenterObject(obj)) { return false; } const params = safeGetParams(obj); return safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Module; }) .map(moduleObj => { const obj = moduleObj; const params = safeGetParams(obj); const circuits = safeGetArrayProperty(params, constants_1.CIRCUITS_KEY); const subtype = safeGetStringPropertyOptional(params, constants_1.OBJ_SUBTYPE_KEY); return { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), features: transformFeatures(circuits, includeAllCircuits, logger), bodies: transformBodies(circuits), heaters: transformHeaters(circuits), type: subtype ? subtype.toUpperCase() : undefined, }; }); }; const transformPanels = (response, includeAllCircuits = false, logger) => { if (!response) { return []; } // Handle response format - could be array or object with array property const responseArray = Array.isArray(response) ? response : safeGetArrayProperty(response, 'panels'); if (!responseArray || !Array.isArray(responseArray)) { return []; } return responseArray .filter(obj => { if (!isIntelliCenterObject(obj)) { return false; } const params = safeGetParams(obj); return safeGetStringProperty(params, constants_1.OBJ_TYPE_KEY) === types_1.ObjectType.Panel; }) .map(panelObj => { const obj = panelObj; const params = safeGetParams(obj); const objList = safeGetArrayProperty(params, constants_1.OBJ_LIST_KEY); return { id: safeGetStringProperty(obj, constants_1.OBJ_ID_KEY), modules: transformModules(objList, includeAllCircuits, logger), features: transformFeatures(objList, includeAllCircuits, logger), // Some features are directly on panel. pumps: transformPumps(objList, logger), sensors: transformTempSensors(objList), }; }); }; exports.transformPanels = transformPanels; const fahrenheitToCelsius = (fValue) => { return (fValue - 32) / 1.8; }; exports.fahrenheitToCelsius = fahrenheitToCelsius; const celsiusToFahrenheit = (cValue) => { return cValue * 1.8 + 32; }; exports.celsiusToFahrenheit = celsiusToFahrenheit; const getIntelliBriteColor = (hue, saturation) => { let color = types_1.Color.White; // All other IntelliBrite colors have a saturation of 100. if (saturation > (types_1.Color.Red.saturation - types_1.Color.White.saturation) / 2) { if (hue < (types_1.Color.Green.hue - types_1.Color.Red.hue) / 2 + types_1.Color.Red.hue) { color = types_1.Color.Red; } else if (hue < (types_1.Color.Blue.hue - types_1.Color.Green.hue) / 2 + types_1.Color.Green.hue) { color = types_1.Color.Green; } else if (hue < (types_1.Color.Magenta.hue - types_1.Color.Blue.hue) / 2 + types_1.Color.Blue.hue) { color = types_1.Color.Blue; } else { color = types_1.Color.Magenta; } } return color; }; exports.getIntelliBriteColor = getIntelliBriteColor; const isObject = (object) => { if (typeof object === 'object') { for (const key in object) { if (Object.prototype.hasOwnProperty.call(object, key)) { return true; } } } return false; }; exports.isObject = isObject; const mergeResponseArray = (target, responseToAdd) => { responseToAdd.forEach(itemToAdd => { const targetObject = target.find(targetItem => targetItem[constants_1.OBJ_ID_KEY] === itemToAdd[constants_1.OBJ_ID_KEY]); if (targetObject) { (0, exports.mergeResponse)(targetObject, itemToAdd); } else { target.push(itemToAdd); } }); }; exports.mergeResponseArray = mergeResponseArray; const mergeResponse = (target, responseToAdd) => { for (const key in responseToAdd) { if (isSafePropertyKey(responseToAdd, key)) { mergePropertyValue(target, responseToAdd, key); } } }; exports.mergeResponse = mergeResponse; const isSafePropertyKey = (obj, key) => { return Object.prototype.hasOwnProperty.call(obj, key) && key !== '__proto__' && key !== 'constructor' && key !== 'prototype'; }; const mergePropertyValue = (target, source, key) => { const targetValue = target[key]; const sourceValue = source[key]; if (shouldMergeNestedObject(targetValue, sourceValue)) { mergeNestedValue(targetValue, sourceValue); } else { target[key] = sourceValue; } }; const shouldMergeNestedObject = (targetValue, sourceValue) => { return !!targetValue && (0, exports.isObject)(targetValue) && (0, exports.isObject)(sourceValue); }; const mergeNestedValue = (targetValue, sourceValue) => { if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { (0, exports.mergeResponseArray)(targetValue, sourceValue); } else { (0, exports.mergeResponse)(targetValue, sourceValue); } }; // Test-only exports for achieving 100% coverage /* istanbul ignore else */ // eslint-disable-next-line no-undef if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { /* eslint-disable no-undef */ module.exports = { ...module.exports, transformHeaters, transformBodies, transformFeatures, transformPumps, transformTempSensors, transformPumpCircuits, transformModules, }; /* eslint-enable no-undef */ } //# sourceMappingURL=util.js.map