diff --git a/.gitignore b/.gitignore index aad6e46..6d07eb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +#test data +neo.json + # Ignore compiled code dist diff --git a/.vscode/settings.json b/.vscode/settings.json index 45d08a3..16eea8b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "files.eol": "\n", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "editor.rulers": [ 140 ], "eslint.enable": true, @@ -12,6 +12,7 @@ "clientid", "cmds", "EHOSTDOWN", + "Fanv", "hvac", "refreshinterval", "Setpoint", diff --git a/README.md b/README.md index 1f3290a..eccfb19 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,13 @@ This is an 'almost' feature complete implementation of the Que platform in HomeK - Report battery level on zone sensors and get low battery alerts in the home app - Support for homebridge config UI +Fixes/Improvements in version 1.5.0 + - Now supports running in "Fan-Only" mode. An option has been added to the configuration to create a Fan accessory for each zone and the master controller (`Create FAN ONLY devices for each zone`). + + Fixes/Improvements in version 1.2.8 + - Provide option to disable battery checking for hard wired zone controllers (preventing erroneous low battery reports). Cached accessories will need to be removed for changes to take effect. + - Corrected error in humidity sensor detection that may have prevented humidity sensor from being added for zone sensors that support humidity reading. + Fixes/Improvements in version 1.2.7 - Allow master controller to also operate as a zone controller - Resolved issue with logic controlling "Zones Push Master" temp adjustments which was causing setting to fail on first attempt @@ -62,7 +69,6 @@ New in version 1.1.0 Limitations - These options cannot be set via HomeKit: - Quiet Mode - - Fan Only Mode - Constant Fan Operation - Away Mode @@ -105,6 +111,11 @@ If you are not using the Homebridge config UI you can add the following to your             "zonesPushMaster": true | false, "refreshInterval": 60, "deviceSerial": "", + "fanOnlyDevices": true | false, + "wiredZoneSensors": [ + "Zone Name 1", + "Zone Name 2", + ], "maxCoolingTemp": 32, "minCoolingTemp": 20, "maxHeatingTemp": 26, @@ -181,6 +192,27 @@ default: "" In most cases you can exclude this option or leave it blank. If you only have a single air con system in your Que account the plugin will auto-discover the target device serial number. If you have multiple Que systems in your account you will need to specify which system you want to control by entering the serial number here. You can get your device serial numbers by logging in to que.actronair.com.au and looking at the list of authorised devices. +### `fanOnlyDevices` + +type: boolean + +default: false + +Control creation of Fan Only accessories for each zone. When toggled on these accessories will put the system in Fan only mode. Fan speed can be controlled via the Master Controller accessory. +- 1-10% = Auto speed +- 11-30% = Low speed +- 31-65% = Medium speed +- 66-100% = High speed + +#### `wiredZoneSensors` + +type: array[string] (case sensitive) + +default: [] (empty array) + +An array of strings defining zone names utilising hardwired zone sensors. Zone names must match identically with the zone names configured within the Que controller. Be concious of case, spaces and any leading or trailing whitespace in the zone name. +Configuring zones as hardwired will prevent creation of a battery monitoring service and suppress erroneous low battery alerts for these sensors. + #### `maxCoolingTemp` type: number diff --git a/config.schema.json b/config.schema.json index e7b8b03..f1cdcfe 100644 --- a/config.schema.json +++ b/config.schema.json @@ -58,6 +58,31 @@ "required": false, "placeholder": "Leave Blank If You Have A Single Que System - Plugin Will Auto Discover" }, + "fanOnlyDevices": { + "title": "Create FAN ONLY devices for each zone", + "description": "Fan Only devices allow you to run the system in FAN mode", + "type": "boolean" + }, + "defineWiredZoneSensors": { + "title": "Define Wired Zone Sensors to Disable Battery Checks", + "description": "All zones are assumed to be wireless by default with battery checks enabled", + "type": "boolean" + }, + "wiredZoneSensors": { + "title": "Hardwired Zone Sensors", + "description": "Entering zone names here will disable battery checking on hardwired zones.", + "type": "array", + "required": false, + "condition": { + "functionBody": "return model.defineWiredZoneSensors === true;" + }, + "items": { + "title": "Zone Name", + "description": "Name of Zone as Defined on Master Controller", + "placeholder": "Enter zone name exactly as appears on controller - case sensitive", + "type": "string" + } + }, "adjustThresholds": { "title": " Modify default heating cooling threshold temperatures", "description": "Cooling default min/max = 20/32. Heating default min/max = 10/26", diff --git a/package.json b/package.json index 7d5cb4d..c6e88cc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": false, "displayName": "Homebridge Actron Que", "name": "homebridge-actron-que", - "version": "1.2.7", + "version": "1.5.0", "description": "Homebridge plugin for controlling Actron Que controller systems", "license": "Apache-2.0", "repository": { diff --git a/src/fanOnlyMasterAccessory.ts b/src/fanOnlyMasterAccessory.ts new file mode 100644 index 0000000..eb8e4bc --- /dev/null +++ b/src/fanOnlyMasterAccessory.ts @@ -0,0 +1,133 @@ +import { Service, PlatformAccessory, CharacteristicValue, HAPStatus } from 'homebridge'; +import { ClimateMode, FanMode, PowerState } from './types'; +import { ActronQuePlatform } from './platform'; + +// This class represents the master controller +export class FanOnlyMasterAccessory { + private fanService: Service; + + constructor( + private readonly platform: ActronQuePlatform, + private readonly accessory: PlatformAccessory, + ) { + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Actron') + .setCharacteristic(this.platform.Characteristic.Model, this.platform.hvacInstance.type + ' FanOnlyMaster') + .setCharacteristic(this.platform.Characteristic.SerialNumber, this.platform.hvacInstance.serialNo); + + // Get or create the fan service. + this.fanService = this.accessory.getService(this.platform.Service.Fanv2) + || this.accessory.addService(this.platform.Service.Fanv2); + + // Set accessory display name, this is taken from discover devices in platform + this.fanService.setCharacteristic(this.platform.Characteristic.Name, accessory.displayName); + + // register handlers for device control, references the class methods that follow for Set and Get + this.fanService.getCharacteristic(this.platform.Characteristic.Active) + .onSet(this.setPowerState.bind(this)) + .onGet(this.getPowerState.bind(this)); + + this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed) + .onSet(this.setFanMode.bind(this)) + .onGet(this.getFanMode.bind(this)); + + setInterval(() => this.softUpdateDeviceCharacteristics(), this.platform.softRefreshInterval); + + } + + // SET's are async as these need to wait on API response then cache the return value on the hvac Class instance + // GET's run non async as this is a quick retrieval from the hvac class instance cache + // UPDATE is run Async as this polls the API first to confirm current cache state is accurate + async softUpdateDeviceCharacteristics() { + this.fanService.updateCharacteristic(this.platform.Characteristic.Active, this.getPowerState()); + this.fanService.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.getFanMode()); + } + + checkHvacComms() { + if (!this.platform.hvacInstance.cloudConnected) { + this.platform.log.error('Master Controller is offline. Check Master Controller Internet/Wifi connection'); + throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + } + } + + async setPowerState(value: CharacteristicValue) { + this.checkHvacComms(); + switch (value) { + case 0: + await this.platform.hvacInstance.setPowerStateOff(); + break; + case 1: + // If the fan only mode is not running, switch climate mode to fan instead of sending power on event + if (this.platform.hvacInstance.climateMode !== ClimateMode.FAN) { + await this.platform.hvacInstance.setClimateModeFan(); + } + // If power state is not ON - Turn on + if (this.platform.hvacInstance.powerState !== PowerState.ON) { + await this.platform.hvacInstance.setPowerStateOn(); + } + break; + } + this.platform.log.debug('Set FanOnlyMaster Power State -> ', value); + } + + getPowerState(): CharacteristicValue { + // Check climate mode, if it is any value other than FAN then we are not in fan-only mode + // If it is FAN then we need to check if the system is powered on + let powerState: number; + const climateMode = this.platform.hvacInstance.climateMode; + switch (climateMode) { + case ClimateMode.FAN: + powerState = (this.platform.hvacInstance.powerState === PowerState.ON) ? 1 : 0; + break; + default: + powerState = 0; + } + this.platform.log.debug('Got FanOnlyMaster Power State -> ', powerState); + return powerState; + } + + async setFanMode(value: CharacteristicValue) { + this.checkHvacComms(); + switch (true) { + case (+value <= 10): + await this.platform.hvacInstance.setFanModeAuto(); + break; + case (+value <= 30): + await this.platform.hvacInstance.setFanModeLow(); + break; + case (+value <= 65): + await this.platform.hvacInstance.setFanModeMedium(); + break; + case (+value <= 100): + await this.platform.hvacInstance.setFanModeHigh(); + break; + } + this.platform.log.debug('Set FanOnlyMaster Fan Mode 1-10:Auto, 11-30:Low, 31-65:Medium, 66-100:High -> ', value); + } + + getFanMode(): CharacteristicValue { + let currentMode: number; + const fanMode = this.platform.hvacInstance.fanMode; + switch (fanMode) { + case FanMode.AUTO || FanMode.AUTO_CONT: + currentMode = 10; + break; + case FanMode.LOW || FanMode.LOW_CONT: + currentMode = 25; + break; + case FanMode.MEDIUM || FanMode.MEDIUM_CONT: + currentMode = 50; + break; + case FanMode.HIGH || FanMode.HIGH_CONT: + currentMode = 100; + break; + default: + currentMode = 0; + this.platform.log.debug('Failed To Get FanOnlyMaster Current Fan Mode -> ', fanMode); + } + this.platform.log.debug('Got FanOnlyMaster Current Fan Mode -> ', fanMode); + return currentMode; + } +} \ No newline at end of file diff --git a/src/fanOnlyZoneAccessory.ts b/src/fanOnlyZoneAccessory.ts new file mode 100644 index 0000000..15a24ed --- /dev/null +++ b/src/fanOnlyZoneAccessory.ts @@ -0,0 +1,88 @@ +import { Service, PlatformAccessory, CharacteristicValue, HAPStatus } from 'homebridge'; +import { ClimateMode } from './types'; +import { ActronQuePlatform } from './platform'; +import { HvacZone } from './hvacZone'; + +// This class represents the zone controller +export class FanOnlyZoneAccessory { + private fanService: Service; + + constructor( + private readonly platform: ActronQuePlatform, + private readonly accessory: PlatformAccessory, + private readonly zone: HvacZone, + ) { + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Actron') + .setCharacteristic(this.platform.Characteristic.Model, this.platform.hvacInstance.type + ' FanOnlyZone') + .setCharacteristic(this.platform.Characteristic.SerialNumber, this.zone.sensorId); + + // Get or create the fan service. + this.fanService = this.accessory.getService(this.platform.Service.Fanv2) + || this.accessory.addService(this.platform.Service.Fanv2); + + // Set accessory display name, this is taken from discover devices in platform + this.fanService.setCharacteristic(this.platform.Characteristic.Name, accessory.displayName); + + // register handlers for device control, references the class methods that follow for Set and Get + this.fanService.getCharacteristic(this.platform.Characteristic.Active) + .onSet(this.setEnableState.bind(this)) + .onGet(this.getEnableState.bind(this)); + + + setInterval(() => this.softUpdateDeviceCharacteristics(), this.platform.softRefreshInterval); + + } + + // SET's are async as these need to wait on API response then cache the return value on the hvac Class instance + // GET's run non async as this is a quick retrieval from the hvac class instance cache + // UPDATE is run Async as this polls the API first to confirm current cache state is accurate + async softUpdateDeviceCharacteristics() { + this.fanService.updateCharacteristic(this.platform.Characteristic.Active, this.getEnableState()); + } + + checkHvacComms() { + if (!this.platform.hvacInstance.cloudConnected) { + this.platform.log.error('Master Controller is offline. Check Master Controller Internet/Wifi connection'); + throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + } + } + + async setEnableState(value: CharacteristicValue) { + this.checkHvacComms(); + switch (value) { + case 0: + await this.zone.setZoneDisable(); + break; + case 1: + // If the fan only mode is not running, switch climate mode to fan instead of sending enable event + if (this.platform.hvacInstance.climateMode !== ClimateMode.FAN) { + await this.platform.hvacInstance.setClimateModeFan(); + } + // after checking mode, check if the state is enabled or not + if (this.zone.zoneEnabled === false) { + await this.zone.setZoneEnable(); + } + break; + } + this.platform.log.debug(`Set FanOnlyZone ${this.zone.zoneName} Enable State -> `, value); + } + + getEnableState(): CharacteristicValue { + // Check climate mode, if it is any value other than FAN then we are not in fan-only mode + // If it is FAN then we need to check if the zone is enabled + let enableState: number; + const climateMode = this.platform.hvacInstance.climateMode; + switch (climateMode) { + case ClimateMode.FAN: + enableState = (this.zone.zoneEnabled === true) ? 1 : 0; + break; + default: + enableState = 0; + } + this.platform.log.debug(`Got FanOnlyZone ${this.zone.zoneName} Enable State -> `, enableState); + return enableState; + } +} \ No newline at end of file diff --git a/src/hvac.ts b/src/hvac.ts index e1935b1..9a674cb 100644 --- a/src/hvac.ts +++ b/src/hvac.ts @@ -33,7 +33,8 @@ export class HvacUnit { private readonly log: Logger, private readonly hbUserStoragePath: string, readonly zonesFollowMaster = true, - readonly zonesPushMaster = true) { + readonly zonesPushMaster = true, + readonly wiredZoneSensors: string[] = []) { this.name = name; } @@ -83,7 +84,8 @@ export class HvacUnit { if (targetInstance) { targetInstance.pushStatusUpdate(zone); } else { - this.zoneInstances.push(new HvacZone(this.log, this.apiInterface, zone)); + const zoneBatteryChecking = this.wiredZoneSensors.includes(zone.zoneName) ? false : true; + this.zoneInstances.push(new HvacZone(this.log, this.apiInterface, zone, zoneBatteryChecking)); } } return status; diff --git a/src/hvacZone.ts b/src/hvacZone.ts index c3fe655..f259024 100644 --- a/src/hvacZone.ts +++ b/src/hvacZone.ts @@ -22,6 +22,7 @@ export class HvacZone { private readonly log: Logger, readonly apiInterface: QueApi, zoneStatus: ZoneStatus, + readonly zoneBatteryChecking: boolean, ) { this.zoneName = zoneStatus.zoneName; @@ -35,14 +36,15 @@ export class HvacZone { this.minCoolSetPoint = zoneStatus.minCoolSetPoint; this.currentHeatingSetTemp = zoneStatus.currentHeatingSetTemp; this.currentCoolingSetTemp = zoneStatus.currentCoolingSetTemp; - this.zoneSensorBattery = zoneStatus.zoneSensorBattery; + // wired sensors report battery level as 255; this prevents an error where battery level is set higher than 100 + this.zoneSensorBattery = zoneStatus.zoneSensorBattery > 100 ? 100 : zoneStatus.zoneSensorBattery; // some zone sensor versions do not report humidity if (zoneStatus.currentHumidity === 'notSupported') { this.zoneHumiditySensor = false; this.currentHumidity = 0; } else { - this.zoneHumiditySensor = false; + this.zoneHumiditySensor = true; this.currentHumidity = zoneStatus.currentHumidity; } } @@ -56,7 +58,8 @@ export class HvacZone { this.minCoolSetPoint = zoneStatus.minCoolSetPoint; this.currentHeatingSetTemp = zoneStatus.currentHeatingSetTemp; this.currentCoolingSetTemp = zoneStatus.currentCoolingSetTemp; - this.zoneSensorBattery = zoneStatus.zoneSensorBattery; + // wired sensors report battery level as 255; this prevents an error where battery level is set higher than 100 + this.zoneSensorBattery = zoneStatus.zoneSensorBattery > 100 ? 100 : zoneStatus.zoneSensorBattery; this.currentHumidity = this.zoneHumiditySensor ? zoneStatus.currentHumidity as number : 0; } diff --git a/src/masterControllerAccessory.ts b/src/masterControllerAccessory.ts index 70eda25..0f44953 100644 --- a/src/masterControllerAccessory.ts +++ b/src/masterControllerAccessory.ts @@ -127,14 +127,27 @@ export class MasterControllerAccessory { await this.platform.hvacInstance.setPowerStateOff(); break; case 1: - await this.platform.hvacInstance.setPowerStateOn(); + // If the fan only mode is running, switch climate mode to cool instead of sending power on event + if (this.platform.hvacInstance.climateMode === ClimateMode.FAN) { + await this.platform.hvacInstance.setClimateModeCool(); + } + // If power state is not ON - Turn on + if (this.platform.hvacInstance.powerState !== PowerState.ON) { + await this.platform.hvacInstance.setPowerStateOn(); + } break; } this.platform.log.debug('Set Master Power State -> ', value); } getPowerState(): CharacteristicValue { - const powerState = (this.platform.hvacInstance.powerState === PowerState.ON) ? 1 : 0; + // Report as powered off if the fan mode is set to FAN + let powerState: number; + if (this.platform.hvacInstance.climateMode === ClimateMode.FAN) { + powerState = 0; + } else { + powerState = (this.platform.hvacInstance.powerState === PowerState.ON) ? 1 : 0; + } // this.platform.log.debug('Got Master Power State -> ', powerState); return powerState; } @@ -195,6 +208,10 @@ export class MasterControllerAccessory { case ClimateMode.COOL: currentMode = this.platform.Characteristic.TargetHeaterCoolerState.COOL; break; + // Returning climate mode of cool when fan-only is running + case ClimateMode.FAN: + currentMode = this.platform.Characteristic.TargetHeaterCoolerState.COOL; + break; default: currentMode = 0; this.platform.log.debug('Failed To Get Master Target Climate Mode -> ', climateMode); @@ -246,20 +263,20 @@ export class MasterControllerAccessory { async setFanMode(value: CharacteristicValue) { this.checkHvacComms(); switch (true) { - case (value <= 30): + case (+value <= 10): + await this.platform.hvacInstance.setFanModeAuto(); + break; + case (+value <= 30): await this.platform.hvacInstance.setFanModeLow(); break; - case (value <= 60): + case (+value <= 65): await this.platform.hvacInstance.setFanModeMedium(); break; - case (value <= 90): + case (+value <= 100): await this.platform.hvacInstance.setFanModeHigh(); break; - case (value <= 100): - await this.platform.hvacInstance.setFanModeAuto(); - break; } - this.platform.log.debug('Set Master Fan Mode 91-100:Auto, 1-30:Low, 31-60:Medium, 61-90:High -> ', value); + this.platform.log.debug('Set Master Fan Mode 1-10:Auto, 11-30:Low, 31-65:Medium, 66-100:High -> ', value); } getFanMode(): CharacteristicValue { @@ -267,22 +284,22 @@ export class MasterControllerAccessory { const fanMode = this.platform.hvacInstance.fanMode; switch (fanMode) { case FanMode.AUTO || FanMode.AUTO_CONT: - currentMode = 100; + currentMode = 10; break; case FanMode.LOW || FanMode.LOW_CONT: - currentMode = 29; + currentMode = 25; break; case FanMode.MEDIUM || FanMode.MEDIUM_CONT: - currentMode = 59; + currentMode = 50; break; case FanMode.HIGH || FanMode.HIGH_CONT: - currentMode = 89; + currentMode = 100; break; default: currentMode = 0; this.platform.log.debug('Failed To Get Master Current Fan Mode -> ', fanMode); } - // this.platform.log.debug('Got Master Current Fan Mode -> ', fanMode); + this.platform.log.debug('Got Master Current Fan Mode -> ', fanMode); return currentMode; } } \ No newline at end of file diff --git a/src/platform.ts b/src/platform.ts index e49b0c5..5912b0f 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -3,6 +3,8 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { MasterControllerAccessory } from './masterControllerAccessory'; import { ZoneControllerAccessory } from './zoneControllerAccessory'; +import { FanOnlyMasterAccessory } from './fanOnlyMasterAccessory'; +import { FanOnlyZoneAccessory } from './fanOnlyZoneAccessory'; import { OutdoorUnitAccessory } from './outdoorUnitAccessory'; import { HvacUnit } from './hvac'; import { HvacZone } from './hvacZone'; @@ -20,6 +22,8 @@ export class ActronQuePlatform implements DynamicPlatformPlugin { readonly userProvidedSerialNo: string = ''; readonly zonesFollowMaster: boolean = true; readonly zonesPushMaster: boolean = true; + readonly fanOnlyDevices: boolean = false; + readonly wiredZoneSensors: string[] = []; readonly hardRefreshInterval: number = 60000; readonly softRefreshInterval: number = 5000; readonly maxCoolingTemp: number = 32; @@ -54,9 +58,21 @@ export class ActronQuePlatform implements DynamicPlatformPlugin { } else { this.zonesPushMaster = true; } + if (config['fanOnlyDevices']) { + this.fanOnlyDevices = config['fanOnlyDevices']; + this.log.debug('Create Fan Only devices set to', this.fanOnlyDevices); + } else { + this.fanOnlyDevices = false; + } + if (config['wiredZoneSensors']) { + this.wiredZoneSensors = config['wiredZoneSensors']; + this.log.debug('These zones are hardwired, battery checking is disabled', this.wiredZoneSensors); + } else { + this.wiredZoneSensors = []; + } if (config['refreshInterval']) { this.hardRefreshInterval = config['refreshInterval'] * 1000; - this.log.debug('Auto refresh interval set to seconds', this.hardRefreshInterval/1000); + this.log.debug('Auto refresh interval set to seconds', this.hardRefreshInterval / 1000); } else { this.hardRefreshInterval = 60000; } @@ -122,7 +138,7 @@ export class ActronQuePlatform implements DynamicPlatformPlugin { try { // Instantiate an instance of HvacUnit and connect the actronQueApi this.hvacInstance = new HvacUnit(this.clientName, this.log, this.api.user.storagePath(), - this.zonesFollowMaster, this.zonesPushMaster); + this.zonesFollowMaster, this.zonesPushMaster, this.wiredZoneSensors); let hvacSerial = ''; hvacSerial = await this.hvacInstance.actronQueApi(this.username, this.password, this.userProvidedSerialNo); // Make sure we have hvac master and zone data before adding devices @@ -149,11 +165,27 @@ export class ActronQuePlatform implements DynamicPlatformPlugin { instance: zone, }); } + if (this.fanOnlyDevices) { + devices.push({ + type: 'fanOnlyMaster', + uniqueId: hvacSerial + '-fanOnly', + displayName: this.clientName + '-fanOnly', + instance: this.hvacInstance, + }); + for (const zone of this.hvacInstance.zoneInstances) { + devices.push({ + type: 'fanOnlyZone', + uniqueId: zone.sensorId + '-fanOnly', + displayName: zone.zoneName + '-fanOnly', + instance: zone, + }); + } + } this.log.debug('Discovered Devices \n', devices); // loop over the discovered devices and register each one if it has not already been registered for (const device of devices) { - // create uuid first then see if an accessory with the same uuid has already been registered and restored from - // the cached devices we stored in the `configureAccessory` method above + // create uuid first then see if an accessory with the same uuid has already been registered and restored from + // the cached devices we stored in the `configureAccessory` method above const uuid = this.api.hap.uuid.generate(device.uniqueId); const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); @@ -170,26 +202,48 @@ export class ActronQuePlatform implements DynamicPlatformPlugin { this.log.info('Restoring Outdoor Unit accessory from cache:', existingAccessory.displayName); new OutdoorUnitAccessory(this, existingAccessory); - } else if (!existingAccessory && device.type === 'masterController'){ + } else if (existingAccessory && device.type === 'fanOnlyMaster') { + this.log.info('Restoring Fan Only Master accessory from cache:', existingAccessory.displayName); + new FanOnlyMasterAccessory(this, existingAccessory); + + } else if (existingAccessory && device.type === 'fanOnlyZone') { + this.log.info('Restoring Fan only Zone accessory from cache:', existingAccessory.displayName); + new FanOnlyZoneAccessory(this, existingAccessory, device.instance as HvacZone); + + } else if (!existingAccessory && device.type === 'masterController') { this.log.info('Adding new accessory:', device.displayName); const accessory = new this.api.platformAccessory(device.displayName, uuid); accessory.context.device = device; new MasterControllerAccessory(this, accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - } else if (!existingAccessory && device.type === 'zoneController'){ + } else if (!existingAccessory && device.type === 'zoneController') { this.log.info('Adding new accessory:', device.displayName); const accessory = new this.api.platformAccessory(device.displayName, uuid); accessory.context.device = device; new ZoneControllerAccessory(this, accessory, device.instance as HvacZone); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - } else if (!existingAccessory && device.type === 'outdoorUnit'){ + } else if (!existingAccessory && device.type === 'outdoorUnit') { this.log.info('Adding new accessory:', device.displayName); const accessory = new this.api.platformAccessory(device.displayName, uuid); accessory.context.device = device; new OutdoorUnitAccessory(this, accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + + } else if (!existingAccessory && device.type === 'fanOnlyMaster') { + this.log.info('Adding new accessory:', device.displayName); + const accessory = new this.api.platformAccessory(device.displayName, uuid); + accessory.context.device = device; + new FanOnlyMasterAccessory(this, accessory); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + + } else if (!existingAccessory && device.type === 'fanOnlyZone') { + this.log.info('Adding new accessory:', device.displayName); + const accessory = new this.api.platformAccessory(device.displayName, uuid); + accessory.context.device = device; + new FanOnlyZoneAccessory(this, accessory, device.instance as HvacZone); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } } catch (error) { diff --git a/src/zoneControllerAccessory.ts b/src/zoneControllerAccessory.ts index 66b403d..9a4b0b6 100644 --- a/src/zoneControllerAccessory.ts +++ b/src/zoneControllerAccessory.ts @@ -8,7 +8,8 @@ export class ZoneControllerAccessory { private hvacService: Service; // some versions of the zone sensor do not support humidity private humidityService: Service | null; - private batteryService: Service; + // if sensors are hardwired it may be desirable to disable the battery service to avoid low battery alerts + private batteryService: Service | null; constructor( private readonly platform: ActronQuePlatform, @@ -29,10 +30,6 @@ export class ZoneControllerAccessory { // Set accessory display name, this is taken from discover devices in platform this.hvacService.setCharacteristic(this.platform.Characteristic.Name, accessory.displayName); - // Get or create the humidity sensor service. - this.batteryService = this.accessory.getService(this.platform.Service.Battery) - || this.accessory.addService(this.platform.Service.Battery); - // Get or create the humidity sensor service if the zone sensor supports humidity readings if (this.zone.zoneHumiditySensor) { this.humidityService = this.accessory.getService(this.platform.Service.HumiditySensor) @@ -44,13 +41,20 @@ export class ZoneControllerAccessory { this.humidityService = null; } - // get battery low - this.batteryService.getCharacteristic(this.platform.Characteristic.StatusLowBattery) - .onGet(this.getBatteryStatus.bind(this)); - - // get battery level - this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel) - .onGet(this.getBatteryLevel.bind(this)); + // If the zone sensor is hardwired it may be desirable to disable the battery service to avoid low battery alerts + if (this.zone.zoneBatteryChecking) { + // Get or create the battery monitoring service. + this.batteryService = this.accessory.getService(this.platform.Service.Battery) + || this.accessory.addService(this.platform.Service.Battery); + // get battery low + this.batteryService.getCharacteristic(this.platform.Characteristic.StatusLowBattery) + .onGet(this.getBatteryStatus.bind(this)); + // get battery level + this.batteryService.getCharacteristic(this.platform.Characteristic.BatteryLevel) + .onGet(this.getBatteryLevel.bind(this)); + } else { + this.batteryService = null; + } // register handlers for device control, references the class methods that follow for Set and Get this.hvacService.getCharacteristic(this.platform.Characteristic.Active) @@ -101,8 +105,10 @@ export class ZoneControllerAccessory { this.hvacService.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.getCurrentTemperature()); this.hvacService.updateCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature, this.getHeatingThresholdTemperature()); this.hvacService.updateCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature, this.getCoolingThresholdTemperature()); - this.batteryService.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.getBatteryStatus()); - this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.getBatteryLevel()); + if (this.batteryService) { + this.batteryService.updateCharacteristic(this.platform.Characteristic.StatusLowBattery, this.getBatteryStatus()); + this.batteryService.updateCharacteristic(this.platform.Characteristic.BatteryLevel, this.getBatteryLevel()); + } if (this.humidityService) { this.humidityService.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.getHumidity()); @@ -142,15 +148,32 @@ export class ZoneControllerAccessory { await this.zone.setZoneDisable(); break; case 1: - await this.zone.setZoneEnable(); + // If the fan only mode is running, switch climate mode to cool instead of sending enable event + if (this.platform.hvacInstance.climateMode === ClimateMode.FAN) { + await this.platform.hvacInstance.setClimateModeCool(); + } + // after checking mode, check if the state is enabled or not + if (this.zone.zoneEnabled === false) { + await this.zone.setZoneEnable(); + } break; } this.platform.log.debug(`Set Zone ${this.zone.zoneName} Enable State -> `, value); } getEnableState(): CharacteristicValue { - const enableState = (this.zone.zoneEnabled === true) ? 1 : 0; - // this.platform.log.debug(`Got Zone ${this.zone.zoneName} Enable State -> `, enableState); + // Check climate mode, if it is any value other than FAN then we are not in fan-only mode + // If it is FAN then we need to check if the zone is enabled + let enableState: number; + const climateMode = this.platform.hvacInstance.climateMode; + switch (climateMode) { + case ClimateMode.FAN: + enableState = 0; + break; + default: + enableState = (this.zone.zoneEnabled === true) ? 1 : 0; + } + this.platform.log.debug(`Got Zone ${this.zone.zoneName} Enable State -> `, enableState); return enableState; } @@ -208,6 +231,10 @@ export class ZoneControllerAccessory { case ClimateMode.COOL: currentMode = this.platform.Characteristic.TargetHeaterCoolerState.COOL; break; + // Returning climate mode of cool when fan-only is running + case ClimateMode.FAN: + currentMode = this.platform.Characteristic.TargetHeaterCoolerState.COOL; + break; default: currentMode = 0; this.platform.log.debug('Failed To Get Target Climate Mode -> ', climateMode); @@ -225,17 +252,17 @@ export class ZoneControllerAccessory { async setHeatingThresholdTemperature(value: CharacteristicValue) { this.checkHvacComms(); if (this.platform.hvacInstance.zonesPushMaster === true) { - if (value > this.zone.maxHeatSetPoint) { + if (+value > this.zone.maxHeatSetPoint) { await this.platform.hvacInstance.setHeatTemp(value as number); await this.platform.hvacInstance.getStatus(); - } else if (value < this.zone.minHeatSetPoint) { + } else if (+value < this.zone.minHeatSetPoint) { await this.platform.hvacInstance.setHeatTemp(value as number + 2); await this.platform.hvacInstance.getStatus(); } } else { - if (value > this.zone.maxHeatSetPoint) { + if (+value > this.zone.maxHeatSetPoint) { value = this.zone.maxHeatSetPoint; - } else if (value < this.zone.minHeatSetPoint) { + } else if (+value < this.zone.minHeatSetPoint) { value = this.zone.minHeatSetPoint; } } @@ -253,20 +280,20 @@ export class ZoneControllerAccessory { this.checkHvacComms(); if (this.platform.hvacInstance.zonesPushMaster === true) { this.platform.log.debug('zones push master is set to True'); - if (value > this.zone.maxCoolSetPoint) { + if (+value > this.zone.maxCoolSetPoint) { await this.platform.hvacInstance.setCoolTemp(value as number - 2); this.platform.log.debug(`Value is greater than MAX cool set point of ${this.zone.maxCoolSetPoint}, SETTING MASTER TO -> `, value); await this.platform.hvacInstance.getStatus(); - } else if (value < this.zone.minCoolSetPoint) { + } else if (+value < this.zone.minCoolSetPoint) { await this.platform.hvacInstance.setCoolTemp(value as number); this.platform.log.debug(`Value is less than MIN cool set point of ${this.zone.minCoolSetPoint}, SETTING MASTER TO -> `, value); await this.platform.hvacInstance.getStatus(); } } else { - if (value > this.zone.maxCoolSetPoint) { + if (+value > this.zone.maxCoolSetPoint) { value = this.zone.maxCoolSetPoint; this.platform.log.debug(`Value is greater than max cool set point of ${this.zone.maxCoolSetPoint}, CHANGING TO -> `, value); - } else if (value < this.zone.minCoolSetPoint) { + } else if (+value < this.zone.minCoolSetPoint) { value = this.zone.minCoolSetPoint; this.platform.log.debug(`Value is less than MIN cool set point of ${this.zone.minCoolSetPoint}, CHANGING TO -> `, value); }