homebridge-orbit-irrigation
Version:
Orbit Irrigation System platform plugin for [Homebridge](https://github.com/nfarina/homebridge).
715 lines • 27.2 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
//Pulbic site https://techsupport.orbitbhyve.com
'use strict';
import { PLUGIN_NAME, PLUGIN_VERSION } from './settings.js';
import axios from 'axios';
import ws from 'ws';
import ReconnectingWebSocket from 'reconnecting-websocket';
const endpoint = 'https://api.orbitbhyve.com/v1';
const WS_endpoint = 'wss://api.orbitbhyve.com/v1/events';
const maxPingInterval = 30000;
const minPingInterval = 20000;
const pingInterval = Math.floor(Math.random() * (maxPingInterval - minPingInterval)) + minPingInterval; // Websocket get's timed out after 30s, will set a random value between 20 and 30
export default class OrbitAPI {
platform;
log;
wsp;
constructor(platform, log = platform.log, wsp = new WebSocketProxy(platform, log)) {
this.platform = platform;
this.log = log;
this.wsp = wsp;
}
async getToken(email, password) {
// Get token
try {
this.log.debug('Retrieving API key');
const response = await axios({
method: 'post',
baseURL: endpoint,
url: '/session',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-app-id': 'Bhyve Dashboard',
},
data: {
session: {
email: email,
password: password,
},
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting API key - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get token response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getDevices(token, userId) {
// Get the device details
try {
this.log.debug('Retrieving devices');
const response = await axios({
method: 'get',
baseURL: endpoint,
url: '/devices?user=' + userId,
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting devices - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get devices response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getDevice(token, device) {
// Get the device details
try {
this.log.debug('Retrieving device');
const response = await axios({
method: 'get',
baseURL: endpoint,
url: '/devices/' + device,
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting device - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get device response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getMeshes(token, meshId) {
// Get mesh details
try {
this.log.debug('Retrieving mesh info');
const response = await axios({
method: 'get',
baseURL: endpoint,
url: '/meshes/' + meshId,
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting mesh info - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get mesh info response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getNetworkTopologies(token, networkTopologyId) {
// Get mesh details
try {
this.log.debug('Retrieving network topology info');
const response = await axios({
method: 'get',
baseURL: endpoint,
url: '/network_topologies/' + networkTopologyId,
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting network topologies info - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get network topology info response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getDeviceGraph(token, userId) {
// Get device graph details
try {
this.log.debug('Retrieving device graph info');
const response = await axios({
method: 'post',
baseURL: endpoint,
url: '/graph2',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
data: {
query: [
'devices',
{
user_id: userId,
},
'id',
'name',
'address',
'location_name',
'type',
'hardware_version',
'firmware_version',
'mac_address',
'is_connected',
'mesh_id',
],
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting graph - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get device graph response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
async getTimerPrograms(token, device) {
// Get mesh details
try {
this.log.debug('Retrieving schedules');
const response = await axios({
method: 'get',
baseURL: endpoint,
url: '/sprinkler_timer_programs',
headers: {
'Content-Type': 'application/json',
'User-Agent': `${PLUGIN_NAME}/${PLUGIN_VERSION}`,
'orbit-api-key': token,
'orbit-app-id': 'Bhyve Dashboard',
},
params: {
device_id: device.id,
},
responseType: 'json',
}).catch(err => {
this.log.error('Error getting schedules - %s', err.message);
this.log.debug(JSON.stringify(err, null, 2));
if (err.response) {
this.log.debug(JSON.stringify(err.response.data, null, 2));
}
else if (err.code) {
this.log.debug(err.code);
}
else {
this.log.warn('Error %s', err.name);
}
throw err?.code ?? err;
});
if (response.status == 200) {
if (this.platform.showAPIMessages) {
this.log.debug('get timer programs response', JSON.stringify(response.data, null, 2));
}
return response.data;
}
}
catch (err) {
this.log.debug(err);
throw err;
}
}
startZone(token, device, station, runTime) {
try {
this.log.debug('startZone', device.id, station, runTime);
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: 'manual',
device_id: device.id,
stations: [
{
station: station,
run_time: runTime,
},
],
}))
.catch((err) => {
this.log.error('Error starting zone \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
startSchedule(token, device, program) {
try {
this.log.debug('startZone', device.id, program);
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: 'manual',
device_id: device.id,
program: program,
}))
.catch((err) => {
this.log.error('Error starting zone \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
stopZone(token, device) {
try {
this.log.debug('stopZone');
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: 'manual',
device_id: device.id,
timestamp: new Date().toISOString(),
}))
.catch((err) => {
this.log.error('Error starting zone \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
startMultipleZone(token, device, runTime) {
try {
const body = [];
device.zones.forEach((zone) => {
zone.enabled = true; // need orbit version of enabled
if (zone.enabled) {
body.push({
station: zone.station,
run_time: runTime,
});
}
});
this.log.debug('multiple zone run data', JSON.stringify(body, null, 2));
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: 'manual',
device_id: device.id,
stations: body,
}))
.catch((err) => {
this.log.error('Error starting multiple zone \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
stopDevice(token, device) {
try {
this.log.debug('stopZone');
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: 'manual',
device_id: device.id,
//stations: [],
timestamp: new Date().toISOString(),
}))
.catch((err) => {
this.log.error('Error stopping device \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
deviceStandby(token, device, mode) {
try {
this.log.debug('standby');
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'change_mode',
mode: mode,
device_id: device.id,
timestamp: new Date().toISOString(),
}))
.catch((err) => {
this.log.error('Error setting standby \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
openConnection(token, device) {
try {
this.log.debug('Opening WebSocket Connection');
this.wsp
.connect(token, device)
/*
.then(ws =>
ws.send({
event: 'app_connection',
orbit_session_token: token
})
)
*/
.catch((err) => {
this.log.error('Error opening connection \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
onMessage(token, device, listener) {
try {
this.log.debug('Adding Event Listener for %s', device.name);
this.wsp
.connect(token, device)
.then((ws) => ws.addEventListener('message', (msg) => {
listener(msg.data, device.id);
}))
.catch((err) => {
this.log.error('Error configuring listener \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
sync(token, device) {
try {
this.log.debug('Syncing device %s info', device.name);
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'sync',
device_id: device.id,
}))
.catch((err) => {
this.log.error('Error syncing data \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
identify(token, device) {
try {
this.log.debug('Identify device %s info', device.name);
this.wsp
.connect(token, device)
.then((ws) => ws.send({
event: 'fs_identify',
device_id: device.id,
identify_time_ms: 5000,
}))
.catch((err) => {
this.log.error('Error identify data \n%s', err);
});
}
catch (err) {
this.log.error('something went wrong \n%s', err);
}
}
}
export class WebSocketProxy {
platform;
log;
rws;
ping;
constructor(platform, log, rws = null, ping = null) {
this.platform = platform;
this.log = log;
this.rws = rws;
this.ping = ping;
}
async connect(token, device) {
try {
if (this.rws) {
this.log.debug('%s ready state', device, this.rws.readyState);
switch (this.rws.readyState) {
case ws.CONNECTING:
return this.rws;
case ws.OPEN:
return this.rws;
case ws.CLOSING:
this.rws.reconnect();
return this.rws;
case ws.CLOSED:
this.rws.reconnect();
return this.rws;
}
}
return new Promise((resolve, reject) => {
try {
const options = {
WebSocket: ws,
maxReconnectionDelay: 10000, //64000
minReconnectionDelay: Math.floor(1000 + Math.random() * 4000),
reconnectionDelayGrowFactor: 1.3,
minUptime: 5000,
connectionTimeout: 8000, //4000-10000
maxRetries: Infinity,
maxEnqueuedMessages: 1,
startClosed: false,
debug: false,
};
this.rws = new ReconnectingWebSocket(WS_endpoint, [], options);
// Intercept send events for logging
const origSend = this.rws.send.bind(this.rws);
this.rws.send = (data, options, callback) => {
switch (this.rws.readyState) {
case ws.CONNECTING:
setTimeout(() => {
if (typeof data === 'object') {
data = JSON.stringify(data, null, 2);
}
if (this.platform.showOutgoingMessages) {
//this.log.debug(JSON.parse(data).event)
if (JSON.parse(data).event != 'ping') {
this.log.debug('sending outgoing message %s', data);
}
}
origSend(data, options, callback);
}, 2000);
break;
case ws.OPEN:
if (typeof data === 'object') {
data = JSON.stringify(data, null, 2);
}
if (this.platform.showOutgoingMessages) {
//this.log.debug(JSON.parse(data).event)
if (JSON.parse(data).event != 'ping') {
this.log.debug('sending outgoing message %s', data);
}
}
origSend(data, options, callback);
break;
case ws.CLOSING:
break;
case ws.CLOSED:
this.log.info('connection not opened');
break;
}
};
this.rws.onopen = (event) => {
try {
this.rws.send({
event: 'app_connection',
orbit_session_token: token,
});
this.log.debug('connection open', JSON.stringify({
type: event.type,
}, null, 2));
this.log.info('WebSocket opened');
// start ping
this.ping = setInterval(() => {
this.rws.send({ event: 'ping' });
}, pingInterval);
resolve(this.rws);
}
catch (err) {
this.log.error('Error with open event \n%s', err);
}
};
this.rws.onclose = (event) => {
try {
this.log.debug('connection closed', JSON.stringify({
type: event.type,
wasClean: event.wasClean,
code: event.code,
reason: event.reason,
}, null, 2));
clearInterval(this.ping);
if ((event.code == 1000 || event.code == 1005 || event.code == 1006) && this.rws.readyState == 3) {
this.log.info('WebSocket closed');
this.log.warn('Devices will not sync until WebSocket connection is restored.');
}
}
catch (err) {
this.log.error('Error with close event \n%s', err);
}
};
this.rws.onmessage = (msg) => {
try {
if (this.platform.showIncomingMessages) {
this.log.debug('incoming message', JSON.parse(msg.data));
}
}
catch (err) {
this.log.error('Error with ,essage event \n%s', err);
}
};
this.rws.onerror = (event) => {
try {
this.log.debug('ready state', this.rws.readyState);
this.log.debug('connection error', event.error);
switch (this.rws.readyState) {
case ws.CONNECTING:
this.log.debug('WebSocket connecting');
break;
case ws.OPEN:
this.log.debug('WebSocket opened');
break;
case ws.CLOSING:
this.log.debug('WebSocket closing');
break;
case ws.CLOSED:
this.log.debug('WebSocket closed');
break;
}
reject(event);
}
catch (err) {
this.log.error('Error with error event \n%s', err);
}
};
}
catch (error) {
this.log.error('caught', error.message);
if (this.rws) {
//check if connected
if (this.rws.readyState == ws.OPEN) {
this.log.warn('error, closing connection');
this.rws.close((1000), ('Session terminated by client'));
try {
clearInterval(this.ping);
this.rws = null;
}
catch (err) {
this.log.error('Error closing connection \n%s', err);
}
}
}
}
});
}
catch (err) {
this.log.error('Opps', err);
}
}
}
//# sourceMappingURL=orbitapi.js.map