├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.schema.json ├── package.json ├── src ├── orbitapi.ts └── platform.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes 2 | 3 | ## 1.5.3 4 | 5 | - [Enhancement] Changed unhandled message to debug to reduce unnecessary logging 6 | 7 | ## 1.5.2 8 | 9 | - [Fix] Removed unnecessary caching of device 10 | 11 | ## 1.5.1 12 | 13 | - [Enhancement] Converted to TypeScript 14 | - [Enhancement] Reworked code to simplify functions 15 | 16 | ## 1.4.5 17 | 18 | - [Fix] Add handling of no time remaining 19 | - [Fix] Stopped warning for clear_low_battery message 20 | 21 | ## 1.4.4 22 | 23 | - [Enhancement] Removed the maxRetries on the websocket so it will keep trying to reconnect forever 24 | 25 | ## 1.4.3 26 | 27 | - [Enhancement] Added config.schame.json to enable Homebridge Config UI 28 | 29 | ## 1.4.1 30 | 31 | - [Enhancement] Improved getToken and getDevices error handling -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 MortJC 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homebridge-platform-orbit 2 | Orbit Irrigation System platform plugin for [HomeBridge](https://github.com/nfarina/homebridge). 3 | 4 | ## Installation 5 | 1. Install this plugin using: npm install -g homebridge-platform-orbit 6 | 2. Edit ``config.json`` and add your login detail. 7 | 3. Run Homebridge 8 | 9 | ## Config.json example 10 | ``` 11 | "platforms": [ 12 | { 13 | "platform": "orbit", 14 | "name" : "orbit", 15 | "email": "joe.blogs@gmail.com", 16 | "password": "MySecretPassword" 17 | } 18 | ] 19 | ``` 20 | ## Credit 21 | 1. [codyc1515](https://github.com/codyc1515/homebridge-orbit-bhyve) who's code provide an initial framework as to how to set up various homebridge services and interact with the orbit device. 22 | 2. [blacksmithlabs](https://github.com/blacksmithlabs/orbit-bhyve-remote) who's code provide the method of using websockets to interact with the orbit device. -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "orbit", 3 | "pluginType": "platform", 4 | "schema": { 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "title": "Name", 9 | "type": "string", 10 | "default": "orbit", 11 | "required": true 12 | }, 13 | "email": { 14 | "title": "Email", 15 | "type": "string", 16 | "default": "joe.blogs@gmail.com", 17 | "required": true 18 | }, 19 | "password": { 20 | "title": "Password", 21 | "type": "string", 22 | "default": "MySecretPassword", 23 | "required": true 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-platform-orbit", 3 | "version": "1.5.3", 4 | "description": "Orbit Irrigation System platform plugin for [HomeBridge](https://github.com/nfarina/homebridge).", 5 | "dependencies": { 6 | "bent": "^7.3.2", 7 | "reconnecting-websocket": "^4.4.0", 8 | "ws": "^7.4.7" 9 | }, 10 | "devDependencies": { 11 | "@types/bent": "^7.3.2", 12 | "@types/ws": "^7.4.7", 13 | "@types/node": "10.17.19", 14 | "homebridge": "^1.0.4", 15 | "rimraf": "^3.0.2", 16 | "typescript": "^3.8.3" 17 | }, 18 | "engines": { 19 | "homebridge": ">=0.4.0", 20 | "node": ">=8.6.0" 21 | }, 22 | "main": "dist/platform.js", 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1", 25 | "build": "tsc" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/MortJC/homebridge-platform-orbit.git" 30 | }, 31 | "keywords": [ 32 | "homebridge-plugin", 33 | "homekit", 34 | "orbit", 35 | "bhyve", 36 | "irrigation" 37 | ], 38 | "author": "James Carvosso", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/MortJC/homebridge-platform-orbit/issues" 42 | }, 43 | "homepage": "https://github.com/MortJC/homebridge-platform-orbit#readme" 44 | } 45 | -------------------------------------------------------------------------------- /src/orbitapi.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "homebridge"; 2 | 3 | import bent from 'bent'; 4 | import WS from 'ws'; 5 | import ReconnectingWebSocket from 'reconnecting-websocket'; 6 | 7 | const endpoint = 'https://api.orbitbhyve.com/v1'; 8 | const ws_endpoint = 'wss://api.orbitbhyve.com/v1'; 9 | 10 | const WSpingINTERVAL = 25000 // Websocket get's timed out after 30s, so ping every 25s 11 | 12 | export class OrbitAPI { 13 | 14 | private readonly log: Logger; 15 | private readonly email: string; 16 | private readonly password: string; 17 | private token: string; 18 | 19 | constructor(log: Logger, email: string, password: string) { 20 | this.log = log; 21 | this.email = email; 22 | this.password = password; 23 | this.token = ""; 24 | } 25 | 26 | async login() { 27 | // Log in 28 | try { 29 | const postJSON = bent('POST', 'json'); 30 | let response = await postJSON(endpoint + "/session", 31 | { 32 | "session": { 33 | "email": this.email, 34 | "password": this.password 35 | } 36 | }, 37 | { 38 | "orbit-app-id": "Orbit Support Dashboard", 39 | "orbit-api-key": "null" 40 | }); 41 | this.token = response['orbit_api_key']; 42 | } catch (error) { 43 | throw error; 44 | } 45 | } 46 | 47 | async getDevices(): Promise { 48 | let devices: OrbitDevice[] = []; 49 | 50 | // Get the device details 51 | try { 52 | const getJSON = bent('GET', 'json'); 53 | let response = await getJSON(endpoint + "/devices", {}, 54 | { 55 | "orbit-app-id": "Orbit Support Dashboard", 56 | "orbit-api-key": this.token 57 | }); 58 | response.forEach((result: any) => { 59 | if (result['type'] == "sprinkler_timer") { 60 | 61 | // Create the device 62 | let device = new OrbitDevice(this.log, this.token, result['id'], result['name'], result['hardware_version'], result['firmware_version'], result['is_connected']); 63 | 64 | // Create zones 65 | result['zones'].forEach((zone: any) => { 66 | device.addZone(zone['station'], zone['name']); 67 | }); 68 | 69 | devices.push(device); 70 | } 71 | }); 72 | return devices; 73 | } catch (error) { 74 | throw error; 75 | } 76 | } 77 | 78 | } 79 | 80 | 81 | export class OrbitDevice { 82 | private readonly log: Logger; 83 | private readonly token: string; 84 | public readonly id: string; 85 | public readonly name: string; 86 | public readonly hardware_version: string; 87 | public readonly firmware_version: string; 88 | public readonly is_connected: boolean; 89 | public readonly zones: {}[]; 90 | private messageQueue: string[]; 91 | private rws: ReconnectingWebSocket; 92 | 93 | 94 | constructor(log: Logger, token: string, id: string, name: string, hardware_version: string, firmware_version: string, is_connected: boolean) { 95 | this.log = log; 96 | this.token = token; 97 | this.id = id; 98 | this.name = name; 99 | this.hardware_version = hardware_version; 100 | this.firmware_version = firmware_version; 101 | this.is_connected = is_connected; 102 | this.zones = []; 103 | this.messageQueue = [] 104 | 105 | // Create the Reconnecting Web Socket 106 | this.rws = new ReconnectingWebSocket(`${ws_endpoint}/events`, [], { 107 | WebSocket: WS, 108 | connectionTimeout: 10000, 109 | maxReconnectionDelay: 64000, 110 | minReconnectionDelay: 2000, 111 | reconnectionDelayGrowFactor: 2 112 | }); 113 | 114 | // Intercept send events for logging and queuing 115 | const origSend = this.rws.send.bind(this.rws); 116 | this.rws.send = (data: string) => { 117 | if (this.rws.readyState === WS.OPEN) { 118 | this.log.debug('TX', data); 119 | origSend(data); 120 | } 121 | else { 122 | this.messageQueue.push(data); 123 | } 124 | }; 125 | 126 | // Ping 127 | setInterval(() => { 128 | this.rws.send(JSON.stringify({ event: 'ping' })); 129 | }, WSpingINTERVAL); 130 | 131 | // On Open, process any queued messages 132 | this.rws.onopen = (openEvent: WS.OpenEvent) => { 133 | this.log.debug('WebSocket', openEvent.type); 134 | while (this.messageQueue.length > 0) { 135 | let data: string = this.messageQueue.shift()!; 136 | this.log.debug('TX', data); 137 | origSend(data); 138 | } 139 | }; 140 | 141 | // On Close 142 | this.rws.onclose = (closeEvent: WS.CloseEvent) => { 143 | this.log.debug('WebSocket', closeEvent.type); 144 | }; 145 | 146 | // On Error 147 | this.rws.onerror = (errorEvent: WS.ErrorEvent) => { 148 | this.log.error('WebSocket Error', errorEvent); 149 | this.rws.close(); 150 | }; 151 | } 152 | 153 | 154 | addZone(station: string, name: string) { 155 | this.zones.push({ "station": station, "name": name }); 156 | } 157 | 158 | 159 | openConnection() { 160 | this.log.debug('openConnection'); 161 | this.rws.send(JSON.stringify({ 162 | event: "app_connection", 163 | orbit_session_token: this.token, 164 | subscribe_device_id: this.id 165 | })); 166 | } 167 | 168 | 169 | onMessage(listner: Function) { 170 | this.log.debug('onMessage'); 171 | this.rws.onmessage = (messageEvent: MessageEvent) => { 172 | this.log.debug('RX', messageEvent.data); 173 | listner(messageEvent.data); 174 | }; 175 | } 176 | 177 | 178 | sync() { 179 | this.log.debug('sync'); 180 | this.rws.send(JSON.stringify({ 181 | event: "sync", 182 | device_id: this.id 183 | })); 184 | } 185 | 186 | 187 | startZone(station: number, run_time: number) { 188 | this.log.debug('startZone', station, run_time); 189 | this.rws.send(JSON.stringify({ 190 | event: "change_mode", 191 | mode: "manual", 192 | device_id: this.id, 193 | timestamp: new Date().toISOString(), 194 | stations: [ 195 | { "station": station, "run_time": run_time } 196 | ] 197 | })); 198 | } 199 | 200 | 201 | stopZone() { 202 | this.log.debug('stopZone'); 203 | this.rws.send(JSON.stringify({ 204 | event: "change_mode", 205 | mode: "manual", 206 | device_id: this.id, 207 | timestamp: new Date().toISOString(), 208 | stations: [] 209 | })); 210 | } 211 | } -------------------------------------------------------------------------------- /src/platform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | API, 3 | HAP, 4 | PlatformAccessory, 5 | PlatformConfig, 6 | Logger, 7 | Service 8 | } from "homebridge"; 9 | 10 | const PluginName = 'homebridge-platform-orbit'; 11 | const PlatformName = 'orbit'; 12 | 13 | let hap: HAP; 14 | 15 | export = (api: API) => { 16 | hap = api.hap; 17 | api.registerPlatform(PluginName, PlatformName, PlatformOrbit); 18 | }; 19 | 20 | import { OrbitAPI, OrbitDevice } from './orbitapi'; 21 | 22 | class PlatformOrbit { 23 | private readonly email: string = ""; 24 | private readonly password: string = ""; 25 | private accessories: { [uuid: string]: PlatformAccessory } = {}; 26 | 27 | constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) { 28 | if (!config || !config["email"] || !config["password"]) { 29 | this.log.error("Platform config incorrect or missing. Check the config.json file."); 30 | } 31 | else { 32 | 33 | this.email = config["email"]; 34 | this.password = config["password"]; 35 | 36 | this.log.info('Starting PlatformOrbit using homebridge API', api.version); 37 | if (api) { 38 | 39 | // save the api for use later 40 | this.api = api; 41 | 42 | // if finished loading cache accessories 43 | this.api.on("didFinishLaunching", () => { 44 | 45 | // Load the orbit devices 46 | this.loadDevices(); 47 | 48 | }); 49 | } 50 | } 51 | } 52 | 53 | 54 | loadDevices() { 55 | this.log.debug("Loading the devices"); 56 | 57 | // login to the API and get the token 58 | let orbitAPI: OrbitAPI = new OrbitAPI(this.log, this.email, this.password); 59 | orbitAPI.login() 60 | .then(() => { 61 | 62 | // get an array of the devices 63 | orbitAPI.getDevices() 64 | .then((devices: OrbitDevice[]) => { 65 | 66 | // loop through each device 67 | devices.forEach((device: OrbitDevice) => { 68 | 69 | // Generate irrigation service uuid 70 | const uuid: string = hap.uuid.generate(device.id); 71 | 72 | // Check if device is already loaded from cache 73 | if (this.accessories[uuid]) { 74 | this.log.info('Configuring cached device', device.name); 75 | 76 | // Setup Irrigation Accessory and Irrigation Service 77 | let irrigationAccessory = this.accessories[uuid]; 78 | irrigationAccessory.context.timeEnding = []; 79 | this.api.updatePlatformAccessories([irrigationAccessory]); 80 | this.configureIrrigationService(irrigationAccessory.getService(hap.Service.IrrigationSystem)!); 81 | 82 | // Find the valve Services associated with the accessory 83 | irrigationAccessory.services.forEach((service: any) => { 84 | if (hap.Service.Valve.UUID === service.UUID) { 85 | 86 | // Configure Valve Service 87 | this.configureValveService(irrigationAccessory, service, device); 88 | } 89 | }); 90 | } 91 | else { 92 | this.log.info('Creating and configuring new device', device.name); 93 | 94 | // Create Irrigation Accessory and Irrigation Service 95 | let irrigationAccessory = new this.api.platformAccessory(device.name, uuid); 96 | irrigationAccessory.context.timeEnding = []; 97 | let irrigationSystemService = this.createIrrigationService(irrigationAccessory, device); 98 | this.configureIrrigationService(irrigationSystemService); 99 | 100 | // Create and configure Values services and link to Irrigation Service 101 | device.zones.forEach((zone: any) => { 102 | let valveService = this.createValveService(irrigationAccessory, zone['station'], zone['name']); 103 | irrigationSystemService.addLinkedService(valveService); 104 | this.configureValveService(irrigationAccessory, valveService, device); 105 | }); 106 | 107 | // Register platform accessory 108 | this.log.debug('Registering platform accessory'); 109 | this.api.registerPlatformAccessories(PluginName, PlatformName, [irrigationAccessory]); 110 | this.accessories[uuid] = irrigationAccessory; 111 | } 112 | 113 | device.openConnection(); 114 | device.onMessage(this.processMessage.bind(this)); 115 | device.sync(); 116 | 117 | }); 118 | }).catch((error) => { 119 | this.log.error('Unable to get devices', error); 120 | }); 121 | }) 122 | .catch((error) => { 123 | this.log.error('Unable to get token', error); 124 | }); 125 | } 126 | 127 | 128 | configureAccessory(accessory: PlatformAccessory) { 129 | // Add cached devices to the accessories arrary 130 | this.log.info('Loading accessory from cache', accessory.displayName); 131 | this.accessories[accessory.UUID] = accessory; 132 | } 133 | 134 | 135 | createIrrigationService(irrigationAccessory: PlatformAccessory, device: OrbitDevice): Service { 136 | this.log.debug('Create Irrigation service', device.id); 137 | 138 | // Update AccessoryInformation Service 139 | irrigationAccessory.getService(hap.Service.AccessoryInformation)! 140 | .setCharacteristic(hap.Characteristic.Name, device.name) 141 | .setCharacteristic(hap.Characteristic.Manufacturer, "Orbit") 142 | .setCharacteristic(hap.Characteristic.SerialNumber, device.id) 143 | .setCharacteristic(hap.Characteristic.Model, device.hardware_version) 144 | .setCharacteristic(hap.Characteristic.FirmwareRevision, device.firmware_version); 145 | 146 | // Add Irrigation System Service 147 | let irrigationSystemService = irrigationAccessory.addService(hap.Service.IrrigationSystem, device.name) 148 | .setCharacteristic(hap.Characteristic.Name, device.name) 149 | .setCharacteristic(hap.Characteristic.Active, hap.Characteristic.Active.ACTIVE) 150 | .setCharacteristic(hap.Characteristic.InUse, hap.Characteristic.InUse.NOT_IN_USE) 151 | .setCharacteristic(hap.Characteristic.ProgramMode, hap.Characteristic.ProgramMode.NO_PROGRAM_SCHEDULED) 152 | .setCharacteristic(hap.Characteristic.RemainingDuration, 0) 153 | .setCharacteristic(hap.Characteristic.StatusFault, device.is_connected ? hap.Characteristic.StatusFault.NO_FAULT : hap.Characteristic.StatusFault.GENERAL_FAULT); 154 | 155 | return irrigationSystemService; 156 | } 157 | 158 | 159 | configureIrrigationService(irrigationSystemService: Service) { 160 | this.log.debug('Configure Irrigation service', irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value) 161 | 162 | // Configure Irrigation System Service 163 | irrigationSystemService 164 | .getCharacteristic(hap.Characteristic.Active) 165 | .onGet(() => { 166 | this.log.debug("IrrigationSystem", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "Active = ", irrigationSystemService.getCharacteristic(hap.Characteristic.Active).value ? "ACTIVE" : "INACTIVE"); 167 | return irrigationSystemService.getCharacteristic(hap.Characteristic.Active).value; 168 | }) 169 | .onSet((value) => { 170 | this.log.info("Set irrigation system ", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, " to ", value ? "ACTIVE" : "INACTIVE"); 171 | irrigationSystemService.getCharacteristic(hap.Characteristic.Active).updateValue(value); 172 | }); 173 | 174 | irrigationSystemService 175 | .getCharacteristic(hap.Characteristic.ProgramMode) 176 | .onGet(() => { 177 | this.log.debug("IrrigationSystem", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "ProgramMode = ", irrigationSystemService.getCharacteristic(hap.Characteristic.ProgramMode).value); 178 | return irrigationSystemService.getCharacteristic(hap.Characteristic.ProgramMode).value; 179 | }); 180 | 181 | irrigationSystemService 182 | .getCharacteristic(hap.Characteristic.InUse) 183 | .onGet(() => { 184 | this.log.debug("IrrigationSystem", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "InUse = ", irrigationSystemService.getCharacteristic(hap.Characteristic.InUse).value ? "IN_USE" : "NOT_IN_USE"); 185 | return irrigationSystemService.getCharacteristic(hap.Characteristic.InUse).value; 186 | }); 187 | 188 | irrigationSystemService 189 | .getCharacteristic(hap.Characteristic.StatusFault) 190 | .onGet(() => { 191 | this.log.debug("IrrigationSystem", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "StatusFault = ", irrigationSystemService.getCharacteristic(hap.Characteristic.StatusFault).value ? "GENERAL_FAULT" : "NO_FAULT"); 192 | return irrigationSystemService.getCharacteristic(hap.Characteristic.StatusFault).value; 193 | }); 194 | 195 | irrigationSystemService 196 | .getCharacteristic(hap.Characteristic.RemainingDuration) 197 | .onGet(() => { 198 | this.log.debug("IrrigationSystem", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "RemainingDuration = ", irrigationSystemService.getCharacteristic(hap.Characteristic.RemainingDuration).value); 199 | return irrigationSystemService.getCharacteristic(hap.Characteristic.RemainingDuration).value; 200 | }); 201 | } 202 | 203 | 204 | createValveService(irrigationAccessory: PlatformAccessory, station: string, name: string): Service { 205 | this.log.debug("Create Valve service " + name + " with station " + station); 206 | 207 | // Add Valve Service 208 | let valve = irrigationAccessory.addService(hap.Service.Valve, name, station); 209 | valve 210 | .setCharacteristic(hap.Characteristic.Active, hap.Characteristic.Active.INACTIVE) 211 | .setCharacteristic(hap.Characteristic.InUse, hap.Characteristic.InUse.NOT_IN_USE) 212 | .setCharacteristic(hap.Characteristic.ValveType, hap.Characteristic.ValveType.IRRIGATION) 213 | .setCharacteristic(hap.Characteristic.SetDuration, 300) 214 | .setCharacteristic(hap.Characteristic.RemainingDuration, 0) 215 | .setCharacteristic(hap.Characteristic.IsConfigured, hap.Characteristic.IsConfigured.CONFIGURED) 216 | .setCharacteristic(hap.Characteristic.ServiceLabelIndex, station) 217 | .setCharacteristic(hap.Characteristic.StatusFault, hap.Characteristic.StatusFault.NO_FAULT) 218 | .setCharacteristic(hap.Characteristic.Name, name); 219 | 220 | return valve 221 | } 222 | 223 | 224 | configureValveService(irrigationAccessory: PlatformAccessory, valveService: Service, device: OrbitDevice) { 225 | this.log.debug("Configure Valve service", valveService.getCharacteristic(hap.Characteristic.Name).value); 226 | 227 | // Configure Valve Service 228 | valveService 229 | .getCharacteristic(hap.Characteristic.Active) 230 | .onGet(() => { 231 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "Active = ", valveService.getCharacteristic(hap.Characteristic.Active).value ? "ACTIVE" : "INACTIVE"); 232 | return valveService.getCharacteristic(hap.Characteristic.Active).value; 233 | }) 234 | .onSet((value) => { 235 | // Prepare message for API 236 | let station = valveService.getCharacteristic(hap.Characteristic.ServiceLabelIndex).value as number; 237 | let run_time = valveService.getCharacteristic(hap.Characteristic.SetDuration).value as number / 60; 238 | 239 | if (value == hap.Characteristic.Active.ACTIVE) { 240 | // Turn on the valve 241 | this.log.info("Start zone", valveService.getCharacteristic(hap.Characteristic.Name).value, "for", run_time, "mins"); 242 | device.startZone(station, run_time); 243 | } else { 244 | // Turn off the valve 245 | this.log.info("Stop zone", valveService.getCharacteristic(hap.Characteristic.Name).value); 246 | device.stopZone(); 247 | } 248 | }); 249 | 250 | valveService 251 | .getCharacteristic(hap.Characteristic.InUse) 252 | .onGet(() => { 253 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "InUse = ", valveService.getCharacteristic(hap.Characteristic.InUse).value ? "IN_USE" : "NOT_IN_USE"); 254 | return valveService.getCharacteristic(hap.Characteristic.InUse).value; 255 | }); 256 | 257 | valveService 258 | .getCharacteristic(hap.Characteristic.IsConfigured) 259 | .onGet(() => { 260 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "IsConfigured = ", valveService.getCharacteristic(hap.Characteristic.IsConfigured).value ? "CONFIGURED" : "NOT_CONFIGURED"); 261 | return valveService.getCharacteristic(hap.Characteristic.IsConfigured).value; 262 | }) 263 | .onSet((value) => { 264 | this.log.info("Set valve ", valveService.getCharacteristic(hap.Characteristic.Name).value, " isConfigured to ", value); 265 | valveService.getCharacteristic(hap.Characteristic.IsConfigured).updateValue(value); 266 | }); 267 | 268 | valveService 269 | .getCharacteristic(hap.Characteristic.StatusFault) 270 | .onGet(() => { 271 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "StatusFault = ", valveService.getCharacteristic(hap.Characteristic.StatusFault).value ? "GENERAL_FAULT" : "NO_FAULT"); 272 | return valveService.getCharacteristic(hap.Characteristic.StatusFault).value; 273 | }); 274 | 275 | valveService 276 | .getCharacteristic(hap.Characteristic.ValveType) 277 | .onGet(() => { 278 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "ValveType = ", valveService.getCharacteristic(hap.Characteristic.ValveType).value); 279 | return valveService.getCharacteristic(hap.Characteristic.ValveType).value; 280 | }); 281 | 282 | valveService 283 | .getCharacteristic(hap.Characteristic.SetDuration) 284 | .onGet(() => { 285 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "SetDuration = ", valveService.getCharacteristic(hap.Characteristic.SetDuration).value); 286 | return valveService.getCharacteristic(hap.Characteristic.SetDuration).value; 287 | }) 288 | .onSet((value) => { 289 | // Update the value run duration 290 | this.log.info("Set valve ", valveService.getCharacteristic(hap.Characteristic.Name).value, " SetDuration to ", value); 291 | valveService.getCharacteristic(hap.Characteristic.SetDuration).updateValue(value); 292 | }); 293 | 294 | valveService 295 | .getCharacteristic(hap.Characteristic.RemainingDuration) 296 | .onGet(() => { 297 | // Calc remain duration 298 | let station = valveService.getCharacteristic(hap.Characteristic.ServiceLabelIndex).value as number; 299 | let timeRemaining = Math.max(Math.round((irrigationAccessory.context.timeEnding[station] - Date.now()) / 1000), 0); 300 | if (isNaN(timeRemaining)) { 301 | timeRemaining = 0; 302 | } 303 | this.log.debug("Valve", valveService.getCharacteristic(hap.Characteristic.Name).value, "RemainingDuration =", timeRemaining); 304 | return timeRemaining; 305 | }) 306 | 307 | irrigationAccessory.context.timeEnding[valveService.getCharacteristic(hap.Characteristic.ServiceLabelIndex).value as number] = 0; 308 | } 309 | 310 | 311 | processMessage(message: any) { 312 | // Incoming data 313 | let jsonData = JSON.parse(message); 314 | 315 | // Find the irrigation system and process message 316 | let irrigationAccessory: PlatformAccessory = this.accessories[hap.uuid.generate(jsonData['device_id'])]; 317 | let irrigationSystemService: Service = irrigationAccessory.getService(hap.Service.IrrigationSystem)!; 318 | 319 | switch (jsonData['event']) { 320 | 321 | case "watering_in_progress_notification": 322 | this.log.debug("Watering_in_progress_notification Station", "device =", irrigationSystemService.getCharacteristic(hap.Characteristic.Name).value, "station =", jsonData['current_station'], "Runtime =", jsonData['run_time']); 323 | 324 | // Update Irrigation System Service 325 | irrigationSystemService.getCharacteristic(hap.Characteristic.InUse).updateValue(hap.Characteristic.InUse.IN_USE); 326 | 327 | // Find the valve Services 328 | irrigationAccessory.services.forEach((service: Service) => { 329 | if (hap.Service.Valve.UUID === service.UUID) { 330 | 331 | // Update Valve Services 332 | let station: number = service.getCharacteristic(hap.Characteristic.ServiceLabelIndex).value as number; 333 | if (station == jsonData['current_station']) { 334 | service.getCharacteristic(hap.Characteristic.Active).updateValue(hap.Characteristic.Active.ACTIVE); 335 | service.getCharacteristic(hap.Characteristic.InUse).updateValue(hap.Characteristic.InUse.IN_USE); 336 | service.getCharacteristic(hap.Characteristic.RemainingDuration).updateValue(jsonData['run_time'] * 60); 337 | irrigationAccessory.context.timeEnding[station] = Date.now() + parseInt(jsonData['run_time']) * 60 * 1000; 338 | } else { 339 | service.getCharacteristic(hap.Characteristic.Active).updateValue(hap.Characteristic.Active.INACTIVE); 340 | service.getCharacteristic(hap.Characteristic.InUse).updateValue(hap.Characteristic.InUse.NOT_IN_USE); 341 | service.getCharacteristic(hap.Characteristic.RemainingDuration).updateValue(0); 342 | } 343 | }; 344 | }); 345 | break; 346 | 347 | case "watering_complete": 348 | case "device_idle": 349 | this.log.debug("Watering_complete or device_idle"); 350 | 351 | // Update Irrigation System Service 352 | irrigationSystemService.getCharacteristic(hap.Characteristic.InUse).updateValue(hap.Characteristic.InUse.NOT_IN_USE); 353 | 354 | // Find the valve Services 355 | irrigationAccessory.services.forEach(function (service: Service) { 356 | 357 | // Update Valve hap.Service 358 | if (hap.Service.Valve.UUID === service.UUID) { 359 | service.getCharacteristic(hap.Characteristic.Active).updateValue(hap.Characteristic.Active.INACTIVE); 360 | service.getCharacteristic(hap.Characteristic.InUse).updateValue(hap.Characteristic.InUse.NOT_IN_USE); 361 | service.getCharacteristic(hap.Characteristic.RemainingDuration).updateValue(0); 362 | } 363 | }); 364 | break; 365 | 366 | case "change_mode": 367 | this.log.debug("change_mode", jsonData['mode']); 368 | 369 | // Update the ProgramMode 370 | switch (jsonData['mode']) { 371 | case "off": 372 | irrigationSystemService.getCharacteristic(hap.Characteristic.ProgramMode).updateValue(hap.Characteristic.ProgramMode.NO_PROGRAM_SCHEDULED); 373 | break; 374 | case "auto": 375 | irrigationSystemService.getCharacteristic(hap.Characteristic.ProgramMode).updateValue(hap.Characteristic.ProgramMode.PROGRAM_SCHEDULED); 376 | break; 377 | case "manual": 378 | irrigationSystemService.getCharacteristic(hap.Characteristic.ProgramMode).updateValue(hap.Characteristic.ProgramMode.PROGRAM_SCHEDULED_MANUAL_MODE_); 379 | break; 380 | } 381 | break; 382 | 383 | case "program_changed": 384 | this.log.debug("program_change - do nothing"); 385 | break; 386 | 387 | case "rain_delay": 388 | this.log.debug("rain_delay - do nothing"); 389 | break; 390 | 391 | case "device_connected": 392 | this.log.debug("device_connected"); 393 | irrigationSystemService.getCharacteristic(hap.Characteristic.StatusFault).updateValue(hap.Characteristic.StatusFault.NO_FAULT); 394 | break; 395 | 396 | case "device_disconnected": 397 | this.log.debug("device_disconnected"); 398 | irrigationSystemService.getCharacteristic(hap.Characteristic.StatusFault).updateValue(hap.Characteristic.StatusFault.GENERAL_FAULT); 399 | break; 400 | 401 | case "clear_low_battery": 402 | this.log.debug("clear_low_battery - do nothing"); 403 | break; 404 | 405 | default: 406 | this.log.debug("Unhandled message received: " + jsonData['event']); 407 | break; 408 | } 409 | } 410 | 411 | 412 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "sourceMap": true, 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "include": [ 12 | "src" 13 | ] 14 | } --------------------------------------------------------------------------------