homebridge-myplace
Version:
Exec Plugin bringing Advanatge Air MyPlace system to Homekit
644 lines (566 loc) • 17.8 kB
JavaScript
// This script is to generate a complete configuration file needed for the myPlace plugin
// This script can handle up to 3 independent myPlace (BB) systems
//
// This script is invoked from the plugin when homebridge restart
// Read CLI args
// Input arguments (typically from process.argv)
let AAIP1 = process.argv[2];
let AAname1 = process.argv[3];
let extraTimers1 = process.argv[4];
let AAdebug1 = process.argv[5];
let AAIP2 = process.argv[6];
let AAname2 = process.argv[7];
let extraTimers2 = process.argv[8];
let AAdebug2 = process.argv[9];
let AAIP3 = process.argv[10];
let AAname3 = process.argv[11];
let extraTimers3 = process.argv[12];
let AAdebug3 = process.argv[13];
let MYPLACE_SH_PATH = process.argv[14];
// Define variables
let myPlaceBasicKeys = {};
let myPlaceModelQueue = {};
let myPlaceConstants = { constants: [] };
let myPlaceQueueTypes = { queueTypes: [] };
let myPlaceAccessories = { accessories: [] };
// Declare an ID array
let id = [];
function createBasicKeys(debugMyPlace) {
myPlaceBasicKeys = {
title: "MyPlace configuration auto-generated by ConfigCreator",
debug: debugMyPlace,
outputConstants: false,
statusMsg: true,
timeout: 60000,
stateChangeResponseTime: 0
};
}
function createModelQueue(sysType, tspModel, queue) {
myPlaceModelQueue = {
manufacturer: "Advantage Air Australia",
model: sysType,
serialNumber: tspModel,
queue: queue
};
}
function createConstants(ip, IPA, debug) {
const debugA = debug === "true" ? "-debug" : "";
const constant = {
key: ip,
value: `${IPA}${debugA}`
};
myPlaceConstants.constants.push(constant);
}
function createQueueTypes(queue) {
const queueType= {
queue: queue,
queueType: "WoRm2"
};
myPlaceQueueTypes.queueTypes.push(queueType);
}
function createThermostat(name, zone, ac, ip) {
let ac_l = ` ${ac}`;
if (zone !== "") zone = `${zone} `;
if (ac_l === " ac1") ac_l = "";
const myPlaceThermostat = {
type: "Thermostat",
displayName: name,
currentHeatingCoolingState: "OFF",
targetHeatingCoolingState: "OFF",
currentTemperature: 24,
targetTemperature: 24,
temperatureDisplayUnits: "CELSIUS",
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "currentHeatingCoolingState" },
{ characteristic: "targetHeatingCoolingState" },
{ characteristic: "currentTemperature" },
{ characteristic: "targetTemperature" }
],
props: {
targetTemperature: {
maxValue: 32,
minValue: 16,
minStep: 1
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${zone}${ip}${ac_l}`
};
// Attach the fan speed type to this switch as a linkedType
createLinkedTypesFanSpeed(`${name} FanSpeed`, myPlaceThermostat, ac, ip);
}
function createLinkedTypesFanSpeed(name, motherAcc, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Create the linked fan accessory
const myPlaceLinkedTypesFanSpeed = {
type: "Fan",
displayName: name,
on: true,
rotationSpeed: 25,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" },
{ characteristic: "rotationSpeed" }
],
props: {
rotationSpeed: {
minStep: 1
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `fanSpeed ${ip}${ac_l}`
};
// Attach the fan accessory to the Thermostat's `linkedTypes`
motherAcc.linkedTypes = [myPlaceLinkedTypesFanSpeed];
myPlaceAccessories.accessories.push(motherAcc);
}
function createFanSwitch(name, AAname, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Build the fan switch object
const myPlaceFanSwitch = {
type: "Switch",
displayName: name,
on: false,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${ip}${ac_l}`
};
// Attach the fan speed type to this switch as a linkedType
createLinkedTypesFanSpeed(`${AAname} FanSpeed`, myPlaceFanSwitch, ac, ip);
}
function createTimerLightbulb(name, suffix, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Construct the lightbulb accessory
const myPlaceTimerLightbulb = {
type: "Lightbulb",
displayName: name,
on: false,
brightness: 0,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" },
{ characteristic: "brightness" }
],
props: {
brightness: {
minStep: 1
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${suffix} ${ip}${ac_l}`
};
myPlaceAccessories.accessories.push(myPlaceTimerLightbulb);
}
function createZoneFan(name, zoneStr, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Build the zone fan object
const myPlaceZoneFan = {
type: "Fan",
displayName: name,
on: false,
rotationSpeed: 100,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" },
{ characteristic: "rotationSpeed" }
],
props: {
rotationSpeed: {
minStep: 5
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${zoneStr} ${ip}${ac_l}`
};
myPlaceAccessories.accessories.push(myPlaceZoneFan);
}
function createZoneFanv2(name, thisZoneName, zoneStr, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Create the base Fanv2 accessory
const myPlaceZoneFanv2 = {
type: "Fanv2",
displayName: name,
active: 0,
rotationSpeed: 100,
rotationDirection: 1,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "active" },
{ characteristic: "rotationSpeed" },
{ characteristic: "rotationDirection" }
],
props: {
rotationSpeed: {
minStep: 5
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${zoneStr} ${ip}${ac_l}`,
linkedTypes: []
};
// Call the external function to generate the linked thermostat accessory
const myPlaceZoneThermostatLinkedTypes = createZoneThermostat(`${thisZoneName} Thermostat`, zoneStr, ac, ip);
// Append linked thermostat
myPlaceZoneFanv2.linkedTypes.push(myPlaceZoneThermostatLinkedTypes);
myPlaceAccessories.accessories.push(myPlaceZoneFanv2);
}
function createZoneThermostat(name, zone, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
if (zone !== "") zone = `${zone} `;
// Construct the thermostat object
return {
type: "Thermostat",
displayName: name,
currentHeatingCoolingState: "OFF",
targetHeatingCoolingState: "OFF",
currentTemperature: 24,
targetTemperature: 24,
temperatureDisplayUnits: "CELSIUS",
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "targetHeatingCoolingState" },
{ characteristic: "currentTemperature" },
{ characteristic: "targetTemperature" }
],
props: {
targetTemperature: {
maxValue: 32,
minValue: 16,
minStep: 1
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${zone}${ip}${ac_l}`
};
}
function createZoneFanv2noRotationDirection(name, thisZoneName, zoneStr, ac, ip) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Build Fanv2 object without rotationDirection
const myPlaceZoneFanv2noRotationDirection = {
type: "Fanv2",
displayName: name,
active: 0,
rotationSpeed: 100,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "active" },
{ characteristic: "rotationSpeed" }
],
props: {
rotationSpeed: {
minStep: 5
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `${zoneStr} ${ip}${ac_l}`,
linkedTypes: []
};
// Generate linked thermostat accessory
const myPlaceZoneThermostatLinkedTypes = createZoneThermostat(`${thisZoneName} Thermostat`, zoneStr, ac, ip);
// Add linked thermostat
myPlaceZoneFanv2noRotationDirection.linkedTypes.push(myPlaceZoneThermostatLinkedTypes);
myPlaceAccessories.accessories.push(myPlaceZoneFanv2noRotationDirection);
}
function createLightbulbNoDimmer(name, id, ip) {
// Remove quotes from id, like Bash's ${id//\"/}
id = id.replace(/"/g, "");
const myPlaceLightbulbNoDimmer = {
type: "Lightbulb",
displayName: name,
on: false,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `ligID:${id} ${ip}`
};
myPlaceAccessories.accessories.push(myPlaceLightbulbNoDimmer);
}
function createLightbulbWithDimmer(name, id, ip) {
// Remove quotes from id (like Bash's ${id//\"/})
id = id.replace(/"/g, "");
const myPlaceLightbulbWithDimmer = {
type: "Lightbulb",
displayName: name,
on: false,
brightness: 80,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" },
{ characteristic: "brightness" }
],
props: {
brightness: {
minStep: 1
}
},
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `ligID:${id} ${ip}`
};
myPlaceAccessories.accessories.push(myPlaceLightbulbWithDimmer);
}
function createFanNoRotationSpeed(name, id, ip) {
// Remove quotes from id
id = id.replace(/"/g, "");
const myPlaceFanNoRotationSpeed = {
type: "Fan",
displayName: name,
on: false,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `ligID:${id} ${ip}`
};
myPlaceAccessories.accessories.push(myPlaceFanNoRotationSpeed);
}
function createGarageDoorOpener(name, id, ip) {
// Remove quotes from id
id = id.replace(/"/g, "");
const myPlaceGarageDoorOpener = {
type: "GarageDoorOpener",
displayName: name,
obstructionDetected: false,
currentDoorState: 1,
targetDoorState: 1,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "currentDoorState" },
{ characteristic: "targetDoorState" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `thiID:${id} ${ip}`
};
myPlaceAccessories.accessories.push(myPlaceGarageDoorOpener);
}
function createWindowCovering(name, id, ip) {
// Remove quotes from id
id = id.replace(/"/g, "");
const myPlaceWindowCovering = {
type: "WindowCovering",
displayName: name,
obstructionDetected: false,
currentPosition: 0,
positionState: 2,
targetPosition: 0,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "currentPosition" },
{ characteristic: "targetPosition" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `thiID:${id} ${ip}`
};
myPlaceAccessories.accessories.push(myPlaceWindowCovering);
}
function assembleMyPlaceConfig() {
return {
name: "MyPlace",
...myPlaceBasicKeys,
...myPlaceConstants,
...myPlaceQueueTypes,
...myPlaceAccessories,
platform: "MyPlace"
};
}
async function main({
AAIP1, AAname1, extraTimers1, AAdebug1,
AAIP2, AAname2, extraTimers2, AAdebug2,
AAIP3, AAname3, extraTimers3, AAdebug3,
MYPLACE_SH_PATH
}) {
// Helper regex to validate IP and IP:port
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
const ipPortRegex = /^(\d{1,3}\.){3}\d{1,3}:\d+$/;
// Validate and normalize IPs
function normalizeIP(ip) {
if (ip === undefined || ip === null || ip === "undefined" || ip === "") return "";
if (ipRegex.test(ip)) return `${ip}:2025`;
if (ipPortRegex.test(ip)) return ip;
throw new Error(`ERROR: the specified IP address ${ip} is in wrong format`);
}
try {
AAIP1 = normalizeIP(AAIP1);
AAIP2 = normalizeIP(AAIP2);
AAIP3 = normalizeIP(AAIP3);
} catch (err) {
console.error(err.message);
process.exit(1);
}
// Determine number of tablets/devices
let noOfTablets = 0;
if (AAIP1) noOfTablets = 1;
if (AAIP2) noOfTablets = 2;
if (AAIP3) noOfTablets = 3;
// Initialize global config objects (assumed to be declared globally or passed)
// createBasicKeys, myPlaceConstants, myPlaceQueueTypes, myPlaceModelQueue, myPlaceAccessories, myPlaceConfig
for (let n = 1; n <= noOfTablets; n++) {
let ip, IPA, AAname, extraTimers, debug, queue;
if (n === 1) {
ip = "\${AAIP1}";
IPA = AAIP1;
AAname = AAname1;
extraTimers = extraTimers1;
debug = AAdebug1;
queue = "AAA";
} else if (n === 2) {
ip = "\${AAIP2}";
IPA = AAIP2;
AAname = AAname2;
extraTimers = extraTimers2;
debug = AAdebug2;
queue = "AAB";
} else if (n === 3) {
ip = "\${AAIP3}";
IPA = AAIP3;
AAname = AAname3;
extraTimers = extraTimers3;
debug = AAdebug3;
queue = "AAC";
}
// Fetch system data
let myAirData;
try {
const response = await fetch(`http://${IPA}/getSystemData`, {timeout: 45000});
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
myAirData = await response.json();
} catch {
console.error(`ERROR: AdvantageAir system is inaccessible or your IP address ${IPA} is invalid!`);
process.exit(1);
}
// Extract system info (use safe chaining or checks as needed)
const sysName = (myAirData.system?.name ?? "").replace(/ /g, "_").replace(/['"]/g, "");
if (!sysName) {
console.error("ERROR: failed to get system name from response!");
process.exit(1);
}
const sysType = (myAirData.system?.sysType ?? "").replace(/ /g, "_").replace(/"/g, "");
const tspModel = (myAirData.system?.tspModel ?? "").replace(/ /g, "_").replace(/"/g, "");
const hasAircons = myAirData.system?.hasAircons ?? false;
const noOfAircons = myAirData.system?.noOfAircons ?? 0;
const hasLights = myAirData.system?.hasLights ?? false;
const hasThings = myAirData.system?.hasThings ?? false;
// Set debugMyPlace flag based on any debug true
const debugMyPlace = (AAdebug1 === "true" || AAdebug2 === "true" || AAdebug3 === "true");
// Create basic keys, constants and queueTypes
createBasicKeys(debugMyPlace);
createModelQueue(sysType, tspModel, queue);
createConstants(ip, IPA, debug);
createQueueTypes(queue);
// Aircon processing
if (hasAircons) {
for (let a = 1; a <= noOfAircons; a++) {
const ac = `ac${a}`;
const aircon = myAirData.aircons?.[ac]?.info;
if (!aircon) continue;
// Check zones for zoneSetTemp
let zoneSetTemp = false;
const nZones = aircon.noOfZones ?? 0;
for (let b = 1; b <= nZones; b++) {
const zoneStr = `z${String(b).padStart(2, "0")}`;
const rssi = myAirData.aircons?.[ac]?.zones?.[zoneStr]?.rssi ?? 0;
if (rssi !== 0) {
zoneSetTemp = true;
break;
}
}
if (a >= 2) {
thisAAname = `${AAname}${a}`;
} else {
thisAAname = AAname;
}
const AAzone = zoneSetTemp ? "" : "noOtherThermostat";
// Create aircon configs
createThermostat(thisAAname, AAzone, ac, ip);
createFanSwitch(`${thisAAname} Fan`, AAname, ac, ip);
createTimerLightbulb(`${thisAAname} Timer`, "timer", ac, ip);
if (extraTimers === "true") {
createTimerLightbulb(`${thisAAname} Fan Timer`, "fanTimer", ac, ip);
createTimerLightbulb(`${thisAAname} Cool Timer`, "coolTimer", ac, ip);
createTimerLightbulb(`${thisAAname} Heat Timer`, "heatTimer", ac, ip);
}
// Create zone configs
const myZoneValue = aircon.myZone ?? 0;
for (let b = 1; b <= nZones; b++) {
const zoneStr = `z${String(b).padStart(2, "0")}`;
const thisZoneName = myAirData.aircons?.[ac]?.zones?.[zoneStr]?.name ?? "";
const rssi = myAirData.aircons?.[ac]?.zones?.[zoneStr]?.rssi ?? 0;
if (rssi === 0) {
createZoneFan(`${thisZoneName} Zone`, zoneStr, ac, ip);
} else if (myZoneValue !== 0) {
createZoneFanv2(`${thisZoneName} Zone`, thisZoneName, zoneStr, ac, ip);
} else {
createZoneFanv2noRotationDirection(`${thisZoneName} Zone`, thisZoneName, zoneStr, ac, ip);
}
}
}
}
// Lighting processing
if (hasLights) {
const idArray = Object.keys(myAirData.myLights?.lights ?? {});
for (const id of idArray) {
const light = myAirData.myLights.lights[id];
const name = light?.name ?? "";
const value = light?.value ?? null;
if (name.endsWith(" Fan")) {
createFanNoRotationSpeed(name, id, ip);
} else if (value === null) {
createLightbulbNoDimmer(name, id, ip);
} else {
createLightbulbWithDimmer(name, id, ip);
}
}
}
// Things processing (garage/blinds)
if (hasThings) {
const idArray = Object.keys(myAirData.myThings?.things ?? {});
for (const id of idArray) {
const thing = myAirData.myThings.things[id];
const name = thing?.name ?? "";
const channelDipState = thing?.channelDipState ?? "";
if (channelDipState == "3") {
createGarageDoorOpener(name, id, ip);
} else if (channelDipState == "1" || channelDipState == "2") {
createWindowCovering(name, id, ip);
}
}
}
}
// Assemble final config
const myPlaceConfig = assembleMyPlaceConfig();
console.log("DONE! createMyPlaceConfig completed successfully!");
console.log(JSON.stringify(myPlaceConfig));
process.exit(0);
}
main({
AAIP1, AAname1, extraTimers1, AAdebug1,
AAIP2, AAname2, extraTimers2, AAdebug2,
AAIP3, AAname3, extraTimers3, AAdebug3,
MYPLACE_SH_PATH
});