/
HubitatSystemObject.js
218 lines (172 loc) · 10.2 KB
/
HubitatSystemObject.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
'use strict';
// var URL = require('url').URL;
var fetch = require("node-fetch");
var exports = module.exports;
var WebSocket = require('ws');
var HSM = require ("../lib/Setup HomeSafetyMonitor");
// Functions in class HomeSeerSystem
// async initialize( MakerAPIURL ) // Called once to set up the internal data structures that store information about Hubitat Devices.
class HubitatSystem {
constructor(log, config, api) {
this.Initialized = false;
this.UpdateDeviceTraits = [];// Each entry of this array is a data structure that identifies the HomeKit Services and Characteristics that receive event notifications when a Hubitat attribute for the device changes. The index value is the Maker API device ID of the Hubitat device. Thus, for example, UpdateDeviceTraits[50] would be the entry for Maker API device #50. Each entry includes a sub-array .notifyObjects which is an array of HomeKit Services and Characteristics placed that receive the emitted emitter's .on() event. Items are place into .notifyObjects by the .updateOnHubEvents() method. Each of those characteristics or services has associated with it a sub array .hubitatAttributesOfInterest which is a sub-array listing all the Hubitat attributes of interst to that Characteristic or Service.
this.allDevices = []; // Be careful w th this one! The indices are sequential and do not match device number. This is an array of all devices reported back from Hubitat.
this.name = "Hubitat System";
this.network = { origin:undefined, host: undefined, access_token: undefined, api_number: undefined};
this.webSocket = [];
this.log = log;
this.config = config
}
async initialize(MakerAPIURL) {
var that = this;
// Break MakerAPI Url into host, api number, and access token
that.deviceURL = new URL(MakerAPIURL);
that.network.origin = that.deviceURL.origin
that.network.host = that.deviceURL.host;
that.network.access_token = that.deviceURL.search;
that.network.MakerAPI = MakerAPIURL;
var arr = that.deviceURL.pathname.split('/');
that.network.api_number = parseInt(arr[3]);
// Get every device that Hubitat Maker API has been selected in Maker API
that.allDevices = await fetch(that.deviceURL).then ( (response) => response.json() );
that.initialized = true;
return Promise.resolve(true);;
}
// The following "send" function is used to control devices.
send = function(id, command, ...args) {
var control = new URL(this.network.origin);
var that = this;
control.pathname = `/apps/api/${this.network.api_number}/devices/${id}/${command}`
if(args.length > 0) control.pathname += "/" + args.join(",");
control.search = this.network.access_token;
fetch(control)
.then( response => response.json())
.then( data => {
that.log(`Sending to Hubitat device id: ${id} the command ${command} with arguments ${args.join(",")}.`);
return data
})
return;
}
// The following "send" is specifically to control Home Safety Monitor.
HSMsend = function(command, ...args) {
var control = new URL(this.network.origin);
var that = this;
control.pathname = `/apps/api/${this.network.api_number}/hsm/${command}`
// As of HSM 2.2.5, args.length should always be 0, but this is included in case that changes in the future
if(args.length > 0) control.pathname += "/" + args.join(",");
control.search = this.network.access_token;
that.log(`Sending to Hubitat Home Safety Monitor the command ${command} in control string ${control}.`);
fetch(control)
.then( response => response.json())
.then( data => {
that.log(`Sent to Hubitat Home Safety Monitor the command ${command} and received response ${JSON.stringify(data)}.`);
return data
})
return;
}
listenForChanges() {
var that = this;
var heartbeatInterval;
var refreshInterval;
function heartbeat() {
clearTimeout(this.pingTimeout)
this.pingTimeout = setTimeout(() => { this.terminate(); }, 30000 + 1000);
};
const listenURL = new URL("ws://" + that.network.host+ "/eventsocket");
function startWebsocket() {
that.webSocket = new WebSocket (listenURL.href)
.on('message', (data) => {
var receivedData = JSON.parse(data);
that.log(`Received from device id: ${receivedData.deviceId} a data value ${receivedData.value} in report type ${receivedData.name} with Display Name ${receivedData.displayName} and source ${receivedData.source}.`);
// Only concerned about "DEVICE" or "LOCATION" types. "LOCATION" is used by HSM.
if (receivedData.source == "DEVICE" || receivedData.source == "LOCATION") {
// Only concerned about "LOCATION" events for HSM (for which deviceID == 0), so return if you get a LOCATION and deviceID !== 0.
if ((receivedData.source == "LOCATION") && (receivedData.deviceId !== 0)) return; // 0 is for HSM; Everything else is not valid!
// that.UpdateDeviceTraits[receivedData.deviceId] is the list of Services/ Characteristics that are interested in an update (if undefined, tehre aren't any for this device ID)
if (that.UpdateDeviceTraits[receivedData.deviceId] === undefined) return;
// Its possible that multiple Services or Characteristics are registered to received emitted events, so loop through each one.
// For this Object (Service or Characteristic), does the name of the event as received from Hubitat match one of the names that was registered to receive events using updateOnHubEvents(), if so, emit the event to that Object (Service or Characteristic), else, ignore it.
that.UpdateDeviceTraits[receivedData.deviceId].notifyObjects
.filter((thisObject) => thisObject.hubitatAttributesOfInterest.includes(receivedData.name))
.forEach((thisObject) => {
thisObject.emit("HubValueChanged", receivedData, thisObject)
})
}
})
.on('pong', () => { // Just check to make sure Hubitat is alive and well!;
that.webSocket.isAlive = true;
} )
.on('open', () => { // Set up some error recovery and functions to establish initial settings.
that.log(`Open event. Established connection to Hubitat WebSocket EventStream`);
that.webSocket.isAlive = true;
clearInterval(heartbeatInterval);
clearInterval(refreshInterval);
// Check that the connection to Hubitat remains good.
heartbeatInterval = setInterval(() => {
// that.log("Sending Heartbeat....");
that.webSocket.isAlive = false;
that.webSocket.ping( function noop() {});
}, 30000);
var BoundRefreshAll = that.refreshAll.bind(that) // refreshAll needed to be bound to "that" to be passed the correct values when called in setInterval.
BoundRefreshAll();
// Do a refresh every 30 minutes just to be sure synchronization is maintained with Hubitat.
refreshInterval = setInterval(BoundRefreshAll, 1800000)
})
.on('error', () => {
that.log(`Error. Hubitat WebSocket EventStream connection error!`);
})
.on ('close',() => { // Recover if Hubitat gets rebooted!
that.log(`Connection Closd - Connection to Hubitat WebSocket EventStream was closed. this may be due to a Hubitat Hub error or reboot. Will try to reopen every 30 seconds.`);
that.webSocket.terminate();
clearInterval(heartbeatInterval);
clearInterval(refreshInterval);
setTimeout( () => { // Try to reconnect every 30 seconds if the connection is lost.
that.log(`Trying to reconnect.`);
startWebsocket();
}, 30000);
})
}
startWebsocket();
}
async refreshAll() // Grabs device data from Hubitat just to be sure Homebridge remains in sync. There is no reason it should ever get out of sync, but this is an extra precaution.
{
var that = this;
var deviceID;
var report = [];
HSM.HSMRefresh(that) // Refresh Home Safety Monitor Intrusion Alert status.
this.log(`Calling Refresh on each device to update HomeKit with Current Hubitat Device Data.`);
// Update the allDevices array with the latest data from Maker API.
that.allDevices = await fetch(that.deviceURL).then ( response => response.json() );
// Process the list of traits of interest to each device. Remember, UpdateDeviceTraits is indexed by the Hubitat Device ID #.
that.UpdateDeviceTraits.forEach((traitsList, hubitatDeviceID) => {
if (hubitatDeviceID == 0) return;
// Want the Hubitat data for this device ID (it's 'fullDeviceData') , but the allDevices array is not indexed by the deviceID, so use the .find() function to get the right entry.
var fullDeviceData = this.allDevices.find( (it) => { return (it.id == hubitatDeviceID)})
// Filter out "pushed", "held", "doubleTapped" traits since you don't want to re-trigger any Stateless ProgrammableSwitches which could trigger HomeKit automations. You only want to refresh items that are "stateful" -- for those, re-emitting the same event doesn't cause a trigger in HomeKit.
// For a given Hubitat device ID, there may be multiple Services / Characteristics that have registered to be updated and so may need multiple emits.
traitsList.notifyObjects.forEach((thisHomekitObject) => {
thisHomekitObject.hubitatAttributesOfInterest
.filter((thisAttributeName) => {! ["pushed", "held", "doubleTapped"].includes(thisAttributeName)} )
.forEach((thisAttributeName) => {
if ((fullDeviceData.attributes[thisAttributeName]) === undefined) return
report = {
"name": thisAttributeName,
"value": fullDeviceData.attributes[thisAttributeName]
};
thisHomekitObject.emit("HubValueChanged", report, thisHomekitObject);
})
})
})
}
registerObjectToReceiveUpdates(id, HomeKitObject, emitOnTheseHubitatAttributes) {
// HomeKitObject is either a Service or a Characteristic.
// listenForTheseHubitatAttributes is a listing of Hubitat attributes.
HomeKitObject.id = id;
HomeKitObject.hubitatAttributesOfInterest ??= [];
this.UpdateDeviceTraits[id] ??= []
this.UpdateDeviceTraits[id].notifyObjects ??= [];
HomeKitObject.hubitatAttributesOfInterest.push(...emitOnTheseHubitatAttributes);
this.UpdateDeviceTraits[id].notifyObjects.push(HomeKitObject);
}
}
module.exports = HubitatSystem;