diff --git a/app.ts b/app.ts
index 5a62ac96..e3f09537 100755
--- a/app.ts
+++ b/app.ts
@@ -1,19 +1,19 @@
-/* nodejs-poolController. An application to control pool equipment.
-Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see .
-*/
+/* nodejs-poolController. An application to control pool equipment.
+Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
// add source map support for .js to .ts files
require('source-map-support').install();
@@ -75,9 +75,10 @@ export async function stopAsync(): Promise {
}
if (process.platform === 'win32') {
let rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.on('SIGINT', function() { stopAsync(); });
+ rl.on('SIGINT', async function() { stopAsync(); });
}
else {
- process.on('SIGINT', function() { return stopAsync(); });
+ process.stdin.resume()
+ process.on('SIGINT', async function() { return stopAsync(); });
}
initAsync();
\ No newline at end of file
diff --git a/controller/Equipment.ts b/controller/Equipment.ts
index 6c4dea2a..6d706f75 100755
--- a/controller/Equipment.ts
+++ b/controller/Equipment.ts
@@ -174,19 +174,25 @@ export class PoolSystem implements IPoolSystem {
}
public searchForAdditionalDevices() {
if (this.controllerType === ControllerType.Unknown || typeof this.controllerType === 'undefined' && !conn.mockPort){
- logger.info("Searching for chlorinators and pumps");
+ logger.info("Searching chlorinators, pumps and chem controllers");
EquipmentStateMessage.initVirtual();
sys.board.virtualChlorinatorController.search();
sys.board.virtualPumpControllers.search();
+ sys.board.virtualChemControllers.search();
}
- else if (this.controllerType === ControllerType.Virtual){
+ else {
+ if (this.controllerType === ControllerType.Virtual){
+ state.mode = 0;
+ state.status = 1;
+ sys.equipment.setEquipmentIds();
+ state.emitControllerChange();
+ }
+ // try to start any virtual controllers that are present irregardless of overall controller virtual status
sys.board.virtualPumpControllers.start();
sys.board.virtualChlorinatorController.start();
- state.mode = 0;
- state.status = 1;
- sys.equipment.setEquipmentIds();
- state.emitControllerChange();
+ sys.board.virtualChemControllers.start();
}
+
}
public board: SystemBoard=new SystemBoard(this);
public processVersionChanges(ver: ConfigVersion) { this.board.requestConfiguration(ver); }
diff --git a/controller/boards/EasyTouchBoard.ts b/controller/boards/EasyTouchBoard.ts
index a52179fc..50e6e5a1 100644
--- a/controller/boards/EasyTouchBoard.ts
+++ b/controller/boards/EasyTouchBoard.ts
@@ -283,7 +283,7 @@ export class EasyTouchBoard extends SystemBoard {
this.checkConfiguration();
}
- public async stopAsync() { this._configQueue.close(); return Promise.resolve([]); }
+ public async stopAsync() { this._configQueue.close(); return super.stopAsync(); }
}
export class TouchConfigRequest extends ConfigRequest {
constructor(setcat: number, items?: number[], oncomplete?: Function) {
diff --git a/controller/boards/IntelliCenterBoard.ts b/controller/boards/IntelliCenterBoard.ts
index fdaa5817..61ef592d 100644
--- a/controller/boards/IntelliCenterBoard.ts
+++ b/controller/boards/IntelliCenterBoard.ts
@@ -250,7 +250,7 @@ export class IntelliCenterBoard extends SystemBoard {
sys.configVersion.valves = ver.valves;
}
}
- public async stopAsync() { this._configQueue.close(); return Promise.resolve([]);}
+ public async stopAsync() { this._configQueue.close(); return super.stopAsync();}
public initExpansionModules(ocp0A: number, ocp0B: number, ocp1A: number, ocp2A: number, ocp3A: number) {
let inv = { bodies: 0, circuits: 0, valves: 0, shared: false, dual: false, covers: 0, chlorinators: 0, chemControllers: 0 };
this.processMasterModules(sys.equipment.modules, ocp0A, ocp0B, inv);
diff --git a/controller/boards/SystemBoard.ts b/controller/boards/SystemBoard.ts
index 954d8a37..85544f0f 100644
--- a/controller/boards/SystemBoard.ts
+++ b/controller/boards/SystemBoard.ts
@@ -20,7 +20,7 @@ import { webApp } from '../../web/Server';
import { conn } from '../comms/Comms';
import { Message, Outbound, Protocol } from '../comms/messages/Messages';
import { utils } from '../Constants';
-import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, CustomName, CustomNameCollection, EggTimer, Feature, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, Valve } from '../Equipment';
+import { Body, ChemController, Chlorinator, Circuit, CircuitGroup, CircuitGroupCircuit, ConfigVersion, CustomName, CustomNameCollection, EggTimer, Feature, General, Heater, ICircuit, LightGroup, LightGroupCircuit, Location, Options, Owner, PoolSystem, Pump, Schedule, sys, Valve, ControllerType } from '../Equipment';
import { EquipmentNotFoundError, InvalidEquipmentDataError, InvalidEquipmentIdError, ParameterOutOfRangeError } from '../Errors';
import { BodyTempState, ChemControllerState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, PumpState, state, TemperatureState, VirtualCircuitState } from '../State';
@@ -625,11 +625,9 @@ export class SystemBoard {
public async stopAsync() {
// turn off chlor
sys.board.virtualChlorinatorController.stop();
- let p = [];
- p.push(this.turnOffAllCircuits());
- p.push(sys.board.virtualChemControllers.stopAsync());
- p.push(sys.board.virtualPumpControllers.stopAsync());
- return Promise.all(p);
+ if (sys.controllerType === ControllerType.Virtual) this.turnOffAllCircuits();
+ sys.board.virtualChemControllers.stop();
+ return sys.board.virtualPumpControllers.stopAsync()
}
public async turnOffAllCircuits() {
// turn off all circuits/features
@@ -647,7 +645,6 @@ export class SystemBoard {
}
sys.board.virtualPumpControllers.setTargetSpeed();
state.emitEquipmentChanges();
- return Promise.resolve();
}
public system: SystemCommands = new SystemCommands(this);
public bodies: BodyCommands = new BodyCommands(this);
@@ -2500,20 +2497,38 @@ export class ValveCommands extends BoardCommands {
export class ChemControllerCommands extends BoardCommands {
public async setChemControllerAsync(data: any): Promise {
// this is a combined chem config/state setter.
+ let address = typeof data.address !== 'undefined' ? parseInt(data.address, 10) : undefined;
let id = typeof data.id !== 'undefined' ? parseInt(data.id, 10) : -1;
- if (id <= 0) {
+ if (typeof address === 'undefined' && id <= 0) {
// adding a chem controller
id = sys.chemControllers.nextAvailableChemController();
}
- if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max chem controller id exceeded`, id, 'chemController'));
+ if (typeof id === 'undefined' || id > sys.equipment.maxChemControllers) return Promise.reject(new InvalidEquipmentIdError(`Max chem controller id exceeded`, id, 'chemController'));
+ if (typeof address !== 'undefined' && address < 144 || address > 158) return Promise.reject(new InvalidEquipmentIdError(`Max chem controller id exceeded`, id, 'chemController'));
if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid chemController id: ${data.id}`, data.id, 'ChemController'));
let chem: ChemController;
- if (data.address) chem = sys.chemControllers.getItemByAddress(id, true);
+ if (typeof address !== 'undefined') chem = sys.chemControllers.getItemByAddress(address, true);
else chem = sys.chemControllers.getItemById(id, true);
- let schem = state.chemControllers.getItemById(id, true);
+ let schem = state.chemControllers.getItemById(chem.id, true);
+ // Before we send an outbound ic packet, check if we are deleting this controller
+ if (typeof data.type !== 'undefined' && data.type === 0) {
+ // remove
+ sys.chemControllers.removeItemById(data.id);
+ state.chemControllers.removeItemById(data.id);
+ let chem = sys.chemControllers.getItemById(data.id);
+ chem.isActive = false;
+ sys.emitEquipmentChange();
+ return Promise.resolve(chem);
+ }
+ // and check to see if we are changing TO an intellichem or AWAY from an intellichem
+ schem.type = chem.type = parseInt(data.type, 10) || chem.type || 1;
+ chem.isActive = data.isActive || true;
+ chem.isVirtual = data.isVirtual || true;
+ schem.name = chem.name = data.name || chem.name || `Chem Controller ${chem.id}`;
// if we have an IntelliChem, set the values here and let the status
if (chem.type === sys.board.valueMaps.chemControllerTypes.getValue('intellichem')) {
+ sys.board.virtualChemControllers.start();
return new Promise((resolve, reject) => {
const _ph = (typeof data.pHSetpoint !== 'undefined' ? parseFloat(data.pHSetpoint) : chem.pHSetpoint) * 100;
const _orp = (typeof data.orpSetpoint !== 'undefined' ? parseInt(data.orpSetpoint, 10) : chem.pHSetpoint);
@@ -2521,14 +2536,16 @@ export class ChemControllerCommands extends BoardCommands {
const _alk = (typeof data.alkalinity !== 'undefined' ? parseInt(data.alkalinity, 10) : chem.alkalinity);
let out = Outbound.create({
dest: chem.address,
+ source: 16, // ic doesn't seem to like msgs coming from 33
action: 146,
payload: [],
- retries: 0,
+ retries: 1,
response: true,
+ protocol: Protocol.IntelliChem,
onComplete: (err, msg) => {
if (err) reject(err);
else {
- chem.pHSetpoint = _ph;
+ chem.pHSetpoint = _ph / 100;
chem.orpSetpoint = _orp;
chem.calciumHardness = _ch;
chem.alkalinity = _alk;
@@ -2550,25 +2567,12 @@ export class ChemControllerCommands extends BoardCommands {
out.setPayloadByte(7, Math.round(_ch % 256));
out.setPayloadByte(9, parseInt(data.cyanuricAcid, 10), chem.cyanuricAcid);
out.setPayloadByte(10, Math.floor(_alk / 256));
- out.setPayloadByte(12, Math.round(_alk % 256));
- out.setPayloadByte(12, 20); // fixed value?
+ out.setPayloadByte(12, Math.round(_alk % 256));
+ // out.setPayloadByte(12, 20); // fixed value?
conn.queueSendMessage(out);
});
}
- if (typeof data.type !== 'undefined' && data.type === 0) {
- // remove
- sys.chemControllers.removeItemById(data.id);
- state.chemControllers.removeItemById(data.id);
- let chem = sys.chemControllers.getItemById(data.id);
- chem.isActive = false;
- sys.emitEquipmentChange();
- return Promise.resolve(chem);
- }
- schem.type = chem.type = parseInt(data.type, 10) || chem.type || 1;
- chem.isActive = data.isActive || true;
- chem.isVirtual = data.isVirtual || true;
- schem.name = chem.name = data.name || chem.name || `Chem Controller ${chem.id}`;
// config data
chem.body = data.body || chem.body || 32;
chem.address = parseInt(data.address, 10) || chem.address || chem.id;
@@ -2744,22 +2748,23 @@ export class ChemControllerCommands extends BoardCommands {
dest: chem.address,
action: 210,
payload: [210],
- retries: 3,
- timeout: 2000,
+ retries: 1,
protocol: Protocol.IntelliChem,
response: true,
onComplete: (err) => {
if (err) {
- logger.warn(`No response from chem controller: src: 16, dest: 144, action: 210, payload: [210] `);
+ // logger.warn(`No response from chem controller: src: 16, dest: 144, action: 210, payload: [210] `);
logger.info(`No chemController found at address ${chem.address}: ${err.message}`);
sys.chemControllers.getItemById(chem.id).isActive = false;
sys.chemControllers.removeItemById(chem.id);
state.chemControllers.removeItemById(chem.id);
+ setTimeout(sys.board.virtualChemControllers.start, 5000);
state.emitEquipmentChanges(); // emit destroyed chlor if we fail
// reject(`No chemController found at address ${chem.address}: ${err.message}`);
}
else {
- logger.info(`Response from chem controller: src: 16, dest: 144, action: 210, payload: [210] `);
+ logger.info(`Found chem controller id:${chem.id}/address:${chem.address} `);
+ // logger.info(`Response from chem controller: src: 16, dest: 144, action: 210, payload: [210] `);
// resolve(state.chemControllers.getItemById(chem.id, true));
state.chemControllers.getItemById(chem.id, true);
}
@@ -2769,13 +2774,43 @@ export class ChemControllerCommands extends BoardCommands {
//};
}
- public async stopAsync(chem: ChemController) {
- // stop commands
- return Promise.resolve(chem);
- }
- public async runAsync(chem: ChemController) {
+ /* public stop(chem: ChemController) {
+ // stop commands
+ state.chemControllers.getItemById(chem.id).virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('stopped')
+ return Promise.resolve(chem);
+ } */
+ public run(chem: ChemController) {
// run commands
- return Promise.resolve(chem);
+ let schem = state.chemControllers.getItemById(chem.id);
+ if (chem.isActive && chem.isVirtual && chem.type === sys.board.valueMaps.chemControllerTypes.getValue('intellichem')) {
+ if (schem.virtualControllerStatus === sys.board.valueMaps.virtualControllerStatus.getValue('stopped')) {
+ return;
+ }
+ else {
+ let out = Outbound.create({
+ source: 16,
+ dest: chem.address,
+ action: 210,
+ payload: [210],
+ retries: 1,
+ protocol: Protocol.IntelliChem,
+ response: true,
+ onComplete: (err) => {
+ if (err) {
+ logger.warn(`No response from IntelliChem id:${chem.id}/address:${chem.address}`);
+ if (schem.lastComm + (30 * 1000) < new Date().getTime()) {
+ // We have not talked to the chem controller in 30 seconds so we have lost communication.
+ schem.status = schem.alarms.comms = 1;
+ }
+ }
+ setTimeout(sys.board.chemControllers.run, 5000, chem);
+ }
+ });
+ conn.queueSendMessage(out);
+
+ }
+ }
+
}
}
@@ -2827,7 +2862,6 @@ export class VirtualChlorinatorController extends BoardCommands {
logger.warn(`No Chlorinator Found`);
sys.chlorinators.removeItemById(1);
state.chlorinators.removeItemById(1);
- logger.info('no chlor');
}
}
}
@@ -2913,7 +2947,7 @@ export class VirtualPumpController extends BoardCommands {
for (let i = 1; i <= sys.pumps.length; i++) {
let pump = sys.pumps.getItemById(i);
let spump = state.pumps.getItemById(i);
- if (pump.isVirtual) {
+ if (pump.isVirtual && pump.type !== 0) {
bAnyVirtual = true;
logger.info(`Queueing pump ${i} to return to manual control.`);
spump.targetSpeed = 0;
@@ -2937,17 +2971,16 @@ export class VirtualPumpController extends BoardCommands {
state.pumps.getItemById(i).virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('running');
setTimeout(sys.board.pumps.run, 100, pump);
}
- else {
- if (spump.virtualControllerStatus === sys.board.valueMaps.virtualControllerStatus.getValue('running')) {
- spump.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('stopped');
- sys.board.pumps.stopPumpRemoteContol(pump);
- }
- }
+ // else {
+ // if (spump.virtualControllerStatus === sys.board.valueMaps.virtualControllerStatus.getValue('running')) {
+ // spump.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('stopped');
+ // sys.board.pumps.stopPumpRemoteContol(pump);
+ // }
+ // }
}
}
}
export class VirtualChemController extends BoardCommands {
- private _timers: NodeJS.Timeout[] = [];
public async search() {
// TODO: If we are searching for multiple chem controllers this should be a promise.all array
// except even one resolve() could be a success for all. Or we could just return a generic "searching"
@@ -2964,37 +2997,39 @@ export class VirtualChemController extends BoardCommands {
return Promise.resolve('Searching for chem controllers...')
}
// is stopping virtual chem controllers necessary?
- public async stopAsync() {
- let promises = [];
+ public stop() {
// turn off all chem controllers
- for (let i = 1; i <= sys.chemControllers.length; i++) {
- let chem = sys.chemControllers.getItemById(i);
- let schem = state.chemControllers.getItemById(i);
+ let bAnyVirtual = false;
+ for (let i = 0; i < sys.chemControllers.length; i++) {
+ let chem = sys.chemControllers.getItemByIndex(i);
+ let schem = state.chemControllers.getItemById(chem.id);
if (chem.isVirtual && chem.isActive) {
- logger.info(`Queueing chemController ${i} to stop.`);
- promises.push(sys.board.chemControllers.stopAsync(chem));
- typeof this._timers[i] !== 'undefined' && clearTimeout(this._timers[i]);
- state.chemControllers.getItemById(i, true).virtualControllerStatus = 0;
+ logger.info(`Stopping chemController ${i} to stop.`);
+ // sys.board.chemControllers.stop(chem);
+ schem.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('stopped');
+ bAnyVirtual = true;
}
}
- return Promise.all(promises);
}
public start() {
- for (let i = 1; i <= sys.pumps.length; i++) {
- let chem = sys.chemControllers.getItemById(i);
- if (chem.isVirtual && chem.isActive) {
- typeof this._timers[i] !== 'undefined' && clearInterval(this._timers[i]);
- setImmediate(function () { sys.board.chemControllers.runAsync(chem); });
- // TODO: refactor into a wrapper like pumps
- // this won't work with async inside setTimeout/setInterval
- this._timers[i] = setInterval(async function () { await sys.board.chemControllers.runAsync(chem); }, 8000);
- if (state.chemControllers.getItemById(i).virtualControllerStatus !== 1) {
- logger.info(`Starting Virtual Pump Controller: Pump ${chem.id}`);
- state.chemControllers.getItemById(i).virtualControllerStatus = 1;
+ for (let i = 0; i < sys.chemControllers.length; i++) {
+ let chem = sys.chemControllers.getItemByIndex(i);
+ let schem = state.chemControllers.getItemById(chem.id);
+ if (chem.isVirtual && chem.isActive && chem.type === sys.board.valueMaps.chemControllerTypes.getValue('intellichem')) {
+ if (state.chemControllers.getItemById(chem.id).virtualControllerStatus !== sys.board.valueMaps.virtualControllerStatus.getValue('running')) {
+ logger.info(`Starting Virtual Chem Controller: Chem id:${chem.id}/address:${chem.address}`);
+ schem.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('running');
+ setTimeout(sys.board.chemControllers.run, 100, chem);
}
-
+ // else {
+ // if (schem.virtualControllerStatus === sys.board.valueMaps.virtualControllerStatus.getValue('running')) {
+ // schem.virtualControllerStatus = sys.board.valueMaps.virtualControllerStatus.getValue('stopped');
+ // sys.board.chemControllers.stop(chem);
+ // }
+ // }
}
+ else schem.virtualControllerStatus = -1;
}
}
-}
+}
\ No newline at end of file
diff --git a/controller/comms/Comms.ts b/controller/comms/Comms.ts
index 4b59f4f4..0b3591c4 100755
--- a/controller/comms/Comms.ts
+++ b/controller/comms/Comms.ts
@@ -1,19 +1,19 @@
-/* nodejs-poolController. An application to control pool equipment.
-Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as
-published by the Free Software Foundation, either version 3 of the
-License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see .
-*/
+/* nodejs-poolController. An application to control pool equipment.
+Copyright (C) 2016, 2017. Russell Goldin, tagyoureit. russ.goldin@gmail.com
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+*/
import { EventEmitter } from 'events';
import * as SerialPort from 'serialport';
import * as MockBinding from '@serialport/binding-mock';
@@ -243,7 +243,10 @@ export class SendRecieveBuffer {
conn.buffer.writeMessage(msg);
}
}
- if (conn.buffer._outBuffer.length > 0 || typeof conn.buffer._waitingPacket !== 'undefined' || conn.buffer._waitingPacket) {
+ // RG: added the last `|| typeof msg !== 'undef'` because virtual chem controller only sends a single packet
+ // but this condition would be eval'd before the callback of conn.write was calles and the outbound packet
+ // would be sitting idle for eternity.
+ if (conn.buffer._outBuffer.length > 0 || typeof conn.buffer._waitingPacket !== 'undefined' || conn.buffer._waitingPacket || typeof msg !== 'undefined') {
// Come back later as we still have items to send.
conn.buffer.procTimer = setTimeout(() => this.processPackets(), 100);
}
diff --git a/controller/comms/messages/Messages.ts b/controller/comms/messages/Messages.ts
index 448d372e..18de452f 100755
--- a/controller/comms/messages/Messages.ts
+++ b/controller/comms/messages/Messages.ts
@@ -194,7 +194,9 @@ export class Message {
if (msgIn.protocol !== msgOut.protocol) { return false; }
if (typeof msgIn === 'undefined' || msgIn.protocol !== msgOut.protocol) { return; } // getting here on msg send failure
switch (msgIn.action) {
-
+ case 1: // ack
+ if (msgIn.source === msgOut.dest && msgIn.payload[0] === msgOut.action) return true;
+ break;
default:
// in: 18; out 210 fits msgout & 0x63 pattern
if (msgIn.action === (msgOut.action & 63) && msgIn.source === msgOut.dest) return true;
diff --git a/defaultConfig.json b/defaultConfig.json
index b95d868c..5a611707 100755
--- a/defaultConfig.json
+++ b/defaultConfig.json
@@ -50,7 +50,7 @@
"services": {},
"interfaces": {
"smartThings": {
- "name": "SmartThings/Hubitat",
+ "name": "SmartThings",
"enabled": false,
"fileName": "smartThings-Hubitat.json",
"globals": {},
@@ -59,6 +59,16 @@
"port": 39500
}
},
+ "hubitat": {
+ "name": "Hubitat",
+ "enabled": false,
+ "fileName": "smartThings-Hubitat.json",
+ "globals": {},
+ "options": {
+ "host": "0.0.0.0",
+ "port": 39501
+ }
+ },
"vera": {
"name": "Vera",
"enabled": false,