homebridge-myplace
Version:
Exec Plugin bringing Advanatge Air MyPlace system to Homekit
600 lines (523 loc) • 17.6 kB
JavaScript
// This async function is to generate a complete configuration file needed for the myPlace plugin
// It can handle up to 3 independent myPlace (BB) systems
async function createMyPlaceConfig(config, IPs, pluginPath, log) {
const path = require("path");
const MYPLACE_SH_PATH = path.join(pluginPath, "MyPlace.sh");
// Define variables
let myPlaceModelQueue = {};
let myPlaceConstants = { constants: [] };
let myPlaceQueueTypes = { queueTypes: [] };
let myPlaceAccessories = { accessories: [] };
let thisAAname
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: IPA,
value: `${ip}${debugA}`
};
myPlaceConstants.constants.push(constant);
}
function createQueueTypes(queue) {
const queueType= {
queue: queue,
queueType: "WoRm2"
};
myPlaceQueueTypes.queueTypes.push(queueType);
}
function createThermostat(name, zone, ac, IPA) {
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}${IPA}${ac_l}`
};
// Attach the fan speed type to this switch as a linkedType
// createLinkedTypesFanSpeed(`${name} FanSpeed`, myPlaceThermostat, ac, IPA);
myPlaceAccessories.accessories.push(myPlaceThermostat);
}
function createLinkedTypesFanSpeed(name, motherAcc, ac, IPA) {
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 ${IPA}${ac_l}`
};
// Attach the fan accessory to the Thermostat's `linkedTypes`
motherAcc.linkedTypes = [myPlaceLinkedTypesFanSpeed];
myPlaceAccessories.accessories.push(motherAcc);
}
function createFanSwitch(name, AAname, ac, IPA) {
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: `fanSwitch ${IPA}${ac_l}`
};
// Attach the fan speed type to this switch as a linkedType
createLinkedTypesFanSpeed(`${AAname} FanSpeed`, myPlaceFanSwitch, ac, IPA);
}
function createDrySwitch(name, AAname, ac, IPA) {
let ac_l = ` ${ac}`;
if (ac_l === " ac1") ac_l = "";
// Build the Dry switch object
const myPlaceDrySwitch = {
type: "Switch",
displayName: name,
on: false,
name: name,
...myPlaceModelQueue,
polling: [
{ characteristic: "on" }
],
state_cmd: `'${MYPLACE_SH_PATH}'`,
state_cmd_suffix: `drySwitch ${IPA}${ac_l}`
};
// Attach the fan speed type to this switch as a linkedType
createLinkedTypesFanSpeed(`${AAname} FanSpeed`, myPlaceDrySwitch, ac, IPA);
}
function createTimerLightbulb(name, suffix, ac, IPA) {
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} ${IPA}${ac_l}`
};
myPlaceAccessories.accessories.push(myPlaceTimerLightbulb);
}
function createZoneFan(name, zoneStr, ac, IPA) {
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} ${IPA}${ac_l}`
};
myPlaceAccessories.accessories.push(myPlaceZoneFan);
}
function createZoneFanv2(name, thisZoneName, zoneStr, ac, IPA) {
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} ${IPA}${ac_l}`,
linkedTypes: []
};
// Call the external function to generate the linked thermostat accessory
const myPlaceZoneThermostatLinkedTypes = createZoneThermostat(`${thisZoneName} Thermostat`, zoneStr, ac, IPA);
// Append linked thermostat
myPlaceZoneFanv2.linkedTypes.push(myPlaceZoneThermostatLinkedTypes);
myPlaceAccessories.accessories.push(myPlaceZoneFanv2);
}
function createZoneThermostat(name, zone, ac, IPA) {
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}${IPA}${ac_l}`
};
}
function createZoneFanv2noRotationDirection(name, thisZoneName, zoneStr, ac, IPA) {
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} ${IPA}${ac_l}`,
linkedTypes: []
};
// Generate linked thermostat accessory
const myPlaceZoneThermostatLinkedTypes = createZoneThermostat(`${thisZoneName} Thermostat`, zoneStr, ac, IPA);
// Add linked thermostat
myPlaceZoneFanv2noRotationDirection.linkedTypes.push(myPlaceZoneThermostatLinkedTypes);
myPlaceAccessories.accessories.push(myPlaceZoneFanv2noRotationDirection);
}
function createLightbulbNoDimmer(name, id, IPA) {
// 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} ${IPA}`
};
myPlaceAccessories.accessories.push(myPlaceLightbulbNoDimmer);
}
function createLightbulbWithDimmer(name, id, IPA) {
// 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} ${IPA}`
};
myPlaceAccessories.accessories.push(myPlaceLightbulbWithDimmer);
}
function createFanNoRotationSpeed(name, id, IPA) {
// 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} ${IPA}`
};
myPlaceAccessories.accessories.push(myPlaceFanNoRotationSpeed);
}
function createGarageDoorOpener(name, id, IPA) {
// 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} ${IPA}`
};
myPlaceAccessories.accessories.push(myPlaceGarageDoorOpener);
}
function createWindowCovering(name, id, IPA) {
// 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} ${IPA}`
};
myPlaceAccessories.accessories.push(myPlaceWindowCovering);
}
function assembleMyPlaceConfig() {
return {
name: "MyPlace",
...myPlaceConstants,
...myPlaceQueueTypes,
...myPlaceAccessories,
platform: "MyPlace"
};
}
// Determine number of tablets/devices
let noOfTablets = config.devices.length;
// Initialize global config objects (assumed to be declared globally or passed)
// myPlaceConstants, myPlaceQueueTypes, myPlaceModelQueue, myPlaceAccessories, myPlaceConfig
for (let n = 0; n < noOfTablets; n++) {
let AAname
if (n == 0) {
AAname = config.devices[n]?.name || `Aircon`;
} else {
AAname = config.devices[n]?.name || `Aircon${n + 1}`;
}
const ip = IPs[n];
if (ip === "undefined") continue;
const extraTimers = config.devices[n]?.extraTimers || false;
const debug = config.devices[n]?.debug || false;
const queue=["AAA", "AAB", "AAC"][n]
const IPA = `\${AAIP${n + 1}}`
// Fetch system data
let myAirData;
try {
const response = await fetch(`http://${ip}/getSystemData`);
if (!response.ok) throw new Error(`HTTP error ${response.status}`);
myAirData = await response.json();
} catch {
if (n === noOfTablets - 1) {
throw new Error(`Device ${n + 1} with IP ${ip} is inaccessible (power OFF or wrong IP or wrong port).`);
} else {
log.warn(`⚠️ Device ${n + 1} with IP ${ip} is inaccessible (power OFF or wrong IP or wrong port).`);
continue;
}
}
// Extract system info (use safe chaining or checks as needed)
const sysName = (myAirData.system?.name ?? "").replace(/ /g, "_").replace(/['"]/g, "");
if (!sysName) {
if (n === noOfTablets - 1) {
throw new Error("Failed to get system info from your device ${n + 1}!");
} else {
log.error("⚠️ Failed to get system info from your device ${n + 1}!");
continue;
}
}
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;
// Create constants and queueTypes
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, IPA);
createFanSwitch(`${thisAAname} Fan`, AAname, ac, IPA);
createDrySwitch(`${thisAAname} Dry`, AAname, ac, IPA);
createTimerLightbulb(`${thisAAname} Timer`, "timer", ac, IPA);
if (extraTimers) {
createTimerLightbulb(`${thisAAname} Fan Timer`, "fanTimer", ac, IPA);
createTimerLightbulb(`${thisAAname} Cool Timer`, "coolTimer", ac, IPA);
createTimerLightbulb(`${thisAAname} Heat Timer`, "heatTimer", ac, IPA);
}
// 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, IPA);
} else if (myZoneValue !== 0) {
createZoneFanv2(`${thisZoneName} Zone`, thisZoneName, zoneStr, ac, IPA);
} else {
createZoneFanv2noRotationDirection(`${thisZoneName} Zone`, thisZoneName, zoneStr, ac, IPA);
}
}
}
}
// 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;
// All switches with names starting with "Fan " or "Ex " and those ending with " Fan" or " Ex" will consider to be Fan accessories
const fanPattern = /^(Fan |Ex )|( Fan| Ex)$/;
if (fanPattern.test(name)) {
createFanNoRotationSpeed(name, id, IPA);
} else if (value === null) {
createLightbulbNoDimmer(name, id, IPA);
} else {
createLightbulbWithDimmer(name, id, IPA);
}
}
}
// 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, IPA);
} else if (channelDipState == "1" || channelDipState == "2") {
createWindowCovering(name, id, IPA);
}
}
}
}
// Assemble final config
const myPlaceConfig = assembleMyPlaceConfig();
return myPlaceConfig
}
// export this function
module.exports = { createMyPlaceConfig };