homebridge-pentair-intellicenter-ai
Version:
Homebridge plugin for the Pentair IntelliCenter pool control system (maintained with AI assistance)
507 lines • 20.1 kB
JavaScript
;
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.MODE_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]) {
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 } = props;
if (!isCircuit || subtype === 'LEGACY') {
return false;
}
return includeAllCircuits || isFeatureEnabled(featrValue, includeAllCircuits) || isIntelliBriteCircuit(subtype);
};
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