├── 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 | [](https://www.npmjs.com/package/homebridge-electra-smart)
8 | [](https://www.npmjs.com/package/homebridge-electra-smart)
9 | [](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [](https://discord.gg/ZX3wSZpXaP)
10 | [](https://plugins.hoobs.org?ref=10876) [](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 |