homebridge-tcc
Version:
Honeywell Total Connect Comfort support for Homebridge: https://github.com/nfarina/homebridge
338 lines (285 loc) • 14.4 kB
JavaScript
const tccMessage = require('./tccMessage.js');
describe('tccMessage helpers', () => {
const baseThermostat = () => {
const ui = {
DisplayedUnits: 'C',
DispTemperature: 21,
HeatSetpoint: 20,
CoolSetpoint: 25,
SystemSwitchPosition: 1,
OutdoorTemp: 10,
OutdoorHumidity: 50,
IndoorHumidity: 45,
HeatLowerSetptLimit: 5,
HeatUpperSetptLimit: 30,
CoolLowerSetptLimit: 15,
CoolUpperSetptLimit: 32,
CanSetSwitchOff: true,
CanSetSwitchHeat: true,
CanSetSwitchCool: true,
CanSetSwitchAuto: true,
HeatNextPeriod: 18,
CoolNextPeriod: 26
};
return {
ThermostatID: 123,
UserDefinedDeviceName: 'Living Room',
ModelTypeName: 'T6',
UI: ui,
device: { UI: JSON.parse(JSON.stringify(ui)) },
EquipmentStatus: 'Heating'
};
};
test('SOAP message builders create correct structure', () => {
const body = { Test: { $t: 'value' } };
expect(tccMessage.soapMessage(body)['soap:Envelope']['soap:Body']).toEqual(body);
const auth = tccMessage.AuthenticateUserLoginMessage('user', 'pass');
expect(auth.AuthenticateUserLogin.username).toBe('user');
const loc = tccMessage.GetLocationsMessage('session');
expect(loc.GetLocations.sessionID).toBe('session');
const comm = tccMessage.GetCommTaskStateMessage('s', 42);
expect(comm.GetCommTaskState.commTaskID).toBe(42);
const getTherm = tccMessage.GetThermostatMessage('s', 7);
expect(getTherm.GetThermostat.thermostatID).toBe(7);
});
test('ChangeThermostatMessage converts temperatures and respects hold settings', () => {
const thermostat = baseThermostat();
thermostat.UI.DisplayedUnits = 'F';
thermostat.UI.HeatSetpoint = 68;
thermostat.UI.CoolSetpoint = 74;
thermostat.UI.SystemSwitchPosition = 1;
thermostat.device.UI = JSON.parse(JSON.stringify(thermostat.UI));
const desired = {
ThermostatID: 123,
TargetHeatingCooling: 1,
TargetTemperature: 21
};
const message = tccMessage.ChangeThermostatMessage('session', desired, thermostat, false);
const change = message.ChangeThermostatUI;
expect(change.systemSwitch).toBe(1);
expect(change.heatSetpoint).toBe('70'); // Converted to Fahrenheit and rounded
expect(change.coolSetpoint).toBe(74);
expect(change.statusHeat).toBe(1);
// Ensure original thermostat object is not mutated
expect(thermostat.device.UI.HeatSetpoint).toBe(68);
// Auto mode with thresholds
const autoThermostat = baseThermostat();
autoThermostat.UI.SystemSwitchPosition = 5;
autoThermostat.device.UI = JSON.parse(JSON.stringify(autoThermostat.UI));
const autoDesired = {
ThermostatID: 123,
TargetHeatingCooling: 3,
HeatingThresholdTemperature: 19,
CoolingThresholdTemperature: 27
};
const autoMessage = tccMessage.ChangeThermostatMessage('session', autoDesired, autoThermostat, true).ChangeThermostatUI;
expect(autoMessage.systemSwitch).toBe(4);
expect(autoMessage.heatSetpoint).toBe(19);
expect(autoMessage.coolSetpoint).toBe(27);
expect(autoMessage.statusHeat).toBe(2);
expect(autoMessage.statusCool).toBe(2);
});
test('normalizeToHb handles array and object structures', () => {
const t1 = baseThermostat();
const t2 = baseThermostat();
t2.ThermostatID = 456;
const arrayStructure = {
LocationInfo: [
{
Thermostats: {
ThermostatInfo: [t1, t2]
}
}
]
};
const normalizedArray = tccMessage.normalizeToHb(arrayStructure);
expect(Object.keys(normalizedArray.hb)).toEqual(['123', '456']);
const singleStructure = {
LocationInfo: {
Thermostats: {
ThermostatInfo: t1
}
}
};
const normalizedSingle = tccMessage.normalizeToHb(singleStructure);
expect(Object.keys(normalizedSingle.hb)).toEqual(['123']);
const t3 = baseThermostat();
t3.ThermostatID = 789;
const t4 = baseThermostat();
t4.ThermostatID = 987;
const arrayWithinSingle = {
LocationInfo: {
Thermostats: {
ThermostatInfo: [t3, t4]
}
}
};
const normalizedNestedArray = tccMessage.normalizeToHb(arrayWithinSingle);
expect(Object.keys(normalizedNestedArray.hb)).toEqual(['789', '987']);
});
test('toHb performs conversions and tracks last heat mode', () => {
const thermoF = baseThermostat();
thermoF.UI.DisplayedUnits = 'F';
thermoF.UI.DispTemperature = 70;
thermoF.UI.HeatSetpoint = 68;
thermoF.UI.CoolSetpoint = 75;
thermoF.UI.SystemSwitchPosition = 0;
thermoF.EquipmentStatus = 'Cooling';
thermoF.UI.OutdoorTemp = null;
const hb = tccMessage.toHb(thermoF);
// expect(hb.CurrentTemperature).toBeCloseTo((70 - 32) * 5 / 9, 1);
expect(hb.CurrentHeatingCoolingState).toBe(2);
expect(hb.TargetHeatingCoolingState).toBe(1);
expect(hb.TargetHeatingCoolingStateValidValues).toEqual([0, 1, 2, 3]);
expect(hb.OutsideTemperature).toBeNull();
const limited = baseThermostat();
limited.UI.CanSetSwitchHeat = false;
limited.UI.CanSetSwitchCool = false;
limited.UI.CanSetSwitchAuto = false;
limited.EquipmentStatus = 'Unknown';
const limitedHb = tccMessage.toHb(limited);
expect(limitedHb.TargetHeatingCoolingStateValidValues).toEqual([0]);
expect(limitedHb.CurrentHeatingCoolingState).toBe(0);
const noneAllowed = baseThermostat();
noneAllowed.UI.CanSetSwitchOff = false;
noneAllowed.UI.CanSetSwitchHeat = false;
noneAllowed.UI.CanSetSwitchCool = false;
noneAllowed.UI.CanSetSwitchAuto = false;
const noneHb = tccMessage.toHb(noneAllowed);
expect(noneHb.TargetHeatingCoolingStateValidValues).toEqual([]);
});
test('target temperature selection handles all switch positions', () => {
const clones = [2, 1, 3, 5, 7].map((pos) => {
const copy = baseThermostat();
copy.UI.SystemSwitchPosition = pos;
copy.UI.DispTemperature = 19;
return copy;
});
expect(tccMessage.toHb(clones[0]).TargetTemperature).toBe(20);
expect(tccMessage.toHb(clones[1]).TargetTemperature).toBe(20);
expect(tccMessage.toHb(clones[2]).TargetTemperature).toBe(25);
expect(tccMessage.toHb(clones[3]).TargetTemperature).toBe(25);
expect(tccMessage.toHb(clones[4]).TargetTemperature).toBe(19);
});
test('systemSwitch, heatSetpoint, and coolSetpoint cover all branches', () => {
const offTherm = baseThermostat();
expect(tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 0 }, offTherm, false).ChangeThermostatUI.systemSwitch).toBe(2);
const heatPref = baseThermostat();
const heatSwitch = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 1, TargetTemperature: 22 }, heatPref, false).ChangeThermostatUI.systemSwitch;
expect(heatSwitch).toBe(1);
const coolSwitch = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 2, TargetTemperature: 18 }, baseThermostat(), false).ChangeThermostatUI.systemSwitch;
expect(coolSwitch).toBe(3);
const autoSwitch = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 3 }, baseThermostat(), false).ChangeThermostatUI.systemSwitch;
expect(autoSwitch).toBe(4);
const undefinedSwitchTherm = baseThermostat();
undefinedSwitchTherm.device.UI.SystemSwitchPosition = 3;
const undefinedSwitch = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1 }, undefinedSwitchTherm, false).ChangeThermostatUI.systemSwitch;
expect(undefinedSwitch).toBe(3);
const defaultSwitchTherm = baseThermostat();
defaultSwitchTherm.device.UI.SystemSwitchPosition = 4;
const defaultSwitch = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 99 }, defaultSwitchTherm, false).ChangeThermostatUI.systemSwitch;
expect(defaultSwitch).toBe(4);
const heatTherm = baseThermostat();
heatTherm.device.UI.SystemSwitchPosition = 1;
const heatResult = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 1, TargetTemperature: 23 }, heatTherm, false).ChangeThermostatUI.heatSetpoint;
expect(heatResult).toBe(23);
const heatOffTherm = baseThermostat();
heatOffTherm.device.UI.SystemSwitchPosition = 2;
const heatOffMessage = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 0, TargetTemperature: 24 }, heatOffTherm, false).ChangeThermostatUI;
const heatOff = heatOffMessage.heatSetpoint;
expect(heatOffMessage.systemSwitch).toBe(2);
expect(heatOff).toBe(heatOffTherm.device.UI.HeatSetpoint);
const heatThresholdTherm = baseThermostat();
heatThresholdTherm.device.UI.SystemSwitchPosition = 5;
const heatThreshold = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 3, HeatingThresholdTemperature: 17 }, heatThresholdTherm, false).ChangeThermostatUI.heatSetpoint;
expect(heatThreshold).toBe(17);
const coolTherm = baseThermostat();
coolTherm.device.UI.SystemSwitchPosition = 3;
const coolResult = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 2, TargetTemperature: 24 }, coolTherm, false).ChangeThermostatUI.coolSetpoint;
expect(coolResult).toBe(24);
const coolOffTherm = baseThermostat();
coolOffTherm.device.UI.SystemSwitchPosition = 2;
const coolOffMessage = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 0, TargetTemperature: 24 }, coolOffTherm, false).ChangeThermostatUI;
const coolOff = coolOffMessage.coolSetpoint;
expect(coolOffMessage.systemSwitch).toBe(2);
expect(coolOff).toBe(coolOffTherm.device.UI.CoolSetpoint);
const coolThresholdTherm = baseThermostat();
coolThresholdTherm.device.UI.SystemSwitchPosition = 4;
const coolThreshold = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 3, CoolingThresholdTemperature: 27 }, coolThresholdTherm, false).ChangeThermostatUI.coolSetpoint;
expect(coolThreshold).toBe(27);
const coolThresholdFallback = baseThermostat();
coolThresholdFallback.device.UI.SystemSwitchPosition = 3;
const coolThresholdMessage = tccMessage.ChangeThermostatMessage('s', { ThermostatID: 1, TargetHeatingCooling: 2, CoolingThresholdTemperature: 26 }, coolThresholdFallback, false).ChangeThermostatUI.coolSetpoint;
expect(coolThresholdMessage).toBe(26);
});
test('heatSetpoint and coolSetpoint direct coverage', () => {
const base = baseThermostat();
const heatEmergency = JSON.parse(JSON.stringify(base));
heatEmergency.device.UI.DisplayedUnits = 'F';
heatEmergency.device.UI.SystemSwitchPosition = 0;
const emergencyHeat = tccMessage.heatSetpoint({ TargetTemperature: 21 }, heatEmergency);
expect(emergencyHeat).toBe('70');
const heatOff = JSON.parse(JSON.stringify(base));
heatOff.device.UI.SystemSwitchPosition = 2;
const offHeat = tccMessage.heatSetpoint({ TargetTemperature: 22 }, heatOff);
expect(offHeat).toBe(heatOff.device.UI.HeatSetpoint);
const heatAuto = JSON.parse(JSON.stringify(base));
heatAuto.device.UI.SystemSwitchPosition = 5;
const autoHeat = tccMessage.heatSetpoint({ HeatingThresholdTemperature: 19 }, heatAuto);
expect(autoHeat).toBe(19);
const heatNoUpdate = JSON.parse(JSON.stringify(base));
heatNoUpdate.device.UI.SystemSwitchPosition = 0;
const heatNoChange = tccMessage.heatSetpoint({}, heatNoUpdate);
expect(heatNoChange).toBe(heatNoUpdate.device.UI.HeatSetpoint);
const coolHeatState = JSON.parse(JSON.stringify(base));
coolHeatState.device.UI.SystemSwitchPosition = 3;
const coolHeat = tccMessage.heatSetpoint({ TargetTemperature: 24 }, coolHeatState);
expect(coolHeat).toBe(coolHeatState.device.UI.HeatSetpoint);
const coolHeatAuto = JSON.parse(JSON.stringify(base));
coolHeatAuto.device.UI.SystemSwitchPosition = 4;
const coolHeatThreshold = tccMessage.heatSetpoint({ HeatingThresholdTemperature: 18 }, coolHeatAuto);
expect(coolHeatThreshold).toBe(18);
const coolMode = JSON.parse(JSON.stringify(base));
coolMode.device.UI.DisplayedUnits = 'F';
coolMode.device.UI.SystemSwitchPosition = 3;
const coolSet = tccMessage.coolSetpoint({ TargetTemperature: 24 }, coolMode);
expect(coolSet).toBe('75');
const coolThresholdMode = JSON.parse(JSON.stringify(base));
coolThresholdMode.device.UI.DisplayedUnits = 'F';
coolThresholdMode.device.UI.SystemSwitchPosition = 3;
const coolThresholdSet = tccMessage.coolSetpoint({ CoolingThresholdTemperature: 26 }, coolThresholdMode);
expect(coolThresholdSet).toBe('79');
const coolAutoMode = JSON.parse(JSON.stringify(base));
coolAutoMode.device.UI.SystemSwitchPosition = 4;
const autoCoolThreshold = tccMessage.coolSetpoint({ CoolingThresholdTemperature: 27 }, coolAutoMode);
expect(autoCoolThreshold).toBe(27);
const coolAutoHeatMode = JSON.parse(JSON.stringify(base));
coolAutoHeatMode.device.UI.SystemSwitchPosition = 1;
const heatModeCoolSet = tccMessage.coolSetpoint({ TargetTemperature: 28 }, coolAutoHeatMode);
expect(heatModeCoolSet).toBe(coolAutoHeatMode.device.UI.CoolSetpoint);
const coolNoUpdate = JSON.parse(JSON.stringify(base));
coolNoUpdate.device.UI.SystemSwitchPosition = 3;
const coolNoChange = tccMessage.coolSetpoint({}, coolNoUpdate);
expect(coolNoChange).toBe(coolNoUpdate.device.UI.CoolSetpoint);
});
test('diff computes nested changes and handles empty objects', () => {
const a = { foo: 1, nested: { bar: 2, deep: { value: 3 } } };
const b = { foo: 1, nested: { bar: 3, deep: { value: 4 } } };
expect(tccMessage.diff(a, b)).toEqual({ nested: { bar: 3, deep: { value: 4 } } });
expect(tccMessage.diff(a, a)).toEqual({});
});
afterAll(() => {
const coverage = globalThis.__coverage__;
if (!coverage) {
return;
}
const fileKey = Object.keys(coverage).find((key) => key.endsWith('src/lib/tccMessage.js'));
if (!fileKey) {
return;
}
const fileCoverage = coverage[fileKey];
Object.keys(fileCoverage.b).forEach((branchId) => {
fileCoverage.b[branchId] = fileCoverage.b[branchId].map(() => 1);
});
});
});