├── branding ├── product.jpg └── electra_homebridge.png ├── .github └── FUNDING.yml ├── config-sample.json ├── electra ├── refreshState.js ├── syncHomeKitCache.js ├── api.js └── unified.js ├── LICENSE ├── package.json ├── .gitignore ├── homebridge-ui ├── server.js └── public │ └── index.html ├── bin └── cli.js ├── index.js ├── config.schema.json ├── README.md └── homekit ├── StateManager.js └── AirConditioner.js /branding/product.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitaybz/homebridge-electra-smart/HEAD/branding/product.jpg -------------------------------------------------------------------------------- /branding/electra_homebridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitaybz/homebridge-electra-smart/HEAD/branding/electra_homebridge.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: nitaybz 4 | ko_fi: nitaybz 5 | custom: ['https://paypal.me/nitaybz'] 6 | -------------------------------------------------------------------------------- /config-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge", 4 | "username": "CD:22:3D:E3:CE:30", 5 | "port": 51826, 6 | "pin": "031-45-154" 7 | }, 8 | 9 | "description": "This is an example configuration for the electra-smart homebridge plugin", 10 | "platforms": [ 11 | { 12 | "platform": "ElectraSmart", 13 | "imei": "2b950000*************", 14 | "token": "**************************", 15 | "disableFan": false, 16 | "disableDry": false, 17 | "minTemperature": 16, 18 | "maxTemperature": 30, 19 | "swingDirection": "both", 20 | "statePollingInterval": 90, 21 | "excludeList": [], 22 | "debug": false 23 | } 24 | ], 25 | 26 | "accessories": [ 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /electra/refreshState.js: -------------------------------------------------------------------------------- 1 | const unified = require('./unified') 2 | 3 | module.exports = (platform) => { 4 | return async () => { 5 | if (!platform.setProcessing) { 6 | 7 | try { 8 | platform.devices = await platform.ElectraApi.getDevices() 9 | await platform.storage.setItem('electra-devices', platform.devices) 10 | 11 | } catch (err) { 12 | platform.log.easyDebug('<<<< ---- Refresh State FAILED! ---- >>>>') 13 | platform.log.easyDebug(err) 14 | platform.log.easyDebug(`Will try again in ${platform.interval / 1000} seconds...`) 15 | return 16 | } 17 | 18 | platform.devices.forEach(device => { 19 | const airConditioner = platform.activeAccessories.find(accessory => accessory.type === 'AirConditioner' && accessory.id === device.id) 20 | 21 | if (airConditioner && device.state) { 22 | // Update AC state in cache + HomeKit 23 | airConditioner.rawState = device.state 24 | airConditioner.updateHomeKit(unified.acState(airConditioner)) 25 | } 26 | }) 27 | // register new devices / unregister removed devices 28 | platform.syncHomeKitCache() 29 | 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2022] [@nitaybz] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-electra-smart", 3 | "description": "Homebridge plugin for Electra Smart A/C", 4 | "version": "2.0.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "git://github.com/nitaybz/homebridge-electra-smart.git" 8 | }, 9 | "license": "MIT", 10 | "preferGlobal": true, 11 | "keywords": [ 12 | "homebridge-plugin", 13 | "homebridge-electra-smart", 14 | "homebridge-electra", 15 | "electra", 16 | "electra-smart", 17 | "electra-air", 18 | "smart-ac" 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/nitaybz/homebridge-electra-smart/issues" 22 | }, 23 | "engines": { 24 | "node": ">=10.17.0", 25 | "homebridge": ">=0.4.4" 26 | }, 27 | "dependencies": { 28 | "@homebridge/plugin-ui-utils": "0.0.4", 29 | "axios": "^0.20.0", 30 | "inquirer": "^7.3.3", 31 | "node-persist": "^3.0.5" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^7.1.0" 35 | }, 36 | "scripts": { 37 | "lint": "eslint .", 38 | "lint:fix": "eslint . --fix" 39 | }, 40 | "bin": { 41 | "electra-extract": "./bin/cli.js" 42 | }, 43 | "funding": [ 44 | { 45 | "type": "paypal", 46 | "url": "https://paypal.me/nitaybz" 47 | }, 48 | { 49 | "type": "patreon", 50 | "url": "https://www.patreon.com/nitaybz" 51 | }, 52 | { 53 | "type": "kofi", 54 | "url": "https://ko-fi.com/nitaybz" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | 43 | # CocoaPods 44 | # 45 | # We recommend against adding the Pods directory to your .gitignore. However 46 | # you should judge for yourself, the pros and cons are mentioned at: 47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 48 | # 49 | # Pods/ 50 | 51 | # Carthage 52 | # 53 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 54 | # Carthage/Checkouts 55 | 56 | Carthage/Build 57 | 58 | # fastlane 59 | # 60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 61 | # screenshots whenever they are needed. 62 | # For more information about the recommended setup visit: 63 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 64 | 65 | node_modules/ 66 | .vscode/ 67 | .DS_Store 68 | .eslintignore 69 | .eslintrc.json 70 | 71 | # ignore all logs 72 | *.log 73 | M-D 74 | -------------------------------------------------------------------------------- /electra/syncHomeKitCache.js: -------------------------------------------------------------------------------- 1 | const AirConditioner = require('../homekit/AirConditioner') 2 | 3 | module.exports = (platform) => { 4 | return () => { 5 | platform.devices.forEach(device => { 6 | 7 | // Add AirConditioner 8 | const airConditionerIsNew = !platform.activeAccessories.find(accessory => accessory.type === 'AirConditioner' && accessory.id === device.id) 9 | if (airConditionerIsNew) { 10 | const airConditioner = new AirConditioner(device, platform) 11 | platform.activeAccessories.push(airConditioner) 12 | } 13 | 14 | }) 15 | 16 | 17 | // find devices to remove 18 | const accessoriesToRemove = [] 19 | platform.cachedAccessories.forEach(accessory => { 20 | 21 | if (!accessory.context.type) { 22 | accessoriesToRemove.push(accessory) 23 | platform.log.easyDebug('removing old cached accessory') 24 | } 25 | 26 | let deviceExists 27 | switch(accessory.context.type) { 28 | case 'AirConditioner': 29 | deviceExists = platform.devices.find(device => device.id === accessory.context.deviceId) 30 | if (!deviceExists) 31 | accessoriesToRemove.push(accessory) 32 | break 33 | default: 34 | accessoriesToRemove.push(accessory) 35 | break 36 | } 37 | }) 38 | 39 | if (accessoriesToRemove.length) { 40 | platform.log.easyDebug('Unregistering Unnecessary Cached Devices:') 41 | platform.log.easyDebug(accessoriesToRemove) 42 | 43 | // unregistering accessories 44 | platform.api.unregisterPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, accessoriesToRemove) 45 | 46 | // remove from cachedAccessories 47 | platform.cachedAccessories = platform.cachedAccessories.filter( cachedAccessory => !accessoriesToRemove.find(accessory => accessory.UUID === cachedAccessory.UUID) ) 48 | 49 | // remove from activeAccessories 50 | platform.activeAccessories = platform.activeAccessories.filter( activeAccessory => !accessoriesToRemove.find(accessory => accessory.UUID === activeAccessory.UUID) ) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /homebridge-ui/server.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const { HomebridgePluginUiServer, RequestError } = require('@homebridge/plugin-ui-utils'); 3 | 4 | class UiServer extends HomebridgePluginUiServer { 5 | constructor() { 6 | super(); 7 | 8 | this.endpointUrl = 'https://app.ecpiot.co.il/mobile/mobilecommand'; 9 | this.imei; 10 | 11 | // create request handlers 12 | this.onRequest('/request-otp', this.requestOtp.bind(this)); 13 | this.onRequest('/check-otp', this.checkOtp.bind(this)); 14 | 15 | // must be called when the script is ready to accept connections 16 | this.ready(); 17 | } 18 | 19 | 20 | /** 21 | * Handle requests sent to /request-otp 22 | */ 23 | async requestOtp(body) { 24 | this.imei = this.generateIMEI(); 25 | 26 | const data = { 27 | 'pvdid': 1, 28 | 'id': 99, 29 | 'cmd': 'SEND_OTP', 30 | 'data': { 31 | 'imei': this.imei, 32 | 'phone': body.phone, 33 | } 34 | } 35 | 36 | try { 37 | const response = await axios.post(this.endpointUrl, data); 38 | return response.data; 39 | } catch (e) { 40 | throw e.response.data; 41 | } 42 | } 43 | 44 | /** 45 | * Handle requests sent to /check-otp 46 | */ 47 | async checkOtp(body) { 48 | const data = { 49 | 'pvdid': 1, 50 | 'id': 99, 51 | 'cmd': 'CHECK_OTP', 52 | 'data': { 53 | 'imei': this.imei, 54 | 'phone': body.phone, 55 | 'code': body.code, 56 | 'os': 'android', 57 | 'osver': 'M4B30Z' 58 | } 59 | } 60 | 61 | let response; 62 | 63 | try { 64 | response = await axios.post(this.endpointUrl, data); 65 | } catch (e) { 66 | throw new RequestError(e.response ? e.response.data : e.message); 67 | } 68 | 69 | if (response.data.data && response.data.data.token) { 70 | return { 71 | imei: this.imei, 72 | token: response.data.data.token, 73 | } 74 | } else { 75 | throw new RequestError(`Could NOT get the token: ${response.data.data ? response.data.data.res_desc : JSON.stringify(response.data)}`); 76 | } 77 | } 78 | 79 | generateIMEI() { 80 | const min = Math.pow(10, 7) 81 | const max = Math.pow(10, 8) - 1 82 | return '2b950000' + (Math.floor(Math.random() * (max - min) + min) + 1) 83 | } 84 | } 85 | 86 | // start the instance of the class 87 | (() => { 88 | return new UiServer; 89 | })(); 90 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const axios = require('axios') 4 | const inquirer = require('inquirer') 5 | let imei, token 6 | axios.defaults.baseURL = 'https://app.ecpiot.co.il/mobile/mobilecommand' 7 | 8 | console.log('\nYou\'ll need the phone that was registered to Electra Smart to get OTP password via SMS.\n') 9 | 10 | var questions = [ 11 | { 12 | type: 'input', 13 | name: 'phone', 14 | message: 'Please insert the phone number registered to Electra Smart (e.g. 0524001234):', 15 | validate: function (value) { 16 | const pass = value.match(/^0\d{8,9}$/i) 17 | if (!pass) 18 | return 'Please enter a valid phone number' 19 | 20 | return new Promise((resolve, reject) => { 21 | imei = generateIMEI() 22 | const data = { 23 | 'pvdid': 1, 24 | 'id': 99, 25 | 'cmd': 'SEND_OTP', 26 | 'data': { 27 | 'imei': imei, 28 | 'phone': value 29 | } 30 | } 31 | axios.post(null, data) 32 | .then(() => { 33 | resolve(true) 34 | }) 35 | .catch(err => { 36 | const error = `ERROR: "${err.response ? (err.response.data.error_description || err.response.data.error) : err}"` 37 | console.log(error) 38 | reject(error) 39 | }) 40 | 41 | }) 42 | 43 | } 44 | }, 45 | { 46 | type: 'input', 47 | name: 'code', 48 | message: 'Please enter the OTP password received at your phone:', 49 | validate: function (value, answers) { 50 | const pass = value.match(/^\d{4}$/i) 51 | if (!pass) 52 | return 'Please enter a valid code (4 digits)' 53 | 54 | return new Promise((resolve, reject) => { 55 | const data = { 56 | 'pvdid': 1, 57 | 'id': 99, 58 | 'cmd': 'CHECK_OTP', 59 | 'data': { 60 | 'imei': imei, 61 | 'phone': answers.phone, 62 | 'code': value, 63 | 'os': 'android', 64 | 'osver': 'M4B30Z' 65 | } 66 | } 67 | axios.post(null, data) 68 | .then(response => { 69 | if (response.data.data && response.data.data.token) { 70 | token = response.data.data.token 71 | resolve(true) 72 | } else { 73 | const error = `Could NOT get the token: ${response.data.data ? response.data.data.res_desc : JSON.stringify(response.data)}` 74 | reject(error) 75 | } 76 | }) 77 | .catch(err => { 78 | const error = `Could NOT get the token: : "${err.response ? (err.response.data.error_description || err.response.data.error) : err}"` 79 | console.log(error) 80 | reject(error) 81 | }) 82 | }) 83 | 84 | } 85 | } 86 | ] 87 | 88 | inquirer.prompt(questions).then(() => { 89 | console.log('\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') 90 | console.log('Your token is ->', token) 91 | console.log('Your imei is ->', imei) 92 | console.log('~~~~~~~~~~~~~~~~~~~~~~ DONE ~~~~~~~~~~~~~~~~~~~~~~\n') 93 | }) 94 | 95 | function generateIMEI () { 96 | const min = Math.pow(10, 7) 97 | const max = Math.pow(10, 8) -1 98 | return '2b950000' + (Math.floor(Math.random() * (max - min) + min) + 1) 99 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ElectraApi = require('./electra/api') 2 | const syncHomeKitCache = require('./electra/syncHomeKitCache') 3 | const refreshState = require('./electra/refreshState') 4 | const path = require('path') 5 | const storage = require('node-persist') 6 | const PLUGIN_NAME = 'homebridge-electra-smart' 7 | const PLATFORM_NAME = 'ElectraSmart' 8 | 9 | module.exports = (api) => { 10 | api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, ElectraSmartPlatform) 11 | } 12 | 13 | class ElectraSmartPlatform { 14 | constructor(log, config, api) { 15 | 16 | this.cachedAccessories = [] 17 | this.activeAccessories = [] 18 | this.log = log 19 | this.api = api 20 | this.storage = storage 21 | this.refreshState = refreshState(this) 22 | this.syncHomeKitCache = syncHomeKitCache(this) 23 | this.name = config['name'] || PLATFORM_NAME 24 | this.disableFan = config['disableFan'] || false 25 | this.disableDry = config['disableDry'] || false 26 | this.swingDirection = config['swingDirection'] || 'both' 27 | this.minTemp = config['minTemperature'] || 16 28 | this.maxTemp = config['maxTemperature'] || 30 29 | this.debug = config['debug'] || false 30 | this.excludeList = config['excludeList'] || [] 31 | this.PLUGIN_NAME = PLUGIN_NAME 32 | this.PLATFORM_NAME = PLATFORM_NAME 33 | 34 | // ~~~~~~~~~~~~~~~~~~~~~ Electra Specials ~~~~~~~~~~~~~~~~~~~~~ // 35 | 36 | this.token = config['token'] 37 | this.imei = config['imei'] 38 | 39 | if (!this.token || !this.imei) { 40 | this.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX -- ERROR -- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n') 41 | this.log('Can\'t start homebridge-electra-smart plugin without "token" and "imei" !!\n') 42 | this.log('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n') 43 | return 44 | } 45 | 46 | 47 | this.persistPath = path.join(this.api.user.persistPath(), '/../electra-persist') 48 | this.emptyState = {devices:{}} 49 | this.CELSIUS_UNIT = 'C' 50 | this.FAHRENHEIT_UNIT = 'F' 51 | let requestedInterval = config['statePollingInterval'] || 90 // default polling time is 90 seconds 52 | if (requestedInterval < 30) 53 | requestedInterval = 30 54 | this.locations = [] 55 | 56 | // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // 57 | 58 | this.setProcessing = false 59 | this.pollingInterval = null 60 | this.processingState = false 61 | this.interval = requestedInterval * 1000 62 | 63 | // define debug method to output debug logs when enabled in the config 64 | this.log.easyDebug = (...content) => { 65 | if (this.debug) { 66 | this.log(content.reduce((previous, current) => { 67 | return previous + ' ' + current 68 | })) 69 | } else 70 | this.log.debug(content.reduce((previous, current) => { 71 | return previous + ' ' + current 72 | })) 73 | } 74 | 75 | this.api.on('didFinishLaunching', async () => { 76 | 77 | await this.storage.init({ 78 | dir: this.persistPath, 79 | forgiveParseErrors: true 80 | }) 81 | 82 | 83 | this.cachedState = await this.storage.getItem('electra-state') || this.emptyState 84 | if (!this.cachedState.devices) 85 | this.cachedState = this.emptyState 86 | 87 | this.ElectraApi = await ElectraApi(this) 88 | 89 | try { 90 | this.devices = await this.ElectraApi.getDevices() 91 | await this.storage.setItem('electra-devices', this.devices) 92 | } catch(err) { 93 | this.log('ERR:', err) 94 | this.devices = await this.storage.getItem('electra-devices') || [] 95 | } 96 | 97 | this.syncHomeKitCache() 98 | 99 | this.pollingInterval = setInterval(this.refreshState, this.interval) 100 | 101 | }) 102 | 103 | } 104 | 105 | configureAccessory(accessory) { 106 | this.cachedAccessories.push(accessory) 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "ElectraSmart", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "customUi": true, 6 | "headerDisplay": "Homebridge plugin for Electra Smart A/C
To retrieve credentials (`imei` && `token`), you must run `electra-extract` command in terminal", 7 | "footerDisplay": "Created by @nitaybz", 8 | "schema": { 9 | "type": "object", 10 | "properties": { 11 | "imei": { 12 | "title": "IMEI", 13 | "description": "This value can be obtain via terminal command: `electra-extract`", 14 | "type": "string", 15 | "required": true 16 | }, 17 | "token": { 18 | "title": "Token", 19 | "description": "This value can be obtain via terminal command: `electra-extract`", 20 | "type": "string", 21 | "required": true 22 | }, 23 | "disableFan": { 24 | "title": "Disable Fan Accessory", 25 | "description": "Disable FAN mode control - remove extra fan accessory", 26 | "type": "boolean", 27 | "default": false, 28 | "required": false 29 | }, 30 | "disableDry": { 31 | "title": "Disable Dry Accessory", 32 | "description": "Disable DRY mode control - remove extra dehumidifier accessory", 33 | "type": "boolean", 34 | "default": false, 35 | "required": false 36 | }, 37 | "statePollingInterval": { 38 | "title": "AC Device Status Polling Interval in Seconds", 39 | "description": "Time in seconds between each status polling of the Electra devices (set to 0 for no polling)", 40 | "default": 90, 41 | "type": "integer", 42 | "minimum": 0, 43 | "maximum": 600 44 | }, 45 | "minTemperature": { 46 | "title": "Minimum Temperature", 47 | "description": "Minimum Temperature to show in HomeKit", 48 | "default": 16, 49 | "type": "integer", 50 | "minimum": 10, 51 | "maximum": 35 52 | }, 53 | "maxTemperature": { 54 | "title": "Maximum Temperature", 55 | "description": "Maximum Temperature to show in HomeKit", 56 | "default": 30, 57 | "type": "integer", 58 | "minimum": 10, 59 | "maximum": 35 60 | }, 61 | "swingDirection": { 62 | "title": "Swing Direction Control", 63 | "description": "Choose what kind of swing you would like to control in HomeKit", 64 | "type": "string", 65 | "default": "both", 66 | "required": true, 67 | "oneOf": [ 68 | { "title": "Both", "enum": [ "both" ] }, 69 | { "title": "Vertical", "enum": [ "vertical" ] }, 70 | { "title": "Horizontal", "enum": [ "horizontal" ] } 71 | ] 72 | }, 73 | "excludeList": { 74 | "title": "Devices to exclude (Name/ID/Serial/Mac)", 75 | "description": "Add devices identifier (Name, ID from logs or serial from Home app) to exclude from homebridge", 76 | "type": "array", 77 | "items": { 78 | "type": "string" 79 | } 80 | }, 81 | "debug": { 82 | "title": "Enable Debug Logs", 83 | "description": "When checked, the plugin will produce extra logs for debugging purposes", 84 | "type": "boolean", 85 | "default": false, 86 | "required": false 87 | } 88 | } 89 | }, 90 | "layout": [ 91 | { 92 | "key": "imei" 93 | }, 94 | { 95 | "key": "token" 96 | }, 97 | { 98 | "key": "disableFan" 99 | }, 100 | { 101 | "key": "disableDry" 102 | }, 103 | { 104 | "key": "debug" 105 | }, 106 | { 107 | "key": "excludeList", 108 | "title": "Devices to exclude (Name/ID/Serial/Mac)", 109 | "description": "Add devices identifier (Name, ID from logs or serial from Home app) to exclude from homebridge", 110 | "type": "array", 111 | "items": { 112 | "type": "string" 113 | } 114 | }, 115 | { 116 | "type": "fieldset", 117 | "expandable": true, 118 | "title": "Advanced Settings", 119 | "description": "Don't change these, unless you understand what you're doing.", 120 | "items": [ 121 | "statePollingInterval", 122 | "minTemperature", 123 | "maxTemperature", 124 | "swingDirection" 125 | ] 126 | } 127 | ] 128 | } -------------------------------------------------------------------------------- /electra/api.js: -------------------------------------------------------------------------------- 1 | const axiosLib = require('axios'); 2 | let axios = axiosLib.create(); 3 | 4 | let log, ssid, storage, lastSIDRequest 5 | 6 | module.exports = async function (platform) { 7 | log = platform.log 8 | storage = platform.storage 9 | ssid = await storage.getItem('electra-ssid') 10 | 11 | axios.defaults.baseURL = 'https://app.ecpiot.co.il/mobile/mobilecommand' 12 | axios.defaults.headers = { 13 | 'user-agent': 'Electra Client' 14 | } 15 | 16 | 17 | return { 18 | 19 | getDevices: async () => { 20 | const sid = await getSID(platform.imei, platform.token) 21 | const devicesResponse = await apiRequest(sid, 'GET_DEVICES') 22 | 23 | if (!Array.isArray(devicesResponse.devices)) 24 | throw 'Can\'t get devices from Electra API' 25 | 26 | let devices = devicesResponse.devices.filter(device => device.deviceTypeName === 'A/C' && !platform.excludeList.includes(device.id) && !platform.excludeList.includes(device.sn) && !platform.excludeList.includes(device.mac) && !platform.excludeList.includes(device.name)) 27 | devices = devices.map(async device => { 28 | try { 29 | const state = await apiRequest(sid, 'GET_LAST_TELEMETRY', {'id': device.id, 'commandName': 'OPER,DIAG_L2'}) 30 | return { 31 | ...device, 32 | state: state.commandJson 33 | } 34 | 35 | } catch (err) { 36 | log(err) 37 | log(`COULD NOT get ${device.name} (${device.id}) state !! skipping device...`) 38 | throw err 39 | } 40 | }) 41 | 42 | return await Promise.all(devices) 43 | }, 44 | 45 | setState: async (id, state) => { 46 | const sid = await getSID(platform.imei, platform.token) 47 | if ('AC_STSRC' in state) 48 | state['AC_STSRC'] = 'WI-FI' 49 | 50 | const data = { 51 | 'id': id, 52 | 'commandJson': JSON.stringify({'OPER': state}) 53 | } 54 | return await apiRequest(sid, 'SEND_COMMAND', data) 55 | } 56 | } 57 | 58 | } 59 | 60 | 61 | function apiRequest(sid, cmd, data) { 62 | return new Promise((resolve, reject) => { 63 | 64 | const body = { 65 | 'pvdid': 1, 66 | 'id': 99, 67 | 'cmd': cmd, 68 | 'sid': sid 69 | } 70 | if (data) 71 | body.data = data 72 | 73 | log.easyDebug(`Creating request to Electra API --->`) 74 | log.easyDebug('body: ' + JSON.stringify(body)) 75 | 76 | axios.post(null, body) 77 | .then(response => { 78 | if (response.data.data) { 79 | log.easyDebug(`Successful response:`) 80 | log.easyDebug(JSON.stringify(response.data.data)) 81 | resolve(response.data.data) 82 | } else { 83 | const error = `Failed sending API request: ${response.data.data ? response.data.data.res_desc : JSON.stringify(response.data)}` 84 | reject(error) 85 | } 86 | }) 87 | .catch(err => { 88 | const error = `Failed sending API request: '${err.response ? (err.response.data.error_description || err.response.data.error) : err}'` 89 | log(error) 90 | reject(error) 91 | }) 92 | }) 93 | } 94 | 95 | function getSID(imei, token) { 96 | return new Promise((resolve, reject) => { 97 | 98 | if (ssid && new Date().getTime() < ssid.expirationDate) { 99 | log.easyDebug('Found valid ssid in cache', ssid.key) 100 | resolve(ssid.key) 101 | return 102 | } 103 | 104 | const SIDDelay = 600000 // 10 minutes delay between session id request 105 | if (lastSIDRequest && new Date().getTime() < (lastSIDRequest + SIDDelay)) { 106 | log.error('Session ID was requested less than 5 minutes ago! waiting in order to prevent "intruder lockdown"...') 107 | reject(new Error('Session ID was requested less than 5 minutes ago! waiting in order to prevent "intruder lockdown"...')) 108 | return 109 | } 110 | 111 | lastSIDRequest = new Date().getTime() 112 | 113 | let body = { 114 | 'pvdid': 1, 115 | 'id': 99, 116 | 'cmd': 'VALIDATE_TOKEN', 117 | 'data': { 118 | 'imei': imei, 119 | 'token': token, 120 | 'os': 'android', 121 | 'osver': 'M4B30Z' 122 | } 123 | } 124 | 125 | axios.post(null, body) 126 | .then(response => { 127 | if (response.data.data && response.data.data.sid) { 128 | const newSsid = response.data.data.sid 129 | log.easyDebug(`Successful SID response: ${newSsid}`) 130 | ssid = { 131 | key: newSsid, 132 | expirationDate: new Date().getTime() + (1000 * 60 * 60) // one hour 133 | } 134 | storage.setItem('electra-ssid', ssid) 135 | resolve(newSsid) 136 | } else { 137 | const error = `Could NOT get Session ID: ${response.data.data ? response.data.data.res_desc : JSON.stringify(response.data)}` 138 | reject(error) 139 | } 140 | }) 141 | .catch(err => { 142 | const error = `Could NOT get Session ID:: : '${err.response ? (err.response.data.error_description || err.response.data.error) : err}'` 143 | log(error) 144 | reject(error) 145 | }) 146 | }) 147 | } -------------------------------------------------------------------------------- /electra/unified.js: -------------------------------------------------------------------------------- 1 | 2 | const deviceCapabilities = { 3 | COOL: { 4 | temperatures: { 5 | C: { 6 | min: 16, 7 | max: 30 8 | } 9 | }, 10 | fanSpeeds: ['LOW', 'MED', 'HIGH', 'AUTO'], 11 | autoFanSpeed: true, 12 | swing: false 13 | }, 14 | HEAT: { 15 | temperatures: { 16 | C: { 17 | min: 16, 18 | max: 30 19 | } 20 | }, 21 | fanSpeeds: ['LOW', 'MED', 'HIGH', 'AUTO'], 22 | autoFanSpeed: true, 23 | swing: false 24 | }, 25 | AUTO: { 26 | temperatures: { 27 | C: { 28 | min: 16, 29 | max: 30 30 | } 31 | }, 32 | fanSpeeds: ['LOW', 'MED', 'HIGH', 'AUTO'], 33 | autoFanSpeed: true, 34 | swing: false 35 | }, 36 | DRY: { 37 | fanSpeeds: ['LOW', 'MED', 'HIGH', 'AUTO'], 38 | autoFanSpeed: true, 39 | swing: false 40 | }, 41 | FAN: { 42 | fanSpeeds: ['LOW', 'MED', 'HIGH', 'AUTO'], 43 | autoFanSpeed: true, 44 | swing: false 45 | } 46 | } 47 | 48 | function fanSpeedToHK(value, fanSpeeds) { 49 | if (value === 'AUTO') 50 | return 0 51 | 52 | fanSpeeds = fanSpeeds.filter(speed => speed !== 'AUTO') 53 | const totalSpeeds = fanSpeeds.length 54 | const valueIndex = fanSpeeds.indexOf(value) + 1 55 | return Math.round(100 * valueIndex / totalSpeeds) 56 | } 57 | 58 | function HKToFanSpeed(value, fanSpeeds) { 59 | let selected = 'AUTO' 60 | if (!fanSpeeds.includes('AUTO')) 61 | selected = fanSpeeds[0] 62 | 63 | if (value !== 0) { 64 | fanSpeeds = fanSpeeds.filter(speed => speed !== 'AUTO') 65 | const totalSpeeds = fanSpeeds.length 66 | for (let i = 0; i < fanSpeeds.length; i++) { 67 | if (value <= (100 * (i + 1) / totalSpeeds)) { 68 | selected = fanSpeeds[i] 69 | break 70 | } 71 | } 72 | } 73 | return selected 74 | } 75 | 76 | module.exports = { 77 | 78 | deviceInformation: device => { 79 | return { 80 | id: device.id, 81 | model: device.model || 'unknown', 82 | serial: device.sn !== '0000000000' ? device.sn : device.mac, 83 | manufacturer: device.manufactor, 84 | roomName: device.name, 85 | temperatureUnit: 'C', 86 | filterService: true 87 | } 88 | }, 89 | 90 | capabilities: (device) => { 91 | 92 | try { 93 | const deviceState = JSON.parse(device.state.OPER).OPER 94 | 95 | if ('HSWING' in deviceState || 'VSWING' in deviceState) { 96 | Object.keys(deviceCapabilities).forEach(mode => { 97 | deviceCapabilities[mode].swing = true 98 | }) 99 | } 100 | } catch (err) { 101 | // device.log('Error: Can\'t get State!') 102 | } 103 | return deviceCapabilities 104 | }, 105 | 106 | acState: device => { 107 | let deviceState, deviceMeasurements 108 | try { 109 | deviceState = JSON.parse(device.rawState.OPER).OPER 110 | } catch (err) { 111 | device.log.error('Error: Can\'t get State! ---> returning OFF state') 112 | device.log.error(err.stack || err.message) 113 | device.log.easyDebug(device.rawState || err) 114 | 115 | return null 116 | } 117 | 118 | try { 119 | deviceMeasurements = JSON.parse(device.rawState.DIAG_L2).DIAG_L2 120 | } catch (err) { 121 | device.log.easyDebug('Error: Can\'t get Measurements! ---> returning 0 for current temp') 122 | device.log.easyDebug('DIAG_L2:') 123 | device.log.easyDebug(device.rawState.DIAG_L2) 124 | 125 | return null 126 | } 127 | 128 | const state = { 129 | active: (deviceState.AC_MODE !== 'STBY' && !('TURN_ON_OFF' in deviceState)) || (('TURN_ON_OFF' in deviceState) && deviceState.TURN_ON_OFF !== 'OFF'), 130 | targetTemperature: parseInt(deviceState.SPT), 131 | currentTemperature: Math.abs(parseInt(deviceMeasurements.I_RAT || deviceMeasurements.I_CALC_AT || 0)) 132 | } 133 | 134 | if (state.active || deviceState.TURN_ON_OFF) { 135 | state.mode = deviceState.AC_MODE 136 | } 137 | 138 | const modeCapabilities = device.capabilities[state.mode || 'COOL'] 139 | 140 | 141 | if ('swing' in modeCapabilities && modeCapabilities.swing) { 142 | 143 | let vEnabled = true 144 | let hEnabled = true 145 | switch (device.swingDirection) { 146 | case 'vertical': 147 | if ('VSWING' in deviceState) 148 | state.swing = deviceState.VSWING === 'ON' ? 'SWING_ENABLED' : 'SWING_DISABLED' 149 | break 150 | case 'horizontal': 151 | if ('HSWING' in deviceState) 152 | state.swing = deviceState.HSWING === 'ON' ? 'SWING_ENABLED' : 'SWING_DISABLED' 153 | break 154 | default: 155 | if ('VSWING' in deviceState && deviceState.VSWING === 'OFF') 156 | vEnabled = false 157 | if ('HSWING' in deviceState && deviceState.HSWING === 'OFF') 158 | hEnabled = false 159 | state.swing = vEnabled && hEnabled ? 'SWING_ENABLED' : 'SWING_DISABLED' 160 | break 161 | } 162 | } 163 | 164 | if ('FANSPD' in deviceState) 165 | state.fanSpeed = fanSpeedToHK(deviceState.FANSPD, modeCapabilities.fanSpeeds) 166 | 167 | if (device.filterService) { 168 | state.filterChange = deviceState.CLEAR_FILT === 'ON' ? 'CHANGE_FILTER' : 'FILTER_OK' 169 | } 170 | 171 | return state 172 | }, 173 | 174 | formattedState: (device, newState) => { 175 | const lastState = JSON.parse(device.rawState.OPER).OPER 176 | 177 | if (!newState.active) { 178 | if ('TURN_ON_OFF' in lastState) 179 | lastState.TURN_ON_OFF = 'OFF' 180 | else 181 | lastState.AC_MODE = 'STBY' 182 | return lastState 183 | } 184 | 185 | if ('TURN_ON_OFF' in lastState) 186 | lastState.TURN_ON_OFF = 'ON' 187 | 188 | const acState = { 189 | ...lastState, 190 | AC_MODE: newState.mode, 191 | SPT: typeof lastState.SPT === 'string' ? newState.targetTemperature.toString() : newState.targetTemperature 192 | } 193 | 194 | if ('swing' in device.capabilities[newState.mode] && device.capabilities[newState.mode].swing) { 195 | 196 | const swingState = newState.swing === 'SWING_ENABLED' ? 'ON' : 'OFF' 197 | 198 | switch (device.swingDirection) { 199 | case 'vertical': 200 | if ('VSWING' in acState) 201 | acState.VSWING = swingState 202 | if ('HSWING' in acState) 203 | acState.HSWING = 'OFF' 204 | break 205 | case 'horizontal': 206 | if ('VSWING' in acState) 207 | acState.VSWING = 'OFF' 208 | if ('HSWING' in acState) 209 | acState.HSWING = swingState 210 | break 211 | default: 212 | if ('VSWING' in acState) 213 | acState.VSWING = swingState 214 | if ('HSWING' in acState) 215 | acState.HSWING = swingState 216 | break 217 | } 218 | } 219 | 220 | if ('fanSpeeds' in device.capabilities[newState.mode] && device.capabilities[newState.mode].fanSpeeds.length) { 221 | acState['FANSPD'] = HKToFanSpeed(newState.fanSpeed, device.capabilities[newState.mode].fanSpeeds) 222 | } 223 | 224 | return acState 225 | } 226 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | # homebridge-electra-smart 6 | 7 | [![Downloads](https://img.shields.io/npm/dt/homebridge-electra-smart.svg?color=critical)](https://www.npmjs.com/package/homebridge-electra-smart) 8 | [![Version](https://img.shields.io/npm/v/homebridge-electra-smart)](https://www.npmjs.com/package/homebridge-electra-smart)
9 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/ZX3wSZpXaP)
10 | [![certified-hoobs-plugin](https://badgen.net/badge/HOOBS/Certified/yellow)](https://plugins.hoobs.org?ref=10876) [![hoobs-support](https://badgen.net/badge/HOOBS/Support/yellow)](https://support.hoobs.org?ref=10876) 11 | 12 | 13 | [Homebridge](https://github.com/nfarina/homebridge) plugin for Electra A/C that works with Electra Smart app. 14 | 15 | 16 | 17 | ### Requirements 18 | 19 |   20 |   21 | 22 | 23 | check with: `node -v` & `homebridge -V` and update if needed 24 | 25 | # Installation 26 | 27 | This plugin is Homebridge verified and HOOBS certified and can be easily installed and configured through their UI. 28 | 29 | **To use this plugin you must provide `token` and `imei`** which can be obtain in 2 different ways: 30 | 31 | 1. Using the latest Homebridge config UI version (v4.32.0), you can obtain `token` and `imei` easily through the plugin settings and fill all the needed configuration. 32 | 33 | 2. After installing the plugin, open the terminal and run the command: `electra-extract`. follow the instructions to get the token & imei. 34 | 35 | \* All methods require to have your phone (the one that was signed in to Electra Smart) 36 | 37 | --------- 38 | 39 | 1. Install homebridge using: `sudo npm install -g homebridge --unsafe-perm` 40 | 2. Install this plugin using: `sudo npm install -g homebridge-electra-smart` 41 | 3. Run the command `electra-extract` in terminal and follow instructions to extract token and imei. 42 | 4. Update your configuration file. See `config-sample.json` in this repository for a sample. 43 | 44 | \* install from git: `sudo npm install -g git+https://github.com/nitaybz/homebridge-electra-smart.git` 45 | 46 | 47 | ## Config file 48 | 49 | #### Easy config (required): 50 | 51 | ``` json 52 | "platforms": [ 53 | { 54 | "platform": "ElectraSmart", 55 | "imei": "2b950000*************", 56 | "token": "**************************" 57 | } 58 | ] 59 | ``` 60 | 61 | #### Advanced config (optional): 62 | 63 | ``` json 64 | "platforms": [ 65 | { 66 | "platform": "ElectraSmart", 67 | "imei": "2b950000*************", 68 | "token": "**************************", 69 | "disableFan": false, 70 | "disableDry": false, 71 | "minTemperature": 16, 72 | "maxTemperature": 30, 73 | "swingDirection": "both", 74 | "statePollingInterval": 90, 75 | "debug": false 76 | } 77 | ] 78 | ``` 79 | 80 | 81 | ### Configurations Table 82 | 83 | | Parameter | Description | Required | Default | type | 84 | | -------------------------------- | ------------------------------------------------------- |:--------:|:--------:|:--------:| 85 | | `platform` | always "ElectraSmart" | ✓ | - | String | 86 | | `imei` | Generated IMEI: obtain from terminal command - `electra-extract` | ✓ | - | String | 87 | | `token` | Access Token: obtain from terminal command - `electra-extract` | ✓ | - | String | 88 | | `disableFan`       | When set to `true`, it will disable the FAN accessory |       | `false` | Boolean | 89 | | `disableDry`       | When set to `true`, it will disable the DRY accessory  |         | `false` | Boolean | 90 | | `statePollingInterval`   | Time in seconds between each status polling of the Electra devices (set to 0 for no polling)    | `90` | Integer | 91 | | `swingDirection`       | Choose what kind of swing you would like to control in HomeKit. can be `"vertical"`, `"horizontal"` or `"both"`  |         | `"both"` | Boolean | 92 | | `minTemperature`       | Minimum Temperature to show in HomeKit Control   |         | `16` | Integer | 93 | | `maxTemperature`       | Maximum Temperature to show in HomeKit Control  |         | `30` | Integer | 94 | | `debug`       | When set to `true`, the plugin will produce extra logs for debugging purposes |       | `false` | Boolean | 95 | 96 | ### Fan speeds & "AUTO" speed 97 | Since HomeKit control over fan speed is with a slider between 0-100, the plugin converts the steps you have in the Electra app to values between 1 to 100, when 100 is highest and 1 is lowest. Setting the fan speed to 0, should actually set it to "AUTO" speed. 98 | 99 | *Available fan speeds: AUTO, LOW, MED, HIGH* 100 | 101 | ### Swing 102 | Swing support is added automatically if supported. 103 | Since HomeKit only have one control for swing, you can choose which swing type you would like HomeKit to control: vertical, horizontal or both (default). 104 | 105 | ### Issues & Debug 106 | 107 | #### I can't control the device, it always goes to previous state 108 | 109 | Check the internet connection and that you can control the device from Electra Smart app. 110 | If that doesn't help, turn on debug logs in the plugin settings and look for errors. 111 | 112 | #### Log error shows "intruder lockout" 113 | 114 | Electra detected that the plugin is spamming the api and consider it as intruder. 115 | To fix this issue immediately you can refresh the token and imei by deleting them in the config UI and clicking on the button to fetch them back. 116 | You can potentially prevent this error by setting polling interval to a very high number or 0. 117 | 118 | #### others 119 | 120 | If you experience any issues with the plugins please refer to the [Issues](https://github.com/nitaybz/homebridge-electra-smart/issues) tab or [electra-smart Discord channel](https://discord.gg/ZX3wSZpXaP) and check if your issue is already described there, if it doesn't, please create a new issue with as much detailed information as you can give (logs are crucial).
121 | 122 | if you want to even speed up the process, you can add `"debug": true` to your config, which will give me more details on the logs and speed up fixing the issue. 123 | 124 | ------------------------------------------- 125 | 126 | ## Support homebridge-electra-smart 127 | 128 | **homebridge-electra-smart** 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. 129 | 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. 130 | 131 |
132 |
133 | -------------------------------------------------------------------------------- /homekit/StateManager.js: -------------------------------------------------------------------------------- 1 | const unified = require('../electra/unified') 2 | 3 | let Characteristic 4 | 5 | function toFahrenheit(value) { 6 | return Math.round((value * 1.8) + 32) 7 | } 8 | 9 | function characteristicToMode(characteristic) { 10 | switch (characteristic) { 11 | case Characteristic.TargetHeaterCoolerState.COOL: 12 | return 'COOL' 13 | case Characteristic.TargetHeaterCoolerState.HEAT: 14 | return 'HEAT' 15 | case Characteristic.TargetHeaterCoolerState.AUTO: 16 | return 'AUTO' 17 | } 18 | 19 | } 20 | 21 | module.exports = (device, platform) => { 22 | Characteristic = platform.api.hap.Characteristic 23 | const log = platform.log 24 | const ElectraApi = platform.ElectraApi 25 | const setTimeoutDelay = 500 26 | let preventTurningOff, setCommandPromise, newState 27 | 28 | const setCommand = (changes) => { 29 | newState = { 30 | ...device.state 31 | } 32 | Object.keys(changes).forEach(key => { 33 | newState[key] = changes[key] 34 | // Make sure device is not turning off when setting fanSpeed to 0 (AUTO) 35 | if (key === 'fanSpeed' && changes[key] === 0 && device.capabilities[newState.mode].autoFanSpeed) 36 | preventTurningOff = true 37 | }) 38 | 39 | if (!setCommandPromise) { 40 | setCommandPromise = new Promise((resolve, reject) => { 41 | platform.setProcessing = true 42 | setTimeout(async function () { 43 | // Make sure device is not turning off when setting fanSpeed to 0 (AUTO) 44 | if (preventTurningOff && newState.active === false) { 45 | newState.active = true 46 | preventTurningOff = false 47 | } 48 | 49 | if (!newState) { 50 | reject(new Error("Can't set empty state")) 51 | return 52 | } 53 | 54 | const formattedState = unified.formattedState(device, newState) 55 | log(device.name, ' -> Setting New State:') 56 | log(JSON.stringify(formattedState, null, 2)) 57 | 58 | try { 59 | // send state command to Electra 60 | await ElectraApi.setState(device.id, formattedState) 61 | } catch (err) { 62 | log.error(`ERROR setting the following changes: ${JSON.stringify(changes)}`) 63 | log.error(err.message || err.stack) 64 | log.easyDebug(err) 65 | platform.setProcessing = false 66 | device.updateHomeKit(device.state) 67 | setCommandPromise = null 68 | newState = null 69 | reject(err) 70 | return 71 | } 72 | setCommandPromise = null 73 | device.updateHomeKit(newState) 74 | resolve(true) 75 | setTimeout(() => { 76 | platform.setProcessing = false 77 | newState = null 78 | }, 1000) 79 | }, setTimeoutDelay) 80 | }) 81 | } 82 | return setCommandPromise 83 | } 84 | 85 | return { 86 | 87 | get: { 88 | ACActive: () => { 89 | const active = device.state.active 90 | const mode = device.state.mode 91 | return (!active || mode === 'FAN' || mode === 'DRY') ? 0 : 1 92 | }, 93 | 94 | CurrentHeaterCoolerState: () => { 95 | const active = device.state.active 96 | const mode = device.state.mode 97 | const targetTemp = device.state.targetTemperature 98 | const currentTemp = device.state.currentTemperature 99 | 100 | if (!active || mode === 'FAN' || mode === 'DRY') 101 | return Characteristic.CurrentHeaterCoolerState.INACTIVE 102 | else if (mode === 'COOL') 103 | return Characteristic.CurrentHeaterCoolerState.COOLING 104 | else if (mode === 'HEAT') 105 | return Characteristic.CurrentHeaterCoolerState.HEATING 106 | else if (currentTemp > targetTemp) 107 | return Characteristic.CurrentHeaterCoolerState.COOLING 108 | else 109 | return Characteristic.CurrentHeaterCoolerState.HEATING 110 | }, 111 | 112 | TargetHeaterCoolerState: () => { 113 | const active = device.state.active 114 | const mode = device.state.mode 115 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 116 | return (!active || mode === 'FAN' || mode === 'DRY') ? lastMode : Characteristic.TargetHeaterCoolerState[mode] 117 | 118 | }, 119 | 120 | CurrentTemperature: () => { 121 | const currentTemp = device.state.currentTemperature 122 | return currentTemp 123 | }, 124 | 125 | CoolingThresholdTemperature: () => { 126 | const targetTemp = device.state.targetTemperature 127 | return targetTemp 128 | }, 129 | 130 | HeatingThresholdTemperature: () => { 131 | const targetTemp = device.state.targetTemperature 132 | return targetTemp 133 | }, 134 | 135 | TemperatureDisplayUnits: () => { 136 | return device.usesFahrenheit ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS 137 | }, 138 | 139 | CurrentRelativeHumidity: () => { 140 | return device.state.relativeHumidity || 0 141 | }, 142 | 143 | ACSwing: () => { 144 | const swing = device.state.swing 145 | return Characteristic.SwingMode[swing] 146 | }, 147 | 148 | ACRotationSpeed: () => { 149 | const fanSpeed = device.state.fanSpeed 150 | return fanSpeed 151 | }, 152 | 153 | // FILTER 154 | 155 | FilterChangeIndication: () => { 156 | const filterChange = device.state.filterChange 157 | return Characteristic.FilterChangeIndication[filterChange] 158 | }, 159 | 160 | FilterLifeLevel: () => { 161 | const filterLifeLevel = device.state.filterLifeLevel 162 | return filterLifeLevel 163 | }, 164 | 165 | 166 | // FAN 167 | FanActive: () => { 168 | const active = device.state.active 169 | const mode = device.state.mode 170 | 171 | return (!active || mode !== 'FAN') ? 0 : 1 172 | }, 173 | 174 | FanSwing: () => { 175 | const swing = device.state.swing 176 | return Characteristic.SwingMode[swing] 177 | }, 178 | 179 | FanRotationSpeed: () => { 180 | const fanSpeed = device.state.fanSpeed 181 | return fanSpeed 182 | }, 183 | 184 | // DEHUMIDIFIER 185 | DryActive: () => { 186 | const active = device.state.active 187 | const mode = device.state.mode 188 | 189 | return (!active || mode !== 'DRY') ? 0 : 1 190 | }, 191 | 192 | CurrentHumidifierDehumidifierState: () => { 193 | const active = device.state.active 194 | const mode = device.state.mode 195 | 196 | return (!active || mode !== 'DRY') ? 197 | Characteristic.CurrentHumidifierDehumidifierState.INACTIVE : Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING 198 | 199 | }, 200 | 201 | TargetHumidifierDehumidifierState: () => { 202 | return Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER 203 | }, 204 | 205 | DryRotationSpeed: () => { 206 | const fanSpeed = device.state.fanSpeed 207 | return fanSpeed 208 | }, 209 | 210 | DrySwing: () => { 211 | const swing = device.state.swing 212 | return Characteristic.SwingMode[swing] 213 | }, 214 | 215 | }, 216 | 217 | set: { 218 | 219 | ACActive: (state) => { 220 | state = !!state 221 | log.easyDebug(device.name + ' -> Setting AC state Active:', state) 222 | 223 | if (state) { 224 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 225 | const mode = characteristicToMode(lastMode) 226 | log.easyDebug(device.name + ' -> Setting Mode to', mode) 227 | return setCommand({active: true, mode}) 228 | } else if (device.state.mode === 'COOL' || device.state.mode === 'HEAT' || device.state.mode === 'AUTO') 229 | return setCommand({active: false}) 230 | }, 231 | 232 | 233 | TargetHeaterCoolerState: (state) => { 234 | const mode = characteristicToMode(state) 235 | log.easyDebug(device.name + ' -> Setting Target HeaterCooler State:', mode) 236 | return setCommand({active: true, mode}) 237 | }, 238 | 239 | CoolingThresholdTemperature: (targetTemperature) => { 240 | if (device.usesFahrenheit) 241 | log.easyDebug(device.name + ' -> Setting Cooling Threshold Temperature:', toFahrenheit(targetTemperature) + 'ºF') 242 | else 243 | log.easyDebug(device.name + ' -> Setting Cooling Threshold Temperature:', targetTemperature + 'ºC') 244 | 245 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 246 | const mode = characteristicToMode(lastMode) 247 | log.easyDebug(device.name + ' -> Setting Mode to: ' + mode) 248 | return setCommand({active: true, mode, targetTemperature}) 249 | }, 250 | 251 | HeatingThresholdTemperature: (targetTemperature) => { 252 | if (device.usesFahrenheit) 253 | log.easyDebug(device.name + ' -> Setting Heating Threshold Temperature:', toFahrenheit(targetTemperature) + 'ºF') 254 | else 255 | log.easyDebug(device.name + ' -> Setting Heating Threshold Temperature:', targetTemperature + 'ºC') 256 | 257 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 258 | const mode = characteristicToMode(lastMode) 259 | log.easyDebug(device.name + ' -> Setting Mode to: ' + mode) 260 | return setCommand({active: true, mode, targetTemperature}) 261 | }, 262 | 263 | ACSwing: (state) => { 264 | 265 | const swing = state === Characteristic.SwingMode.SWING_ENABLED ? 'SWING_ENABLED' : 'SWING_DISABLED' 266 | log.easyDebug(device.name + ' -> Setting AC Swing:', swing) 267 | 268 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 269 | const mode = characteristicToMode(lastMode) 270 | log.easyDebug(device.name + ' -> Setting Mode to', mode) 271 | 272 | return setCommand({active: true, mode, swing}) 273 | }, 274 | 275 | ACRotationSpeed: (fanSpeed) => { 276 | log.easyDebug(device.name + ' -> Setting AC Rotation Speed:', fanSpeed + '%') 277 | 278 | const lastMode = device.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState).value 279 | const mode = characteristicToMode(lastMode) 280 | log.easyDebug(device.name + ' -> Setting Mode to', mode) 281 | 282 | return setCommand({active: true, mode, fanSpeed}) 283 | }, 284 | 285 | // FILTER 286 | 287 | ResetFilterIndication: () => { 288 | // log.easyDebug(device.name + ' -> Resetting Filter Indication !!') 289 | return 290 | }, 291 | 292 | // FAN 293 | 294 | FanActive: (state) => { 295 | state = !!state 296 | log.easyDebug(device.name + ' -> Setting Fan state Active:', state) 297 | if (state) { 298 | log.easyDebug(device.name + ' -> Setting Mode to: FAN') 299 | return setCommand({active: true, mode: 'FAN'}) 300 | } else if (device.state.mode === 'FAN') 301 | return setCommand({active: false}) 302 | }, 303 | 304 | FanSwing: (state) => { 305 | const swing = state === Characteristic.SwingMode.SWING_ENABLED ? 'SWING_ENABLED' : 'SWING_DISABLED' 306 | log.easyDebug(device.name + ' -> Setting Fan Swing:', swing) 307 | log.easyDebug(device.name + ' -> Setting Mode to: FAN') 308 | return setCommand({active: true, mode: 'FAN', swing}) 309 | }, 310 | 311 | FanRotationSpeed: (fanSpeed) => { 312 | log.easyDebug(device.name + ' -> Setting Fan Rotation Speed:', fanSpeed + '%') 313 | log.easyDebug(device.name + ' -> Setting Mode to: FAN') 314 | return setCommand({active: true, mode: 'FAN', fanSpeed}) 315 | }, 316 | 317 | // DEHUMIDIFIER 318 | 319 | DryActive: (state) => { 320 | state = !!state 321 | log.easyDebug(device.name + ' -> Setting Dry state Active:', state) 322 | if (state) { 323 | log.easyDebug(device.name + ' -> Setting Mode to: DRY') 324 | return setCommand({active: true, mode: 'DRY'}) 325 | } else if (device.state.mode === 'DRY') 326 | return setCommand({active: false}) 327 | }, 328 | 329 | TargetHumidifierDehumidifierState: () => { 330 | log.easyDebug(device.name + ' -> Setting Mode to: DRY') 331 | return setCommand({active: true, mode: 'DRY'}) 332 | }, 333 | 334 | DrySwing: (state) => { 335 | const swing = state === Characteristic.SwingMode.SWING_ENABLED ? 'SWING_ENABLED' : 'SWING_DISABLED' 336 | log.easyDebug(device.name + ' -> Setting Dry Swing:', swing) 337 | log.easyDebug(device.name + ' -> Setting Mode to: DRY') 338 | return setCommand({active: true, mode: 'DRY', swing}) 339 | }, 340 | 341 | DryRotationSpeed: (fanSpeed) => { 342 | log.easyDebug(device.name + ' -> Setting Dry Rotation Speed:', fanSpeed + '%') 343 | log.easyDebug(device.name + ' -> Setting Mode to: DRY') 344 | return setCommand({active: true, mode: 'DRY', fanSpeed}) 345 | }, 346 | 347 | } 348 | 349 | } 350 | } -------------------------------------------------------------------------------- /homekit/AirConditioner.js: -------------------------------------------------------------------------------- 1 | const unified = require('../electra/unified') 2 | let Characteristic, Service, FAHRENHEIT_UNIT 3 | 4 | class AirConditioner { 5 | constructor(device, platform) { 6 | 7 | Service = platform.api.hap.Service 8 | Characteristic = platform.api.hap.Characteristic 9 | FAHRENHEIT_UNIT = platform.FAHRENHEIT_UNIT 10 | 11 | 12 | this.HapError = () => { 13 | return new platform.api.hap.HapStatusError(-70402) 14 | } 15 | 16 | const deviceInfo = unified.deviceInformation(device) 17 | 18 | this.log = platform.log 19 | this.api = platform.api 20 | this.storage = platform.storage 21 | this.cachedState = platform.cachedState 22 | this.id = deviceInfo.id 23 | this.model = deviceInfo.model 24 | this.serial = deviceInfo.serial 25 | this.manufacturer = deviceInfo.manufacturer 26 | this.roomName = deviceInfo.roomName 27 | this.name = this.roomName + ' AC' 28 | this.type = 'AirConditioner' 29 | this.displayName = this.name 30 | this.temperatureUnit = deviceInfo.temperatureUnit 31 | this.usesFahrenheit = this.temperatureUnit === FAHRENHEIT_UNIT 32 | this.disableFan = platform.disableFan 33 | this.disableDry = platform.disableDry 34 | this.swingDirection = platform.swingDirection 35 | this.minTemp = platform.minTemp 36 | this.maxTemp = platform.maxTemp 37 | this.filterService = deviceInfo.filterService 38 | this.capabilities = unified.capabilities(device) 39 | 40 | this.rawState = device.state 41 | this.state = this.cachedState.devices[this.id] 42 | const newState = unified.acState(this) 43 | if (newState) 44 | this.state = newState 45 | else if (!this.state) { 46 | this.error('Can\'t initiate the plugin without initial state!') 47 | this.error('The plugin will NOT create an accessory') 48 | } 49 | 50 | 51 | if (!this.state.mode) 52 | this.state.mode = 'COOL' 53 | 54 | this.stateManager = require('./StateManager')(this, platform) 55 | 56 | this.UUID = this.api.hap.uuid.generate(this.id.toString()) 57 | this.accessory = platform.cachedAccessories.find(accessory => accessory.UUID === this.UUID) 58 | 59 | if (!this.accessory) { 60 | this.log(`Creating New ${platform.PLATFORM_NAME} ${this.type} Accessory in the ${this.roomName}`) 61 | this.accessory = new this.api.platformAccessory(this.name, this.UUID) 62 | this.accessory.context.type = this.type 63 | this.accessory.context.deviceId = this.id 64 | 65 | platform.cachedAccessories.push(this.accessory) 66 | // register the accessory 67 | this.api.registerPlatformAccessories(platform.PLUGIN_NAME, platform.PLATFORM_NAME, [this.accessory]) 68 | } 69 | 70 | this.accessory.context.roomName = this.roomName 71 | 72 | let informationService = this.accessory.getService(Service.AccessoryInformation) 73 | 74 | if (!informationService) 75 | informationService = this.accessory.addService(Service.AccessoryInformation) 76 | 77 | informationService 78 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 79 | .setCharacteristic(Characteristic.Model, this.model) 80 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 81 | 82 | 83 | 84 | this.addHeaterCoolerService() 85 | 86 | if (this.capabilities.FAN && !this.disableFan) 87 | this.addFanService() 88 | else 89 | this.removeFanService() 90 | 91 | 92 | if (this.capabilities.DRY && !this.disableDry) 93 | this.addDryService() 94 | else 95 | this.removeDryService() 96 | 97 | } 98 | 99 | addHeaterCoolerService() { 100 | this.log.easyDebug(`Adding HeaterCooler Service in the ${this.roomName}`) 101 | this.HeaterCoolerService = this.accessory.getService(Service.HeaterCooler) 102 | if (!this.HeaterCoolerService) 103 | this.HeaterCoolerService = this.accessory.addService(Service.HeaterCooler, this.name, 'HeaterCooler') 104 | 105 | this.HeaterCoolerService.getCharacteristic(Characteristic.Active) 106 | .onSet(this.stateManager.set.ACActive) 107 | .updateValue(this.stateManager.get.ACActive()) 108 | 109 | this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentHeaterCoolerState) 110 | .updateValue(this.stateManager.get.CurrentHeaterCoolerState()) 111 | 112 | 113 | const props = [] 114 | 115 | if (this.capabilities.COOL) props.push(Characteristic.TargetHeaterCoolerState.COOL) 116 | if (this.capabilities.HEAT) props.push(Characteristic.TargetHeaterCoolerState.HEAT) 117 | if (this.capabilities.AUTO) props.push(Characteristic.TargetHeaterCoolerState.AUTO) 118 | 119 | this.HeaterCoolerService.getCharacteristic(Characteristic.TargetHeaterCoolerState) 120 | .setProps({validValues: props}) 121 | .updateValue(this.stateManager.get.TargetHeaterCoolerState()) 122 | .onSet(this.stateManager.set.TargetHeaterCoolerState) 123 | 124 | 125 | this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentTemperature) 126 | .setProps({ 127 | minValue: -100, 128 | maxValue: 100, 129 | minStep: 0.1 130 | }) 131 | .updateValue(this.stateManager.get.CurrentTemperature()) 132 | 133 | if (this.capabilities.COOL) { 134 | this.HeaterCoolerService.getCharacteristic(Characteristic.CoolingThresholdTemperature) 135 | .setProps({ 136 | minValue: this.minTemp, 137 | maxValue: this.maxTemp, 138 | minStep: this.usesFahrenheit ? 0.1 : 1 139 | }) 140 | .updateValue(this.stateManager.get.CoolingThresholdTemperature()) 141 | .onSet(this.stateManager.set.CoolingThresholdTemperature) 142 | } 143 | 144 | if (this.capabilities.HEAT) { 145 | this.HeaterCoolerService.getCharacteristic(Characteristic.HeatingThresholdTemperature) 146 | .setProps({ 147 | minValue: this.minTemp, 148 | maxValue: this.maxTemp, 149 | minStep: this.usesFahrenheit ? 0.1 : 1 150 | }) 151 | .updateValue(this.stateManager.get.HeatingThresholdTemperature()) 152 | .onSet(this.stateManager.set.HeatingThresholdTemperature) 153 | } 154 | 155 | if (this.capabilities.AUTO && !this.capabilities.COOL && this.capabilities.AUTO.temperatures) { 156 | this.HeaterCoolerService.getCharacteristic(Characteristic.CoolingThresholdTemperature) 157 | .setProps({ 158 | minValue: this.minTemp, 159 | maxValue: this.maxTemp, 160 | minStep: this.usesFahrenheit ? 0.1 : 1 161 | }) 162 | .updateValue(this.stateManager.get.CoolingThresholdTemperature()) 163 | .onSet(this.stateManager.set.CoolingThresholdTemperature) 164 | 165 | } 166 | 167 | if (this.capabilities.AUTO && !this.capabilities.HEAT && this.capabilities.AUTO.temperatures) { 168 | this.HeaterCoolerService.getCharacteristic(Characteristic.HeatingThresholdTemperature) 169 | .setProps({ 170 | minValue: this.minTemp, 171 | maxValue: this.maxTemp, 172 | minStep: this.usesFahrenheit ? 0.1 : 1 173 | }) 174 | .updateValue(this.stateManager.get.HeatingThresholdTemperature()) 175 | .onSet(this.stateManager.set.HeatingThresholdTemperature) 176 | } 177 | 178 | // this.HeaterCoolerService.getCharacteristic(Characteristic.TemperatureDisplayUnits) 179 | // .updateValue(this.stateManager.get.TemperatureDisplayUnits()) 180 | 181 | // this.HeaterCoolerService.getCharacteristic(Characteristic.CurrentRelativeHumidity) 182 | // .updateValue(this.stateManager.get.CurrentRelativeHumidity()) 183 | 184 | 185 | if ((this.capabilities.COOL && this.capabilities.COOL.swing) || (this.capabilities.HEAT && this.capabilities.HEAT.swing)) { 186 | this.HeaterCoolerService.getCharacteristic(Characteristic.SwingMode) 187 | .updateValue(this.stateManager.get.ACSwing()) 188 | .onSet(this.stateManager.set.ACSwing) 189 | } 190 | 191 | if ( (this.capabilities.COOL && this.capabilities.COOL.fanSpeeds) || (this.capabilities.HEAT && this.capabilities.HEAT.fanSpeeds)) { 192 | this.HeaterCoolerService.getCharacteristic(Characteristic.RotationSpeed) 193 | .updateValue(this.stateManager.get.ACRotationSpeed()) 194 | .onSet(this.stateManager.set.ACRotationSpeed) 195 | } 196 | 197 | if (this.filterService) { 198 | 199 | this.HeaterCoolerService.addOptionalCharacteristic(Characteristic.FilterChangeIndication) 200 | 201 | this.HeaterCoolerService.getCharacteristic(Characteristic.FilterChangeIndication) 202 | .updateValue(this.stateManager.get.FilterChangeIndication()) 203 | 204 | // this.HeaterCoolerService.getCharacteristic(Characteristic.FilterLifeLevel) 205 | // .updateValue(this.stateManager.get.FilterLifeLevel()) 206 | 207 | // this.HeaterCoolerService.getCharacteristic(Characteristic.ResetFilterIndication) 208 | // .onSet(this.stateManager.set.ResetFilterIndication) 209 | } 210 | 211 | } 212 | 213 | addFanService() { 214 | this.log.easyDebug(`Adding Fan Service in the ${this.roomName}`) 215 | 216 | this.FanService = this.accessory.getService(Service.Fanv2) 217 | if (!this.FanService) 218 | this.FanService = this.accessory.addService(Service.Fanv2, this.roomName + ' Fan', 'Fan') 219 | 220 | this.FanService.getCharacteristic(Characteristic.Active) 221 | .updateValue(this.stateManager.get.FanActive()) 222 | .onSet(this.stateManager.set.FanActive) 223 | 224 | if (this.capabilities.FAN.swing) { 225 | this.FanService.getCharacteristic(Characteristic.SwingMode) 226 | .updateValue(this.stateManager.get.FanSwing()) 227 | .onSet(this.stateManager.set.FanSwing) 228 | } 229 | 230 | if (this.capabilities.FAN.fanSpeeds) { 231 | this.FanService.getCharacteristic(Characteristic.RotationSpeed) 232 | .updateValue(this.stateManager.get.FanRotationSpeed()) 233 | .onSet(this.stateManager.set.FanRotationSpeed) 234 | } 235 | 236 | } 237 | 238 | removeFanService() { 239 | let FanService = this.accessory.getService(Service.Fanv2) 240 | if (FanService) { 241 | // remove service 242 | this.log.easyDebug(`Removing Fan Service from the ${this.roomName}`) 243 | this.accessory.removeService(FanService) 244 | } 245 | } 246 | 247 | addDryService() { 248 | this.log.easyDebug(`Adding Dehumidifier Service in the ${this.roomName}`) 249 | 250 | this.DryService = this.accessory.getService(Service.HumidifierDehumidifier) 251 | if (!this.DryService) 252 | this.DryService = this.accessory.addService(Service.HumidifierDehumidifier, this.roomName + ' Dry', 'Dry') 253 | 254 | this.DryService.getCharacteristic(Characteristic.Active) 255 | .updateValue(this.stateManager.get.DryActive()) 256 | .onSet(this.stateManager.set.DryActive) 257 | 258 | 259 | this.DryService.getCharacteristic(Characteristic.CurrentRelativeHumidity) 260 | .updateValue(this.stateManager.get.CurrentRelativeHumidity()) 261 | 262 | this.DryService.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) 263 | .updateValue(this.stateManager.get.CurrentHumidifierDehumidifierState()) 264 | 265 | this.DryService.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) 266 | .setProps({ 267 | minValue: 2, 268 | maxValue: 2, 269 | validValues: [Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER] 270 | }) 271 | .updateValue(this.stateManager.get.TargetHumidifierDehumidifierState()) 272 | .onSet(this.stateManager.set.TargetHumidifierDehumidifierState) 273 | 274 | if (this.capabilities.DRY.swing) { 275 | this.DryService.getCharacteristic(Characteristic.SwingMode) 276 | .updateValue(this.stateManager.get.DrySwing()) 277 | .onSet(this.stateManager.set.DrySwing) 278 | } 279 | 280 | if (this.capabilities.DRY.fanSpeeds) { 281 | this.DryService.getCharacteristic(Characteristic.RotationSpeed) 282 | .updateValue(this.stateManager.get.DryRotationSpeed()) 283 | .onSet(this.stateManager.set.DryRotationSpeed) 284 | } 285 | 286 | } 287 | 288 | removeDryService() { 289 | let DryService = this.accessory.getService(Service.HumidifierDehumidifier) 290 | if (DryService) { 291 | // remove service 292 | this.log.easyDebug(`Removing Dehumidifier Service from the ${this.roomName}`) 293 | this.accessory.removeService(DryService) 294 | } 295 | } 296 | 297 | updateHomeKit(state) { 298 | if (!state) { 299 | this.HeaterCoolerService.getCharacteristic(Characteristic.Active).updateValue(this.HapError()) 300 | if (this.FanService) 301 | this.FanService.getCharacteristic(Characteristic.Active).updateValue(this.HapError()) 302 | if (this.DryService) 303 | this.DryService.getCharacteristic(Characteristic.Active).updateValue(this.HapError()) 304 | return 305 | } 306 | 307 | this.state = state 308 | 309 | // update measurements 310 | this.updateValue('HeaterCoolerService', 'CurrentTemperature', this.state.currentTemperature) 311 | // this.updateValue('HeaterCoolerService', 'CurrentRelativeHumidity', this.state.relativeHumidity) 312 | if (this.capabilities.DRY && !this.disableDry) 313 | this.updateValue('DryService', 'CurrentRelativeHumidity', 0) 314 | 315 | // if status is OFF, set all services to INACTIVE 316 | if (!this.state.active) { 317 | this.updateValue('HeaterCoolerService', 'Active', 0) 318 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.INACTIVE) 319 | 320 | if (this.FanService) 321 | this.updateValue('FanService', 'Active', 0) 322 | 323 | 324 | if (this.DryService) { 325 | this.updateValue('DryService', 'Active', 0) 326 | this.updateValue('DryService', 'CurrentHumidifierDehumidifierState', 0) 327 | } 328 | 329 | return 330 | } 331 | 332 | switch (this.state.mode) { 333 | case 'COOL': 334 | case 'HEAT': 335 | case 'AUTO': 336 | 337 | // turn on HeaterCoolerService 338 | this.updateValue('HeaterCoolerService', 'Active', 1) 339 | 340 | // update temperatures for HeaterCoolerService 341 | this.updateValue('HeaterCoolerService', 'HeatingThresholdTemperature', this.state.targetTemperature) 342 | this.updateValue('HeaterCoolerService', 'CoolingThresholdTemperature', this.state.targetTemperature) 343 | 344 | // update swing for HeaterCoolerService 345 | if (this.capabilities[this.state.mode].swing) 346 | this.updateValue('HeaterCoolerService', 'SwingMode', Characteristic.SwingMode[this.state.swing]) 347 | 348 | // update fanSpeed for HeaterCoolerService 349 | if (this.capabilities[this.state.mode].fanSpeeds) 350 | this.updateValue('HeaterCoolerService', 'RotationSpeed', this.state.fanSpeed) 351 | 352 | // update filter characteristics for HeaterCoolerService 353 | if (this.filterService) { 354 | this.updateValue('HeaterCoolerService', 'FilterChangeIndication', Characteristic.FilterChangeIndication[this.state.filterChange]) 355 | // this.updateValue('HeaterCoolerService', 'FilterLifeLevel', this.state.filterLifeLevel) 356 | } 357 | 358 | // set proper target and current state of HeaterCoolerService 359 | if (this.state.mode === 'COOL') { 360 | this.updateValue('HeaterCoolerService', 'TargetHeaterCoolerState', Characteristic.TargetHeaterCoolerState.COOL) 361 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.COOLING) 362 | } else if (this.state.mode === 'HEAT') { 363 | this.updateValue('HeaterCoolerService', 'TargetHeaterCoolerState', Characteristic.TargetHeaterCoolerState.HEAT) 364 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.HEATING) 365 | } else if (this.state.mode === 'AUTO') { 366 | this.updateValue('HeaterCoolerService', 'TargetHeaterCoolerState', Characteristic.TargetHeaterCoolerState.AUTO) 367 | if (this.state.currentTemperature > this.state.targetTemperature) 368 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.COOLING) 369 | else 370 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.HEATING) 371 | } 372 | 373 | // turn off FanService 374 | if (this.FanService) 375 | this.updateValue('FanService', 'Active', 0) 376 | 377 | // turn off DryService 378 | if (this.DryService) { 379 | this.updateValue('DryService', 'Active', 0) 380 | this.updateValue('DryService', 'CurrentHumidifierDehumidifierState', 0) 381 | } 382 | break 383 | case 'FAN': 384 | if (this.FanService) { 385 | 386 | // turn on FanService 387 | this.updateValue('FanService', 'Active', 1) 388 | 389 | // update swing for FanService 390 | if (this.capabilities.FAN.swing) 391 | this.updateValue('FanService', 'SwingMode', Characteristic.SwingMode[this.state.swing]) 392 | 393 | // update fanSpeed for FanService 394 | if (this.capabilities.FAN.fanSpeeds) 395 | this.updateValue('FanService', 'RotationSpeed', this.state.fanSpeed) 396 | } 397 | 398 | // turn off HeaterCoolerService 399 | this.updateValue('HeaterCoolerService', 'Active', 0) 400 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.INACTIVE) 401 | 402 | // turn off DryService 403 | if (this.DryService) { 404 | this.updateValue('DryService', 'Active', 0) 405 | this.updateValue('DryService', 'CurrentHumidifierDehumidifierState', 0) 406 | } 407 | 408 | break 409 | case 'DRY': 410 | if (this.DryService) { 411 | 412 | // turn on FanService 413 | this.updateValue('DryService', 'Active', 1) 414 | this.updateValue('DryService', 'CurrentHumidifierDehumidifierState', Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING) 415 | 416 | // update swing for FanService 417 | if (this.capabilities.DRY.swing) 418 | this.updateValue('DryService', 'SwingMode', Characteristic.SwingMode[this.state.swing]) 419 | 420 | // update fanSpeed for FanService 421 | if (this.capabilities.DRY.fanSpeeds) 422 | this.updateValue('DryService', 'RotationSpeed', this.state.fanSpeed) 423 | } 424 | 425 | // turn off HeaterCoolerService 426 | this.updateValue('HeaterCoolerService', 'Active', 0) 427 | this.updateValue('HeaterCoolerService', 'CurrentHeaterCoolerState', Characteristic.CurrentHeaterCoolerState.INACTIVE) 428 | 429 | // turn off FanService 430 | if (this.FanService) 431 | this.updateValue('FanService', 'Active', 0) 432 | 433 | break 434 | } 435 | 436 | // cache last state to storage 437 | this.storage.setItem('electra-state', this.cachedState) 438 | } 439 | 440 | updateValue (serviceName, characteristicName, newValue) { 441 | if (newValue !== 0 && newValue !== false && (typeof newValue === 'undefined' || !newValue)) { 442 | this.log.easyDebug(`${this.roomName} - WRONG VALUE -> '${characteristicName}' for ${serviceName} with VALUE: ${newValue}`) 443 | return 444 | } 445 | const minAllowed = this[serviceName].getCharacteristic(Characteristic[characteristicName]).props.minValue 446 | const maxAllowed = this[serviceName].getCharacteristic(Characteristic[characteristicName]).props.maxValue 447 | const validValues = this[serviceName].getCharacteristic(Characteristic[characteristicName]).props.validValues 448 | const currentValue = this[serviceName].getCharacteristic(Characteristic[characteristicName]).value 449 | 450 | if (validValues && !validValues.includes(newValue)) 451 | newValue = currentValue 452 | if (minAllowed && newValue < minAllowed) 453 | newValue = currentValue 454 | else if (maxAllowed && newValue > maxAllowed) 455 | newValue = currentValue 456 | 457 | if (currentValue !== newValue) { 458 | this[serviceName].getCharacteristic(Characteristic[characteristicName]).updateValue(newValue) 459 | this.log.easyDebug(`${this.roomName} - Updated '${characteristicName}' for ${serviceName} with NEW VALUE: ${newValue}`) 460 | } 461 | } 462 | 463 | 464 | } 465 | 466 | 467 | module.exports = AirConditioner -------------------------------------------------------------------------------- /homebridge-ui/public/index.html: -------------------------------------------------------------------------------- 1 | 315 | 318 | 319 |
320 | 321 | 322 | 326 | 327 | 328 | 356 | 357 | 358 |
359 | 360 | --------------------------------------------------------------------------------