diff --git a/README.md b/README.md index 20d24f0..4a1b0cc 100755 --- a/README.md +++ b/README.md @@ -102,9 +102,9 @@ If you don't use Homebridge UI or HOOBS, or if you want to know more about the p | Parameter | Description | Required | Default | type | | -------------------------- | ---------------------------------------------------------------- |:--------:|:--------:|:--------:| | `platform` | always "SensiboAC" | ✓ | - | String | -| `username` | Your Sensibo account username/email | ✓ | - | String | -| `password` | Your Sensibo account password | ✓ | - | String | -| `apiKey` | Your Sensibo account API key (can be used instead of username/password) | | - | String | +| `username` | Your Sensibo account username/email | ✓* | - | String | +| `password` | Your Sensibo account password | ✓* | - | String | +| `apiKey` | Your Sensibo account API key (can be used instead of username/password) | ✓* | - | String | | `allowRepeatedCommands` | Allow the plugin to send the same state command again |       | `false` | Boolean | | `disableAirQuality`     | When set to `true`, will remove Air Quality readings   |         | `false` | Boolean | | `disableCarbonDioxide`   | When set to `true`, will remove Carbon Dioxide readings and warnings |         | `false` | Boolean | @@ -113,16 +113,16 @@ If you don't use Homebridge UI or HOOBS, or if you want to know more about the p | `disableHumidity`     | When set to `true`, will remove Humidity readings |       | `false` | Boolean | | `disableLightSwitch` | Disable the Light service to control the AC Light (via extra light bulb) |         | `false` | Boolean | | `disableHorizontalSwing`   | Disable horizontal swing control (via extra switch)   |         | `false` | Boolean | -| `disableVerticalSwing`   | Disable Vertical swing control (removes option from accessory) |         | `false` | Boolean | -| `enableClimateReactSwitch` | Adding a switch to quickly enable/disable climate react. |       | `false` | Boolean | +| `disableVerticalSwing`   | Disable vertical swing control (removes option from accessory) |         | `false` | Boolean | +| `enableClimateReactSwitch` | Adds a switch to enable/disable Climate React |       | `false` | Boolean | | `enableHistoryStorage`     | When set to `true`, all measurements (temperature & humidity) will be saved and viewable from the Eve app  |       | `false` | Boolean | | `enableOccupancySensor`    | Adds an occupancy sensor to represent the state of someone at home |     | `false` | Boolean | -| `enableSyncButton`       | Adding a switch to quickly toggle the state of the AC without sending commands to the AC.  |       | `false` | Boolean | -| `syncButtonInAccessory`   | When set to `true`, it will remove the extra AC Sync switch if it exists and will show \"AC Sync Button\" attached as a service to the Same AC Accessory (works only when `enableSyncButton` is set to true)  |       | `false` | Boolean | +| `enableSyncButton`       | When set to `true`, adds a standalone **AC Sync Switch** to toggle the state of the AC in Home app, without sending a command to the accessory |       | `false` | Boolean | +| `syncButtonInAccessory`   | When set to `true`, adds an **AC Sync Switch**, like `enableSyncButton` above, attached to the AC Accessory. It will also remove the standalone Sync Switch (if one exists) |       | `false` | Boolean | | `externalHumiditySensor`   | Creates an additional Humidity sensor accessory |     | `false` | Boolean | -| `devicesToExclude`       | Add devices identifier (room name, ID from logs or serial from Home app) to exclude from homebridge |       | - | String[] | +| `devicesToExclude`       | Add devices identifier (room name, ID from logs or serial from Home app) to exclude from homebridge |       | - | String[] | | `ignoreHomeKitDevices` | Automatically ignore, skip or remove HomeKit supported devices |       | `false` | Boolean | -| `locationsToInclude`       | Device location IDs or names to include when discovering Sensibo devices (leave empty for all locations) |       | - | String[] | +| `locationsToInclude`       | Device location IDs or names to include when discovering Sensibo devices (leave empty for all locations) |       | - | String[] | | `debug`       | When set to `true`, the plugin will produce extra logs for debugging purposes |       | `false` | Boolean | ## Advanced Control @@ -146,18 +146,18 @@ In practice: The accessory state will be updated in the background every 90 seconds, this is hard coded and requested specifically by Sensibo company. The state will also refresh every time you open the "Home" app or any related HomeKit app. -### Fan Mode - -If your Sensibo app can control your AC **FAN** mode, this plugin will create extra fan accessory in HomeKit to control the FAN mode of your device. It will also include all the fan speeds and swing possibilities you have for FAN mode. - -To disable the extra fan accessory, add `"disableFan": true` to your config. - ### Dry Mode If your Sensibo app can control your AC **DRY** mode, the plugin will create extra dehumidifier accessory in HomeKit to control the DRY mode of your device. It will also include all the fan speeds and swing possibilities you have for DRY mode. To disable the extra dehumidifier accessory, add `"disableDry": true` to your config. +### Fan Mode + +If your Sensibo app can control your AC **FAN** mode, this plugin will create extra fan accessory in HomeKit to control the FAN mode of your device. It will also include all the fan speeds and swing possibilities you have for FAN mode. + +To disable the extra fan accessory, add `"disableFan": true` to your config. + ### Horizontal Swing If your Sensibo app has **Horizontal Swing** control, the plugin will create an extra switch accessory in HomeKit to control it. @@ -180,7 +180,7 @@ If you have ever found yourself struggling with the above, this feature is exact It allows you to quickly toggle the state in Sensibo and Home app without changing the real state of your device, this will help you to quickly sync between them. -When enabled, this feature creates a new switch accessory in HomeKit. The new switch is stateless, which means that when clicked, it turns back OFF after 1 second. behind the scenes, the plugin changes the state of the device from ON to OFF or the other way around, depends on the current state of the device. all of that, without sending actual commands to the AC! so you can relax while you test this button :) +When enabled, this feature creates a new switch accessory. The switch is stateless, which means that when clicked, it turns back OFF after 1 second. Behind the scenes, the plugin changes the state of the device from ON to OFF or the other way around, depending on the current state of the device, without sending actual commands to the AC. \* *This can be required if your AC has the same command for ON and OFF because it can go out of sync easily.* @@ -210,9 +210,9 @@ To enable the extra **Climate React** switch, add `"enableClimateReactSwitch": t If you have the Filter Cleaning notifications feature in Sensibo (from Sensibo "Plus" subscription or via old account) it will appear in the AC settings in HomeKit in this form: -1. **Filer Life Level** - Relative (0-100%) representation of the filter life level. calculated from the last time it was cleaned until the next time it should be clean +1. **Filer Life Level** - Relative (0-100%) representation of the filter life level. Calculated from the last time it was cleaned until the next time it should be cleaned 2. **Filter Change Indication** - Boolean state represent whether the filter should be cleaned or not (based on usage time). -3. **Reset Filter Indication** - Stateless button appears only in Eve app that resets the counter of the filter life. normally you would click this button right after you cleaned the filters. +3. **Reset Filter Indication** - Stateless button appears only in Eve app that resets the counter of the filter life. Normally you would click this button right after you cleaned the filters. ### History Storage @@ -222,7 +222,7 @@ To enable the **history storage** feature, add `"enableHistoryStorage": true` to ### Fan speeds & "AUTO" speed -Fan speed steps are determined by the steps you have available in the Sensibo app. Since HomeKit control over fan speed is with a slider between 0-100, the plugin converts the steps you have in the Sensibo app to values between 1 to 100, when 100 is highest and 1 is lowest. if "AUTO" speed is available in your setup, setting the fan speed to 0, should actually set it to "AUTO" speed. +Fan speed steps are determined by the steps you have available in the Sensibo app. Since HomeKit control over fan speed is with a slider between 0-100, the plugin converts the steps you have in the Sensibo app to values between 1 to 100, when 100 is highest and 1 is lowest. If "AUTO" speed is available in your setup, setting the fan speed to 0, should actually set it to "AUTO" speed. ### Issues & Debug @@ -236,7 +236,7 @@ Great thanks to Sensibo company and especially Omer Enbar, their CEO & CO-Founde ## Support homebridge-sensibo-ac -**homebridge-sensibo-ac** is a free plugin under the GNU license. it was developed as a contribution to the homebridge/hoobs community with lots of love and thoughts. +**homebridge-sensibo-ac** is a free plugin under the GNU license. It was developed as a contribution to the homebridge/hoobs community with lots of love and thoughts. Creating and maintaining Homebridge plugins consume a lot of time and effort and if you would like to share your appreciation, feel free to "Star" or donate. diff --git a/homekit/AirConditioner.js b/homekit/AirConditioner.js index ab99ea5..c09da9a 100644 --- a/homekit/AirConditioner.js +++ b/homekit/AirConditioner.js @@ -33,7 +33,7 @@ class AirConditioner { this.disableLightSwitch = platform.disableLightSwitch this.syncButtonInAccessory = platform.syncButtonInAccessory this.filterService = deviceInfo.filterService - this.capabilities = unified.capabilities(device) + this.capabilities = unified.capabilities(device, platform) this.state = this.cachedState.devices[this.id] = unified.acState(device) @@ -133,9 +133,11 @@ class AirConditioner { if (this.capabilities.AUTO) { props.push(Characteristic.TargetHeaterCoolerState.AUTO) } + if (this.capabilities.COOL) { props.push(Characteristic.TargetHeaterCoolerState.COOL) } + if (this.capabilities.HEAT) { props.push(Characteristic.TargetHeaterCoolerState.HEAT) } @@ -210,7 +212,7 @@ class AirConditioner { .on('set', this.stateManager.set.ACSwing) } - if ( (this.capabilities.COOL && this.capabilities.COOL.fanSpeeds) || (this.capabilities.HEAT && this.capabilities.HEAT.fanSpeeds)) { + if ((this.capabilities.COOL && this.capabilities.COOL.fanSpeeds) || (this.capabilities.HEAT && this.capabilities.HEAT.fanSpeeds)) { this.HeaterCoolerService.getCharacteristic(Characteristic.RotationSpeed) .on('get', this.stateManager.get.ACRotationSpeed) .on('set', this.stateManager.set.ACRotationSpeed) @@ -549,6 +551,7 @@ class AirConditioner { this.storage.setItem('state', this.cachedState) } + // TODO: create single shared (unified.js?) updateValue function updateValue (serviceName, characteristicName, newValue) { if (newValue !== 0 && newValue !== false && (typeof newValue === 'undefined' || !newValue)) { this.log.easyDebug(`${this.roomName} - WRONG VALUE -> '${characteristicName}' for ${serviceName} with VALUE: ${newValue}`) @@ -560,9 +563,12 @@ class AirConditioner { const validValues = this[serviceName].getCharacteristic(Characteristic[characteristicName]).props.validValues const currentValue = this[serviceName].getCharacteristic(Characteristic[characteristicName]).value + // TODO: return immediately, as it will have the same result if (validValues && !validValues.includes(newValue)) { newValue = currentValue } + + // TODO: return immediately, as it will have the same result if (minAllowed && newValue < minAllowed) { newValue = currentValue } else if (maxAllowed && newValue > maxAllowed) { diff --git a/homekit/AirPurifier.js b/homekit/AirPurifier.js index 50821d8..24a75fe 100644 --- a/homekit/AirPurifier.js +++ b/homekit/AirPurifier.js @@ -24,7 +24,7 @@ class AirPurifier { this.displayName = this.name this.disableLightSwitch = platform.disableLightSwitch this.filterService = deviceInfo.filterService - this.capabilities = unified.capabilities(device) + this.capabilities = unified.capabilities(device, platform) this.state = this.cachedState.devices[this.id] = unified.acState(device) diff --git a/homekit/StateManager.js b/homekit/StateManager.js index 99ccff9..de5fafb 100644 --- a/homekit/StateManager.js +++ b/homekit/StateManager.js @@ -24,12 +24,15 @@ function sanitize(service, characteristic, value) { if (value !== 0 && (typeof value === 'undefined' || !value)) { return currentValue } + if (validValues && !validValues.includes(value)) { return currentValue } + if (minAllowed && value < minAllowed) { return currentValue } + if (maxAllowed && value > maxAllowed) { return currentValue } @@ -105,6 +108,7 @@ module.exports = (device, platform) => { const mode = device.state.mode log.easyDebug(device.name, '(GET) - Target HeaterCooler State is:', active ? mode : 'OFF') + if (!active || mode === 'FAN' || mode === 'DRY') { const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value @@ -166,7 +170,6 @@ module.exports = (device, platform) => { const swing = device.state.swing log.easyDebug(device.name, '(GET) - AC Swing is:', swing) - callback(null, Characteristic.SwingMode[swing]) }, @@ -174,7 +177,6 @@ module.exports = (device, platform) => { const fanSpeed = device.state.fanSpeed log.easyDebug(device.name, '(GET) - AC Rotation Speed is:', fanSpeed + '%') - callback(null, fanSpeed) }, @@ -182,7 +184,6 @@ module.exports = (device, platform) => { const fanSpeed = device.state.fanSpeed log.easyDebug(device.name, '(GET) - Pure Rotation Speed is:', fanSpeed + '%') - callback(null, fanSpeed) }, @@ -221,7 +222,6 @@ module.exports = (device, platform) => { const swing = device.state.swing log.easyDebug(device.name, '(GET) - Fan Swing is:', swing) - callback(null, Characteristic.SwingMode[swing]) }, @@ -276,7 +276,6 @@ module.exports = (device, platform) => { const swing = device.state.swing log.easyDebug(device.name, '(GET) - Dry Swing is:', swing) - callback(null, Characteristic.SwingMode[swing]) }, @@ -300,7 +299,6 @@ module.exports = (device, platform) => { const horizontalSwing = device.state.horizontalSwing log.easyDebug(device.name, '(GET) - Horizontal Swing is:', horizontalSwing) - callback(null, horizontalSwing === 'SWING_ENABLED') }, @@ -309,7 +307,6 @@ module.exports = (device, platform) => { const light = device.state.light log.easyDebug(device.name, '(GET) - Light is', light ? 'ON' : 'OFF') - callback(null, light) }, @@ -366,10 +363,11 @@ module.exports = (device, platform) => { set: { ACActive: (state, callback) => { - state = !!state - log.easyDebug(device.name + ' -> Setting AC state Active:', state) + const status = !!state - if (state) { + log.easyDebug(device.name + ' -> Setting AC state Active:', status) + + if (status) { device.state.active = true const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value const mode = characteristicToMode(lastMode) @@ -384,9 +382,10 @@ module.exports = (device, platform) => { }, PureActive: (state, callback) => { - state = !!state - log.easyDebug(device.name + ' -> Setting Pure state Active:', state) - device.state.active = state + const status = !!state + + log.easyDebug(device.name + ' -> Setting Pure state Active:', status) + device.state.active = status callback() }, @@ -396,7 +395,6 @@ module.exports = (device, platform) => { log.easyDebug(device.name + ' -> Setting Target HeaterCooler State:', mode) device.state.mode = mode device.state.active = true - callback() }, @@ -407,13 +405,13 @@ module.exports = (device, platform) => { log.easyDebug(device.name + ' -> Setting Cooling Threshold Temperature:', temp + 'ºC') } - device.state.active = true const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value const mode = characteristicToMode(lastMode) - device.state.targetTemperature = temp log.easyDebug(device.name + ' -> Setting Mode to: ' + mode) + device.state.targetTemperature = temp device.state.mode = mode + device.state.active = true callback() }, @@ -424,13 +422,13 @@ module.exports = (device, platform) => { log.easyDebug(device.name + ' -> Setting Heating Threshold Temperature:', temp + 'ºC') } - device.state.active = true const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value const mode = characteristicToMode(lastMode) - device.state.targetTemperature = temp log.easyDebug(device.name + ' -> Setting Mode to: ' + mode) + device.state.targetTemperature = temp device.state.mode = mode + device.state.active = true callback() }, @@ -450,16 +448,14 @@ module.exports = (device, platform) => { }, ACRotationSpeed: (speed, callback) => { - log.easyDebug(device.name + ' -> Setting AC Rotation Speed:', speed + '%') - device.state.fanSpeed = speed - const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value const mode = characteristicToMode(lastMode) + log.easyDebug(device.name + ' -> Setting AC Rotation Speed:', speed + '%') log.easyDebug(device.name + ' -> Setting Mode to', mode) - device.state.active = true + device.state.fanSpeed = speed device.state.mode = mode - + device.state.active = true callback() }, @@ -485,15 +481,18 @@ module.exports = (device, platform) => { // FAN FanActive: (state, callback) => { - state = !!state - log.easyDebug(device.name + ' -> Setting Fan state Active:', state) - if (state) { - device.state.active = true + const status = !!state + + log.easyDebug(device.name + ' -> Setting Fan state Active:', status) + + if (status) { log.easyDebug(device.name + ' -> Setting Mode to: FAN') device.state.mode = 'FAN' + device.state.active = true } else if (device.state.mode === 'FAN') { device.state.active = false } + callback() }, @@ -510,25 +509,27 @@ module.exports = (device, platform) => { FanRotationSpeed: (speed, callback) => { log.easyDebug(device.name + ' -> Setting Fan Rotation Speed:', speed + '%') - device.state.fanSpeed = speed - - device.state.active = true log.easyDebug(device.name + ' -> Setting Mode to: FAN') + device.state.fanSpeed = speed device.state.mode = 'FAN' + device.state.active = true callback() }, // DEHUMIDIFIER DryActive: (state, callback) => { - state = !!state - log.easyDebug(device.name + ' -> Setting Dry state Active:', state) - if (state) { + const status = !!state + + log.easyDebug(device.name + ' -> Setting Dry state Active:', status) + + if (status) { device.state.active = true log.easyDebug(device.name + ' -> Setting Mode to: DRY') device.state.mode = 'DRY' } else if (device.state.mode === 'DRY') { device.state.active = false } + callback() }, @@ -552,11 +553,10 @@ module.exports = (device, platform) => { DryRotationSpeed: (speed, callback) => { log.easyDebug(device.name + ' -> Setting Dry Rotation Speed:', speed + '%') - device.state.fanSpeed = speed - - device.state.active = true log.easyDebug(device.name + ' -> Setting Mode to: DRY') + device.state.fanSpeed = speed device.state.mode = 'DRY' + device.state.active = true callback() }, @@ -576,11 +576,13 @@ module.exports = (device, platform) => { }, // AC SYNC BUTTON + // TODO: should be moved to be a 'set' in StateHanlder line 33 SyncButton: (state, callback) => { if (state) { log.easyDebug(device.name + ' -> Syncing AC State => Setting ' + (device.state.active ? 'OFF' : 'ON') + ' state without sending commands') device.state.syncState() } + callback() }, diff --git a/index.js b/index.js index 93450a0..e3db71e 100644 --- a/index.js +++ b/index.js @@ -61,8 +61,6 @@ class SensiboACPlatform { this.FAHRENHEIT_UNIT = 'F' this.locations = [] - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - const requestedInterval = 90000 // Sensibo interval is hardcoded (requested by the brand) this.refreshDelay = 5000 @@ -71,6 +69,8 @@ class SensiboACPlatform { this.processingState = false this.setProcessing = false + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // + // define debug method to output debug logs when enabled in the config this.log.easyDebug = (...content) => { if (this.debug) { diff --git a/sensibo/StateHandler.js b/sensibo/StateHandler.js index e8cc1e7..9877dcf 100644 --- a/sensibo/StateHandler.js +++ b/sensibo/StateHandler.js @@ -6,7 +6,6 @@ module.exports = (device, platform) => { let preventTurningOff = false const sensiboApi = platform.sensiboApi const log = platform.log - // const state = device.state return { get: (target, prop) => { @@ -30,6 +29,7 @@ module.exports = (device, platform) => { } // return a function to sync ac state + // TODO: should be moved to be a 'set' below, see also StateManager line 576 if (prop === 'syncState') { return async() => { try { @@ -114,8 +114,7 @@ module.exports = (device, platform) => { const sensiboNewState = unified.sensiboFormattedState(device, state) - log.easyDebug(device.name, ' -> Setting New State:') - log.easyDebug(JSON.stringify(sensiboNewState, null, 2)) + log.easyDebug(`Device: ${device.name} -> Setting New State: ${JSON.stringify(sensiboNewState, null, 4)}`) try { // send state command to Sensibo @@ -133,7 +132,7 @@ module.exports = (device, platform) => { setTimeout(() => { device.updateHomeKit() platform.setProcessing = false - }, 500) + }, (setTimeoutDelay / 2)) }, setTimeoutDelay) return true diff --git a/sensibo/api.js b/sensibo/api.js index 74a095c..9f1c627 100644 --- a/sensibo/api.js +++ b/sensibo/api.js @@ -186,7 +186,7 @@ module.exports = async function (platform) { throw err } - // TODO: can return an exception if "get" above fails... + // TODO: the below will return an exception if above "get" fails... null check? return allDevices.filter(device => { return (platform.locationsToInclude.length === 0 || platform.locationsToInclude.includes(device.location.id) diff --git a/sensibo/syncHomeKitCache.js b/sensibo/syncHomeKitCache.js index 47063f3..dfc2128 100644 --- a/sensibo/syncHomeKitCache.js +++ b/sensibo/syncHomeKitCache.js @@ -31,6 +31,8 @@ module.exports = (platform) => { return accessory.type === 'AirConditioner' && accessory.id === device.id }) + platform.log.easyDebug(`Device: ${device.id}, airConditionerIsNew: ${airConditionerIsNew}`) + if (airConditionerIsNew) { const airConditioner = new AirConditioner(device, platform) @@ -76,6 +78,8 @@ module.exports = (platform) => { return accessory.type === 'AirPurifier' && accessory.id === device.id }) + platform.log.easyDebug(`Device: ${device.id}, airPurifierIsNew: ${airPurifierIsNew}`) + if (airPurifierIsNew) { const airPurifier = new AirPurifier(device, platform) @@ -94,6 +98,8 @@ module.exports = (platform) => { return accessory.type === 'RoomSensor' && accessory.id === sensor.id }) + platform.log.easyDebug(`Device: ${device.id}, roomSensorIsNew: ${roomSensorIsNew}`) + if (roomSensorIsNew) { const roomSensor = new RoomSensor(sensor, device, platform) diff --git a/sensibo/unified.js b/sensibo/unified.js index 893d57a..3d7a064 100644 --- a/sensibo/unified.js +++ b/sensibo/unified.js @@ -26,7 +26,7 @@ function HKToFanLevel(value, fanLevels) { const totalLevels = fanLevels.length for (let i = 0; i < fanLevels.length; i++) { - if (value <= (100 * (i + 1) / totalLevels)) { + if (value <= (100 * (i + 1) / totalLevels)) { selected = fanLevels[i] break } @@ -75,7 +75,7 @@ module.exports = { } }, - capabilities: device => { + capabilities: (device, platform) => { const capabilities = {} for (const [key, modeCapabilities] of Object.entries(device.remoteCapabilities.modes)) { @@ -127,6 +127,9 @@ module.exports = { if (modeCapabilities.light) { capabilities[mode].light = true } + + platform.log.easyDebug(`Mode: ${mode}, Capabilities: `) + platform.log.easyDebug(capabilities[mode]) } return capabilities @@ -152,7 +155,7 @@ module.exports = { if (acOnSecondsSinceLastFiltersClean > filtersCleanSecondsThreshold) { state.filterLifeLevel = 0 } else { - state.filterLifeLevel = 100 - Math.floor(acOnSecondsSinceLastFiltersClean/filtersCleanSecondsThreshold*100) + state.filterLifeLevel = 100 - Math.floor(acOnSecondsSinceLastFiltersClean / filtersCleanSecondsThreshold * 100) } } @@ -226,6 +229,7 @@ module.exports = { }, sensiboFormattedState: (device, state) => { + device.log.easyDebug(`formatState: ${JSON.stringify(state, null, 4)}`) const acState = { on: state.active, mode: state.mode.toLowerCase(),