Skip to content

Commit

Permalink
Merge pull request #16 from jxg81/fan-only-mode-support
Browse files Browse the repository at this point in the history
Fan only mode support
  • Loading branch information
jxg81 committed Jan 20, 2024
2 parents 7847834 + 368e96c commit 30bc5f9
Show file tree
Hide file tree
Showing 12 changed files with 439 additions and 54 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#test data
neo.json

# Ignore compiled code
dist

Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"files.eol": "\n",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"editor.rulers": [ 140 ],
"eslint.enable": true,
Expand All @@ -12,6 +12,7 @@
"clientid",
"cmds",
"EHOSTDOWN",
"Fanv",
"hvac",
"refreshinterval",
"Setpoint",
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
133 changes: 133 additions & 0 deletions src/fanOnlyMasterAccessory.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
88 changes: 88 additions & 0 deletions src/fanOnlyZoneAccessory.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 4 additions & 2 deletions src/hvac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 30bc5f9

Please sign in to comment.