├── .vscode └── settings.json ├── .gitattributes ├── branding ├── midea.png ├── homebridge.png ├── Dehumidifier.jpeg └── Air_Conditioner.png ├── src ├── enums │ ├── DehumidifierOperationalMode.ts │ ├── MideaSwingMode.ts │ ├── ACOperationalMode.ts │ ├── MideaErrorCodes.ts │ └── MideaDeviceType.ts ├── index.ts ├── crc8.ts ├── commands │ ├── SetCommand.ts │ ├── DehumidifierSetCommand.ts │ └── ACSetCommand.ts ├── PacketBuilder.ts ├── responses │ ├── ApplianceResponse.ts │ ├── DehumidifierApplianceResponse.ts │ └── ACApplianceResponse.ts ├── MigrationHelper.ts ├── Utils.ts ├── BaseCommand.ts ├── Constants.ts ├── MideaPlatform.ts └── MideaAccessory.ts ├── tsconfig.json ├── lib ├── index.js ├── enums │ ├── MideaSwingMode.js │ ├── ACOperationalMode.js │ ├── DehumidifierOperationalMode.js │ ├── MideaOperationalMode.js │ ├── MideaErrorCodes.js │ └── MideaDeviceType.js ├── crc8.js ├── PacketBuilder.js ├── SetCommand.js ├── commands │ ├── SetCommand.js │ ├── DehumidifierSetCommand.js │ └── ACSetCommand.js ├── ApplianceResponse.js ├── responses │ ├── ApplianceResponse.js │ ├── DehumidifierApplianceResponse.js │ └── ACApplianceResponse.js ├── BaseCommand.js ├── Constants.js ├── MigrationHelper.js ├── Utils.js └── MideaPlatform.js ├── LICENSE ├── package.json ├── config.schema.json └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /branding/midea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillaliy/homebridge-midea-air/HEAD/branding/midea.png -------------------------------------------------------------------------------- /branding/homebridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillaliy/homebridge-midea-air/HEAD/branding/homebridge.png -------------------------------------------------------------------------------- /branding/Dehumidifier.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillaliy/homebridge-midea-air/HEAD/branding/Dehumidifier.jpeg -------------------------------------------------------------------------------- /branding/Air_Conditioner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hillaliy/homebridge-midea-air/HEAD/branding/Air_Conditioner.png -------------------------------------------------------------------------------- /src/enums/DehumidifierOperationalMode.ts: -------------------------------------------------------------------------------- 1 | export enum DehumidifierOperationalMode { 2 | Off = 0, 3 | Normal = 1, 4 | Continuous = 2, 5 | Smart = 3, 6 | Dryer = 4 7 | } -------------------------------------------------------------------------------- /src/enums/MideaSwingMode.ts: -------------------------------------------------------------------------------- 1 | // MideaSwingMode enum – Contains the known swing modes 2 | export enum MideaSwingMode { 3 | Horizontal = 3, 4 | Vertical = 12, 5 | Both = 15, 6 | None = 0 7 | } -------------------------------------------------------------------------------- /src/enums/ACOperationalMode.ts: -------------------------------------------------------------------------------- 1 | export enum ACOperationalMode { 2 | Off = 0, 3 | Auto = 1, 4 | Cooling = 2, 5 | Dry = 3, 6 | Heating = 4, 7 | FanOnly = 5, 8 | CustomDry = 6 // automatic dehumidification 9 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "strictNullChecks": false 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules"] 12 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { API } from 'homebridge' 2 | import { MideaPlatform } from './MideaPlatform' 3 | import { MigrationHelper } from './MigrationHelper' 4 | 5 | export = (api: API) => { 6 | new MigrationHelper(console, api.user.configPath()) 7 | api.registerPlatform('homebridge-midea-air', 'midea-air', MideaPlatform); 8 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const MideaPlatform_1 = require("./MideaPlatform"); 3 | const MigrationHelper_1 = require("./MigrationHelper"); 4 | module.exports = (api) => { 5 | new MigrationHelper_1.MigrationHelper(console, api.user.configPath()); 6 | api.registerPlatform('homebridge-midea-air', 'midea-air', MideaPlatform_1.MideaPlatform); 7 | }; 8 | -------------------------------------------------------------------------------- /src/crc8.ts: -------------------------------------------------------------------------------- 1 | import Constants from './Constants' 2 | export default class crc8 { 3 | static calculate(data: any) { 4 | let crc_value = 0; 5 | for (const m of data) { 6 | let k = crc_value ^ m; 7 | if (k > 256) k -= 256; 8 | if (k < 0) k += 256; 9 | crc_value = Constants.crc8_854_table[k]; 10 | } 11 | return crc_value; 12 | } 13 | } -------------------------------------------------------------------------------- /src/enums/MideaErrorCodes.ts: -------------------------------------------------------------------------------- 1 | // MideaErrorCodes enum – This is a list of errors and their meaning to better understand them in code 2 | export enum MideaErrorCodes { 3 | // MideaAir / NetHomePlus 4 | DeviceUnreachable = 3123, 5 | CommandNotAccepted = 3176, 6 | InvalidLogin = 3101, 7 | InvalidSession = 3106, 8 | SignIllegal = 3301, 9 | ValueIllegal = 3004, 10 | NoOpenIdOrUnionId = 3002, 11 | // MSmartHome 12 | UnknownError = 1, 13 | InvalidArgument = 30005, 14 | NoAccessTokenSupplied = 40001, 15 | NoRouteMatchedWithThoseValues = 40404, 16 | BadSignature = 44003, 17 | } -------------------------------------------------------------------------------- /lib/enums/MideaSwingMode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.MideaSwingMode = void 0; 4 | // MideaSwingMode enum – Contains the known swing modes 5 | var MideaSwingMode; 6 | (function (MideaSwingMode) { 7 | MideaSwingMode[MideaSwingMode["Horizontal"] = 3] = "Horizontal"; 8 | MideaSwingMode[MideaSwingMode["Vertical"] = 12] = "Vertical"; 9 | MideaSwingMode[MideaSwingMode["Both"] = 15] = "Both"; 10 | MideaSwingMode[MideaSwingMode["None"] = 0] = "None"; 11 | })(MideaSwingMode = exports.MideaSwingMode || (exports.MideaSwingMode = {})); 12 | -------------------------------------------------------------------------------- /lib/crc8.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const Constants_1 = __importDefault(require("./Constants")); 7 | class crc8 { 8 | static calculate(data) { 9 | let crc_value = 0; 10 | for (const m of data) { 11 | let k = crc_value ^ m; 12 | if (k > 256) 13 | k -= 256; 14 | if (k < 0) 15 | k += 256; 16 | crc_value = Constants_1.default.crc8_854_table[k]; 17 | } 18 | return crc_value; 19 | } 20 | } 21 | exports.default = crc8; 22 | -------------------------------------------------------------------------------- /lib/enums/ACOperationalMode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ACOperationalMode = void 0; 4 | var ACOperationalMode; 5 | (function (ACOperationalMode) { 6 | ACOperationalMode[ACOperationalMode["Off"] = 0] = "Off"; 7 | ACOperationalMode[ACOperationalMode["Auto"] = 1] = "Auto"; 8 | ACOperationalMode[ACOperationalMode["Cooling"] = 2] = "Cooling"; 9 | ACOperationalMode[ACOperationalMode["Dry"] = 3] = "Dry"; 10 | ACOperationalMode[ACOperationalMode["Heating"] = 4] = "Heating"; 11 | ACOperationalMode[ACOperationalMode["FanOnly"] = 5] = "FanOnly"; 12 | ACOperationalMode[ACOperationalMode["CustomDry"] = 6] = "CustomDry"; // automatic dehumidification 13 | })(ACOperationalMode = exports.ACOperationalMode || (exports.ACOperationalMode = {})); 14 | -------------------------------------------------------------------------------- /lib/enums/DehumidifierOperationalMode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.DehumidifierOperationalMode = void 0; 4 | var DehumidifierOperationalMode; 5 | (function (DehumidifierOperationalMode) { 6 | DehumidifierOperationalMode[DehumidifierOperationalMode["Off"] = 0] = "Off"; 7 | DehumidifierOperationalMode[DehumidifierOperationalMode["Normal"] = 1] = "Normal"; 8 | DehumidifierOperationalMode[DehumidifierOperationalMode["Continuous"] = 2] = "Continuous"; 9 | DehumidifierOperationalMode[DehumidifierOperationalMode["Smart"] = 3] = "Smart"; 10 | DehumidifierOperationalMode[DehumidifierOperationalMode["Dryer"] = 4] = "Dryer"; 11 | })(DehumidifierOperationalMode = exports.DehumidifierOperationalMode || (exports.DehumidifierOperationalMode = {})); 12 | -------------------------------------------------------------------------------- /lib/enums/MideaOperationalMode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.MideaOperationalMode = void 0; 4 | var MideaOperationalMode; 5 | (function (MideaOperationalMode) { 6 | MideaOperationalMode[MideaOperationalMode["Off"] = 0] = "Off"; 7 | MideaOperationalMode[MideaOperationalMode["Auto"] = 1] = "Auto"; 8 | MideaOperationalMode[MideaOperationalMode["Cooling"] = 2] = "Cooling"; 9 | MideaOperationalMode[MideaOperationalMode["Dry"] = 3] = "Dry"; 10 | MideaOperationalMode[MideaOperationalMode["Heating"] = 4] = "Heating"; 11 | MideaOperationalMode[MideaOperationalMode["FanOnly"] = 5] = "FanOnly"; 12 | MideaOperationalMode[MideaOperationalMode["CustomDry"] = 6] = "CustomDry"; // automatic dehumidification 13 | })(MideaOperationalMode = exports.MideaOperationalMode || (exports.MideaOperationalMode = {})); 14 | -------------------------------------------------------------------------------- /src/enums/MideaDeviceType.ts: -------------------------------------------------------------------------------- 1 | export enum MideaDeviceType { 2 | Plug = 0x10, 3 | RemoteController = 0x11, 4 | AirBox = 0x12, 5 | Light = 0x13, 6 | Curtain = 0x14, 7 | MBox = 0x1B, 8 | 9 | Dehumidifier = 0xA1, 10 | AirConditioner = 0xAC, 11 | 12 | MicroWaveOven = 0xB0, 13 | BigOven = 0xB1, 14 | SteamerOven = 0xB2, 15 | Sterilizer = 0xB3, 16 | Toaster = 0xB4, 17 | Hood = 0xB6, 18 | Hob = 0xB7, 19 | VacuumCleaner = 0xB8, 20 | Induction = 0xB9, 21 | 22 | Refrigerator = 0xCA, 23 | MDV = 0xCC, 24 | AirWaterHeater = 0xCD, 25 | 26 | PulsatorWasher = 0xDA, 27 | DurmWasher = 0xDB, 28 | ClothesDryer = 0xDC, 29 | 30 | DishWasher = 0xE1, 31 | ElectricWaterHeater = 0xE2, 32 | GasWaterHeater = 0xE3, 33 | RiceCooker = 0xEA, 34 | InductionCooker = 0xEB, 35 | PressureCooker = 0xEC, 36 | WaterPurifier = 0xED, 37 | SoybeanMachine = 0xEF, 38 | 39 | ElectricFanner = 0xFA, 40 | ElectricHeater = 0xFB, 41 | AirPurifier = 0xFC, 42 | Humidifier = 0xFD, 43 | AirConditionFanner = 0xFE, 44 | 45 | AllType = 0xFF 46 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 hillaliy 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. 22 | -------------------------------------------------------------------------------- /src/commands/SetCommand.ts: -------------------------------------------------------------------------------- 1 | import BaseCommand from '../BaseCommand'; 2 | import { MideaDeviceType } from '../enums/MideaDeviceType' 3 | import { MideaSwingMode } from '../enums/MideaSwingMode' 4 | 5 | export default class SetCommand extends BaseCommand { 6 | 7 | constructor(device_type: MideaDeviceType) { 8 | super(device_type); 9 | }; 10 | // Byte 0x0b 11 | get audibleFeedback() { 12 | return (this.data[0x0b] & 0x40) !== 0; 13 | }; 14 | 15 | set audibleFeedback(feedbackEnabled: boolean) { 16 | this.data[0x0b] &= ~0x40; // Clear the audible bits 17 | this.data[0x0b] |= feedbackEnabled ? 0x40 : 0; 18 | }; 19 | 20 | get powerState() { 21 | return (this.data[0x0b] & 0x01) !== 0; 22 | }; 23 | 24 | set powerState(state) { 25 | this.data[0x0b] &= ~0x01; // Clear the power bit 26 | this.data[0x0b] |= state ? 0x01 : 0; 27 | }; 28 | // Byte 0x0d 29 | get fanSpeed() { 30 | return this.data[0x0d] & 0x7f; 31 | }; 32 | 33 | set fanSpeed(speed: number) { 34 | this.data[0x0d] &= ~0x7f; // Clear the fan speed part 35 | this.data[0x0d] |= speed & 0x7f; 36 | }; 37 | // Byte 0x11 38 | get swingMode() { 39 | return this.data[0x11]; 40 | }; 41 | 42 | set swingMode(mode: MideaSwingMode) { 43 | this.data[0x11] = 0x30; // Clear the mode bit 44 | this.data[0x11] |= mode & 0x3f; 45 | }; 46 | }; -------------------------------------------------------------------------------- /lib/enums/MideaErrorCodes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.MideaErrorCodes = void 0; 4 | // MideaErrorCodes enum – This is a list of errors and their meaning to better understand them in code 5 | var MideaErrorCodes; 6 | (function (MideaErrorCodes) { 7 | // MideaAir / NetHomePlus 8 | MideaErrorCodes[MideaErrorCodes["DeviceUnreachable"] = 3123] = "DeviceUnreachable"; 9 | MideaErrorCodes[MideaErrorCodes["CommandNotAccepted"] = 3176] = "CommandNotAccepted"; 10 | MideaErrorCodes[MideaErrorCodes["InvalidLogin"] = 3101] = "InvalidLogin"; 11 | MideaErrorCodes[MideaErrorCodes["InvalidSession"] = 3106] = "InvalidSession"; 12 | MideaErrorCodes[MideaErrorCodes["SignIllegal"] = 3301] = "SignIllegal"; 13 | MideaErrorCodes[MideaErrorCodes["ValueIllegal"] = 3004] = "ValueIllegal"; 14 | MideaErrorCodes[MideaErrorCodes["NoOpenIdOrUnionId"] = 3002] = "NoOpenIdOrUnionId"; 15 | // MSmartHome 16 | MideaErrorCodes[MideaErrorCodes["UnknownError"] = 1] = "UnknownError"; 17 | MideaErrorCodes[MideaErrorCodes["InvalidArgument"] = 30005] = "InvalidArgument"; 18 | MideaErrorCodes[MideaErrorCodes["NoAccessTokenSupplied"] = 40001] = "NoAccessTokenSupplied"; 19 | MideaErrorCodes[MideaErrorCodes["NoRouteMatchedWithThoseValues"] = 40404] = "NoRouteMatchedWithThoseValues"; 20 | MideaErrorCodes[MideaErrorCodes["BadSignature"] = 44003] = "BadSignature"; 21 | })(MideaErrorCodes = exports.MideaErrorCodes || (exports.MideaErrorCodes = {})); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-midea-air", 3 | "version": "1.7.3", 4 | "description": "Homebridge plugin for Midea AC units", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/hillaliy/homebridge-midea-air.git" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/hillaliy/homebridge-midea-air/issues" 11 | }, 12 | "main": "lib/index.js", 13 | "files": [ 14 | "/lib", 15 | "README.md", 16 | "LICENSE", 17 | "config.schema.json" 18 | ], 19 | "engines": { 20 | "node": ">=16.0.0", 21 | "homebridge": ">=1.5.0" 22 | }, 23 | "author": "Yossi Hillali ", 24 | "license": "MIT", 25 | "dependencies": { 26 | "axios": "^1.2.1", 27 | "axios-cookiejar-support": "^4.0.3", 28 | "node-tunnel": "^4.0.1", 29 | "querystring": "^0.2.1", 30 | "strftime": "^0.10.1", 31 | "tough-cookie": "^4.1.2", 32 | "traverse": "^0.6.7", 33 | "tunnel": "0.0.6" 34 | }, 35 | "keywords": [ 36 | "homebridge", 37 | "homebridge-plugin", 38 | "midea", 39 | "midea-ac", 40 | "homekit", 41 | "air conditioner" 42 | ], 43 | "devDependencies": { 44 | "@types/node": "^18.11.11", 45 | "@types/tough-cookie": "^4.0.2", 46 | "@types/tunnel": "0.0.3", 47 | "homebridge": "^1.5.0", 48 | "ts-node": "^10.9.1", 49 | "typescript": "^4.9.4" 50 | }, 51 | "scripts": { 52 | "build": "tsc" 53 | }, 54 | "funding": { 55 | "type": "paypal", 56 | "url": "https://www.paypal.me/hillaliy" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/PacketBuilder.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class PacketBuilder { 4 | constructor() { 5 | this._command = []; 6 | this.packet = []; 7 | // Init the packet with the header data. 8 | this.packet = [ 9 | 90, 90, 10 | 1, 16, 11 | 92, 0, 12 | 32, 13 | 0, 14 | 1, 0, 0, 0, 15 | 189, 179, 57, 14, 12, 5, 20, 20, 16 | 29, 129, 0, 0, 0, 16, 0, 0, 17 | 0, 4, 2, 0, 0, 1, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 0, 0 // Byte 37-39 22 | ]; 23 | } 24 | set command(command) { 25 | this._command = command.finalize(); 26 | } 27 | finalize() { 28 | // Append the command data to the packet 29 | this.packet = this.packet.concat(this._command); 30 | // Append a basic checksum of the command to the packet (This is apart from the CRC8 that was added in the command) 31 | this.packet = this.packet.concat([this.checksum(this._command.slice(1))]); 32 | // Ehh... I dunno, but this seems to make things work. Padding with 0's 33 | this.packet = this.packet.concat(new Array(49 - this._command.length).fill(0)); 34 | // Set the packet length in the packet! 35 | this.packet[0x04] = this.packet.length; 36 | return this.packet; 37 | } 38 | checksum(data) { 39 | return 255 - (data.reduce((a, b) => a + b) % 256) + 1; 40 | } 41 | } 42 | exports.default = PacketBuilder; 43 | -------------------------------------------------------------------------------- /lib/SetCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const BaseCommand_1 = __importDefault(require("./BaseCommand")); 7 | class SetCommand extends BaseCommand_1.default { 8 | constructor(device_type) { 9 | super(device_type); 10 | } 11 | ; 12 | // Byte 0x0b 13 | get audibleFeedback() { 14 | return (this.data[0x0b] & 0x40) !== 0; 15 | } 16 | ; 17 | set audibleFeedback(feedbackEnabled) { 18 | this.data[0x0b] &= ~0x40; // Clear the audible bits 19 | this.data[0x0b] |= feedbackEnabled ? 0x40 : 0; 20 | } 21 | ; 22 | get powerState() { 23 | return (this.data[0x0b] & 0x01) !== 0; 24 | } 25 | ; 26 | set powerState(state) { 27 | this.data[0x0b] &= ~0x01; // Clear the power bit 28 | this.data[0x0b] |= state ? 0x01 : 0; 29 | } 30 | ; 31 | // Byte 0x0d 32 | get fanSpeed() { 33 | return this.data[0x0d] & 0x7f; 34 | } 35 | ; 36 | set fanSpeed(speed) { 37 | this.data[0x0d] &= ~0x7f; // Clear the fan speed part 38 | this.data[0x0d] |= speed & 0x7f; 39 | } 40 | ; 41 | // Byte 0x11 42 | get swingMode() { 43 | return this.data[0x11]; 44 | } 45 | ; 46 | set swingMode(mode) { 47 | this.data[0x11] = 0x30; // Clear the mode bit 48 | this.data[0x11] |= mode & 0x3f; 49 | } 50 | ; 51 | } 52 | exports.default = SetCommand; 53 | ; 54 | -------------------------------------------------------------------------------- /lib/commands/SetCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const BaseCommand_1 = __importDefault(require("../BaseCommand")); 7 | class SetCommand extends BaseCommand_1.default { 8 | constructor(device_type) { 9 | super(device_type); 10 | } 11 | ; 12 | // Byte 0x0b 13 | get audibleFeedback() { 14 | return (this.data[0x0b] & 0x40) !== 0; 15 | } 16 | ; 17 | set audibleFeedback(feedbackEnabled) { 18 | this.data[0x0b] &= ~0x40; // Clear the audible bits 19 | this.data[0x0b] |= feedbackEnabled ? 0x40 : 0; 20 | } 21 | ; 22 | get powerState() { 23 | return (this.data[0x0b] & 0x01) !== 0; 24 | } 25 | ; 26 | set powerState(state) { 27 | this.data[0x0b] &= ~0x01; // Clear the power bit 28 | this.data[0x0b] |= state ? 0x01 : 0; 29 | } 30 | ; 31 | // Byte 0x0d 32 | get fanSpeed() { 33 | return this.data[0x0d] & 0x7f; 34 | } 35 | ; 36 | set fanSpeed(speed) { 37 | this.data[0x0d] &= ~0x7f; // Clear the fan speed part 38 | this.data[0x0d] |= speed & 0x7f; 39 | } 40 | ; 41 | // Byte 0x11 42 | get swingMode() { 43 | return this.data[0x11]; 44 | } 45 | ; 46 | set swingMode(mode) { 47 | this.data[0x11] = 0x30; // Clear the mode bit 48 | this.data[0x11] |= mode & 0x3f; 49 | } 50 | ; 51 | } 52 | exports.default = SetCommand; 53 | ; 54 | -------------------------------------------------------------------------------- /src/PacketBuilder.ts: -------------------------------------------------------------------------------- 1 | import BaseCommand from './BaseCommand'; 2 | export default class PacketBuilder { 3 | _command: number[] = [] 4 | packet: number[] = [] 5 | constructor() { 6 | 7 | // Init the packet with the header data. 8 | this.packet = [ 9 | 90, 90, // Byte 0-1 - Static MSmart header 10 | 1, 16, // Byte 2-3 - mMessageType 11 | 92, 0, // Byte 4-5 - Packet length (reversed, lb first) 12 | 32, // Byte 6 13 | 0, // Byte 7 14 | 1, 0, 0, 0, // Byte 8-11 - MessageID (rollover at 32767) 15 | 189, 179, 57, 14, 12, 5, 20, 20, // Byte 12-19 - Time and Date (ms/ss/mm/HH/DD/MM/YYYY) 16 | 29, 129, 0, 0, 0, 16, 0, 0, // Byte 20-27 - DeviceID (reversed, lb first) 17 | 0, 4, 2, 0, 0, 1, // Byte 28-33 18 | 0, // Byte 34 19 | 0, // Byte 35 20 | 0, // Byte 36 - sequence number 21 | 0, 0, 0 // Byte 37-39 22 | ]; 23 | } 24 | 25 | set command(command: BaseCommand) { 26 | this._command = command.finalize(); 27 | } 28 | 29 | finalize() { 30 | // Append the command data to the packet 31 | this.packet = this.packet.concat(this._command); 32 | // Append a basic checksum of the command to the packet (This is apart from the CRC8 that was added in the command) 33 | this.packet = this.packet.concat([this.checksum(this._command.slice(1))]); 34 | // Ehh... I dunno, but this seems to make things work. Padding with 0's 35 | this.packet = this.packet.concat(new Array(49 - this._command.length).fill(0)); 36 | // Set the packet length in the packet! 37 | this.packet[0x04] = this.packet.length; 38 | return this.packet; 39 | } 40 | 41 | checksum(data: number[]) { 42 | return 255 - (data.reduce((a: number, b: number) => a + b) % 256) + 1; 43 | } 44 | } -------------------------------------------------------------------------------- /src/commands/DehumidifierSetCommand.ts: -------------------------------------------------------------------------------- 1 | import SetCommand from './SetCommand'; 2 | import { MideaDeviceType } from '../enums/MideaDeviceType'; 3 | 4 | export default class DehumidifierSetCommand extends SetCommand { 5 | 6 | constructor(device_type: MideaDeviceType = MideaDeviceType.Dehumidifier) { 7 | super(device_type); 8 | } 9 | // Byte 0x0c 10 | get operationalMode() { 11 | return this.data[0x0c] & 0x0f; 12 | } 13 | 14 | set operationalMode(mode: any) { 15 | this.data[0x0c] &= ~0x0f; // Clear the mode bits 16 | this.data[0x0c] |= mode; 17 | } 18 | // Byte 0x11 19 | get targetHumidity() { 20 | return this.data[0x11] & 0x7f; 21 | } 22 | 23 | set targetHumidity(humidity: number) { 24 | this.data[0x11] &= ~0x7f // Clear the humidity part 25 | this.data[0x11] |= humidity 26 | } 27 | 28 | // Byte 0x13 29 | get ionMode() { 30 | return (this.data[0x13] & 0x40) !== 0; 31 | } 32 | 33 | set ionMode(ionModeEnabled: boolean) { 34 | this.data[0x13] &= ~0x40; // Clear the ion switch bit 35 | this.data[0x13] |= ionModeEnabled ? 0x40 : 0; 36 | } 37 | 38 | get pumpSwitch() { 39 | return (this.data[0x13] & 0x08) !== 0; 40 | } 41 | 42 | set pumpSwitch(pumpSwitchEnabled: boolean) { 43 | this.data[0x13] &= ~0x08; // Clear the pump switch bit 44 | this.data[0x13] |= pumpSwitchEnabled ? 0x08 : 0; 45 | } 46 | 47 | get pumpSwitchFlag() { 48 | return (this.data[0x13] & 0x10) !== 0; 49 | } 50 | 51 | set pumpSwitchFlag(mode: boolean) { 52 | this.data[0x13] &= ~0x10 // Clear the pump switch bit 53 | this.data[0x13] |= mode ? 0x10 : 0; 54 | } 55 | 56 | get sleepSwitch() { 57 | return (this.data[0x13] & 0x20) !== 0; 58 | } 59 | 60 | set sleepSwitch(sleepSwitchEnabled: boolean) { 61 | this.data[0x13] &= ~0x20 // Clear the sleep switch bit 62 | this.data[0x13] |= sleepSwitchEnabled ? 0x20 : 0; 63 | } 64 | // Byte 0x14 65 | get verticalSwing() { 66 | return (this.data[0x14] & 0x14) !== 0; 67 | } 68 | 69 | set verticalSwing(verticalSwingEnabled: boolean) { 70 | this.data[0x14] &= ~0x14; // Clear the sleep switch bit 71 | this.data[0x14] |= verticalSwingEnabled ? 0x14 : 0; 72 | } 73 | // Byte 0x17 74 | get tankWarningLevel() { 75 | return this.data[0x17]; 76 | } 77 | 78 | set tankWarningLevel(level: any) { 79 | this.data[0x17] = level; 80 | } 81 | } -------------------------------------------------------------------------------- /lib/ApplianceResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class ApplianceResponse { 4 | constructor(data) { 5 | // The response data from the appliance includes a packet header which we don't want 6 | this.data = data.slice(0x32); 7 | } 8 | // Byte 0x01 9 | get applianceError() { 10 | return (this.data[0x01] & 0x80) !== 0; 11 | } 12 | get powerState() { 13 | return (this.data[0x01] & 0x1) !== 0; 14 | } 15 | get imodeResume() { 16 | return (this.data[0x01] & 0x4) !== 0; 17 | } 18 | get timerMode() { 19 | return (this.data[0x01] & 0x10) !== 0; 20 | } 21 | get quickChech() { 22 | return (this.data[0x01] & 0x20) !== 0; 23 | } 24 | // Byte 0x03 25 | get fanSpeed() { 26 | return this.data[0x03] & 0x7f; 27 | } 28 | // Byte 0x04 + 0x06 29 | get onTimer() { 30 | const on_timer_value = this.data[0x04]; 31 | const on_timer_minutes = this.data[0x06]; 32 | return { 33 | status: (on_timer_value & 0x80) >> 7 > 0, 34 | hour: (on_timer_value & 0x7c) >> 2, 35 | minutes: (on_timer_value & 0x3) | ((on_timer_minutes & 0xf0) >> 4), 36 | }; 37 | } 38 | // Byte 0x05 + 0x06 39 | get offTimer() { 40 | const off_timer_value = this.data[0x05]; 41 | const off_timer_minutes = this.data[0x06]; 42 | return { 43 | status: (off_timer_value & 0x80) >> 7 > 0, 44 | hour: (off_timer_value & 0x7c) >> 2, 45 | minutes: (off_timer_value & 0x3) | (off_timer_minutes & 0xf), 46 | }; 47 | } 48 | // Byte 0x07 49 | get swingMode() { 50 | return this.data[0x07] & 0x0f; 51 | } 52 | // Byte 0x09 53 | get childSleepMode() { 54 | return (this.data[0x09] & 0x01) > 0; 55 | } 56 | // Byte 0x0a 57 | get sleepFunction() { 58 | return (this.data[0x0a] & 0x01) > 0; 59 | } 60 | get nightLight() { 61 | // This needs a better name, dunno what it actually means 62 | return (this.data[0x0a] & 0x10) > 0; 63 | } 64 | get peakElec() { 65 | // This needs a better name, dunno what it actually means 66 | return (this.data[0x0a] & 0x20) > 0; 67 | } 68 | get naturalFan() { 69 | // This needs a better name, dunno what it actually means 70 | return (this.data[0x0a] & 0x40) > 0; 71 | } 72 | // Byte 0x0d 73 | get humidity() { 74 | return this.data[0x0d] & 0x7f; 75 | } 76 | } 77 | exports.default = ApplianceResponse; 78 | -------------------------------------------------------------------------------- /lib/responses/ApplianceResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class ApplianceResponse { 4 | constructor(data) { 5 | // The response data from the appliance includes a packet header which we don't want 6 | this.data = data.slice(0x32); 7 | } 8 | // Byte 0x01 9 | get applianceError() { 10 | return (this.data[0x01] & 0x80) !== 0; 11 | } 12 | get powerState() { 13 | return (this.data[0x01] & 0x1) !== 0; 14 | } 15 | get imodeResume() { 16 | return (this.data[0x01] & 0x4) !== 0; 17 | } 18 | get timerMode() { 19 | return (this.data[0x01] & 0x10) !== 0; 20 | } 21 | get quickChech() { 22 | return (this.data[0x01] & 0x20) !== 0; 23 | } 24 | // Byte 0x03 25 | get fanSpeed() { 26 | return this.data[0x03] & 0x7f; 27 | } 28 | // Byte 0x04 + 0x06 29 | get onTimer() { 30 | const on_timer_value = this.data[0x04]; 31 | const on_timer_minutes = this.data[0x06]; 32 | return { 33 | status: (on_timer_value & 0x80) >> 7 > 0, 34 | hour: (on_timer_value & 0x7c) >> 2, 35 | minutes: (on_timer_value & 0x3) | ((on_timer_minutes & 0xf0) >> 4), 36 | }; 37 | } 38 | // Byte 0x05 + 0x06 39 | get offTimer() { 40 | const off_timer_value = this.data[0x05]; 41 | const off_timer_minutes = this.data[0x06]; 42 | return { 43 | status: (off_timer_value & 0x80) >> 7 > 0, 44 | hour: (off_timer_value & 0x7c) >> 2, 45 | minutes: (off_timer_value & 0x3) | (off_timer_minutes & 0xf), 46 | }; 47 | } 48 | // Byte 0x07 49 | get swingMode() { 50 | return this.data[0x07] & 0x0f; 51 | } 52 | // Byte 0x09 53 | get childSleepMode() { 54 | return (this.data[0x09] & 0x01) > 0; 55 | } 56 | // Byte 0x0a 57 | get sleepFunction() { 58 | return (this.data[0x0a] & 0x01) > 0; 59 | } 60 | get nightLight() { 61 | // This needs a better name, dunno what it actually means 62 | return (this.data[0x0a] & 0x10) > 0; 63 | } 64 | get peakElec() { 65 | // This needs a better name, dunno what it actually means 66 | return (this.data[0x0a] & 0x20) > 0; 67 | } 68 | get naturalFan() { 69 | // This needs a better name, dunno what it actually means 70 | return (this.data[0x0a] & 0x40) > 0; 71 | } 72 | // Byte 0x0d 73 | get humidity() { 74 | return this.data[0x0d] & 0x7f; 75 | } 76 | } 77 | exports.default = ApplianceResponse; 78 | -------------------------------------------------------------------------------- /src/responses/ApplianceResponse.ts: -------------------------------------------------------------------------------- 1 | import { MideaSwingMode } from '../enums/MideaSwingMode' 2 | 3 | export default class ApplianceResponse { 4 | data: any; 5 | constructor(data: any) { 6 | // The response data from the appliance includes a packet header which we don't want 7 | this.data = data.slice(0x32); 8 | } 9 | // Byte 0x01 10 | get applianceError() { 11 | return (this.data[0x01] & 0x80) !== 0; 12 | } 13 | 14 | get powerState() { 15 | return (this.data[0x01] & 0x1) !== 0; 16 | } 17 | 18 | get imodeResume() { 19 | return (this.data[0x01] & 0x4) !== 0; 20 | } 21 | 22 | get timerMode() { 23 | return (this.data[0x01] & 0x10) !== 0; 24 | } 25 | 26 | get quickChech() { 27 | return (this.data[0x01] & 0x20) !== 0; 28 | } 29 | 30 | // Byte 0x03 31 | get fanSpeed(): number { 32 | return this.data[0x03] & 0x7f; 33 | } 34 | // Byte 0x04 + 0x06 35 | get onTimer(): any { 36 | const on_timer_value = this.data[0x04]; 37 | const on_timer_minutes = this.data[0x06]; 38 | return { 39 | status: (on_timer_value & 0x80) >> 7 > 0, 40 | hour: (on_timer_value & 0x7c) >> 2, 41 | minutes: (on_timer_value & 0x3) | ((on_timer_minutes & 0xf0) >> 4), 42 | }; 43 | } 44 | // Byte 0x05 + 0x06 45 | get offTimer(): any { 46 | const off_timer_value = this.data[0x05]; 47 | const off_timer_minutes = this.data[0x06]; 48 | return { 49 | status: (off_timer_value & 0x80) >> 7 > 0, 50 | hour: (off_timer_value & 0x7c) >> 2, 51 | minutes: (off_timer_value & 0x3) | (off_timer_minutes & 0xf), 52 | }; 53 | } 54 | // Byte 0x07 55 | get swingMode(): MideaSwingMode { 56 | return this.data[0x07] & 0x0f; 57 | } 58 | 59 | // Byte 0x09 60 | get childSleepMode() { 61 | return (this.data[0x09] & 0x01) > 0; 62 | } 63 | 64 | // Byte 0x0a 65 | get sleepFunction() { 66 | return (this.data[0x0a] & 0x01) > 0; 67 | } 68 | 69 | get nightLight() { 70 | // This needs a better name, dunno what it actually means 71 | return (this.data[0x0a] & 0x10) > 0; 72 | } 73 | 74 | get peakElec() { 75 | // This needs a better name, dunno what it actually means 76 | return (this.data[0x0a] & 0x20) > 0; 77 | } 78 | 79 | get naturalFan() { 80 | // This needs a better name, dunno what it actually means 81 | return (this.data[0x0a] & 0x40) > 0; 82 | } 83 | // Byte 0x0d 84 | get humidity() { 85 | return this.data[0x0d] & 0x7f; 86 | } 87 | } -------------------------------------------------------------------------------- /lib/commands/DehumidifierSetCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const SetCommand_1 = __importDefault(require("./SetCommand")); 7 | const MideaDeviceType_1 = require("../enums/MideaDeviceType"); 8 | class DehumidifierSetCommand extends SetCommand_1.default { 9 | constructor(device_type = MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 10 | super(device_type); 11 | } 12 | // Byte 0x0c 13 | get operationalMode() { 14 | return this.data[0x0c] & 0x0f; 15 | } 16 | set operationalMode(mode) { 17 | this.data[0x0c] &= ~0x0f; // Clear the mode bits 18 | this.data[0x0c] |= mode; 19 | } 20 | // Byte 0x11 21 | get targetHumidity() { 22 | return this.data[0x11] & 0x7f; 23 | } 24 | set targetHumidity(humidity) { 25 | this.data[0x11] &= ~0x7f; // Clear the humidity part 26 | this.data[0x11] |= humidity; 27 | } 28 | // Byte 0x13 29 | get ionMode() { 30 | return (this.data[0x13] & 0x40) !== 0; 31 | } 32 | set ionMode(ionModeEnabled) { 33 | this.data[0x13] &= ~0x40; // Clear the ion switch bit 34 | this.data[0x13] |= ionModeEnabled ? 0x40 : 0; 35 | } 36 | get pumpSwitch() { 37 | return (this.data[0x13] & 0x08) !== 0; 38 | } 39 | set pumpSwitch(pumpSwitchEnabled) { 40 | this.data[0x13] &= ~0x08; // Clear the pump switch bit 41 | this.data[0x13] |= pumpSwitchEnabled ? 0x08 : 0; 42 | } 43 | get pumpSwitchFlag() { 44 | return (this.data[0x13] & 0x10) !== 0; 45 | } 46 | set pumpSwitchFlag(mode) { 47 | this.data[0x13] &= ~0x10; // Clear the pump switch bit 48 | this.data[0x13] |= mode ? 0x10 : 0; 49 | } 50 | get sleepSwitch() { 51 | return (this.data[0x13] & 0x20) !== 0; 52 | } 53 | set sleepSwitch(sleepSwitchEnabled) { 54 | this.data[0x13] &= ~0x20; // Clear the sleep switch bit 55 | this.data[0x13] |= sleepSwitchEnabled ? 0x20 : 0; 56 | } 57 | // Byte 0x14 58 | get verticalSwing() { 59 | return (this.data[0x14] & 0x14) !== 0; 60 | } 61 | set verticalSwing(verticalSwingEnabled) { 62 | this.data[0x14] &= ~0x14; // Clear the sleep switch bit 63 | this.data[0x14] |= verticalSwingEnabled ? 0x14 : 0; 64 | } 65 | // Byte 0x17 66 | get tankWarningLevel() { 67 | return this.data[0x17]; 68 | } 69 | set tankWarningLevel(level) { 70 | this.data[0x17] = level; 71 | } 72 | } 73 | exports.default = DehumidifierSetCommand; 74 | -------------------------------------------------------------------------------- /src/responses/DehumidifierApplianceResponse.ts: -------------------------------------------------------------------------------- 1 | import ApplianceResponse from './ApplianceResponse'; 2 | 3 | export default class DehumidifierApplianceResponse extends ApplianceResponse { 4 | // Byte 0x02 5 | get operationalMode() { 6 | return (this.data[0x02] & 0xf) 7 | } 8 | 9 | get operationalModeFc() { 10 | return (this.data[0x02] & 0xf0) >> 4 11 | } 12 | // Byte 0x07 13 | get targetHumidity() { 14 | if (this.data[0x07] > 100) { 15 | return 100 16 | } else { 17 | return this.data[0x07]; 18 | } 19 | 20 | } 21 | // Byte 0x08 22 | get targetHumidityDecimal() { 23 | const humidityDecimal = (this.data[0x08] & 0xf) * 0.0625; 24 | return this.targetHumidity + humidityDecimal; 25 | } 26 | // Byte 0x09 27 | get filterIndicator() { 28 | return (this.data[0x09] & 0x80) !== 0; 29 | } 30 | 31 | get ionMode() { 32 | return (this.data[0x09] & 0x40) !== 0; 33 | } 34 | 35 | get speepSwitch() { 36 | return (this.data[0x09] & 0x20) !== 0; 37 | } 38 | 39 | get pumpSwitchFlag() { 40 | return (this.data[0x09] & 0x10) !== 0; 41 | } 42 | 43 | get pumpSwitch() { 44 | return (this.data[0x09] & 0x8) !== 0; 45 | } 46 | 47 | get displayClass() { 48 | return this.data[0x09] & 0x7; 49 | } 50 | 51 | // Byte 0x0a 52 | get defrosting() { 53 | return (this.data[0x0a] & 0x80) !== 0; 54 | } 55 | 56 | get waterLevel() { 57 | return this.data[0x0a] & 0x7f; 58 | } 59 | 60 | get waterLevelFull() { 61 | return this.waterLevel >= 100; 62 | } 63 | // Byte 0x0b 64 | get dustTime() { 65 | return this.data[0x0b] * 2; 66 | } 67 | // Byte 0x0c 68 | get rareShow() { 69 | return (this.data[0x0c] & 0x38) >> 3; 70 | } 71 | 72 | get dust() { 73 | return this.data[0x0c] & 0x7; 74 | } 75 | // Byte 0x0d 76 | get pm25() { 77 | return this.data[0x0d] + (this.data[0x0e] * 256) 78 | } 79 | // Byte 0x0f 80 | get waterLevelWarningLevel() { 81 | return this.data[0xf]; 82 | } 83 | // Byte 0x10 84 | get currentHumidity() { 85 | return this.data[0x10]; 86 | } 87 | // Byte 0x11 88 | get indoorTemperature() { 89 | return (this.data[0x11] - 50) / 2 90 | // if (this.indoorTemperature < -19) { 91 | // this.indoorTemperature = -20; 92 | // } else if (this.indoorTemperature > 50) { 93 | // this.indoorTemperature = 50 94 | // } 95 | } 96 | // Byte 0x12 97 | get indoorTemperatureDecimal() { 98 | return (this.data[0x12] & 0xf) * 0.1 99 | } 100 | // Byte 0x13 101 | get verticalSwing() { 102 | return (this.data[0x12] & 0x20) !== 0; 103 | } 104 | 105 | get horizontalSwing() { 106 | return (this.data[0x12] & 0x10) !== 0; 107 | } 108 | } -------------------------------------------------------------------------------- /lib/enums/MideaDeviceType.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.MideaDeviceType = void 0; 4 | var MideaDeviceType; 5 | (function (MideaDeviceType) { 6 | MideaDeviceType[MideaDeviceType["Plug"] = 16] = "Plug"; 7 | MideaDeviceType[MideaDeviceType["RemoteController"] = 17] = "RemoteController"; 8 | MideaDeviceType[MideaDeviceType["AirBox"] = 18] = "AirBox"; 9 | MideaDeviceType[MideaDeviceType["Light"] = 19] = "Light"; 10 | MideaDeviceType[MideaDeviceType["Curtain"] = 20] = "Curtain"; 11 | MideaDeviceType[MideaDeviceType["MBox"] = 27] = "MBox"; 12 | MideaDeviceType[MideaDeviceType["Dehumidifier"] = 161] = "Dehumidifier"; 13 | MideaDeviceType[MideaDeviceType["AirConditioner"] = 172] = "AirConditioner"; 14 | MideaDeviceType[MideaDeviceType["MicroWaveOven"] = 176] = "MicroWaveOven"; 15 | MideaDeviceType[MideaDeviceType["BigOven"] = 177] = "BigOven"; 16 | MideaDeviceType[MideaDeviceType["SteamerOven"] = 178] = "SteamerOven"; 17 | MideaDeviceType[MideaDeviceType["Sterilizer"] = 179] = "Sterilizer"; 18 | MideaDeviceType[MideaDeviceType["Toaster"] = 180] = "Toaster"; 19 | MideaDeviceType[MideaDeviceType["Hood"] = 182] = "Hood"; 20 | MideaDeviceType[MideaDeviceType["Hob"] = 183] = "Hob"; 21 | MideaDeviceType[MideaDeviceType["VacuumCleaner"] = 184] = "VacuumCleaner"; 22 | MideaDeviceType[MideaDeviceType["Induction"] = 185] = "Induction"; 23 | MideaDeviceType[MideaDeviceType["Refrigerator"] = 202] = "Refrigerator"; 24 | MideaDeviceType[MideaDeviceType["MDV"] = 204] = "MDV"; 25 | MideaDeviceType[MideaDeviceType["AirWaterHeater"] = 205] = "AirWaterHeater"; 26 | MideaDeviceType[MideaDeviceType["PulsatorWasher"] = 218] = "PulsatorWasher"; 27 | MideaDeviceType[MideaDeviceType["DurmWasher"] = 219] = "DurmWasher"; 28 | MideaDeviceType[MideaDeviceType["ClothesDryer"] = 220] = "ClothesDryer"; 29 | MideaDeviceType[MideaDeviceType["DishWasher"] = 225] = "DishWasher"; 30 | MideaDeviceType[MideaDeviceType["ElectricWaterHeater"] = 226] = "ElectricWaterHeater"; 31 | MideaDeviceType[MideaDeviceType["GasWaterHeater"] = 227] = "GasWaterHeater"; 32 | MideaDeviceType[MideaDeviceType["RiceCooker"] = 234] = "RiceCooker"; 33 | MideaDeviceType[MideaDeviceType["InductionCooker"] = 235] = "InductionCooker"; 34 | MideaDeviceType[MideaDeviceType["PressureCooker"] = 236] = "PressureCooker"; 35 | MideaDeviceType[MideaDeviceType["WaterPurifier"] = 237] = "WaterPurifier"; 36 | MideaDeviceType[MideaDeviceType["SoybeanMachine"] = 239] = "SoybeanMachine"; 37 | MideaDeviceType[MideaDeviceType["ElectricFanner"] = 250] = "ElectricFanner"; 38 | MideaDeviceType[MideaDeviceType["ElectricHeater"] = 251] = "ElectricHeater"; 39 | MideaDeviceType[MideaDeviceType["AirPurifier"] = 252] = "AirPurifier"; 40 | MideaDeviceType[MideaDeviceType["Humidifier"] = 253] = "Humidifier"; 41 | MideaDeviceType[MideaDeviceType["AirConditionFanner"] = 254] = "AirConditionFanner"; 42 | MideaDeviceType[MideaDeviceType["AllType"] = 255] = "AllType"; 43 | })(MideaDeviceType = exports.MideaDeviceType || (exports.MideaDeviceType = {})); 44 | -------------------------------------------------------------------------------- /lib/BaseCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const crc8_1 = __importDefault(require("./crc8")); 7 | const MideaDeviceType_1 = require("./enums/MideaDeviceType"); 8 | class BaseCommand { 9 | constructor(device_type) { 10 | this.device_type = device_type; 11 | if (device_type == MideaDeviceType_1.MideaDeviceType.AirConditioner) { 12 | // More magic numbers. I'm sure each of these have a purpose, but none of it is documented in english. I might make an effort to google translate the SDK 13 | // full = [170, 35, 172, 0, 0, 0, 0, 0, 3, 2, 64, 67, 70, 102, 127, 127, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 14, 187, 137, 169, 223, 88, 121, 170, 108, 162, 36, 170, 80, 242, 143, null]; 14 | this.data = [ 15 | 170, 16 | 35, 17 | 172, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | 0, 23 | 3, 24 | 2, 25 | // Command Header End 26 | // Data Start 27 | 64, 28 | 64, 29 | 70, 30 | 102, 31 | 127, 32 | 127, 33 | 0, 34 | 48, 35 | 0, 36 | 0, 37 | 0, 38 | // Padding 39 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 40 | // 0, 0, 0, 6, 14, 187, 41 | // Data End 42 | ]; 43 | this.data[0x02] = device_type; 44 | } 45 | else if (device_type == MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 46 | this.data = [ 47 | // Command Header 48 | 170, 49 | 34, 50 | 161, 51 | 0, 52 | 0, 53 | 0, 54 | 0, 55 | 0, 56 | 3, 57 | 2, 58 | // Command Header End 59 | // Data Start 60 | 72, 61 | 64, 62 | 1, 63 | 50, 64 | 0, 65 | 0, 66 | 0, 67 | 0, 68 | 0, 69 | 0, 70 | 0, 71 | // Padding 72 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 73 | 0, 0, 0, 74 | // Data End 75 | ]; 76 | } 77 | else { 78 | // Unknown/Unsupported: default to AirCon 79 | this.data = [170, 35, 172, 0, 0, 0, 0, 0, 3, 2, 64, 67, 70, 102, 127, 127, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 80 | } 81 | } 82 | finalize() { 83 | // Add the CRC8 84 | this.data[this.data.length - 1] = crc8_1.default.calculate(this.data.slice(16)); 85 | // Set the length of the command data 86 | this.data[0x01] = this.data.length; 87 | return this.data; 88 | } 89 | } 90 | exports.default = BaseCommand; 91 | -------------------------------------------------------------------------------- /lib/responses/DehumidifierApplianceResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const ApplianceResponse_1 = __importDefault(require("./ApplianceResponse")); 7 | class DehumidifierApplianceResponse extends ApplianceResponse_1.default { 8 | // Byte 0x02 9 | get operationalMode() { 10 | return (this.data[0x02] & 0xf); 11 | } 12 | get operationalModeFc() { 13 | return (this.data[0x02] & 0xf0) >> 4; 14 | } 15 | // Byte 0x07 16 | get targetHumidity() { 17 | if (this.data[0x07] > 100) { 18 | return 100; 19 | } 20 | else { 21 | return this.data[0x07]; 22 | } 23 | } 24 | // Byte 0x08 25 | get targetHumidityDecimal() { 26 | const humidityDecimal = (this.data[0x08] & 0xf) * 0.0625; 27 | return this.targetHumidity + humidityDecimal; 28 | } 29 | // Byte 0x09 30 | get filterIndicator() { 31 | return (this.data[0x09] & 0x80) !== 0; 32 | } 33 | get ionMode() { 34 | return (this.data[0x09] & 0x40) !== 0; 35 | } 36 | get speepSwitch() { 37 | return (this.data[0x09] & 0x20) !== 0; 38 | } 39 | get pumpSwitchFlag() { 40 | return (this.data[0x09] & 0x10) !== 0; 41 | } 42 | get pumpSwitch() { 43 | return (this.data[0x09] & 0x8) !== 0; 44 | } 45 | get displayClass() { 46 | return this.data[0x09] & 0x7; 47 | } 48 | // Byte 0x0a 49 | get defrosting() { 50 | return (this.data[0x0a] & 0x80) !== 0; 51 | } 52 | get waterLevel() { 53 | return this.data[0x0a] & 0x7f; 54 | } 55 | get waterLevelFull() { 56 | return this.waterLevel >= 100; 57 | } 58 | // Byte 0x0b 59 | get dustTime() { 60 | return this.data[0x0b] * 2; 61 | } 62 | // Byte 0x0c 63 | get rareShow() { 64 | return (this.data[0x0c] & 0x38) >> 3; 65 | } 66 | get dust() { 67 | return this.data[0x0c] & 0x7; 68 | } 69 | // Byte 0x0d 70 | get pm25() { 71 | return this.data[0x0d] + (this.data[0x0e] * 256); 72 | } 73 | // Byte 0x0f 74 | get waterLevelWarningLevel() { 75 | return this.data[0xf]; 76 | } 77 | // Byte 0x10 78 | get currentHumidity() { 79 | return this.data[0x10]; 80 | } 81 | // Byte 0x11 82 | get indoorTemperature() { 83 | return (this.data[0x11] - 50) / 2; 84 | // if (this.indoorTemperature < -19) { 85 | // this.indoorTemperature = -20; 86 | // } else if (this.indoorTemperature > 50) { 87 | // this.indoorTemperature = 50 88 | // } 89 | } 90 | // Byte 0x12 91 | get indoorTemperatureDecimal() { 92 | return (this.data[0x12] & 0xf) * 0.1; 93 | } 94 | // Byte 0x13 95 | get verticalSwing() { 96 | return (this.data[0x12] & 0x20) !== 0; 97 | } 98 | get horizontalSwing() { 99 | return (this.data[0x12] & 0x10) !== 0; 100 | } 101 | } 102 | exports.default = DehumidifierApplianceResponse; 103 | -------------------------------------------------------------------------------- /src/MigrationHelper.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { Logger } from 'homebridge' 3 | 4 | // MigrationHelper – Migrates the user's config.json from the old accessory to a platform 5 | export class MigrationHelper { 6 | log: Logger 7 | configFilePath: string = '' 8 | 9 | constructor(log: Logger, configFilePath: string) { 10 | this.log = log; 11 | if (fs.existsSync(configFilePath)) { 12 | this.configFilePath = configFilePath 13 | let file = fs.readFileSync(configFilePath) 14 | let config = JSON.parse(file.toString()) 15 | let migratedConfig = this.migrate(config); 16 | if (migratedConfig) { 17 | this.backupConfig(); 18 | if (this.writeNewConfig(migratedConfig)) { 19 | this.restartHomebridge(); 20 | } 21 | } 22 | } 23 | } 24 | // This method writes the new config file after the old accessory has been removed and the new platform added 25 | writeNewConfig(config: object) { 26 | let configString = JSON.stringify(config, null, '\t') 27 | fs.writeFile(this.configFilePath, configString, 'utf-8', (err: Error) => { 28 | if (err) { 29 | this.log.error("Could not rewrite config file to use platform instead of accessory, please adjust your configuration manually.") 30 | this.log.error(err.toString()) 31 | return false; 32 | } else { 33 | this.log.info("Successfully migrated configuration to platform. Killing homebridge proccess to restart it") 34 | return true; 35 | } 36 | }) 37 | return false 38 | } 39 | // This method creates a backup of the user's existing configuration 40 | backupConfig() { 41 | let file = fs.readFileSync(this.configFilePath) 42 | try { 43 | fs.writeFile(this.configFilePath + '.bak', file, 'utf-8', (err: Error) => { 44 | if (err) { 45 | this.log.warn("Error making backup of config") 46 | } else { 47 | this.log.debug("Made backup of config.json") 48 | } 49 | }); 50 | } catch (e) { 51 | return false; 52 | } 53 | } 54 | // This method performs the permutation of the config object and removes the old accessory and adds the platform 55 | migrate(config: any) { 56 | let platformObject = { 57 | "platform": "midea-air", 58 | "interval": 30, 59 | "user": "", 60 | "password": "" 61 | } 62 | if (config.hasOwnProperty('platforms')) { 63 | for (let i = 0; i < config.platforms.length; i++) { 64 | if (config.platforms[i].platform == 'midea-air') { 65 | // We already have a platform, return 66 | return null 67 | } 68 | } 69 | } else { 70 | // We don't even have any platforms defined, let's create a new array 71 | config.platforms = [] 72 | } 73 | if (config.hasOwnProperty('accessories')) { 74 | for (let i = 0; i < config.accessories.length; i++) { 75 | if (config.accessories[i].accessory === 'midea-air') { 76 | // We have an existing installation 77 | this.log.warn('Found existing Midea-air accessory, migrating'); 78 | platformObject.user = config.accessories[i].user 79 | platformObject.password = config.accessories[i].password 80 | platformObject.interval = config.accessories[i].interval 81 | // Remove accessory 82 | config.accessories.splice(i, 1) 83 | } 84 | } 85 | config.platforms.push(platformObject) 86 | return config 87 | } 88 | } 89 | // Crude method of "restarting" homebridge, ideally there should be something better here 90 | restartHomebridge() { 91 | process.exit(1) 92 | } 93 | } -------------------------------------------------------------------------------- /lib/Constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | class Constants { 4 | } 5 | exports.default = Constants; 6 | Constants.crc8_854_table = [ 7 | 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 8 | 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, 9 | 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 10 | 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, 11 | 0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 12 | 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62, 13 | 0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 14 | 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff, 15 | 0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 16 | 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07, 17 | 0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 18 | 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a, 19 | 0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 20 | 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24, 21 | 0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 22 | 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9, 23 | 0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 24 | 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd, 25 | 0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 26 | 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50, 27 | 0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 28 | 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee, 29 | 0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 30 | 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73, 31 | 0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 32 | 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b, 33 | 0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 34 | 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16, 35 | 0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 36 | 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8, 37 | 0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 38 | 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35, 39 | ]; 40 | Constants.UpdateCommand_AirCon = [ 41 | 170, 42 | 32, 43 | 172, 44 | 0, 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | 0, 50 | 3, 51 | // Command Header End 52 | // Data Start 53 | 65, 54 | 129, 55 | 0, 56 | 255, 57 | 3, 58 | 255, 59 | 0, 60 | 2, 61 | 0, 62 | 0, 63 | 0, 64 | // Padding 65 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 66 | 3, 67 | 205, 156, 16, 184, 113, 186, 162, 129, 39, 12, 160, 157, 100, 102, 118, 15, 154, 166 68 | ]; 69 | Constants.UpdateCommand_Dehumidifier = [ 70 | 170, 71 | 32, 72 | 161, 73 | 0, 74 | 0, 75 | 0, 76 | 0, 77 | 0, 78 | 0, 79 | 3, 80 | // Command Header End 81 | // Data Start 82 | 65, 83 | 129, 84 | 0, 85 | 255, 86 | 3, 87 | 255, 88 | 0, 89 | 2, 90 | 0, 91 | 0, 92 | 0, 93 | // Padding 94 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 95 | 11, 36, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 96 | ]; 97 | Constants.UserAgent = 'Dalvik/2.22.0 (Linux; U; Android 9.0; SM-G935F Build/NRD90M)'; 98 | // These values are from the official Midea Air app, adjust if you want to use different credentials 99 | Constants.AppId = '1117'; 100 | Constants.AppKey = 'ff0cf6f5f0c3471de36341cab3f7a9af'; 101 | Constants.Language = 'en-US'; 102 | Constants.ClientType = '1'; // 0: PC, 1: Android, 2: IOS 103 | Constants.RequestFormat = '2'; // JSON 104 | Constants.RequestSource = '1010'; // '17' 105 | ; 106 | -------------------------------------------------------------------------------- /lib/MigrationHelper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.MigrationHelper = void 0; 7 | const fs_1 = __importDefault(require("fs")); 8 | // MigrationHelper – Migrates the user's config.json from the old accessory to a platform 9 | class MigrationHelper { 10 | constructor(log, configFilePath) { 11 | this.configFilePath = ''; 12 | this.log = log; 13 | if (fs_1.default.existsSync(configFilePath)) { 14 | this.configFilePath = configFilePath; 15 | let file = fs_1.default.readFileSync(configFilePath); 16 | let config = JSON.parse(file.toString()); 17 | let migratedConfig = this.migrate(config); 18 | if (migratedConfig) { 19 | this.backupConfig(); 20 | if (this.writeNewConfig(migratedConfig)) { 21 | this.restartHomebridge(); 22 | } 23 | } 24 | } 25 | } 26 | // This method writes the new config file after the old accessory has been removed and the new platform added 27 | writeNewConfig(config) { 28 | let configString = JSON.stringify(config, null, '\t'); 29 | fs_1.default.writeFile(this.configFilePath, configString, 'utf-8', (err) => { 30 | if (err) { 31 | this.log.error("Could not rewrite config file to use platform instead of accessory, please adjust your configuration manually."); 32 | this.log.error(err.toString()); 33 | return false; 34 | } 35 | else { 36 | this.log.info("Successfully migrated configuration to platform. Killing homebridge proccess to restart it"); 37 | return true; 38 | } 39 | }); 40 | return false; 41 | } 42 | // This method creates a backup of the user's existing configuration 43 | backupConfig() { 44 | let file = fs_1.default.readFileSync(this.configFilePath); 45 | try { 46 | fs_1.default.writeFile(this.configFilePath + '.bak', file, 'utf-8', (err) => { 47 | if (err) { 48 | this.log.warn("Error making backup of config"); 49 | } 50 | else { 51 | this.log.debug("Made backup of config.json"); 52 | } 53 | }); 54 | } 55 | catch (e) { 56 | return false; 57 | } 58 | } 59 | // This method performs the permutation of the config object and removes the old accessory and adds the platform 60 | migrate(config) { 61 | let platformObject = { 62 | "platform": "midea-air", 63 | "interval": 30, 64 | "user": "", 65 | "password": "" 66 | }; 67 | if (config.hasOwnProperty('platforms')) { 68 | for (let i = 0; i < config.platforms.length; i++) { 69 | if (config.platforms[i].platform == 'midea-air') { 70 | // We already have a platform, return 71 | return null; 72 | } 73 | } 74 | } 75 | else { 76 | // We don't even have any platforms defined, let's create a new array 77 | config.platforms = []; 78 | } 79 | if (config.hasOwnProperty('accessories')) { 80 | for (let i = 0; i < config.accessories.length; i++) { 81 | if (config.accessories[i].accessory === 'midea-air') { 82 | // We have an existing installation 83 | this.log.warn('Found existing Midea-air accessory, migrating'); 84 | platformObject.user = config.accessories[i].user; 85 | platformObject.password = config.accessories[i].password; 86 | platformObject.interval = config.accessories[i].interval; 87 | // Remove accessory 88 | config.accessories.splice(i, 1); 89 | } 90 | } 91 | config.platforms.push(platformObject); 92 | return config; 93 | } 94 | } 95 | // Crude method of "restarting" homebridge, ideally there should be something better here 96 | restartHomebridge() { 97 | process.exit(1); 98 | } 99 | } 100 | exports.MigrationHelper = MigrationHelper; 101 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | // Utils – Utility functions 2 | import crypto from "crypto"; 3 | import Constants from './Constants'; 4 | 5 | export default class Utils { 6 | static encode(data: number[]): number[] { 7 | const normalized = []; 8 | for (let b of data) { 9 | if (b >= 128) { 10 | b = b - 256; 11 | }; 12 | normalized.push(b); 13 | }; 14 | return normalized; 15 | }; 16 | 17 | static decode(data: number[]): number[] { 18 | const normalized = []; 19 | for (let b of data) { 20 | if (b < 0) { 21 | b = b + 256; 22 | }; 23 | normalized.push(b); 24 | }; 25 | return normalized; 26 | }; 27 | // Returns a timestamp in the format YYYYMMDDHHmmss 28 | static getStamp(): string { 29 | const date = new Date(); 30 | return date 31 | .toISOString() 32 | .slice(0, 19) 33 | .replace(/-/g, "") 34 | .replace(/:/g, "") 35 | .replace(/T/g, ""); 36 | }; 37 | 38 | static formatResponse(arr: any[]) { 39 | let output: string[] = [] 40 | for (let i = 0; i < arr.length; i++) { 41 | let intValue = parseInt(arr[i]); 42 | output.push((intValue).toString(2)) 43 | }; 44 | return output; 45 | }; 46 | 47 | static getSign(path: string, form: any, appKey: string) { 48 | if (path !== '' && form) { 49 | let postfix = `/v1${path}`; 50 | postfix = postfix.split('?')[0]; 51 | // Maybe this will help, should remove any query string parameters in the URL from the sign 52 | const ordered: any = {}; 53 | Object.keys(form) 54 | .sort() 55 | .forEach(function (key: any) { 56 | ordered[key] = form[key]; 57 | }); 58 | const query = Object.keys(ordered) 59 | .map((key) => key + "=" + ordered[key]) 60 | .join("&"); 61 | return crypto 62 | .createHash("sha256") 63 | .update(postfix + query + appKey) 64 | .digest("hex"); 65 | } else { 66 | return false; 67 | }; 68 | }; 69 | 70 | static decryptAes(reply: string, dataKey: string) { 71 | if (reply && dataKey != '') { 72 | const decipher = crypto.createDecipheriv("aes-128-ecb", dataKey, ""); 73 | const dec = decipher.update(reply, "hex", "utf8"); 74 | return dec.split(",").map(Number); 75 | } else { 76 | return []; 77 | }; 78 | }; 79 | 80 | static decryptAesString(reply: string, dataKey: string) { 81 | if (reply && dataKey != '') { 82 | const decipher = crypto.createDecipheriv("aes-128-ecb", dataKey, ""); 83 | const dec = decipher.update(reply, "hex", "utf8"); 84 | return dec; 85 | } else { 86 | return ''; 87 | }; 88 | }; 89 | 90 | static encryptAes(query: number[], dataKey: string) { 91 | if (query && dataKey != '') { 92 | const cipher = crypto.createCipheriv("aes-128-ecb", dataKey, ""); 93 | let ciph = cipher.update(query.join(","), "utf8", "hex"); 94 | ciph += cipher.final("hex"); 95 | return ciph; 96 | } else { 97 | return false; 98 | }; 99 | }; 100 | 101 | static encryptAesString(query: string, dataKey: string) { 102 | if (dataKey != '') { 103 | const cipher = crypto.createCipheriv("aes-128-ecb", dataKey, ""); 104 | let ciph = cipher.update(query, "utf8", "hex"); 105 | ciph += cipher.final("hex"); 106 | return ciph; 107 | } else { 108 | return false; 109 | }; 110 | }; 111 | 112 | static getSignPassword(loginId: string, password: string, appKey: string) { 113 | if (loginId !== '' && password !== '') { 114 | const pw = crypto 115 | .createHash("sha256") 116 | .update(password) 117 | .digest("hex"); 118 | return crypto 119 | .createHash("sha256") 120 | .update(loginId + pw + appKey) 121 | .digest("hex"); 122 | } else { 123 | return ''; 124 | }; 125 | }; 126 | 127 | static generateDataKey(accessToken: string, appKey: string) { 128 | if (accessToken != '') { 129 | const md5AppKey = crypto 130 | .createHash("md5") 131 | .update(appKey).digest("hex"); 132 | const decipher = crypto.createDecipheriv("aes-128-ecb", md5AppKey.slice(0, 16), ""); 133 | const dec = decipher.update(accessToken, "hex", "utf8"); 134 | return dec; 135 | }; 136 | return ''; 137 | }; 138 | 139 | static encryptIAMPassword(loginId: string, password: string, appKey: string) { 140 | const passwordHash = crypto 141 | .createHash('md5') 142 | .update(password) 143 | .digest(); 144 | const password2ndHash = crypto 145 | .createHash('md5') 146 | .update(passwordHash) 147 | .digest(); 148 | return crypto 149 | .createHash('sha256') 150 | .update(loginId + password2ndHash + appKey) 151 | .digest('hex'); 152 | }; 153 | 154 | static getSignProxied(form: any) { 155 | const msg = `meicloud${form}${Utils.randomString}`; 156 | return crypto 157 | .createHmac('sha256', 'PROD_VnoClJI9aikS8dyy') 158 | .update(msg) 159 | .digest('hex'); 160 | }; 161 | static randomString = Math.floor(new Date().getTime() / 1000).toString(); 162 | static pushToken = crypto.randomBytes(120).toString('base64'); 163 | static reqId = crypto.randomBytes(16).toString('hex'); 164 | 165 | }; -------------------------------------------------------------------------------- /src/BaseCommand.ts: -------------------------------------------------------------------------------- 1 | import crc8 from './crc8'; 2 | 3 | import { MideaDeviceType } from './enums/MideaDeviceType'; 4 | 5 | export default class BaseCommand { 6 | data: any[] 7 | device_type: MideaDeviceType 8 | 9 | constructor(device_type: MideaDeviceType) { 10 | 11 | this.device_type = device_type 12 | 13 | if (device_type == MideaDeviceType.AirConditioner) { 14 | // More magic numbers. I'm sure each of these have a purpose, but none of it is documented in english. I might make an effort to google translate the SDK 15 | // full = [170, 35, 172, 0, 0, 0, 0, 0, 3, 2, 64, 67, 70, 102, 127, 127, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 14, 187, 137, 169, 223, 88, 121, 170, 108, 162, 36, 170, 80, 242, 143, null]; 16 | this.data = [ 17 | 170, // 0 - Sync header 18 | 35, // 1 - Message length setting 19 | 172, // 2 - Device type (172 for Air Conditioner) 20 | 0, // 3 - Frame sync check (not used, 0x00) 21 | 0, // 4 - Reserved 0x00 22 | 0, // 4 - Reserved 0x00 23 | 0, // 6 - Message Id 24 | 0, // 7 - Framework protocol version 25 | 3, // 8 - Home appliance protocol 26 | 2, // 9 - Message type setting identification 27 | // Command Header End 28 | // Data Start 29 | 64, // 10 - Data request/response: Set up 30 | 64, // 11 - power state: 0/1 + audible feedback: 64 31 | 70, // 12 - Operational mode + Target Temperature 32 | 102, // 13 - Fan speed 20/40/60/80/102 33 | 127, // 14 - On timer 34 | 127, // 15 - Off timer 35 | 0, // 16 - Common timer 36 | 48, // 17 - Swing mode 37 | 0, // 18 - Turbo fan 38 | 0, // 19 - Eco mode / Dryer / Purifier 39 | 0, // 20 - TurboMode / Screen display / Fahrenheit 40 | // Padding 41 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 42 | // 0, 0, 0, 6, 14, 187, 43 | // Data End 44 | ]; 45 | this.data[0x02] = device_type; 46 | } else if (device_type == MideaDeviceType.Dehumidifier) { 47 | this.data = [ 48 | // Command Header 49 | 170, // 0 - Sync header setting 50 | 34, // 1 - Message length 51 | 161, // 2 - Device type (161 for Dehumidifier) 52 | 0, // 3 - Frame sync check (not used, 0x00) 53 | 0, // 4 - Reserved 0x00 54 | 0, // 5 - Reserved 0x00 55 | 0, // 6 - Message Id 56 | 0, // 7 - Framework protocol version 57 | 3, // 8 - Device Agreement Version 58 | 2, // 9 - Command (2) or Query (3) 59 | // Command Header End 60 | // Data Start 61 | 72, // 10 - Command type: Set (72), Query (65) 62 | 64, // 11 - power state: 0/1 + audible feedback: 64 63 | 1, // 12 - Operational mode (1: target, 2: continuous, 3: smart, 4: dry) 64 | 50, // 13 - Timing + wind speed 40/60/80 65 | 0, // 14 - On timer 66 | 0, // 15 - Off timer 67 | 0, // 16 - Common timer 68 | 0, // 17 - Target humidity 69 | 0, // 18 - Target humidity (float)? 70 | 0, // 19 - Display and other settings 71 | 0, // 20 - Swing and other settings. In dehumidifier, matches mode 72 | // Padding 73 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 74 | 0, 0, 0, 75 | // Data End 76 | ]; 77 | } else { 78 | // Unknown/Unsupported: default to AirCon 79 | this.data = [170, 35, 172, 0, 0, 0, 0, 0, 3, 2, 64, 67, 70, 102, 127, 127, 0, 48, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 80 | } 81 | } 82 | 83 | finalize() { 84 | // Add the CRC8 85 | this.data[this.data.length - 1] = crc8.calculate(this.data.slice(16)); 86 | // Set the length of the command data 87 | this.data[0x01] = this.data.length; 88 | return this.data; 89 | } 90 | } -------------------------------------------------------------------------------- /src/responses/ACApplianceResponse.ts: -------------------------------------------------------------------------------- 1 | import { userInfo } from 'os'; 2 | import ApplianceResponse from './ApplianceResponse' 3 | 4 | export default class ACApplianceResponse extends ApplianceResponse { 5 | // Byte 0x02 6 | get operationalMode(): number { 7 | return (this.data[0x02] & 0xe0) >> 5; 8 | } 9 | 10 | get targetTemperature() { 11 | if ((this.data[0x02] & 0x10) !== 0) { 12 | return (this.data[0x02] & 0xf) + 16.0 + 0.5 13 | } else { 14 | return (this.data[0x02] & 0xf) + 16.0 + 0.0 15 | } 16 | } 17 | // Byte 0x07 18 | get horizontalSwing() { 19 | return (this.data[0x07] & 0xc) >> 2 20 | } 21 | 22 | get verticalSwing() { 23 | return (this.data[0x07] & 0x3) 24 | } 25 | // Byte 0x08 26 | get comfortSleepValue(): any { 27 | return this.data[0x08] & 0x03; 28 | } 29 | 30 | get powerSaving(): any { 31 | return (this.data[0x08] & 0x08) !== 0; 32 | } 33 | 34 | get lowFrequencyFan() { 35 | return (this.data[0x08] & 0x10) !== 0; 36 | } 37 | 38 | get turboFan() { 39 | return (this.data[0x08] & 0x20) !== 0; 40 | } 41 | 42 | get feelOwn() { 43 | return (this.data[0x08] & 0x80) !== 0; 44 | } 45 | // Byte 0x09 46 | get comfortSleep() { 47 | return (this.data[0x09] & 0x40) !== 0; 48 | } 49 | 50 | get naturalWind() { 51 | return (this.data[0x09] & 0x02) !== 0; 52 | } 53 | 54 | get ecoMode() { 55 | return (this.data[0x09] & 0x10) !== 0; 56 | } 57 | 58 | get purifier() { 59 | return (this.data[0x09] & 0x20) !== 0; 60 | } 61 | 62 | get dryer() { 63 | // This actually means 13°C(55°F)~35°C(95°F) according to my manual. Also dehumidifying. 64 | return (this.data[0x09] & 0x04) !== 0; 65 | } 66 | 67 | get ptc() { 68 | return (this.data[0x09] & 0x18) >> 3; 69 | } 70 | get auxHeat() { 71 | return (this.data[0x09] & 0x08) !== 0; 72 | } 73 | // Byte 0x0a 74 | get catchCold() { 75 | // This needs a better name, dunno what it actually means 76 | return (this.data[0x0a] & 0x08) > 0; 77 | } 78 | 79 | get turboMode() { 80 | return (this.data[0x0a] & 0x02) !== 0; 81 | } 82 | 83 | get useFahrenheit() { 84 | return (this.data[0x0a] & 0x04) !== 0; 85 | } 86 | 87 | get preventFreezing() { 88 | return (this.data[0x0a] & 0x20) !== 0; 89 | } 90 | // Byte 0x0b 91 | get indoorTemperature() { 92 | let indoorTempInteger 93 | let indoorTemperatureDot 94 | let indoorTempDecimal 95 | if (this.data[0] === 0xc0) { 96 | if (((this.data[11] - 50) / 2) < -19 || ((this.data[11] - 50) / 2) > 50) { 97 | return 0xff 98 | } else { 99 | indoorTempInteger = ((this.data[11] - 50) / 2) 100 | } 101 | indoorTemperatureDot = this.getBits(this.data, 15, 0, 3) 102 | indoorTempDecimal = indoorTemperatureDot * 0.1 103 | if (this.data[11] > 49) { 104 | return indoorTempInteger + indoorTempDecimal 105 | } else { 106 | return indoorTempInteger - indoorTempDecimal 107 | } 108 | } 109 | if ((this.data[0] === 0xa0) || (this.data[0] === 0xa1)) { 110 | if (this.data[0] === 0xa0) { 111 | if ((this.data[1] >> 2) - 4 === 0) { 112 | indoorTempInteger = -1 113 | } else { 114 | indoorTempInteger = (this.data[1] >> 2) + 12 115 | } 116 | if (((this.data[1] >> 1) & 0x01) === 1) { 117 | indoorTempDecimal = 0.5 118 | } else { 119 | indoorTempDecimal = 0 120 | } 121 | } 122 | if (this.data[0] === 0xa1) { 123 | if ((((this.data[13] - 50) / 2) < -19) || (((this.data[13] - 50) / 2) > 50)) { 124 | return 0xff 125 | } else { 126 | indoorTempInteger = ((this.data[13] - 50) / 2) 127 | } 128 | indoorTempDecimal = (this.data[18] & 0x0f) * 0.1 129 | } 130 | if ((this.data[13]) > 49) { 131 | return indoorTempInteger + indoorTempDecimal 132 | } else { 133 | return indoorTempInteger - indoorTempDecimal 134 | } 135 | } 136 | return 0xff 137 | } 138 | // Byte 0x0c 139 | get outdoorTemperature() { 140 | return (this.data[0x0c] - 50) / 2.0; 141 | } 142 | 143 | getBit(pByte: any, pIndex: any) { 144 | return (pByte >> pIndex) & 0x01 145 | } 146 | 147 | getBits(pBytes: any, pIndex: any, pStartIndex: any, pEndIndex: any) { 148 | let StartIndex 149 | let EndIndex 150 | if (pStartIndex > pEndIndex) { 151 | StartIndex = pEndIndex 152 | EndIndex = pStartIndex 153 | } else { 154 | StartIndex = pStartIndex 155 | EndIndex = pEndIndex 156 | } 157 | let tempVal = 0x00; 158 | let i = StartIndex; 159 | while (i <= EndIndex) { 160 | tempVal = tempVal | this.getBit(pBytes[pIndex], i) << (i - StartIndex) 161 | i += 1 162 | } 163 | return tempVal 164 | } 165 | } -------------------------------------------------------------------------------- /src/commands/ACSetCommand.ts: -------------------------------------------------------------------------------- 1 | import SetCommand from './SetCommand' 2 | import { MideaSwingMode } from '../enums/MideaSwingMode' 3 | import { MideaDeviceType } from '../enums/MideaDeviceType'; 4 | 5 | export default class ACSetCommand extends SetCommand { 6 | 7 | constructor(device_type: MideaDeviceType = MideaDeviceType.AirConditioner) { 8 | super(device_type); 9 | }; 10 | // Byte 0x0c 11 | get operationalMode() { 12 | return (this.data[0x0c] & 0xe0) >> 5; 13 | }; 14 | 15 | set operationalMode(mode: number) { 16 | this.data[0x0c] &= ~0xe0; // Clear the mode bit 17 | this.data[0x0c] |= (mode & 0x7) << 5; 18 | }; 19 | 20 | get targetTemperature() { 21 | // return this.data[0x0c] & 0x0f; 22 | return (this.data[0x0c] & 0x0f) + 16 + this.temperatureDecimal(); 23 | }; 24 | 25 | set targetTemperature(temperatureCelsius: any) { 26 | // this.data[0x0c] &= ~0x0f; // Clear the temperature bits 27 | // this.data[0x0c] |= (Math.trunc(temperatureCelsius) & 0x0f); // Clear the temperature bits, except the 0.5 bit, which will be set properly in all cases 28 | // this.temperatureDecimal = (Math.trunc(Math.round(temperatureCelsius * 2)) % 2 !== 0); // set the +0.5 bit 29 | 30 | let temperatureInteger; 31 | if (temperatureCelsius < 16 || temperatureCelsius > 31) { 32 | this.data[0x0c] &= ~0x0f // Clear the temperature bits 33 | this.temperatureDecimal = 0; 34 | } else { 35 | temperatureInteger = Math.trunc(temperatureCelsius) 36 | this.temperatureDecimal = temperatureCelsius - temperatureInteger; 37 | this.data[0x0c] |= Math.trunc(temperatureInteger) & 0x0f; 38 | } 39 | }; 40 | 41 | get temperatureDecimal() { 42 | return (this.data[0x0c] & 0x10) !== 0 ? 0.5 : 0; 43 | }; 44 | 45 | set temperatureDecimal(temperatureDecimalEnabled: any) { 46 | // add 0.5C to the temperature value. not intended to be called directly. targetTemperature set calls this if needed 47 | this.data[0x0c] &= ~0x10; // Clear the mode bits 48 | if (temperatureDecimalEnabled === 0.5) { 49 | this.data[0x0c] |= 0x10; 50 | } 51 | }; 52 | // Byte 0x11 53 | get horizontalSwing() { 54 | return (this.data[0x11] & 0x3) >> 2 55 | } 56 | 57 | set horizontalSwing(mode: any) { 58 | this.data[0x11] &= ~0x3 // Clear the mode bit 59 | this.data[0x11] |= mode ? 0x73 : 0; 60 | } 61 | 62 | get verticalSwing() { 63 | return (this.data[0x11] & 0xc) >> 2 64 | } 65 | 66 | set verticalSwing(mode: any) { 67 | this.data[0x11] &= 0xc; // Clear the mode bit 68 | this.data[0x11] |= mode ? 0x3c : 0; 69 | }; 70 | // Byte 0x12 71 | get turboFan() { 72 | return (this.data[0x12] & 0x20) !== 0; 73 | }; 74 | 75 | set turboFan(turboFanEnabled: boolean) { 76 | this.data[0x12] &= ~0x40; // Clear the mode bit 77 | this.data[0x12] |= turboFanEnabled ? 0x20 : 0; 78 | }; 79 | // Byte 0x13 80 | get dryer() { 81 | return (this.data[0x13] & 0x4) !== 0; 82 | } 83 | 84 | set dryer(dryerEnabled: boolean) { 85 | this.data[0x13] &= ~0x4; // Clear the mode bit 86 | this.data[0x13] |= dryerEnabled ? 0x4 : 0; 87 | } 88 | 89 | get purifier() { 90 | return (this.data[0x13] & 0x20) !== 0; 91 | } 92 | 93 | set purifier(purifierEnabled: boolean) { 94 | this.data[0x13] &= ~0x20; // Clear the mode bit 95 | this.data[0x13] |= purifierEnabled ? 0x20 : 0; 96 | } 97 | 98 | get ecoMode() { 99 | return (this.data[0x13] & 0x80) !== 0; 100 | }; 101 | 102 | set ecoMode(ecoModeEnabled: boolean) { 103 | this.data[0x13] &= ~0x80 // Clear the mode bit 104 | this.data[0x13] |= ecoModeEnabled ? 0x80 : 0; 105 | }; 106 | // Byte 0x14 107 | get useFahrenheit() { 108 | return (this.data[0x14] & 0x04) !== 0; 109 | }; 110 | 111 | set useFahrenheit(useFahrenheitEnabled: boolean) { 112 | // set the unit to fahrenheit from celcius 113 | this.data[0x14] &= ~0x04; // Clear the mode bits 114 | this.data[0x14] |= useFahrenheitEnabled ? 0x04 : 0; 115 | }; 116 | 117 | get comfortSleep() { 118 | // Activates sleep mode 119 | return (this.data[0x14] & 0x80) !== 0; 120 | } 121 | 122 | set comfortSleep(comfortSleepEnabled: boolean) { 123 | this.data[0x14] &= ~0x80; // Clear the comfort sleep switch 124 | this.data[0x14] |= comfortSleepEnabled ? 0x80 : 0; 125 | this.data[0x12] &= ~0x03; // Clear the comfort value 126 | this.data[0x12] |= comfortSleepEnabled ? 0x03 : 0; 127 | } 128 | 129 | get turboMode() { 130 | return (this.data[0x14] & 0x02) !== 0; 131 | } 132 | 133 | set turboMode(turboModeEnabled: boolean) { 134 | this.data[0x14] &= (~0x02); // Clear the mode bit 135 | this.data[0x14] |= turboModeEnabled ? 0x02 : 0; 136 | } 137 | 138 | get screenDisplay() { 139 | return (this.data[0x14] & 0x10) !== 0; 140 | }; 141 | 142 | set screenDisplay(screenDisplayEnabled: boolean) { 143 | // the LED lights on the AC. these display temperature and are often too bright during nights 144 | this.data[0x14] &= ~0x10; // Clear the mode bit 145 | this.data[0x14] |= screenDisplayEnabled ? 0x10 : 0; 146 | }; 147 | }; -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | export default class Constants { 2 | public static crc8_854_table = [ 3 | 0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 4 | 0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, 5 | 0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 6 | 0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, 7 | 0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 8 | 0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62, 9 | 0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 10 | 0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff, 11 | 0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 12 | 0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07, 13 | 0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 14 | 0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a, 15 | 0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 16 | 0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24, 17 | 0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 18 | 0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9, 19 | 0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 20 | 0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd, 21 | 0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 22 | 0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50, 23 | 0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 24 | 0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee, 25 | 0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 26 | 0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73, 27 | 0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 28 | 0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b, 29 | 0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 30 | 0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16, 31 | 0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 32 | 0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8, 33 | 0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 34 | 0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35, 35 | ]; 36 | 37 | public static UpdateCommand_AirCon = [ 38 | 170, // 0 - Sync header 39 | 32, // 1 - Message length request 40 | 172, // 2 - Device type (172 for Air Conditioner) 41 | 0, // 3 - Frame sync check (not used, 0x00) 42 | 0, // 4 - Reserved 0x00 43 | 0, // 5 - Reserved 0x00 44 | 0, // 6 - Message Id 45 | 0, // 7 - Framework protocol version 46 | 0, // 8 - Device Agreement Version 47 | 3, // 9 - Message type request identification 48 | // Command Header End 49 | // Data Start 50 | 65, // 10 - Data request/response: check status 51 | 129, // 11 - Power state 52 | 0, // 12 - Operational mode 53 | 255, // 13 54 | 3, // 14 - On timer 55 | 255, // 15 - Off timer 56 | 0, // 16 - Common timer 57 | 2, // 17 - Room temperature request: 0x02 - indoor temperature, 0x03 - outdoor temperature 58 | 0, // 18 59 | 0, // 19 60 | 0, // 20 61 | // Padding 62 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 63 | 3, // Message ID 64 | 205, 156, 16, 184, 113, 186, 162, 129, 39, 12, 160, 157, 100, 102, 118, 15, 154, 166 65 | ]; 66 | 67 | public static UpdateCommand_Dehumidifier = [ 68 | 170, // 0 - Sync header 69 | 32, // 1 - Message length request 70 | 161, // 2 - Device type (161 for Dehumidifier) 71 | 0, // 3 - Frame sync check (not used, 0x00) 72 | 0, // 4 - Reserved 0x00 73 | 0, // 5 - Reserved 0x00 74 | 0, // 6 - Message Id 75 | 0, // 7 - Framework protocol version 76 | 0, // 8 - Device Agreement Version 77 | 3, // 9 - Message type request identification 78 | // Command Header End 79 | // Data Start 80 | 65, // 10 - Data request 81 | 129, // 11 82 | 0, // 12 - Operational mode (1: target, 2: continuous, 3: smart, 4: dry) 83 | 255, // 13 - Timing + wind speed 40/60/80 84 | 3, // 14 - On timer 85 | 255, // 15 - Off timer 86 | 0, // 16 - Common timer 87 | 2, // 17 - Target humidity 88 | 0, // 18 89 | 0, // 19 - Display and other settings 90 | 0, // 20 - Swing and other settings. In dehumidifier, matches mode 91 | // Padding 92 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 93 | 11, 36, 164, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 94 | ]; 95 | 96 | public static UserAgent: string = 'Dalvik/2.22.0 (Linux; U; Android 9.0; SM-G935F Build/NRD90M)' 97 | // These values are from the official Midea Air app, adjust if you want to use different credentials 98 | public static AppId: string = '1117' 99 | public static AppKey: string = 'ff0cf6f5f0c3471de36341cab3f7a9af' 100 | public static Language: string = 'en-US' 101 | public static ClientType: string = '1' // 0: PC, 1: Android, 2: IOS 102 | public static RequestFormat: string = '2' // JSON 103 | public static RequestSource: string = '1010' // '17' 104 | 105 | public static supportedApps: { 106 | NetHomePlus: { 107 | appKey: '3742e9e5842d4ad59c2db887e12449f9', 108 | appId: '1017', 109 | apiURL: 'https://mapp.appsmb.com', 110 | signKey: 'xhdiwjnchekd4d512chdjx5d8e4c394D2D7S', 111 | proxied: null, 112 | }, 113 | MideaAir: { 114 | appKey: 'ff0cf6f5f0c3471de36341cab3f7a9af', 115 | appId: '1117', 116 | apiURL: 'https://mapp.appsmb.com', 117 | signKey: 'xhdiwjnchekd4d512chdjx5d8e4c394D2D7S', 118 | proxied: null, 119 | }, 120 | MSmartHome: { 121 | appKey: 'ac21b9f9cbfe4ca5a88562ef25e2b768', 122 | appId: '1010', 123 | apiURL: 'https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=', 124 | signKey: 'xhdiwjnchekd4d512chdjx5d8e4c394D2D7S', 125 | iotKey: 'meicloud', 126 | hmacKey: 'PROD_VnoClJI9aikS8dyy', 127 | proxied: 'v5', 128 | } 129 | }; 130 | }; -------------------------------------------------------------------------------- /lib/commands/ACSetCommand.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const SetCommand_1 = __importDefault(require("./SetCommand")); 7 | const MideaDeviceType_1 = require("../enums/MideaDeviceType"); 8 | class ACSetCommand extends SetCommand_1.default { 9 | constructor(device_type = MideaDeviceType_1.MideaDeviceType.AirConditioner) { 10 | super(device_type); 11 | } 12 | ; 13 | // Byte 0x0c 14 | get operationalMode() { 15 | return (this.data[0x0c] & 0xe0) >> 5; 16 | } 17 | ; 18 | set operationalMode(mode) { 19 | this.data[0x0c] &= ~0xe0; // Clear the mode bit 20 | this.data[0x0c] |= (mode & 0x7) << 5; 21 | } 22 | ; 23 | get targetTemperature() { 24 | // return this.data[0x0c] & 0x0f; 25 | return (this.data[0x0c] & 0x0f) + 16 + this.temperatureDecimal(); 26 | } 27 | ; 28 | set targetTemperature(temperatureCelsius) { 29 | // this.data[0x0c] &= ~0x0f; // Clear the temperature bits 30 | // this.data[0x0c] |= (Math.trunc(temperatureCelsius) & 0x0f); // Clear the temperature bits, except the 0.5 bit, which will be set properly in all cases 31 | // this.temperatureDecimal = (Math.trunc(Math.round(temperatureCelsius * 2)) % 2 !== 0); // set the +0.5 bit 32 | let temperatureInteger; 33 | if (temperatureCelsius < 16 || temperatureCelsius > 31) { 34 | this.data[0x0c] &= ~0x0f; // Clear the temperature bits 35 | this.temperatureDecimal = 0; 36 | } 37 | else { 38 | temperatureInteger = Math.trunc(temperatureCelsius); 39 | this.temperatureDecimal = temperatureCelsius - temperatureInteger; 40 | this.data[0x0c] |= Math.trunc(temperatureInteger) & 0x0f; 41 | } 42 | } 43 | ; 44 | get temperatureDecimal() { 45 | return (this.data[0x0c] & 0x10) !== 0 ? 0.5 : 0; 46 | } 47 | ; 48 | set temperatureDecimal(temperatureDecimalEnabled) { 49 | // add 0.5C to the temperature value. not intended to be called directly. targetTemperature set calls this if needed 50 | this.data[0x0c] &= ~0x10; // Clear the mode bits 51 | if (temperatureDecimalEnabled === 0.5) { 52 | this.data[0x0c] |= 0x10; 53 | } 54 | } 55 | ; 56 | // Byte 0x11 57 | get horizontalSwing() { 58 | return (this.data[0x11] & 0x3) >> 2; 59 | } 60 | set horizontalSwing(mode) { 61 | this.data[0x11] &= ~0x3; // Clear the mode bit 62 | this.data[0x11] |= mode ? 0x73 : 0; 63 | } 64 | get verticalSwing() { 65 | return (this.data[0x11] & 0xc) >> 2; 66 | } 67 | set verticalSwing(mode) { 68 | this.data[0x11] &= 0xc; // Clear the mode bit 69 | this.data[0x11] |= mode ? 0x3c : 0; 70 | } 71 | ; 72 | // Byte 0x12 73 | get turboFan() { 74 | return (this.data[0x12] & 0x20) !== 0; 75 | } 76 | ; 77 | set turboFan(turboFanEnabled) { 78 | this.data[0x12] &= ~0x40; // Clear the mode bit 79 | this.data[0x12] |= turboFanEnabled ? 0x20 : 0; 80 | } 81 | ; 82 | // Byte 0x13 83 | get dryer() { 84 | return (this.data[0x13] & 0x4) !== 0; 85 | } 86 | set dryer(dryerEnabled) { 87 | this.data[0x13] &= ~0x4; // Clear the mode bit 88 | this.data[0x13] |= dryerEnabled ? 0x4 : 0; 89 | } 90 | get purifier() { 91 | return (this.data[0x13] & 0x20) !== 0; 92 | } 93 | set purifier(purifierEnabled) { 94 | this.data[0x13] &= ~0x20; // Clear the mode bit 95 | this.data[0x13] |= purifierEnabled ? 0x20 : 0; 96 | } 97 | get ecoMode() { 98 | return (this.data[0x13] & 0x80) !== 0; 99 | } 100 | ; 101 | set ecoMode(ecoModeEnabled) { 102 | this.data[0x13] &= ~0x80; // Clear the mode bit 103 | this.data[0x13] |= ecoModeEnabled ? 0x80 : 0; 104 | } 105 | ; 106 | // Byte 0x14 107 | get useFahrenheit() { 108 | return (this.data[0x14] & 0x04) !== 0; 109 | } 110 | ; 111 | set useFahrenheit(useFahrenheitEnabled) { 112 | // set the unit to fahrenheit from celcius 113 | this.data[0x14] &= ~0x04; // Clear the mode bits 114 | this.data[0x14] |= useFahrenheitEnabled ? 0x04 : 0; 115 | } 116 | ; 117 | get comfortSleep() { 118 | // Activates sleep mode 119 | return (this.data[0x14] & 0x80) !== 0; 120 | } 121 | set comfortSleep(comfortSleepEnabled) { 122 | this.data[0x14] &= ~0x80; // Clear the comfort sleep switch 123 | this.data[0x14] |= comfortSleepEnabled ? 0x80 : 0; 124 | this.data[0x12] &= ~0x03; // Clear the comfort value 125 | this.data[0x12] |= comfortSleepEnabled ? 0x03 : 0; 126 | } 127 | get turboMode() { 128 | return (this.data[0x14] & 0x02) !== 0; 129 | } 130 | set turboMode(turboModeEnabled) { 131 | this.data[0x14] &= (~0x02); // Clear the mode bit 132 | this.data[0x14] |= turboModeEnabled ? 0x02 : 0; 133 | } 134 | get screenDisplay() { 135 | return (this.data[0x14] & 0x10) !== 0; 136 | } 137 | ; 138 | set screenDisplay(screenDisplayEnabled) { 139 | // the LED lights on the AC. these display temperature and are often too bright during nights 140 | this.data[0x14] &= ~0x10; // Clear the mode bit 141 | this.data[0x14] |= screenDisplayEnabled ? 0x10 : 0; 142 | } 143 | ; 144 | } 145 | exports.default = ACSetCommand; 146 | ; 147 | -------------------------------------------------------------------------------- /lib/responses/ACApplianceResponse.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | const ApplianceResponse_1 = __importDefault(require("./ApplianceResponse")); 7 | class ACApplianceResponse extends ApplianceResponse_1.default { 8 | // Byte 0x02 9 | get operationalMode() { 10 | return (this.data[0x02] & 0xe0) >> 5; 11 | } 12 | get targetTemperature() { 13 | if ((this.data[0x02] & 0x10) !== 0) { 14 | return (this.data[0x02] & 0xf) + 16.0 + 0.5; 15 | } 16 | else { 17 | return (this.data[0x02] & 0xf) + 16.0 + 0.0; 18 | } 19 | } 20 | // Byte 0x07 21 | get horizontalSwing() { 22 | return (this.data[0x07] & 0xc) >> 2; 23 | } 24 | get verticalSwing() { 25 | return (this.data[0x07] & 0x3); 26 | } 27 | // Byte 0x08 28 | get comfortSleepValue() { 29 | return this.data[0x08] & 0x03; 30 | } 31 | get powerSaving() { 32 | return (this.data[0x08] & 0x08) !== 0; 33 | } 34 | get lowFrequencyFan() { 35 | return (this.data[0x08] & 0x10) !== 0; 36 | } 37 | get turboFan() { 38 | return (this.data[0x08] & 0x20) !== 0; 39 | } 40 | get feelOwn() { 41 | return (this.data[0x08] & 0x80) !== 0; 42 | } 43 | // Byte 0x09 44 | get comfortSleep() { 45 | return (this.data[0x09] & 0x40) !== 0; 46 | } 47 | get naturalWind() { 48 | return (this.data[0x09] & 0x02) !== 0; 49 | } 50 | get ecoMode() { 51 | return (this.data[0x09] & 0x10) !== 0; 52 | } 53 | get purifier() { 54 | return (this.data[0x09] & 0x20) !== 0; 55 | } 56 | get dryer() { 57 | // This actually means 13°C(55°F)~35°C(95°F) according to my manual. Also dehumidifying. 58 | return (this.data[0x09] & 0x04) !== 0; 59 | } 60 | get ptc() { 61 | return (this.data[0x09] & 0x18) >> 3; 62 | } 63 | get auxHeat() { 64 | return (this.data[0x09] & 0x08) !== 0; 65 | } 66 | // Byte 0x0a 67 | get catchCold() { 68 | // This needs a better name, dunno what it actually means 69 | return (this.data[0x0a] & 0x08) > 0; 70 | } 71 | get turboMode() { 72 | return (this.data[0x0a] & 0x02) !== 0; 73 | } 74 | get useFahrenheit() { 75 | return (this.data[0x0a] & 0x04) !== 0; 76 | } 77 | get preventFreezing() { 78 | return (this.data[0x0a] & 0x20) !== 0; 79 | } 80 | // Byte 0x0b 81 | get indoorTemperature() { 82 | let indoorTempInteger; 83 | let indoorTemperatureDot; 84 | let indoorTempDecimal; 85 | if (this.data[0] === 0xc0) { 86 | if (((this.data[11] - 50) / 2) < -19 || ((this.data[11] - 50) / 2) > 50) { 87 | return 0xff; 88 | } 89 | else { 90 | indoorTempInteger = ((this.data[11] - 50) / 2); 91 | } 92 | indoorTemperatureDot = this.getBits(this.data, 15, 0, 3); 93 | indoorTempDecimal = indoorTemperatureDot * 0.1; 94 | if (this.data[11] > 49) { 95 | return indoorTempInteger + indoorTempDecimal; 96 | } 97 | else { 98 | return indoorTempInteger - indoorTempDecimal; 99 | } 100 | } 101 | if ((this.data[0] === 0xa0) || (this.data[0] === 0xa1)) { 102 | if (this.data[0] === 0xa0) { 103 | if ((this.data[1] >> 2) - 4 === 0) { 104 | indoorTempInteger = -1; 105 | } 106 | else { 107 | indoorTempInteger = (this.data[1] >> 2) + 12; 108 | } 109 | if (((this.data[1] >> 1) & 0x01) === 1) { 110 | indoorTempDecimal = 0.5; 111 | } 112 | else { 113 | indoorTempDecimal = 0; 114 | } 115 | } 116 | if (this.data[0] === 0xa1) { 117 | if ((((this.data[13] - 50) / 2) < -19) || (((this.data[13] - 50) / 2) > 50)) { 118 | return 0xff; 119 | } 120 | else { 121 | indoorTempInteger = ((this.data[13] - 50) / 2); 122 | } 123 | indoorTempDecimal = (this.data[18] & 0x0f) * 0.1; 124 | } 125 | if ((this.data[13]) > 49) { 126 | return indoorTempInteger + indoorTempDecimal; 127 | } 128 | else { 129 | return indoorTempInteger - indoorTempDecimal; 130 | } 131 | } 132 | return 0xff; 133 | } 134 | // Byte 0x0c 135 | get outdoorTemperature() { 136 | return (this.data[0x0c] - 50) / 2.0; 137 | } 138 | getBit(pByte, pIndex) { 139 | return (pByte >> pIndex) & 0x01; 140 | } 141 | getBits(pBytes, pIndex, pStartIndex, pEndIndex) { 142 | let StartIndex; 143 | let EndIndex; 144 | if (pStartIndex > pEndIndex) { 145 | StartIndex = pEndIndex; 146 | EndIndex = pStartIndex; 147 | } 148 | else { 149 | StartIndex = pStartIndex; 150 | EndIndex = pEndIndex; 151 | } 152 | let tempVal = 0x00; 153 | let i = StartIndex; 154 | while (i <= EndIndex) { 155 | tempVal = tempVal | this.getBit(pBytes[pIndex], i) << (i - StartIndex); 156 | i += 1; 157 | } 158 | return tempVal; 159 | } 160 | } 161 | exports.default = ACApplianceResponse; 162 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "midea-air", 3 | "pluginType": "platform", 4 | "singular": true, 5 | "customUi": false, 6 | "headerDisplay": "Homebridge plugin for Midea AC", 7 | "footerDisplay": "Created by @hillaliy", 8 | "schema": { 9 | "type": "object", 10 | "properties": { 11 | "user": { 12 | "title": "Midea account email", 13 | "type": "string", 14 | "required": true 15 | }, 16 | "password": { 17 | "title": "Midea account password", 18 | "type": "string", 19 | "x-schema-form": { 20 | "type": "password", 21 | "required": true 22 | } 23 | }, 24 | "registeredApp": { 25 | "title": "Midea Registered app", 26 | "type": "string", 27 | "required": true, 28 | "default": "NetHomePlus", 29 | "oneOf": [ 30 | { 31 | "title": "NetHomePlus", 32 | "enum": ["NetHomePlus"] 33 | }, 34 | { 35 | "title": "MideaAir", 36 | "enum": ["MideaAir"] 37 | }, 38 | { 39 | "title": "MSmartHome", 40 | "enum": ["MSmartHome"] 41 | } 42 | ] 43 | }, 44 | "interval": { 45 | "title": "Update interval in seconds", 46 | "description": "Time in seconds between each status polling of the Midea devices, Default is 30", 47 | "type": "integer", 48 | "default": 30, 49 | "minimum": 0, 50 | "maximum": 600 51 | }, 52 | "devices": { 53 | "type": "array", 54 | "items": { 55 | "title": "Device", 56 | "type": "object", 57 | "properties": { 58 | "deviceId": { 59 | "title": "Device ID", 60 | "type": "string" 61 | }, 62 | "supportedSwingMode": { 63 | "title": "Supported Swing Mode (Only for AC)", 64 | "description": "None, Vertical, Horizontal, Both You have to select which type your device supports", 65 | "type": "string", 66 | "default": "both", 67 | "required": true, 68 | "oneOf": [ 69 | { 70 | "title": "None", 71 | "enum": ["None"] 72 | }, 73 | { 74 | "title": "Vertical", 75 | "enum": ["Vertical"] 76 | }, 77 | { 78 | "title": "Horizontal", 79 | "enum": ["Horizontal"] 80 | }, 81 | { 82 | "title": "Both", 83 | "enum": ["Both"] 84 | } 85 | ] 86 | }, 87 | "temperatureSteps": { 88 | "title": "Change Temperature Steps on HomeKit (Only for AC)", 89 | "description": "The options are 1˚ or 0.5˚, default is: 1˚", 90 | "type": "number", 91 | "default": 1, 92 | "required": false, 93 | "oneOf": [ 94 | { 95 | "title": "0.5", 96 | "enum": [0.5] 97 | }, 98 | { 99 | "title": "1", 100 | "enum": [1] 101 | } 102 | ] 103 | }, 104 | "minTemp": { 105 | "title": "Change minimum Threshold Temperature (Only for AC)", 106 | "description": "Default is: 17˚", 107 | "type": "integer", 108 | "default": 17, 109 | "minimum": 10, 110 | "maximum": 35 111 | }, 112 | "maxTemp": { 113 | "title": "Change maximum Threshold Temperature (Only for AC)", 114 | "description": "Default is: 30˚", 115 | "type": "integer", 116 | "default": 30, 117 | "minimum": 10, 118 | "maximum": 35 119 | }, 120 | "fanOnlyMode": { 121 | "title": "Enable Fan Mode (Only For AC)", 122 | "type": "boolean", 123 | "default": false, 124 | "required": false 125 | }, 126 | "OutdoorTemperature": { 127 | "title": "Enable Outdoor Temperture Sensor (Only for AC)", 128 | "type": "boolean", 129 | "default": false, 130 | "required": false 131 | }, 132 | "useFahrenheit": { 133 | "title": "Set Fahrenheit to default (Only for AC)", 134 | "type": "boolean", 135 | "default": false, 136 | "required": false 137 | }, 138 | "audibleFeedback": { 139 | "title": "Set audibleFeedback", 140 | "type": "boolean", 141 | "default": false, 142 | "required": false 143 | } 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "layout": [ 150 | { 151 | "key": "properties", 152 | "type": "array", 153 | "items": ["user", "password", "registeredApp"] 154 | }, 155 | { 156 | "key": "devices", 157 | "type": "array", 158 | "expandable": true, 159 | "buttonText": "Add device", 160 | "items": [ 161 | "devices[].deviceId", 162 | "devices[].supportedSwingMode", 163 | "devices[].temperatureSteps", 164 | "devices[].minTemp", 165 | "devices[].maxTemp", 166 | "devices[].fanOnlyMode", 167 | "devices[].OutdoorTemperature", 168 | "devices[].useFahrenheit", 169 | "devices[].audibleFeedback" 170 | ] 171 | }, 172 | { 173 | "key": "Advanced Settings", 174 | "type": "fieldset", 175 | "expandable": true, 176 | "description": "Don't change these, unless you understand what you're doing.", 177 | "items": ["interval"] 178 | } 179 | ] 180 | } 181 | -------------------------------------------------------------------------------- /lib/Utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | // Utils – Utility functions 7 | const crypto_1 = __importDefault(require("crypto")); 8 | class Utils { 9 | static encode(data) { 10 | const normalized = []; 11 | for (let b of data) { 12 | if (b >= 128) { 13 | b = b - 256; 14 | } 15 | ; 16 | normalized.push(b); 17 | } 18 | ; 19 | return normalized; 20 | } 21 | ; 22 | static decode(data) { 23 | const normalized = []; 24 | for (let b of data) { 25 | if (b < 0) { 26 | b = b + 256; 27 | } 28 | ; 29 | normalized.push(b); 30 | } 31 | ; 32 | return normalized; 33 | } 34 | ; 35 | // Returns a timestamp in the format YYYYMMDDHHmmss 36 | static getStamp() { 37 | const date = new Date(); 38 | return date 39 | .toISOString() 40 | .slice(0, 19) 41 | .replace(/-/g, "") 42 | .replace(/:/g, "") 43 | .replace(/T/g, ""); 44 | } 45 | ; 46 | static formatResponse(arr) { 47 | let output = []; 48 | for (let i = 0; i < arr.length; i++) { 49 | let intValue = parseInt(arr[i]); 50 | output.push((intValue).toString(2)); 51 | } 52 | ; 53 | return output; 54 | } 55 | ; 56 | static getSign(path, form, appKey) { 57 | if (path !== '' && form) { 58 | let postfix = `/v1${path}`; 59 | postfix = postfix.split('?')[0]; 60 | // Maybe this will help, should remove any query string parameters in the URL from the sign 61 | const ordered = {}; 62 | Object.keys(form) 63 | .sort() 64 | .forEach(function (key) { 65 | ordered[key] = form[key]; 66 | }); 67 | const query = Object.keys(ordered) 68 | .map((key) => key + "=" + ordered[key]) 69 | .join("&"); 70 | return crypto_1.default 71 | .createHash("sha256") 72 | .update(postfix + query + appKey) 73 | .digest("hex"); 74 | } 75 | else { 76 | return false; 77 | } 78 | ; 79 | } 80 | ; 81 | static decryptAes(reply, dataKey) { 82 | if (reply && dataKey != '') { 83 | const decipher = crypto_1.default.createDecipheriv("aes-128-ecb", dataKey, ""); 84 | const dec = decipher.update(reply, "hex", "utf8"); 85 | return dec.split(",").map(Number); 86 | } 87 | else { 88 | return []; 89 | } 90 | ; 91 | } 92 | ; 93 | static decryptAesString(reply, dataKey) { 94 | if (reply && dataKey != '') { 95 | const decipher = crypto_1.default.createDecipheriv("aes-128-ecb", dataKey, ""); 96 | const dec = decipher.update(reply, "hex", "utf8"); 97 | return dec; 98 | } 99 | else { 100 | return ''; 101 | } 102 | ; 103 | } 104 | ; 105 | static encryptAes(query, dataKey) { 106 | if (query && dataKey != '') { 107 | const cipher = crypto_1.default.createCipheriv("aes-128-ecb", dataKey, ""); 108 | let ciph = cipher.update(query.join(","), "utf8", "hex"); 109 | ciph += cipher.final("hex"); 110 | return ciph; 111 | } 112 | else { 113 | return false; 114 | } 115 | ; 116 | } 117 | ; 118 | static encryptAesString(query, dataKey) { 119 | if (dataKey != '') { 120 | const cipher = crypto_1.default.createCipheriv("aes-128-ecb", dataKey, ""); 121 | let ciph = cipher.update(query, "utf8", "hex"); 122 | ciph += cipher.final("hex"); 123 | return ciph; 124 | } 125 | else { 126 | return false; 127 | } 128 | ; 129 | } 130 | ; 131 | static getSignPassword(loginId, password, appKey) { 132 | if (loginId !== '' && password !== '') { 133 | const pw = crypto_1.default 134 | .createHash("sha256") 135 | .update(password) 136 | .digest("hex"); 137 | return crypto_1.default 138 | .createHash("sha256") 139 | .update(loginId + pw + appKey) 140 | .digest("hex"); 141 | } 142 | else { 143 | return ''; 144 | } 145 | ; 146 | } 147 | ; 148 | static generateDataKey(accessToken, appKey) { 149 | if (accessToken != '') { 150 | const md5AppKey = crypto_1.default 151 | .createHash("md5") 152 | .update(appKey).digest("hex"); 153 | const decipher = crypto_1.default.createDecipheriv("aes-128-ecb", md5AppKey.slice(0, 16), ""); 154 | const dec = decipher.update(accessToken, "hex", "utf8"); 155 | return dec; 156 | } 157 | ; 158 | return ''; 159 | } 160 | ; 161 | static encryptIAMPassword(loginId, password, appKey) { 162 | const passwordHash = crypto_1.default 163 | .createHash('md5') 164 | .update(password) 165 | .digest(); 166 | const password2ndHash = crypto_1.default 167 | .createHash('md5') 168 | .update(passwordHash) 169 | .digest(); 170 | return crypto_1.default 171 | .createHash('sha256') 172 | .update(loginId + password2ndHash + appKey) 173 | .digest('hex'); 174 | } 175 | ; 176 | static getSignProxied(form) { 177 | const msg = `meicloud${form}${Utils.randomString}`; 178 | return crypto_1.default 179 | .createHmac('sha256', 'PROD_VnoClJI9aikS8dyy') 180 | .update(msg) 181 | .digest('hex'); 182 | } 183 | ; 184 | } 185 | exports.default = Utils; 186 | Utils.randomString = Math.floor(new Date().getTime() / 1000).toString(); 187 | Utils.pushToken = crypto_1.default.randomBytes(120).toString('base64'); 188 | Utils.reqId = crypto_1.default.randomBytes(16).toString('hex'); 189 | ; 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | 7 | 8 | # Homebridge Midea Air 9 | 10 | [![Downloads](https://img.shields.io/npm/dt/homebridge-midea-air.svg?color=critical)](https://www.npmjs.com/package/homebridge-midea-air) 11 | [![Version](https://img.shields.io/npm/v/homebridge-midea-air)](https://www.npmjs.com/package/homebridge-midea-air) 12 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
13 | Homebridge Discord Channel - midea air
14 | [![Homebridge Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/homebridge-432663330281226270)
15 | 16 | Programming is not easy.
17 | If you like this plugin or want to contribute to future development, a donation will help.

18 | 19 | ## [Homebridge](https://github.com/nfarina/homebridge) plugin to control Midea Air Conditioner & Dehumidifier units. 20 | 21 |   22 | 23 | 24 | 25 | 26 | **_Requirements:_**
27 |   28 |   29 | 30 | 31 | ## ⚠️ Knowing Issues 32 | 33 | - This plugin don't fully supported `Midea Mission II / Blanc / OSK105`, you can only get device status.
34 | - Using the Midea app and `Homebridge Midea Air plugin` at the same time causes a login error. Try to use [NetHome Plus](https://apps.apple.com/us/app/nethome-plus/id1008001920) app instead.
35 | - This plugin don't support `MSmartHome app`.
36 | - Audible Feedback on Dehumidifier don't work. 37 | 38 | ## 🛰️ Supported Devices 39 | 40 | - This Plugin support Midea providers dongle - OSK102 / OSK103 (Hualing, Senville, Klimaire, AirCon, Century, Pridiom, Thermocore, Comfee, Alpine Home Air, Artel, Beko, Electrolux, Galactic, Idea, Inventor, Kaisai, Mitsui, Mr. Cool, Neoclima, Olimpia Splendid, Pioneer, QLIMA, Royal Clima, Qzen, Toshiba, Carrier, Goodman, Friedrich, Samsung, Kenmore, Trane, Lennox, LG, Electra and much more) and should be able to access all device in the user's account.
41 | - However, many devices may not be supported or function incorrectly. 42 | This is due to the lack of documentation of the raw MSmart API.
43 | - If you encounter any problems, please open a new issue and specify your device model. 44 | 45 | ## ⚙️ Configuration 46 | 47 | You can use the plugin settings or add this to the platforms array in your config.json: 48 | 49 | { 50 | "user": "MIDEA_ACCOUNT_EMAIL", 51 | "password": "MIDEA_PASSWORD", 52 | "registeredApp": "NetHomePlus", 53 | "interval": 30, 54 | "devices": [ 55 | { 56 | "deviceId": "DeviceID-1", 57 | "supportedSwingMode": "Both", 58 | "temperatureSteps": 1, 59 | "minTemp": 17, 60 | "maxTemp": 30, 61 | "fanOnlyMode": false, 62 | "OutdoorTemperature": false, 63 | "useFahrenheit": false, 64 | "audibleFeedback": false 65 | }, 66 | { 67 | "deviceId": "DeviceID-2", 68 | "supportedSwingMode": "Both", 69 | "temperatureSteps": 1, 70 | "minTemp": 17, 71 | "maxTemp": 30, 72 | "fanOnlyMode": false, 73 | "OutdoorTemperature": false, 74 | "useFahrenheit": false, 75 | "audibleFeedback": false 76 | } 77 | ], 78 | "platform": "midea-air" 79 | } 80 | 81 | ## ⚙️ Optional per-device Configuration Values 82 | 83 | To set specific per-device values, you need to add deviceId that can find in: 84 | 85 | 1. Homebridge console log. ([midea-air] Created device: Kitchen, with ID: `XXXXXXXXXXXXXX`, and type: 172) 86 | 2. HomeKit app, device settings, info. 87 | 88 | ### 📟 Temperature Display Units (AC Only) 89 | 90 | This Plugin support Celsius & Fahrenheit (You can set the Default unit on Homebridge config).
91 | Display Units can set in HomeKit app, device settings.
92 | `This is just to control the temperature unit of the AC's display. The target temperature setter always expects a celsius temperature (resolution of 0.5C), as does the midea API` 93 | 94 | ### 🎚️ Temperature Steps (AC Only) 95 | 96 | This option change Temperature Steps on HomeKit. 97 | You can choose 1˚ or 0.5˚, default is: 1˚ 98 | 99 | ### 🎚️ Temperature Threshold (AC Only) 100 | 101 | This option change Temperature Thrashold. 102 | Defaults: minimum 17˚ / maximum 30˚ 103 | 104 | ### 💨 Rotation Speed and Swing 105 | 106 | Rotation Speed and Swing mode can set in the HomeKit app, device settings. 107 | Rotation Speed values are: 108 | | Air Conditioner | Dehumidifier | 109 | | --- | --- | 110 | | 0% Device Off | 0% Device Off | 111 | | 20% Silent | 30% Silent | 112 | | 40% Low | ... | 113 | | 60% Middle | 60% Medium | 114 | | 80% High | ... | 115 | | 100% Auto | 100% Turbo | 116 | 117 | Dehumidifier does not have an Swing mode, therefore in config.json select "None". 118 | 119 | ### 💧 Dehumidifier Relative Humidity 120 | 121 | There is a difference between Midea app / Homebridge to HomeKit. 122 | HomeKit Relative Humidity work between 0%-100% - Apple Policy. 123 | | App / Homebridge | HomeKit | 124 | | --- | --- | 125 | | 35% | 0% | 126 | | 40% | 10% | 127 | | 45% | 20% | 128 | | 50% | 30% | 129 | | 55% | 40% | 130 | | 60% | 50% | 131 | | 65% | 60% | 132 | | 70% | 70% | 133 | | 75% | 80% | 134 | | 80% | 90% | 135 | | 85% | 100% | 136 | 137 | ### 💦 Dehumidifier Modes 138 | 139 | Dehumidifier has 4 Operational modes. You can change modes according to the following table: 140 | 141 | | Device | HomeKit | 142 | | ---------- | ------------ | 143 | | Normal | HUMIDIFIER | 144 | | Continuous | --- | 145 | | Smart | AUTO | 146 | | Dryer | DEHUMIDIFIER | 147 | 148 | Continuous mode will be considered as Auto mode. 149 | 150 | ### 🌪️ Fan Mode (AC only) 151 | 152 | This allows you to enable a Fan mode service. 153 | 154 | ### 🌤️ Outdoor Temperature Sensor (AC Only) 155 | 156 | This allows you to enable Outdoor Temperature service, if the AC support. 157 | 158 | ### 🔈 Audible Feedback 159 | 160 | This set the Audible Feedback (beep sound). 161 | 162 | ## 🙏 Credits 163 | 164 | This plugin would not have been possible without the fundamentals of all the Midea API clients in Python provided. 165 | -------------------------------------------------------------------------------- /src/MideaPlatform.ts: -------------------------------------------------------------------------------- 1 | import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; 2 | 3 | const axios = require('axios'); 4 | const strftime = require('strftime'); 5 | 6 | // import tunnel from 'tunnel'; 7 | 8 | const { wrapper: axiosCookieJarSupport } = require('axios-cookiejar-support'); 9 | import tough from 'tough-cookie'; 10 | import qs from 'querystring'; 11 | import Utils from './Utils'; 12 | import Constants from './Constants'; 13 | import PacketBuilder from './PacketBuilder'; 14 | 15 | import ACSetCommand from './commands/ACSetCommand'; 16 | import DehumidifierSetCommand from './commands/DehumidifierSetCommand'; 17 | 18 | import ACApplianceResponse from './responses/ACApplianceResponse'; 19 | import DehumidifierApplianceResponse from './responses/DehumidifierApplianceResponse'; 20 | 21 | import { MideaAccessory } from './MideaAccessory'; 22 | import { MideaDeviceType } from './enums/MideaDeviceType'; 23 | import { MideaErrorCodes } from './enums/MideaErrorCodes'; 24 | 25 | export class MideaPlatform implements DynamicPlatformPlugin { 26 | public readonly Service: typeof Service = this.api.hap.Service; 27 | public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; 28 | jar: any; 29 | updateInterval: any = null; 30 | reauthInterval: any = null; 31 | accessToken: string = ''; 32 | sessionId: string = ''; 33 | userId: string = ''; 34 | dataKey: string = ''; 35 | apiClient: any; 36 | public readonly accessories: PlatformAccessory[] = []; 37 | mideaAccessories: MideaAccessory[] = [] 38 | 39 | constructor( 40 | public readonly log: Logger, 41 | public readonly config: PlatformConfig, 42 | public readonly api: API 43 | ) { 44 | axiosCookieJarSupport(axios); 45 | this.jar = new tough.CookieJar() 46 | if (this.config.registeredApp === 'MSmartHome') { 47 | this.log.info('Using proxy login'); 48 | const form = { 49 | appId: '1010', 50 | format: 2, 51 | clientType: 1, 52 | language: 'en_US', 53 | src: '1010', 54 | stamp: strftime('%Y%m%d%H%M%S'), 55 | reqId: Utils.reqId, 56 | loginAccount: this.config['user'], 57 | } 58 | const formJSON = JSON.stringify(form); 59 | const signature = Utils.getSignProxied(formJSON); 60 | this.apiClient = axios.create({ 61 | baseURL: 'https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=/v1', 62 | headers: { 63 | Authorization: `Basic ${Buffer.from('1010:meicloud').toString('base64')}`, 64 | sign: signature, 65 | secretVersion: '1', 66 | random: Utils.randomString, 67 | 'Content-Type': 'application/json', 68 | accessToken: this.accessToken, 69 | }, 70 | jar: this.jar, 71 | }) 72 | } else { 73 | this.apiClient = axios.create({ 74 | baseURL: 'https://mapp.appsmb.com/v1', 75 | headers: { 76 | 'User-Agent': Constants.UserAgent, 77 | 'Content-Type': 'application/x-www-form-urlencoded' 78 | }, 79 | jar: this.jar 80 | }); 81 | }; 82 | this.log = log; 83 | this.config = config; 84 | api.on('didFinishLaunching', () => { 85 | this.onReady(); 86 | }); 87 | }; 88 | 89 | async onReady() { 90 | try { 91 | await this.login() 92 | this.log.debug('Login successful') 93 | try { 94 | await this.getUserList() 95 | this.updateValues() 96 | } catch (err) { 97 | this.log.debug('getUserList failed') 98 | }; 99 | this.updateInterval = setInterval(() => { 100 | this.updateValues(); 101 | }, this.config['interval'] * 1000); 102 | } catch (err) { 103 | this.log.debug('Login failed') 104 | }; 105 | }; 106 | 107 | async login() { 108 | return new Promise(async (resolve, reject) => { 109 | const url = '/user/login/id/get'; 110 | const form: any = { 111 | loginAccount: this.config['user'], 112 | clientType: Constants.ClientType, 113 | src: Constants.RequestSource, 114 | appId: Constants.AppId, 115 | format: Constants.RequestFormat, 116 | stamp: strftime('%Y%m%d%H%M%S'), 117 | language: Constants.Language, 118 | reqid: Utils.reqId, 119 | }; 120 | const sign = Utils.getSign(url, form, Constants.AppKey); 121 | form.sign = sign; 122 | try { 123 | const response = await this.apiClient.post(url, qs.stringify(form)) 124 | if (response.data?.errorCode && response.data.errorCode !== '0') { 125 | this.log.debug(`Login request failed with error: ${response.data.msg}`) 126 | } else { 127 | const loginId: string = response.data.result.loginId; 128 | const password: string = Utils.getSignPassword(loginId, this.config.password, Constants.AppKey); 129 | const url = '/user/login'; 130 | const form: any = { 131 | loginAccount: this.config['user'], 132 | src: Constants.RequestSource, 133 | format: Constants.RequestFormat, 134 | stamp: strftime('%Y%m%d%H%M%S'), 135 | language: Constants.Language, 136 | password: password, 137 | clientType: Constants.ClientType, 138 | appId: Constants.AppId, 139 | }; 140 | const sign = Utils.getSign(url, form, Constants.AppKey); 141 | form.sign = sign; 142 | try { 143 | const loginResponse = await this.apiClient.post(url, qs.stringify(form)); 144 | if (loginResponse.data.errorCode && loginResponse.data.errorCode !== '0') { 145 | this.log.debug(`Login request 2 returned error: ${loginResponse.data.msg}`); 146 | reject(); 147 | } else { 148 | this.accessToken = loginResponse.data.result.accessToken; 149 | this.sessionId = loginResponse.data.result.sessionId; 150 | this.userId = loginResponse.data.result.userId 151 | this.dataKey = Utils.generateDataKey(this.accessToken, Constants.AppKey); 152 | resolve(); 153 | }; 154 | } catch (err) { 155 | this.log.debug(`Login request 2 failed with: ${err}`) 156 | reject(); 157 | }; 158 | }; 159 | } catch (err) { 160 | this.log.debug(`Login request failed with: ${err}`); 161 | reject(); 162 | }; 163 | }); 164 | }; 165 | 166 | async getUserList() { 167 | this.log.debug('getUserList called'); 168 | return new Promise(async (resolve, reject) => { 169 | const form: any = { 170 | src: Constants.RequestSource, 171 | format: Constants.RequestFormat, 172 | stamp: strftime('%Y%m%d%H%M%S'), 173 | language: Constants.Language, 174 | sessionId: this.sessionId 175 | }; 176 | const url = '/appliance/user/list/get'; 177 | const sign = Utils.getSign(url, form, Constants.AppKey); 178 | form.sign = sign; 179 | try { 180 | const response = await this.apiClient.post(url, qs.stringify(form)) 181 | if (response.data.errorCode && response.data.errorCode !== '0') { 182 | this.log.error(`getUserList returned error: ${response.data.msg}`); 183 | reject(); 184 | } else { 185 | if (response.data.result?.list && response.data.result.list.length > 0) { 186 | response.data.result.list.forEach(async (currentElement: any) => { 187 | if (parseInt(currentElement.type) === MideaDeviceType.AirConditioner || parseInt(currentElement.type) === MideaDeviceType.Dehumidifier) { 188 | const uuid = this.api.hap.uuid.generate(currentElement.id) 189 | const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid) 190 | if (existingAccessory) { 191 | this.log.debug('Restoring cached accessory', existingAccessory.displayName) 192 | existingAccessory.context.deviceId = currentElement.id 193 | existingAccessory.context.deviceType = parseInt(currentElement.type) 194 | existingAccessory.context.name = currentElement.name 195 | existingAccessory.context.userId = currentElement.userId 196 | existingAccessory.context.modelNumber = currentElement.modelNumber 197 | existingAccessory.context.sn = Utils.decryptAesString(currentElement.sn, this.dataKey) 198 | this.log.debug(`Model Number:${existingAccessory.context.modelNumber}`) 199 | this.log.debug(`Serial Number:${existingAccessory.context.sn}`) 200 | 201 | this.api.updatePlatformAccessories([existingAccessory]) 202 | 203 | var ma = new MideaAccessory(this, existingAccessory, currentElement.id, parseInt(currentElement.type), currentElement.name, currentElement.userId) 204 | this.mideaAccessories.push(ma) 205 | } else { 206 | this.log.debug(`Adding new device: ${currentElement.name}`) 207 | const accessory = new this.api.platformAccessory(currentElement.name, uuid) 208 | accessory.context.deviceId = currentElement.id 209 | accessory.context.deviceType = parseInt(currentElement.type) 210 | accessory.context.name = currentElement.name 211 | accessory.context.userId = currentElement.userId 212 | accessory.context.modelNumber = currentElement.modelNumber 213 | accessory.context.sn = Utils.decryptAesString(currentElement.sn, this.dataKey) 214 | this.log.debug(`Model Number:${accessory.context.modelNumber}`) 215 | this.log.debug(`Serial Number:${accessory.context.sn}`) 216 | 217 | var ma = new MideaAccessory(this, accessory, currentElement.id, parseInt(currentElement.type), currentElement.name, currentElement.userId) 218 | this.api.registerPlatformAccessories('homebridge-midea-air', 'midea-air', [accessory]) 219 | this.mideaAccessories.push(ma) 220 | }; 221 | } else { 222 | this.log.warn(`Device: ${currentElement.name} is of unsupported type: ${MideaDeviceType[parseInt(currentElement.type)]}`) 223 | this.log.warn('Please open an issue on GitHub with your specific device model') 224 | }; 225 | }); 226 | resolve(); 227 | } else { 228 | this.log.error('getUserList invalid response'); 229 | reject(); 230 | }; 231 | }; 232 | } catch (err) { 233 | this.log.debug(`getUserList error: ${err}`); 234 | reject(); 235 | }; 236 | }); 237 | }; 238 | 239 | async sendCommand(device: MideaAccessory, order: any, intent: string) { 240 | return new Promise(async (resolve, reject) => { 241 | if (device) { 242 | const orderEncode = Utils.encode(order); 243 | const orderEncrypt = Utils.encryptAes(orderEncode, this.dataKey); 244 | const form: any = { 245 | applianceId: device.deviceId, 246 | src: Constants.RequestSource, 247 | format: Constants.RequestFormat, 248 | funId: "0000", //maybe it is also "FC02" 249 | order: orderEncrypt, 250 | stamp: strftime('%Y%m%d%H%M%S'), 251 | language: Constants.Language, 252 | sessionId: this.sessionId, 253 | }; 254 | const url = '/appliance/transparent/send'; 255 | const sign = Utils.getSign(url, form, Constants.AppKey); 256 | form.sign = sign; 257 | try { 258 | const response = await this.apiClient.post(url, qs.stringify(form)) 259 | if (response.data.errorCode && response.data.errorCode !== '0') { 260 | if (response.data.errorCode = MideaErrorCodes.CommandNotAccepted) { 261 | this.log.debug(`Send command to: ${device.name} (${device.deviceId}) ${intent} returned error: ${response.data.msg} (${response.data.errorCode})`) 262 | return; 263 | } else { 264 | this.log.info(`Send command to: ${device.name} (${device.deviceId}) ${intent} returned error: ${response.data.msg} (${response.data.errorCode})`) 265 | return; 266 | } 267 | } else { 268 | this.log.debug(`Send command to: ${device.name} (${device.deviceId}) ${intent} success!`); 269 | let applianceResponse: any 270 | if (device.deviceType === MideaDeviceType.AirConditioner) { 271 | applianceResponse = new ACApplianceResponse(Utils.decode(Utils.decryptAes(response.data.result.reply, this.dataKey))); 272 | 273 | device.targetTemperature = applianceResponse.targetTemperature; 274 | device.indoorTemperature = applianceResponse.indoorTemperature; 275 | device.outdoorTemperature = applianceResponse.outdoorTemperature; 276 | device.swingMode = applianceResponse.swingMode; 277 | device.useFahrenheit = applianceResponse.useFahrenheit; 278 | device.turboFan = applianceResponse.turboFan; 279 | device.ecoMode = applianceResponse.ecoMode; 280 | device.turboMode = applianceResponse.turboMode; 281 | device.comfortSleep = applianceResponse.comfortSleep; 282 | device.dryer = applianceResponse.dryer; 283 | device.purifier = applianceResponse.purifier; 284 | 285 | if (device.useFahrenheit === true) { 286 | this.log.debug(`Target Temperature: ${this.toFahrenheit(device.targetTemperature)}˚F`); 287 | this.log.debug(`Indoor Temperature: ${this.toFahrenheit(device.indoorTemperature)}˚F`); 288 | } else { 289 | this.log.debug(`Target Temperature: ${device.targetTemperature}˚C`); 290 | this.log.debug(`Indoor Temperature: ${device.indoorTemperature}˚C`); 291 | }; 292 | if (applianceResponse.outdoorTemperature < 100) { 293 | if (device.useFahrenheit === true) { 294 | this.log.debug(`Outdoor Temperature: ${this.toFahrenheit(device.outdoorTemperature)}˚F`); 295 | } else { 296 | this.log.debug(`Outdoor Temperature: ${device.outdoorTemperature}˚C`); 297 | }; 298 | }; 299 | this.log.debug(`Swing Mode set to: ${device.swingMode}`); 300 | this.log.debug(`Fahrenheit set to: ${device.useFahrenheit}`); 301 | this.log.debug(`Turbo Fan set to: ${device.turboFan}`); 302 | this.log.debug(`Eco Mode set to: ${device.ecoMode}`); 303 | this.log.debug(`Turbo Mode set to: ${device.turboMode}`); 304 | this.log.debug(`Comfort Sleep set to: ${device.comfortSleep}`); 305 | this.log.debug(`Dryer set to: ${device.dryer}`); 306 | this.log.debug(`Purifier set to: ${device.purifier}`); 307 | 308 | } else if (device.deviceType === MideaDeviceType.Dehumidifier) { 309 | applianceResponse = new DehumidifierApplianceResponse(Utils.decode(Utils.decryptAes(response.data.result.reply, this.dataKey))); 310 | 311 | device.currentHumidity = applianceResponse.currentHumidity; 312 | device.targetHumidity = applianceResponse.targetHumidity; 313 | device.waterLevel = applianceResponse.waterLevel; 314 | 315 | this.log.debug(`Current Humidity: ${device.currentHumidity}`); 316 | this.log.debug(`Target humidity set to: ${device.targetHumidity}`); 317 | this.log.debug(`Water level at: ${device.waterLevel}`); 318 | }; 319 | // Common 320 | device.powerState = applianceResponse.powerState ? 1 : 0; 321 | device.operationalMode = applianceResponse.operationalMode; 322 | device.fanSpeed = applianceResponse.fanSpeed; 323 | 324 | this.log.debug(`Power State set to: ${device.powerState}`); 325 | this.log.debug(`Operational Mode set to: ${device.operationalMode}`); 326 | this.log.debug(`Fan Speed set to: ${device.fanSpeed}`); 327 | 328 | this.log.debug(`Full data: ${Utils.formatResponse(applianceResponse.data)}`) 329 | resolve(); 330 | }; 331 | } catch (err) { 332 | this.log.error(`SendCommand (${intent}) request failed: ${err}`); 333 | reject(); 334 | }; 335 | } else { 336 | this.log.error('No device specified'); 337 | reject(); 338 | }; 339 | }); 340 | }; 341 | 342 | updateValues() { 343 | // STATUS ONLY OR POWER ON/OFF HEADER 344 | const ac_data_header = [90, 90, 1, 16, 89, 0, 32, 0, 80, 0, 0, 0, 169, 65, 48, 9, 14, 5, 20, 20, 213, 50, 1, 0, 0, 17, 0, 0, 0, 4, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0]; 345 | const dh_data_header = [90, 90, 1, 0, 89, 0, 32, 0, 1, 0, 0, 0, 39, 36, 17, 9, 13, 10, 18, 20, 218, 73, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 346 | let data: number[] = [] 347 | 348 | this.accessories.forEach(async (accessory: PlatformAccessory) => { 349 | this.log.debug(`Updating accessory: ${accessory.context.name} (${accessory.context.deviceId})`) 350 | let mideaAccessory = this.mideaAccessories.find(ma => ma.deviceId === accessory.context.deviceId) 351 | if (mideaAccessory === undefined) { 352 | this.log.warn(`Could not find accessory with id: ${accessory.context.deviceId}`) 353 | } else { 354 | // Setup the data payload based on deviceType 355 | if (mideaAccessory.deviceType === MideaDeviceType.AirConditioner) { 356 | data = ac_data_header.concat(Constants.UpdateCommand_AirCon); 357 | } else if (mideaAccessory.deviceType === MideaDeviceType.Dehumidifier) { 358 | data = dh_data_header.concat(Constants.UpdateCommand_Dehumidifier); 359 | }; 360 | this.log.debug(`[updateValues] Header + Command: ${data}`) 361 | try { 362 | await this.sendCommand(mideaAccessory, data, '[updateValues] attempt 1/2') 363 | this.log.debug(`[updateValues] Send update command to: ${mideaAccessory.name} (${mideaAccessory.deviceId})`) 364 | } catch (err) { 365 | // TODO: this should be handled only on invalidSession error. Also all the retry logic could be done better (Promise retry instead of await?) 366 | this.log.warn(`[updateValues] Error sending the command: ${err}. Trying to re-login before re-issuing command...`); 367 | try { 368 | const loginResponse = await this.login() 369 | this.log.debug('[updateValues] Login successful!'); 370 | try { 371 | await this.sendCommand(mideaAccessory, data, '[updateValues] attempt 2/2') 372 | } catch (err) { 373 | this.log.error(`[updateValues] sendCommand command still failed after retrying: ${err}`); 374 | } 375 | } catch (err) { 376 | this.log.error('[updateValues] re-login attempt failed'); 377 | }; 378 | }; 379 | }; 380 | }); 381 | }; 382 | 383 | async sendUpdateToDevice(device?: MideaAccessory) { 384 | if (device) { 385 | let command: any 386 | if (device.deviceType === MideaDeviceType.AirConditioner) { 387 | command = new ACSetCommand(); 388 | command.targetTemperature = device.targetTemperature; 389 | command.swingMode = device.swingMode; 390 | command.useFahrenheit = device.useFahrenheit; 391 | command.ecoMode = device.ecoMode; 392 | // command.screenDisplay = device.screenDisplay; 393 | } else if (device.deviceType === MideaDeviceType.Dehumidifier) { 394 | command = new DehumidifierSetCommand() 395 | this.log.debug(`[sendUpdateToDevice] Generated a new command to set targetHumidity to: ${device.targetHumidity}`) 396 | command.targetHumidity = device.targetHumidity; 397 | }; 398 | command.powerState = device.powerState; 399 | command.audibleFeedback = device.audibleFeedback; 400 | command.operationalMode = device.operationalMode; 401 | command.fanSpeed = device.fanSpeed; 402 | //operational mode for workaround with fan only mode on device 403 | const pktBuilder = new PacketBuilder(); 404 | pktBuilder.command = command; 405 | const data = pktBuilder.finalize(); 406 | this.log.debug(`[sendUpdateToDevice] Header + Command: ${JSON.stringify(data)}`); 407 | try { 408 | await this.sendCommand(device, data, '[sendUpdateToDevice] attempt 1/2') 409 | this.log.debug(`[sendUpdateToDevice] Send command to device: ${device.name} (${device.deviceId})`) 410 | } catch (err) { 411 | this.log.warn(`[sendUpdateToDevice] Error sending the command: ${err}. Trying to re-login before re-issuing command...`); 412 | this.log.debug(`[sendUpdateToDevice] Trying to re-login first`); 413 | try { 414 | const loginResponse = await this.login(); 415 | this.log.debug('Login successful'); 416 | try { 417 | await this.sendCommand(device, data, '[sendUpdateToDevice] attempt 2/2') 418 | } catch (err) { 419 | this.log.error(`[sendUpdateToDevice] Send command still failed after retrying: ${err}`); 420 | }; 421 | } catch (err) { 422 | this.log.warn('[sendUpdateToDevice] re-login attempt failed'); 423 | }; 424 | }; 425 | //after sending, update because sometimes the api hangs 426 | try { 427 | this.log.debug('[sendUpdateToDevice] Fetching again the state of the device after setting new parameters...'); 428 | this.updateValues(); 429 | } catch (err) { 430 | this.log.error(`[sendUpdateToDevice] Something went wrong while fetching the state of the device after setting new paramenters: ${err}`) 431 | }; 432 | }; 433 | }; 434 | 435 | getDeviceSpecificOverrideValue(deviceId: string, key: string) { 436 | if (this.config) { 437 | if (this.config.hasOwnProperty('devices')) { 438 | for (let i = 0; i < this.config.devices.length; i++) { 439 | if (this.config.devices[i].deviceId === deviceId) { 440 | return this.config.devices[i][key]; 441 | }; 442 | }; 443 | }; 444 | }; 445 | return null; 446 | }; 447 | 448 | configureAccessory(accessory: PlatformAccessory) { 449 | this.log.info(`Loading accessory from cache: ${accessory.displayName}`); 450 | // add the restored accessory to the accessories cache so we can track if it has already been registered 451 | this.accessories.push(accessory); 452 | }; 453 | 454 | toFahrenheit(value: number) { 455 | return Math.round((value * 1.8) + 32); 456 | }; 457 | }; -------------------------------------------------------------------------------- /lib/MideaPlatform.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | exports.MideaPlatform = void 0; 7 | const axios = require('axios'); 8 | const strftime = require('strftime'); 9 | // import tunnel from 'tunnel'; 10 | const { wrapper: axiosCookieJarSupport } = require('axios-cookiejar-support'); 11 | const tough_cookie_1 = __importDefault(require("tough-cookie")); 12 | const querystring_1 = __importDefault(require("querystring")); 13 | const Utils_1 = __importDefault(require("./Utils")); 14 | const Constants_1 = __importDefault(require("./Constants")); 15 | const PacketBuilder_1 = __importDefault(require("./PacketBuilder")); 16 | const ACSetCommand_1 = __importDefault(require("./commands/ACSetCommand")); 17 | const DehumidifierSetCommand_1 = __importDefault(require("./commands/DehumidifierSetCommand")); 18 | const ACApplianceResponse_1 = __importDefault(require("./responses/ACApplianceResponse")); 19 | const DehumidifierApplianceResponse_1 = __importDefault(require("./responses/DehumidifierApplianceResponse")); 20 | const MideaAccessory_1 = require("./MideaAccessory"); 21 | const MideaDeviceType_1 = require("./enums/MideaDeviceType"); 22 | const MideaErrorCodes_1 = require("./enums/MideaErrorCodes"); 23 | class MideaPlatform { 24 | constructor(log, config, api) { 25 | this.log = log; 26 | this.config = config; 27 | this.api = api; 28 | this.Service = this.api.hap.Service; 29 | this.Characteristic = this.api.hap.Characteristic; 30 | this.updateInterval = null; 31 | this.reauthInterval = null; 32 | this.accessToken = ''; 33 | this.sessionId = ''; 34 | this.userId = ''; 35 | this.dataKey = ''; 36 | this.accessories = []; 37 | this.mideaAccessories = []; 38 | axiosCookieJarSupport(axios); 39 | this.jar = new tough_cookie_1.default.CookieJar(); 40 | if (this.config.registeredApp === 'MSmartHome') { 41 | this.log.info('Using proxy login'); 42 | const form = { 43 | appId: '1010', 44 | format: 2, 45 | clientType: 1, 46 | language: 'en_US', 47 | src: '1010', 48 | stamp: strftime('%Y%m%d%H%M%S'), 49 | reqId: Utils_1.default.reqId, 50 | loginAccount: this.config['user'], 51 | }; 52 | const formJSON = JSON.stringify(form); 53 | const signature = Utils_1.default.getSignProxied(formJSON); 54 | this.apiClient = axios.create({ 55 | baseURL: 'https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=/v1', 56 | headers: { 57 | Authorization: `Basic ${Buffer.from('1010:meicloud').toString('base64')}`, 58 | sign: signature, 59 | secretVersion: '1', 60 | random: Utils_1.default.randomString, 61 | 'Content-Type': 'application/json', 62 | accessToken: this.accessToken, 63 | }, 64 | jar: this.jar, 65 | }); 66 | } 67 | else { 68 | this.apiClient = axios.create({ 69 | baseURL: 'https://mapp.appsmb.com/v1', 70 | headers: { 71 | 'User-Agent': Constants_1.default.UserAgent, 72 | 'Content-Type': 'application/x-www-form-urlencoded' 73 | }, 74 | jar: this.jar 75 | }); 76 | } 77 | ; 78 | this.log = log; 79 | this.config = config; 80 | api.on('didFinishLaunching', () => { 81 | this.onReady(); 82 | }); 83 | } 84 | ; 85 | async onReady() { 86 | try { 87 | await this.login(); 88 | this.log.debug('Login successful'); 89 | try { 90 | await this.getUserList(); 91 | this.updateValues(); 92 | } 93 | catch (err) { 94 | this.log.debug('getUserList failed'); 95 | } 96 | ; 97 | this.updateInterval = setInterval(() => { 98 | this.updateValues(); 99 | }, this.config['interval'] * 1000); 100 | } 101 | catch (err) { 102 | this.log.debug('Login failed'); 103 | } 104 | ; 105 | } 106 | ; 107 | async login() { 108 | return new Promise(async (resolve, reject) => { 109 | var _a; 110 | const url = '/user/login/id/get'; 111 | const form = { 112 | loginAccount: this.config['user'], 113 | clientType: Constants_1.default.ClientType, 114 | src: Constants_1.default.RequestSource, 115 | appId: Constants_1.default.AppId, 116 | format: Constants_1.default.RequestFormat, 117 | stamp: strftime('%Y%m%d%H%M%S'), 118 | language: Constants_1.default.Language, 119 | reqid: Utils_1.default.reqId, 120 | }; 121 | const sign = Utils_1.default.getSign(url, form, Constants_1.default.AppKey); 122 | form.sign = sign; 123 | try { 124 | const response = await this.apiClient.post(url, querystring_1.default.stringify(form)); 125 | if (((_a = response.data) === null || _a === void 0 ? void 0 : _a.errorCode) && response.data.errorCode !== '0') { 126 | this.log.debug(`Login request failed with error: ${response.data.msg}`); 127 | } 128 | else { 129 | const loginId = response.data.result.loginId; 130 | const password = Utils_1.default.getSignPassword(loginId, this.config.password, Constants_1.default.AppKey); 131 | const url = '/user/login'; 132 | const form = { 133 | loginAccount: this.config['user'], 134 | src: Constants_1.default.RequestSource, 135 | format: Constants_1.default.RequestFormat, 136 | stamp: strftime('%Y%m%d%H%M%S'), 137 | language: Constants_1.default.Language, 138 | password: password, 139 | clientType: Constants_1.default.ClientType, 140 | appId: Constants_1.default.AppId, 141 | }; 142 | const sign = Utils_1.default.getSign(url, form, Constants_1.default.AppKey); 143 | form.sign = sign; 144 | try { 145 | const loginResponse = await this.apiClient.post(url, querystring_1.default.stringify(form)); 146 | if (loginResponse.data.errorCode && loginResponse.data.errorCode !== '0') { 147 | this.log.debug(`Login request 2 returned error: ${loginResponse.data.msg}`); 148 | reject(); 149 | } 150 | else { 151 | this.accessToken = loginResponse.data.result.accessToken; 152 | this.sessionId = loginResponse.data.result.sessionId; 153 | this.userId = loginResponse.data.result.userId; 154 | this.dataKey = Utils_1.default.generateDataKey(this.accessToken, Constants_1.default.AppKey); 155 | resolve(); 156 | } 157 | ; 158 | } 159 | catch (err) { 160 | this.log.debug(`Login request 2 failed with: ${err}`); 161 | reject(); 162 | } 163 | ; 164 | } 165 | ; 166 | } 167 | catch (err) { 168 | this.log.debug(`Login request failed with: ${err}`); 169 | reject(); 170 | } 171 | ; 172 | }); 173 | } 174 | ; 175 | async getUserList() { 176 | this.log.debug('getUserList called'); 177 | return new Promise(async (resolve, reject) => { 178 | var _a; 179 | const form = { 180 | src: Constants_1.default.RequestSource, 181 | format: Constants_1.default.RequestFormat, 182 | stamp: strftime('%Y%m%d%H%M%S'), 183 | language: Constants_1.default.Language, 184 | sessionId: this.sessionId 185 | }; 186 | const url = '/appliance/user/list/get'; 187 | const sign = Utils_1.default.getSign(url, form, Constants_1.default.AppKey); 188 | form.sign = sign; 189 | try { 190 | const response = await this.apiClient.post(url, querystring_1.default.stringify(form)); 191 | if (response.data.errorCode && response.data.errorCode !== '0') { 192 | this.log.error(`getUserList returned error: ${response.data.msg}`); 193 | reject(); 194 | } 195 | else { 196 | if (((_a = response.data.result) === null || _a === void 0 ? void 0 : _a.list) && response.data.result.list.length > 0) { 197 | response.data.result.list.forEach(async (currentElement) => { 198 | if (parseInt(currentElement.type) === MideaDeviceType_1.MideaDeviceType.AirConditioner || parseInt(currentElement.type) === MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 199 | const uuid = this.api.hap.uuid.generate(currentElement.id); 200 | const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); 201 | if (existingAccessory) { 202 | this.log.debug('Restoring cached accessory', existingAccessory.displayName); 203 | existingAccessory.context.deviceId = currentElement.id; 204 | existingAccessory.context.deviceType = parseInt(currentElement.type); 205 | existingAccessory.context.name = currentElement.name; 206 | existingAccessory.context.userId = currentElement.userId; 207 | existingAccessory.context.modelNumber = currentElement.modelNumber; 208 | existingAccessory.context.sn = Utils_1.default.decryptAesString(currentElement.sn, this.dataKey); 209 | this.log.debug(`Model Number:${existingAccessory.context.modelNumber}`); 210 | this.log.debug(`Serial Number:${existingAccessory.context.sn}`); 211 | this.api.updatePlatformAccessories([existingAccessory]); 212 | var ma = new MideaAccessory_1.MideaAccessory(this, existingAccessory, currentElement.id, parseInt(currentElement.type), currentElement.name, currentElement.userId); 213 | this.mideaAccessories.push(ma); 214 | } 215 | else { 216 | this.log.debug(`Adding new device: ${currentElement.name}`); 217 | const accessory = new this.api.platformAccessory(currentElement.name, uuid); 218 | accessory.context.deviceId = currentElement.id; 219 | accessory.context.deviceType = parseInt(currentElement.type); 220 | accessory.context.name = currentElement.name; 221 | accessory.context.userId = currentElement.userId; 222 | accessory.context.modelNumber = currentElement.modelNumber; 223 | accessory.context.sn = Utils_1.default.decryptAesString(currentElement.sn, this.dataKey); 224 | this.log.debug(`Model Number:${accessory.context.modelNumber}`); 225 | this.log.debug(`Serial Number:${accessory.context.sn}`); 226 | var ma = new MideaAccessory_1.MideaAccessory(this, accessory, currentElement.id, parseInt(currentElement.type), currentElement.name, currentElement.userId); 227 | this.api.registerPlatformAccessories('homebridge-midea-air', 'midea-air', [accessory]); 228 | this.mideaAccessories.push(ma); 229 | } 230 | ; 231 | } 232 | else { 233 | this.log.warn(`Device: ${currentElement.name} is of unsupported type: ${MideaDeviceType_1.MideaDeviceType[parseInt(currentElement.type)]}`); 234 | this.log.warn('Please open an issue on GitHub with your specific device model'); 235 | } 236 | ; 237 | }); 238 | resolve(); 239 | } 240 | else { 241 | this.log.error('getUserList invalid response'); 242 | reject(); 243 | } 244 | ; 245 | } 246 | ; 247 | } 248 | catch (err) { 249 | this.log.debug(`getUserList error: ${err}`); 250 | reject(); 251 | } 252 | ; 253 | }); 254 | } 255 | ; 256 | async sendCommand(device, order, intent) { 257 | return new Promise(async (resolve, reject) => { 258 | if (device) { 259 | const orderEncode = Utils_1.default.encode(order); 260 | const orderEncrypt = Utils_1.default.encryptAes(orderEncode, this.dataKey); 261 | const form = { 262 | applianceId: device.deviceId, 263 | src: Constants_1.default.RequestSource, 264 | format: Constants_1.default.RequestFormat, 265 | funId: "0000", 266 | order: orderEncrypt, 267 | stamp: strftime('%Y%m%d%H%M%S'), 268 | language: Constants_1.default.Language, 269 | sessionId: this.sessionId, 270 | }; 271 | const url = '/appliance/transparent/send'; 272 | const sign = Utils_1.default.getSign(url, form, Constants_1.default.AppKey); 273 | form.sign = sign; 274 | try { 275 | const response = await this.apiClient.post(url, querystring_1.default.stringify(form)); 276 | if (response.data.errorCode && response.data.errorCode !== '0') { 277 | if (response.data.errorCode = MideaErrorCodes_1.MideaErrorCodes.CommandNotAccepted) { 278 | this.log.debug(`Send command to: ${device.name} (${device.deviceId}) ${intent} returned error: ${response.data.msg} (${response.data.errorCode})`); 279 | return; 280 | } 281 | else { 282 | this.log.info(`Send command to: ${device.name} (${device.deviceId}) ${intent} returned error: ${response.data.msg} (${response.data.errorCode})`); 283 | return; 284 | } 285 | } 286 | else { 287 | this.log.debug(`Send command to: ${device.name} (${device.deviceId}) ${intent} success!`); 288 | let applianceResponse; 289 | if (device.deviceType === MideaDeviceType_1.MideaDeviceType.AirConditioner) { 290 | applianceResponse = new ACApplianceResponse_1.default(Utils_1.default.decode(Utils_1.default.decryptAes(response.data.result.reply, this.dataKey))); 291 | device.targetTemperature = applianceResponse.targetTemperature; 292 | device.indoorTemperature = applianceResponse.indoorTemperature; 293 | device.outdoorTemperature = applianceResponse.outdoorTemperature; 294 | device.swingMode = applianceResponse.swingMode; 295 | device.useFahrenheit = applianceResponse.useFahrenheit; 296 | device.turboFan = applianceResponse.turboFan; 297 | device.ecoMode = applianceResponse.ecoMode; 298 | device.turboMode = applianceResponse.turboMode; 299 | device.comfortSleep = applianceResponse.comfortSleep; 300 | device.dryer = applianceResponse.dryer; 301 | device.purifier = applianceResponse.purifier; 302 | if (device.useFahrenheit === true) { 303 | this.log.debug(`Target Temperature: ${this.toFahrenheit(device.targetTemperature)}˚F`); 304 | this.log.debug(`Indoor Temperature: ${this.toFahrenheit(device.indoorTemperature)}˚F`); 305 | } 306 | else { 307 | this.log.debug(`Target Temperature: ${device.targetTemperature}˚C`); 308 | this.log.debug(`Indoor Temperature: ${device.indoorTemperature}˚C`); 309 | } 310 | ; 311 | if (applianceResponse.outdoorTemperature < 100) { 312 | if (device.useFahrenheit === true) { 313 | this.log.debug(`Outdoor Temperature: ${this.toFahrenheit(device.outdoorTemperature)}˚F`); 314 | } 315 | else { 316 | this.log.debug(`Outdoor Temperature: ${device.outdoorTemperature}˚C`); 317 | } 318 | ; 319 | } 320 | ; 321 | this.log.debug(`Swing Mode set to: ${device.swingMode}`); 322 | this.log.debug(`Fahrenheit set to: ${device.useFahrenheit}`); 323 | this.log.debug(`Turbo Fan set to: ${device.turboFan}`); 324 | this.log.debug(`Eco Mode set to: ${device.ecoMode}`); 325 | this.log.debug(`Turbo Mode set to: ${device.turboMode}`); 326 | this.log.debug(`Comfort Sleep set to: ${device.comfortSleep}`); 327 | this.log.debug(`Dryer set to: ${device.dryer}`); 328 | this.log.debug(`Purifier set to: ${device.purifier}`); 329 | } 330 | else if (device.deviceType === MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 331 | applianceResponse = new DehumidifierApplianceResponse_1.default(Utils_1.default.decode(Utils_1.default.decryptAes(response.data.result.reply, this.dataKey))); 332 | device.currentHumidity = applianceResponse.currentHumidity; 333 | device.targetHumidity = applianceResponse.targetHumidity; 334 | device.waterLevel = applianceResponse.waterLevel; 335 | this.log.debug(`Current Humidity: ${device.currentHumidity}`); 336 | this.log.debug(`Target humidity set to: ${device.targetHumidity}`); 337 | this.log.debug(`Water level at: ${device.waterLevel}`); 338 | } 339 | ; 340 | // Common 341 | device.powerState = applianceResponse.powerState ? 1 : 0; 342 | device.operationalMode = applianceResponse.operationalMode; 343 | device.fanSpeed = applianceResponse.fanSpeed; 344 | this.log.debug(`Power State set to: ${device.powerState}`); 345 | this.log.debug(`Operational Mode set to: ${device.operationalMode}`); 346 | this.log.debug(`Fan Speed set to: ${device.fanSpeed}`); 347 | this.log.debug(`Full data: ${Utils_1.default.formatResponse(applianceResponse.data)}`); 348 | resolve(); 349 | } 350 | ; 351 | } 352 | catch (err) { 353 | this.log.error(`SendCommand (${intent}) request failed: ${err}`); 354 | reject(); 355 | } 356 | ; 357 | } 358 | else { 359 | this.log.error('No device specified'); 360 | reject(); 361 | } 362 | ; 363 | }); 364 | } 365 | ; 366 | updateValues() { 367 | // STATUS ONLY OR POWER ON/OFF HEADER 368 | const ac_data_header = [90, 90, 1, 16, 89, 0, 32, 0, 80, 0, 0, 0, 169, 65, 48, 9, 14, 5, 20, 20, 213, 50, 1, 0, 0, 17, 0, 0, 0, 4, 2, 0, 0, 1, 0, 0, 0, 0, 0, 0]; 369 | const dh_data_header = [90, 90, 1, 0, 89, 0, 32, 0, 1, 0, 0, 0, 39, 36, 17, 9, 13, 10, 18, 20, 218, 73, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 370 | let data = []; 371 | this.accessories.forEach(async (accessory) => { 372 | this.log.debug(`Updating accessory: ${accessory.context.name} (${accessory.context.deviceId})`); 373 | let mideaAccessory = this.mideaAccessories.find(ma => ma.deviceId === accessory.context.deviceId); 374 | if (mideaAccessory === undefined) { 375 | this.log.warn(`Could not find accessory with id: ${accessory.context.deviceId}`); 376 | } 377 | else { 378 | // Setup the data payload based on deviceType 379 | if (mideaAccessory.deviceType === MideaDeviceType_1.MideaDeviceType.AirConditioner) { 380 | data = ac_data_header.concat(Constants_1.default.UpdateCommand_AirCon); 381 | } 382 | else if (mideaAccessory.deviceType === MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 383 | data = dh_data_header.concat(Constants_1.default.UpdateCommand_Dehumidifier); 384 | } 385 | ; 386 | this.log.debug(`[updateValues] Header + Command: ${data}`); 387 | try { 388 | await this.sendCommand(mideaAccessory, data, '[updateValues] attempt 1/2'); 389 | this.log.debug(`[updateValues] Send update command to: ${mideaAccessory.name} (${mideaAccessory.deviceId})`); 390 | } 391 | catch (err) { 392 | // TODO: this should be handled only on invalidSession error. Also all the retry logic could be done better (Promise retry instead of await?) 393 | this.log.warn(`[updateValues] Error sending the command: ${err}. Trying to re-login before re-issuing command...`); 394 | try { 395 | const loginResponse = await this.login(); 396 | this.log.debug('[updateValues] Login successful!'); 397 | try { 398 | await this.sendCommand(mideaAccessory, data, '[updateValues] attempt 2/2'); 399 | } 400 | catch (err) { 401 | this.log.error(`[updateValues] sendCommand command still failed after retrying: ${err}`); 402 | } 403 | } 404 | catch (err) { 405 | this.log.error('[updateValues] re-login attempt failed'); 406 | } 407 | ; 408 | } 409 | ; 410 | } 411 | ; 412 | }); 413 | } 414 | ; 415 | async sendUpdateToDevice(device) { 416 | if (device) { 417 | let command; 418 | if (device.deviceType === MideaDeviceType_1.MideaDeviceType.AirConditioner) { 419 | command = new ACSetCommand_1.default(); 420 | command.targetTemperature = device.targetTemperature; 421 | command.swingMode = device.swingMode; 422 | command.useFahrenheit = device.useFahrenheit; 423 | command.ecoMode = device.ecoMode; 424 | // command.screenDisplay = device.screenDisplay; 425 | } 426 | else if (device.deviceType === MideaDeviceType_1.MideaDeviceType.Dehumidifier) { 427 | command = new DehumidifierSetCommand_1.default(); 428 | this.log.debug(`[sendUpdateToDevice] Generated a new command to set targetHumidity to: ${device.targetHumidity}`); 429 | command.targetHumidity = device.targetHumidity; 430 | } 431 | ; 432 | command.powerState = device.powerState; 433 | command.audibleFeedback = device.audibleFeedback; 434 | command.operationalMode = device.operationalMode; 435 | command.fanSpeed = device.fanSpeed; 436 | //operational mode for workaround with fan only mode on device 437 | const pktBuilder = new PacketBuilder_1.default(); 438 | pktBuilder.command = command; 439 | const data = pktBuilder.finalize(); 440 | this.log.debug(`[sendUpdateToDevice] Header + Command: ${JSON.stringify(data)}`); 441 | try { 442 | await this.sendCommand(device, data, '[sendUpdateToDevice] attempt 1/2'); 443 | this.log.debug(`[sendUpdateToDevice] Send command to device: ${device.name} (${device.deviceId})`); 444 | } 445 | catch (err) { 446 | this.log.warn(`[sendUpdateToDevice] Error sending the command: ${err}. Trying to re-login before re-issuing command...`); 447 | this.log.debug(`[sendUpdateToDevice] Trying to re-login first`); 448 | try { 449 | const loginResponse = await this.login(); 450 | this.log.debug('Login successful'); 451 | try { 452 | await this.sendCommand(device, data, '[sendUpdateToDevice] attempt 2/2'); 453 | } 454 | catch (err) { 455 | this.log.error(`[sendUpdateToDevice] Send command still failed after retrying: ${err}`); 456 | } 457 | ; 458 | } 459 | catch (err) { 460 | this.log.warn('[sendUpdateToDevice] re-login attempt failed'); 461 | } 462 | ; 463 | } 464 | ; 465 | //after sending, update because sometimes the api hangs 466 | try { 467 | this.log.debug('[sendUpdateToDevice] Fetching again the state of the device after setting new parameters...'); 468 | this.updateValues(); 469 | } 470 | catch (err) { 471 | this.log.error(`[sendUpdateToDevice] Something went wrong while fetching the state of the device after setting new paramenters: ${err}`); 472 | } 473 | ; 474 | } 475 | ; 476 | } 477 | ; 478 | getDeviceSpecificOverrideValue(deviceId, key) { 479 | if (this.config) { 480 | if (this.config.hasOwnProperty('devices')) { 481 | for (let i = 0; i < this.config.devices.length; i++) { 482 | if (this.config.devices[i].deviceId === deviceId) { 483 | return this.config.devices[i][key]; 484 | } 485 | ; 486 | } 487 | ; 488 | } 489 | ; 490 | } 491 | ; 492 | return null; 493 | } 494 | ; 495 | configureAccessory(accessory) { 496 | this.log.info(`Loading accessory from cache: ${accessory.displayName}`); 497 | // add the restored accessory to the accessories cache so we can track if it has already been registered 498 | this.accessories.push(accessory); 499 | } 500 | ; 501 | toFahrenheit(value) { 502 | return Math.round((value * 1.8) + 32); 503 | } 504 | ; 505 | } 506 | exports.MideaPlatform = MideaPlatform; 507 | ; 508 | -------------------------------------------------------------------------------- /src/MideaAccessory.ts: -------------------------------------------------------------------------------- 1 | import { Service, PlatformAccessory, CharacteristicValue, CharacteristicSetCallback, CharacteristicGetCallback } from 'homebridge'; 2 | import { MideaPlatform } from './MideaPlatform' 3 | import { MideaDeviceType } from './enums/MideaDeviceType' 4 | import { MideaSwingMode } from './enums/MideaSwingMode' 5 | import { ACOperationalMode } from './enums/ACOperationalMode' 6 | import { DehumidifierOperationalMode } from './enums/DehumidifierOperationalMode'; 7 | 8 | export class MideaAccessory { 9 | 10 | public deviceId: string = '' 11 | public deviceType: MideaDeviceType = MideaDeviceType.AirConditioner 12 | // AirConditioner 13 | public targetTemperature: number = 24 14 | public indoorTemperature: number = 0 15 | public outdoorTemperature: number = 0 16 | public useFahrenheit: boolean = false; // Default unit is Celsius. this is just to control the temperature unit of the AC's display. The target temperature setter always expects a celsius temperature (resolution of 0.5C), as does the midea API 17 | public turboFan: boolean = false 18 | public fanOnlyMode: boolean = false 19 | public swingMode: number = 0 20 | public supportedSwingMode: MideaSwingMode = MideaSwingMode.None 21 | public temperatureSteps: number = 1 22 | public minTemperature: number = 17 23 | public maxTemperature: number = 30 24 | public ecoMode: boolean = false 25 | public turboMode: boolean = false 26 | public comfortSleep: boolean = false 27 | public dryer: boolean = false 28 | public purifier: boolean = false 29 | public screenDisplay: number = 1 30 | // Dehumidifier 31 | public currentHumidity: number = 0 32 | public targetHumidity: number = 35 33 | public waterLevel: number = 0 34 | // Common 35 | public powerState: number = 0 36 | public audibleFeedback: boolean = false 37 | public operationalMode: number = ACOperationalMode.Off 38 | public fanSpeed: number = 0 39 | 40 | public name: string = '' 41 | public model: string = '' 42 | public userId: string = '' 43 | public firmwareVersion = require('../package.json').version; 44 | 45 | private service!: Service 46 | private fanService!: Service 47 | private outdoorTemperatureService!: Service 48 | 49 | constructor( 50 | private readonly platform: MideaPlatform, 51 | private readonly accessory: PlatformAccessory, 52 | private _deviceId: string, 53 | private _deviceType: MideaDeviceType, 54 | private _name: string, 55 | private _userId: string 56 | ) { 57 | this.deviceId = _deviceId 58 | this.deviceType = _deviceType 59 | this.name = _name 60 | this.userId = _userId 61 | 62 | // Check for device specific overrides 63 | // SwingMode 64 | let smode = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'supportedSwingMode'); 65 | if (smode) { 66 | switch (smode) { 67 | case 'Vertical': 68 | this.supportedSwingMode = MideaSwingMode.Vertical; 69 | break; 70 | case 'Horizontal': 71 | this.supportedSwingMode = MideaSwingMode.Horizontal; 72 | break; 73 | case 'Both': 74 | this.supportedSwingMode = MideaSwingMode.Both; 75 | break; 76 | default: 77 | this.supportedSwingMode = MideaSwingMode.None; 78 | break; 79 | } 80 | } 81 | // Temperature Steps 82 | let tsteps = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'temperatureSteps'); 83 | if (tsteps) { 84 | this.temperatureSteps = tsteps; 85 | } 86 | // Fahrenheit 87 | let fahrenheit = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'useFahrenheit'); 88 | if (fahrenheit) { 89 | this.useFahrenheit = fahrenheit; 90 | } 91 | // Minimum Temperature 92 | let minTemp = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'minTemp'); 93 | if (minTemp) { 94 | this.minTemperature = minTemp; 95 | } 96 | // Maximum Temperature 97 | let maxTemp = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'maxTemp'); 98 | if (maxTemp) { 99 | this.maxTemperature = maxTemp; 100 | } 101 | // audibleFeedback 102 | let aFeedback = this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'audibleFeedback'); 103 | if (aFeedback) { 104 | this.audibleFeedback = aFeedback; 105 | } 106 | 107 | this.platform.log.info('Created device:', this.name + ',', 'with ID:', this.deviceId + ',', 'and type:', this.deviceType) 108 | 109 | if (this.deviceType === MideaDeviceType.AirConditioner) { 110 | this.model = 'Air Conditioner'; 111 | } else if (this.deviceType === MideaDeviceType.Dehumidifier) { 112 | this.model = 'Dehumidifier'; 113 | } else this.model = 'Undefined'; 114 | 115 | this.accessory.getService(this.platform.Service.AccessoryInformation)! 116 | .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Midea') 117 | .setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.firmwareVersion) 118 | .setCharacteristic(this.platform.Characteristic.Model, this.model) 119 | .setCharacteristic(this.platform.Characteristic.SerialNumber, this.deviceId) 120 | 121 | // Air Conditioner 122 | if (this.deviceType === MideaDeviceType.AirConditioner) { 123 | this.service = this.accessory.getService(this.platform.Service.HeaterCooler) || this.accessory.addService(this.platform.Service.HeaterCooler) 124 | this.service.setCharacteristic(this.platform.Characteristic.Name, this.name) 125 | this.service.getCharacteristic(this.platform.Characteristic.Active) 126 | .on('get', this.handleActiveGet.bind(this)) 127 | .on('set', this.handleActiveSet.bind(this)) 128 | this.service.getCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState) 129 | .on('get', this.handleCurrentHeaterCoolerStateGet.bind(this)) 130 | this.service.getCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState) 131 | .on('get', this.handleTargetHeaterCoolerStateGet.bind(this)) 132 | .on('set', this.handleTargetHeaterCoolerStateSet.bind(this)) 133 | .setProps({ 134 | validValues: [ 135 | this.platform.Characteristic.TargetHeaterCoolerState.AUTO, 136 | this.platform.Characteristic.TargetHeaterCoolerState.HEAT, 137 | this.platform.Characteristic.TargetHeaterCoolerState.COOL 138 | ] 139 | }) 140 | this.service.getCharacteristic(this.platform.Characteristic.CurrentTemperature) 141 | .on('get', this.handleCurrentTemperatureGet.bind(this)) 142 | .setProps({ 143 | minValue: -100, 144 | maxValue: 100, 145 | minStep: 0.1 146 | }) 147 | this.service.getCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature) 148 | .on('get', this.handleThresholdTemperatureGet.bind(this)) 149 | .on('set', this.handleThresholdTemperatureSet.bind(this)) 150 | .setProps({ 151 | minValue: this.minTemperature, 152 | maxValue: this.maxTemperature, 153 | minStep: this.temperatureSteps 154 | }) 155 | this.service.getCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature) 156 | .on('get', this.handleThresholdTemperatureGet.bind(this)) 157 | .on('set', this.handleThresholdTemperatureSet.bind(this)) 158 | .setProps({ 159 | minValue: this.minTemperature, 160 | maxValue: this.maxTemperature, 161 | minStep: this.temperatureSteps 162 | }) 163 | this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed) 164 | .on('get', this.handleRotationSpeedGet.bind(this)) 165 | .on('set', this.handleRotationSpeedSet.bind(this)) 166 | this.service.getCharacteristic(this.platform.Characteristic.SwingMode) 167 | .on('get', this.handleSwingModeGet.bind(this)) 168 | .on('set', this.handleSwingModeSet.bind(this)) 169 | this.service.getCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits) 170 | .on('get', this.handleTemperatureDisplayUnitsGet.bind(this)) 171 | .on('set', this.handleTemperatureDisplayUnitsSet.bind(this)) 172 | .setProps({ 173 | validValues: [ 174 | this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT, 175 | this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS 176 | ] 177 | }); 178 | // Use to control Screen display 179 | // this.service.getCharacteristic(this.platform.Characteristic.LockPhysicalControls) 180 | // .on('get', this.handleLockPhysicalControlsGet.bind(this)) 181 | // .on('set', this.handleLockPhysicalControlsSet.bind(this)) 182 | // Update HomeKit 183 | setInterval(() => { 184 | this.service.updateCharacteristic(this.platform.Characteristic.Active, this.powerState); 185 | this.service.updateCharacteristic(this.platform.Characteristic.CurrentHeaterCoolerState, this.currentHeaterCoolerState()); 186 | this.service.updateCharacteristic(this.platform.Characteristic.TargetHeaterCoolerState, this.targetHeaterCoolerState()); 187 | this.service.updateCharacteristic(this.platform.Characteristic.CurrentTemperature, this.indoorTemperature); 188 | this.service.updateCharacteristic(this.platform.Characteristic.CoolingThresholdTemperature, this.targetTemperature); 189 | this.service.updateCharacteristic(this.platform.Characteristic.HeatingThresholdTemperature, this.targetTemperature); 190 | this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.rotationSpeed()); 191 | this.service.updateCharacteristic(this.platform.Characteristic.SwingMode, this.SwingMode()); 192 | this.service.updateCharacteristic(this.platform.Characteristic.TemperatureDisplayUnits, this.useFahrenheit); 193 | // this.service.updateCharacteristic(this.platform.Characteristic.LockPhysicalControls, this.screenDisplay); 194 | }, 5000); 195 | 196 | // Fan Mode 197 | if (this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'fanOnlyMode') === true) { 198 | this.platform.log.debug('Add Fan Mode'); 199 | this.fanService = this.accessory.getService(this.platform.Service.Fanv2) || this.accessory.addService(this.platform.Service.Fanv2); 200 | this.fanService.setCharacteristic(this.platform.Characteristic.Name, 'Fan Mode'); 201 | this.fanService.getCharacteristic(this.platform.Characteristic.Active) 202 | .on('get', this.handleFanActiveGet.bind(this)) 203 | .on('set', this.handleFanActiveSet.bind(this)); 204 | this.fanService.getCharacteristic(this.platform.Characteristic.RotationSpeed) 205 | .on('get', this.handleRotationSpeedGet.bind(this)) 206 | .on('set', this.handleRotationSpeedSet.bind(this)); 207 | this.fanService.getCharacteristic(this.platform.Characteristic.SwingMode) 208 | .on('get', this.handleSwingModeGet.bind(this)) 209 | .on('set', this.handleSwingModeSet.bind(this)); 210 | // Update HomeKit 211 | setInterval(() => { 212 | this.fanService.updateCharacteristic(this.platform.Characteristic.Active, this.fanActive()); 213 | this.fanService.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.rotationSpeed()); 214 | this.fanService.updateCharacteristic(this.platform.Characteristic.SwingMode, this.SwingMode()); 215 | }, 5000); 216 | 217 | } else { 218 | let fanService = this.accessory.getService(this.platform.Service.Fanv2); 219 | this.accessory.removeService(fanService); 220 | }; 221 | 222 | // Outdoor Temperature Sensor 223 | if (this.platform.getDeviceSpecificOverrideValue(this.deviceId, 'OutdoorTemperature') === true) { 224 | this.platform.log.debug('Add Outdoor Temperature Sensor'); 225 | this.outdoorTemperatureService = this.accessory.getService(this.platform.Service.TemperatureSensor) || this.accessory.addService(this.platform.Service.TemperatureSensor); 226 | this.outdoorTemperatureService.setCharacteristic(this.platform.Characteristic.Name, 'Outdoor Temperature'); 227 | this.outdoorTemperatureService.getCharacteristic(this.platform.Characteristic.CurrentTemperature) 228 | .on('get', this.handleOutdoorTemperatureGet.bind(this)) 229 | } else { 230 | let outdoorTemperatureService = this.accessory.getService(this.platform.Service.TemperatureSensor); 231 | this.accessory.removeService(outdoorTemperatureService); 232 | }; 233 | 234 | // Dehumidifier 235 | } else if (this.deviceType === MideaDeviceType.Dehumidifier) { 236 | this.service = this.accessory.getService(this.platform.Service.HumidifierDehumidifier) || this.accessory.addService(this.platform.Service.HumidifierDehumidifier) 237 | this.service.setCharacteristic(this.platform.Characteristic.Name, this.name) 238 | this.service.getCharacteristic(this.platform.Characteristic.Active) 239 | .on('get', this.handleActiveGet.bind(this)) 240 | .on('set', this.handleActiveSet.bind(this)) 241 | this.service.getCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState) 242 | .on('get', this.handleCurrentHumidifierDehumidifierStateGet.bind(this)) 243 | this.service.getCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState) 244 | .on('get', this.handleTargetHumidifierDehumidifierStateGet.bind(this)) 245 | .on('set', this.handleTargetHumidifierDehumidifierStateSet.bind(this)) 246 | .setProps({ 247 | validValues: [ 248 | this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER, 249 | this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER, 250 | this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER 251 | ] 252 | }) 253 | this.service.getCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity) 254 | .on('get', this.handleCurrentRelativeHumidityGet.bind(this)) 255 | .setProps({ 256 | minValue: 0, 257 | maxValue: 100, 258 | minStep: 1 259 | }) 260 | this.service.getCharacteristic(this.platform.Characteristic.RelativeHumidityDehumidifierThreshold) 261 | .on('get', this.handleRelativeHumidityThresholdGet.bind(this)) 262 | .on('set', this.handleRelativeHumidityThresholdSet.bind(this)) 263 | .setProps({ 264 | minValue: 35, 265 | maxValue: 85, 266 | minStep: 5 267 | }) 268 | this.service.getCharacteristic(this.platform.Characteristic.RelativeHumidityHumidifierThreshold) 269 | .on('get', this.handleRelativeHumidityThresholdGet.bind(this)) 270 | .on('set', this.handleRelativeHumidityThresholdSet.bind(this)) 271 | .setProps({ 272 | minValue: 35, 273 | maxValue: 85, 274 | minStep: 5 275 | }) 276 | this.service.getCharacteristic(this.platform.Characteristic.RotationSpeed) 277 | .on('get', this.handleWindSpeedGet.bind(this)) 278 | .on('set', this.handleWindSpeedSet.bind(this)) 279 | this.service.getCharacteristic(this.platform.Characteristic.WaterLevel) 280 | .on('get', this.handleWaterLevelGet.bind(this)) 281 | // Update HomeKit 282 | setInterval(() => { 283 | this.service.updateCharacteristic(this.platform.Characteristic.Active, this.powerState); 284 | this.service.updateCharacteristic(this.platform.Characteristic.CurrentHumidifierDehumidifierState, this.currentHumidifierDehumidifierState()); 285 | this.service.updateCharacteristic(this.platform.Characteristic.TargetHumidifierDehumidifierState, this.TargetHumidifierDehumidifierState()); 286 | this.service.updateCharacteristic(this.platform.Characteristic.CurrentRelativeHumidity, this.currentHumidity); 287 | this.service.updateCharacteristic(this.platform.Characteristic.RelativeHumidityDehumidifierThreshold, this.targetHumidity); 288 | this.service.updateCharacteristic(this.platform.Characteristic.RotationSpeed, this.windSpeed()); 289 | this.service.updateCharacteristic(this.platform.Characteristic.WaterLevel, this.waterLevel); 290 | }, 5000); 291 | } else { 292 | this.platform.log.error('Unsupported device type: ', MideaDeviceType[this.deviceType]); 293 | }; 294 | }; 295 | // Handle requests to get the current value of the "Active" characteristic 296 | handleActiveGet(callback: CharacteristicGetCallback) { 297 | this.platform.log.debug('Triggered GET Active'); 298 | if (this.powerState === 1) { 299 | callback(null, this.platform.Characteristic.Active.ACTIVE); 300 | } else { 301 | callback(null, this.platform.Characteristic.Active.INACTIVE); 302 | }; 303 | }; 304 | // Handle requests to set the "Active" characteristic 305 | handleActiveSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 306 | if (this.powerState !== Number(value)) { 307 | this.platform.log.debug(`Triggered SET Active To: ${value}`); 308 | this.powerState = Number(value); 309 | this.platform.sendUpdateToDevice(this); 310 | }; 311 | callback(null); 312 | }; 313 | // Get the current value of the "CurrentHeaterCoolerState" characteristic 314 | public currentHeaterCoolerState() { 315 | if (this.powerState === this.platform.Characteristic.Active.INACTIVE) { 316 | return this.platform.Characteristic.CurrentHeaterCoolerState.INACTIVE; 317 | } else { 318 | switch (this.operationalMode) { 319 | case ACOperationalMode.Dry: 320 | case ACOperationalMode.Cooling: 321 | if (this.indoorTemperature >= this.targetTemperature) { 322 | return this.platform.Characteristic.CurrentHeaterCoolerState.COOLING; 323 | } else { 324 | return this.platform.Characteristic.CurrentHeaterCoolerState.IDLE; 325 | } 326 | case ACOperationalMode.Heating: 327 | if (this.indoorTemperature <= this.targetTemperature) { 328 | return this.platform.Characteristic.CurrentHeaterCoolerState.HEATING; 329 | } else { 330 | return this.platform.Characteristic.CurrentHeaterCoolerState.IDLE; 331 | } 332 | case ACOperationalMode.Auto: 333 | if (this.indoorTemperature > this.targetTemperature) { 334 | return this.platform.Characteristic.CurrentHeaterCoolerState.COOLING; 335 | } else if (this.indoorTemperature < this.targetTemperature) { 336 | return this.platform.Characteristic.CurrentHeaterCoolerState.HEATING; 337 | } else { 338 | return this.platform.Characteristic.CurrentHeaterCoolerState.IDLE; 339 | }; 340 | }; 341 | }; 342 | }; 343 | // Handle requests to get the current value of the "CurrentHeaterCoolerState" characteristic 344 | handleCurrentHeaterCoolerStateGet(callback: CharacteristicGetCallback) { 345 | this.platform.log.debug('Triggered GET Current HeaterCooler State'); 346 | callback(null, this.currentHeaterCoolerState()); 347 | }; 348 | // Get the current value of the "TargetHeaterCoolerState" characteristic 349 | public targetHeaterCoolerState() { 350 | if (this.operationalMode === ACOperationalMode.Cooling) { 351 | return this.platform.Characteristic.TargetHeaterCoolerState.COOL; 352 | } else if (this.operationalMode === ACOperationalMode.Heating) { 353 | return this.platform.Characteristic.TargetHeaterCoolerState.HEAT; 354 | } else return this.platform.Characteristic.TargetHeaterCoolerState.AUTO; 355 | }; 356 | // Handle requests to get the current value of the "TargetHeaterCoolerState" characteristic 357 | handleTargetHeaterCoolerStateGet(callback: CharacteristicGetCallback) { 358 | this.platform.log.debug('Triggered GET Target HeaterCooler State'); 359 | callback(null, this.targetHeaterCoolerState()); 360 | }; 361 | // Handle requests to set the "TargetHeaterCoolerState" characteristic 362 | handleTargetHeaterCoolerStateSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 363 | if (this.targetHeaterCoolerState() !== value) { 364 | this.platform.log.debug(`Triggered SET HeaterCooler State To: ${value}`); 365 | if (value === this.platform.Characteristic.TargetHeaterCoolerState.AUTO) { 366 | this.operationalMode = ACOperationalMode.Auto; 367 | } 368 | else if (value === this.platform.Characteristic.TargetHeaterCoolerState.COOL) { 369 | this.operationalMode = ACOperationalMode.Cooling; 370 | } 371 | else if (value === this.platform.Characteristic.TargetHeaterCoolerState.HEAT) { 372 | this.operationalMode = ACOperationalMode.Heating; 373 | } 374 | this.platform.sendUpdateToDevice(this); 375 | }; 376 | callback(null); 377 | }; 378 | // Handle requests to get the current value of the "CurrentTemperature" characteristic 379 | handleCurrentTemperatureGet(callback: CharacteristicGetCallback) { 380 | this.platform.log.debug('Triggered GET CurrentTemperature'); 381 | callback(null, this.indoorTemperature); 382 | }; 383 | // Handle requests to get the current value of the "ThresholdTemperature" characteristic 384 | handleThresholdTemperatureGet(callback: CharacteristicGetCallback) { 385 | this.platform.log.debug('Triggered GET ThresholdTemperature'); 386 | callback(null, this.targetTemperature); 387 | }; 388 | // Handle requests to set the "ThresholdTemperature" characteristic 389 | handleThresholdTemperatureSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 390 | if (this.useFahrenheit === true) { 391 | this.platform.log.debug(`Triggered SET ThresholdTemperature To: ${value}˚F`); 392 | } else { 393 | this.platform.log.debug(`Triggered SET ThresholdTemperature To: ${value}˚C`); 394 | }; 395 | if (this.targetTemperature !== Number(value)) { 396 | this.targetTemperature = Number(value); 397 | this.platform.sendUpdateToDevice(this); 398 | }; 399 | callback(null); 400 | }; 401 | // Get the current value of the "RotationSpeed" characteristic 402 | public rotationSpeed() { 403 | // values from device are 20="Silent",40="Low",60="Medium",80="High",100="Full",101/102="Auto" 404 | // New Midea devices has slider between 1%-100% 405 | // convert to good usable slider in homekit in percent 406 | let currentValue = 0; 407 | switch (this.fanSpeed) { 408 | case 20: currentValue = 20; 409 | break; 410 | case 40: currentValue = 40; 411 | break; 412 | case 60: currentValue = 60; 413 | break; 414 | case 80: currentValue = 80; 415 | break; 416 | case 101: 417 | case 102: currentValue = 100; 418 | break; 419 | }; 420 | return currentValue; 421 | }; 422 | // Handle requests to get the current value of the "RotationSpeed" characteristic 423 | handleRotationSpeedGet(callback: CharacteristicGetCallback) { 424 | this.platform.log.debug('Triggered GET RotationSpeed'); 425 | callback(null, this.rotationSpeed()); 426 | }; 427 | // Handle requests to set the "RotationSpeed" characteristic 428 | handleRotationSpeedSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 429 | this.platform.log.debug(`Triggered SET RotationSpeed To: ${value}`); 430 | // transform values in percent 431 | // values from device are 20="Silent",40="Low",60="Medium",80="High",100="Full",101/102="Auto" 432 | if (this.fanSpeed !== value) { 433 | if (value <= 20) { 434 | this.fanSpeed = 20; 435 | } else if (value > 20 && value <= 40) { 436 | this.fanSpeed = 40; 437 | } else if (value > 40 && value <= 60) { 438 | this.fanSpeed = 60; 439 | } else if (value > 60 && value <= 80) { 440 | this.fanSpeed = 80; 441 | } else { 442 | this.fanSpeed = 102; 443 | }; 444 | this.platform.sendUpdateToDevice(this); 445 | }; 446 | callback(null); 447 | }; 448 | // Get the current value of the "swingMode" characteristic 449 | public SwingMode() { 450 | if (this.swingMode !== 0) { 451 | return this.platform.Characteristic.SwingMode.SWING_ENABLED; 452 | } else { 453 | return this.platform.Characteristic.SwingMode.SWING_DISABLED; 454 | }; 455 | }; 456 | // Handle requests to get the current value of the "swingMode" characteristic 457 | handleSwingModeGet(callback: CharacteristicGetCallback) { 458 | this.platform.log.debug('Triggered GET SwingMode'); 459 | callback(null, this.SwingMode()) 460 | }; 461 | // Handle requests to set the "swingMode" characteristic 462 | handleSwingModeSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 463 | this.platform.log.debug(`Triggered SET SwingMode To: ${value}`); 464 | // convert this.swingMode to a 0/1 465 | if (this.swingMode !== value) { 466 | if (value === 0) { 467 | this.swingMode = 0; 468 | } else { 469 | this.swingMode = this.supportedSwingMode; 470 | }; 471 | this.platform.sendUpdateToDevice(this) 472 | }; 473 | callback(null); 474 | }; 475 | // Handle requests to get the current value of the "Temperature Display Units" characteristic 476 | handleTemperatureDisplayUnitsGet(callback: CharacteristicGetCallback) { 477 | this.platform.log.debug('Triggered GET Temperature Display Units'); 478 | if (this.useFahrenheit === true) { 479 | callback(null, this.platform.Characteristic.TemperatureDisplayUnits.FAHRENHEIT); 480 | } else { 481 | callback(null, this.platform.Characteristic.TemperatureDisplayUnits.CELSIUS); 482 | }; 483 | }; 484 | // Handle requests to set the "Temperature Display Units" characteristic 485 | handleTemperatureDisplayUnitsSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 486 | this.platform.log.debug(`Triggered SET Temperature Display Units To: ${value}`); 487 | if (this.useFahrenheit !== value) { 488 | if (value === 1) { 489 | this.useFahrenheit = true; 490 | } else { 491 | this.useFahrenheit = false; 492 | }; 493 | this.platform.sendUpdateToDevice(this); 494 | }; 495 | callback(null); 496 | }; 497 | // Handle requests to get the current value of the "Screen Display" 498 | handleLockPhysicalControlsGet(callback: CharacteristicGetCallback) { 499 | this.platform.log.debug('Triggered GET Screen Display'); 500 | if (this.screenDisplay === 1) { 501 | callback(null, this.platform.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED); 502 | } else { 503 | callback(null, this.platform.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED); 504 | }; 505 | } 506 | // Handle requests to get the current value of the "Screen Display" 507 | handleLockPhysicalControlsSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 508 | if (this.screenDisplay !== Number(value)) { 509 | this.platform.log.debug(`Triggered SET Screen Display To: ${value}`); 510 | this.screenDisplay = Number(value); 511 | this.platform.sendUpdateToDevice(this); 512 | }; 513 | callback(null); 514 | } 515 | // Fan mode 516 | // Get the current value of the "FanActive" characteristic 517 | public fanActive() { 518 | if (this.operationalMode === ACOperationalMode.FanOnly && this.powerState === this.platform.Characteristic.Active.ACTIVE) { 519 | return this.platform.Characteristic.Active.ACTIVE; 520 | } else { 521 | return this.platform.Characteristic.Active.INACTIVE; 522 | }; 523 | }; 524 | // Handle requests to get the current status of "Fan Mode" characteristic 525 | handleFanActiveGet(callback: CharacteristicGetCallback) { 526 | this.platform.log.debug('Triggered GET FanMode'); 527 | callback(null, this.fanActive()); 528 | }; 529 | // Handle requests to set the "Fan Mode" characteristic 530 | handleFanActiveSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 531 | this.platform.log.debug(`Triggered SET FanMode To: ${value}`); 532 | if (value === 1 && this.powerState === 1) { 533 | this.operationalMode = ACOperationalMode.FanOnly; 534 | } else if (value === 1 && this.powerState === 0) { 535 | this.powerState = this.platform.Characteristic.Active.ACTIVE; 536 | this.operationalMode = ACOperationalMode.FanOnly; 537 | } else if (value === 0 && this.powerState === 1) { 538 | this.powerState = this.platform.Characteristic.Active.INACTIVE; 539 | }; 540 | this.platform.sendUpdateToDevice(this); 541 | callback(null); 542 | }; 543 | // Outdoor Temperature Sensor 544 | // Handle requests to get the current value of the "OutdoorTemperature" characteristic 545 | handleOutdoorTemperatureGet(callback: CharacteristicGetCallback) { 546 | this.platform.log.debug('Triggered GET CurrentTemperature'); 547 | callback(null, this.outdoorTemperature); 548 | }; 549 | // HumidifierDehumidifier 550 | // Get the current value of the "CurrentHumidifierDehumidifierState" characteristic 551 | public currentHumidifierDehumidifierState() { 552 | if (this.powerState === 0) { 553 | return this.platform.Characteristic.CurrentHumidifierDehumidifierState.INACTIVE; 554 | } else { 555 | switch (this.operationalMode) { 556 | case DehumidifierOperationalMode.Normal: 557 | return this.platform.Characteristic.CurrentHumidifierDehumidifierState.HUMIDIFYING; 558 | case DehumidifierOperationalMode.Continuous: 559 | case DehumidifierOperationalMode.Smart: 560 | return this.platform.Characteristic.CurrentHumidifierDehumidifierState.IDLE; 561 | case DehumidifierOperationalMode.Dryer: 562 | return this.platform.Characteristic.CurrentHumidifierDehumidifierState.DEHUMIDIFYING; 563 | }; 564 | }; 565 | }; 566 | // Handle requests to get the current value of the "HumidifierDehumidifierState" characteristic 567 | handleCurrentHumidifierDehumidifierStateGet(callback: CharacteristicGetCallback) { 568 | this.platform.log.debug('Triggered GET CurrentHumidifierDehumidifierState'); 569 | callback(null, this.currentHumidifierDehumidifierState()); 570 | }; 571 | // Get the current value of the "TargetHumidifierDehumidifierState" characteristic 572 | public TargetHumidifierDehumidifierState() { 573 | switch (this.operationalMode) { 574 | case DehumidifierOperationalMode.Off: 575 | return this.platform.Characteristic.Active.INACTIVE; 576 | case DehumidifierOperationalMode.Normal: 577 | return this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER; 578 | case DehumidifierOperationalMode.Continuous: 579 | case DehumidifierOperationalMode.Smart: 580 | return this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER; 581 | case DehumidifierOperationalMode.Dryer: 582 | return this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER; 583 | }; 584 | }; 585 | // Handle requests to get the target value of the "HumidifierDehumidifierState" characteristic 586 | handleTargetHumidifierDehumidifierStateGet(callback: CharacteristicGetCallback) { 587 | this.platform.log.debug('Triggered GET TargetHumidifierDehumidifierState'); 588 | callback(null, this.TargetHumidifierDehumidifierState()); 589 | }; 590 | // Handle requests to set the target value of the "HumidifierDehumidifierState" characteristic 591 | handleTargetHumidifierDehumidifierStateSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 592 | this.platform.log.debug(`Triggered SET TargetHumidifierDehumidifierState To: ${value}`); 593 | if (this.TargetHumidifierDehumidifierState() !== value) { 594 | if (value === this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER_OR_DEHUMIDIFIER) { 595 | this.operationalMode = DehumidifierOperationalMode.Smart; 596 | } else if (value === this.platform.Characteristic.TargetHumidifierDehumidifierState.HUMIDIFIER) { 597 | this.operationalMode = DehumidifierOperationalMode.Normal; 598 | } else if (value === this.platform.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER) { 599 | this.operationalMode = DehumidifierOperationalMode.Dryer; 600 | }; 601 | this.platform.sendUpdateToDevice(this); 602 | }; 603 | callback(null); 604 | }; 605 | // Handle requests to get the current value of the "RelativeHumidity" characteristic 606 | handleCurrentRelativeHumidityGet(callback: CharacteristicGetCallback) { 607 | this.platform.log.debug('Triggered GET CurrentRelativeHumidity'); 608 | callback(null, this.currentHumidity); 609 | }; 610 | // Handle requests to get the Relative value of the "HumidityDehumidifierThreshold" characteristic 611 | handleRelativeHumidityThresholdGet(callback: CharacteristicGetCallback) { 612 | this.platform.log.debug('Triggered GET RelativeHumidityDehumidifierThreshold'); 613 | callback(null, this.targetHumidity); 614 | }; 615 | // Handle requests to set the Relative value of the "HumidityDehumidifierThreshold" characteristic 616 | handleRelativeHumidityThresholdSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 617 | if (this.targetHumidity !== value) { 618 | this.platform.log.debug(`Triggered SET RelativeHumidityThreshold To: ${value}`); 619 | this.targetHumidity = Number(value); 620 | this.platform.sendUpdateToDevice(this); 621 | }; 622 | callback(null); 623 | }; 624 | // Get the current value of the "WindSpeed" characteristic 625 | public windSpeed() { 626 | // values from device are 40="Silent",60="Medium",80="Turbo" 627 | // convert to good usable slider in homekit in percent 628 | let currentValue = 0; 629 | switch (this.fanSpeed) { 630 | case 40: currentValue = 30; 631 | break; 632 | case 60: currentValue = 60; 633 | break; 634 | case 80: currentValue = 100; 635 | break; 636 | }; 637 | return currentValue; 638 | }; 639 | // Handle requests to get the current value of the "WindSpeed" characteristic 640 | handleWindSpeedGet(callback: CharacteristicGetCallback) { 641 | this.platform.log.debug('Triggered GET WindSpeed'); 642 | callback(null, this.windSpeed()); 643 | }; 644 | // Handle requests to set the "RotationSpeed" characteristic 645 | handleWindSpeedSet(value: CharacteristicValue, callback: CharacteristicSetCallback) { 646 | this.platform.log.debug(`Triggered SET WindSpeed To: ${value}`); 647 | // transform values in percent 648 | // values from device are 40="Silent",60="Medium",80="Turbo" 649 | if (this.fanSpeed !== value) { 650 | if (value <= 30) { 651 | this.fanSpeed = 40; 652 | } else if (value > 30 && value <= 60) { 653 | this.fanSpeed = 60; 654 | } else if (value > 60 && value <= 100) { 655 | this.fanSpeed = 80; 656 | }; 657 | this.platform.sendUpdateToDevice(this); 658 | }; 659 | callback(null); 660 | }; 661 | // Handle requests to get the current value of the "WaterLevel" characteristic 662 | handleWaterLevelGet(callback: CharacteristicGetCallback) { 663 | this.platform.log.debug('Triggered GET WaterLevel'); 664 | callback(null, this.waterLevel); 665 | }; 666 | }; --------------------------------------------------------------------------------