mirror of
https://github.com/koush/scrypted.git
synced 2026-09-19 18:20:40 +01:00
4
plugins/ecobee/ .npmignore
Normal file
4
plugins/ecobee/ .npmignore
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/*.map
|
||||
4
plugins/ecobee/.gitignore
vendored
Normal file
4
plugins/ecobee/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
out/
|
||||
node_modules/
|
||||
dist/
|
||||
22
plugins/ecobee/.vscode/launch.json
vendored
Normal file
22
plugins/ecobee/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Scrypted Debugger",
|
||||
"address": "${config:scrypted.debugHost}",
|
||||
"port": 10081,
|
||||
"request": "attach",
|
||||
"skipFiles": [
|
||||
"<node_internals>/**"
|
||||
],
|
||||
"preLaunchTask": "scrypted: deploy+debug",
|
||||
"sourceMaps": true,
|
||||
"localRoot": "${workspaceFolder}/out",
|
||||
"remoteRoot": "/plugin/",
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
plugins/ecobee/.vscode/settings.json
vendored
Normal file
4
plugins/ecobee/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
{
|
||||
"scrypted.debugHost": "127.0.0.1",
|
||||
}
|
||||
20
plugins/ecobee/.vscode/tasks.json
vendored
Normal file
20
plugins/ecobee/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "scrypted: deploy+debug",
|
||||
"type": "shell",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "silent",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": true,
|
||||
"clear": false
|
||||
},
|
||||
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
|
||||
},
|
||||
]
|
||||
}
|
||||
15
plugins/ecobee/README.md
Normal file
15
plugins/ecobee/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# An Ecobee thermostat plugin
|
||||
|
||||
## npm commands
|
||||
* npm run scrypted-webpack
|
||||
* npm run scrypted-deploy <ipaddress>
|
||||
* npm run scrypted-debug <ipaddress>
|
||||
|
||||
## scrypted distribution via npm
|
||||
1. Ensure package.json is set up properly for publishing on npm.
|
||||
2. npm publish
|
||||
|
||||
## Visual Studio Code configuration
|
||||
|
||||
* If using a remote server, edit [.vscode/settings.json](blob/master/.vscode/settings.json) to specify the IP Address of the Scrypted server.
|
||||
* Launch Scrypted Debugger from the launch menu.
|
||||
263
plugins/ecobee/package-lock.json
generated
Normal file
263
plugins/ecobee/package-lock.json
generated
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"name": "@scrypted/ecobee",
|
||||
"version": "0.0.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@scrypted/ecobee",
|
||||
"version": "0.0.2",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"terser": "^3.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.7.1"
|
||||
}
|
||||
},
|
||||
"../../sdk": {
|
||||
"version": "0.0.93",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
},
|
||||
"bin": {
|
||||
"scrypted-debug": "bin/scrypted-debug.js",
|
||||
"scrypted-deploy": "bin/scrypted-deploy.js",
|
||||
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
|
||||
"scrypted-package-json": "bin/scrypted-package-json.js",
|
||||
"scrypted-readme": "bin/scrypted-readme.js",
|
||||
"scrypted-webpack": "bin/scrypted-webpack.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@scrypted/sdk": {
|
||||
"resolved": "../../sdk",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "16.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
|
||||
"integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"deprecated": "Critical security vulnerability fixed in v0.21.1. For more information, see https://github.com/axios/axios/pull/3410",
|
||||
"dependencies": {
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"dependencies": {
|
||||
"debug": "=3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz",
|
||||
"integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz",
|
||||
"integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==",
|
||||
"dependencies": {
|
||||
"commander": "^2.19.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.10"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/uglifyjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@scrypted/sdk": {
|
||||
"version": "file:../../sdk",
|
||||
"requires": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.14.5",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.14.5",
|
||||
"@babel/plugin-proposal-optional-chaining": "^7.14.5",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.15.4",
|
||||
"@babel/plugin-transform-typescript": "^7.15.8",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/node": "^16.11.1",
|
||||
"adm-zip": "^0.4.13",
|
||||
"axios": "^0.21.4",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-const-enum": "^1.1.0",
|
||||
"esbuild": "^0.13.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-loader": "^9.2.6",
|
||||
"typescript-json-schema": "^0.50.1",
|
||||
"webpack": "^5.59.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "16.11.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz",
|
||||
"integrity": "sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w==",
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.1.tgz",
|
||||
"integrity": "sha512-0BfJq4NSfQXd+SkFdrvFbG7addhYSBA2mQwISr46pD6E5iqkWg02RAs8vyTT/j0RTnoYmeXauBuSv1qKwR179g==",
|
||||
"requires": {
|
||||
"follow-redirects": "1.5.10",
|
||||
"is-buffer": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||
"requires": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||
"requires": {
|
||||
"debug": "=3.1.0"
|
||||
}
|
||||
},
|
||||
"is-buffer": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.20",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.20.tgz",
|
||||
"integrity": "sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw==",
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "3.17.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz",
|
||||
"integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==",
|
||||
"requires": {
|
||||
"commander": "^2.19.0",
|
||||
"source-map": "~0.6.1",
|
||||
"source-map-support": "~0.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
plugins/ecobee/package.json
Normal file
42
plugins/ecobee/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@scrypted/ecobee",
|
||||
"version": "0.0.6",
|
||||
"description": "Ecobee API plugin",
|
||||
"author": "Scrypted",
|
||||
"license": "Apache",
|
||||
"scripts": {
|
||||
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
|
||||
"prescrypted-vscode-launch": "scrypted-webpack",
|
||||
"scrypted-vscode-launch": "scrypted-deploy-debug",
|
||||
"scrypted-deploy-debug": "scrypted-deploy-debug",
|
||||
"scrypted-debug": "scrypted-debug",
|
||||
"scrypted-deploy": "scrypted-deploy",
|
||||
"scrypted-readme": "scrypted-readme",
|
||||
"scrypted-package-json": "scrypted-package-json",
|
||||
"scrypted-webpack": "scrypted-webpack"
|
||||
},
|
||||
"keywords": [
|
||||
"ecobee",
|
||||
"bryant",
|
||||
"scrypted",
|
||||
"plugin"
|
||||
],
|
||||
"scrypted": {
|
||||
"name": "Ecobee Controller",
|
||||
"singleInstance": true,
|
||||
"type": "DeviceProvider",
|
||||
"interfaces": [
|
||||
"DeviceProvider",
|
||||
"Settings"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@scrypted/sdk": "file:../../sdk",
|
||||
"@types/node": "^16.7.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"terser": "^3.14.1"
|
||||
}
|
||||
}
|
||||
|
||||
638
plugins/ecobee/src/main.ts
Normal file
638
plugins/ecobee/src/main.ts
Normal file
@@ -0,0 +1,638 @@
|
||||
import axios, { AxiosRequestConfig } from 'axios'
|
||||
import sdk, { Device, DeviceInformation, ScryptedDeviceBase, OnOff, DeviceProvider, ScryptedDeviceType, ThermostatMode, Thermometer, HumiditySensor, TemperatureSetting, Settings, Setting, ScryptedInterface, Refresh, TemperatureUnit, HumidityCommand, HumidityMode, HumiditySetting } from '@scrypted/sdk';
|
||||
const { deviceManager, log } = sdk;
|
||||
|
||||
const API_RETRY = 2;
|
||||
|
||||
// Convert Fahrenheit to Celsius, round to 2 decimal places
|
||||
function convertFtoC(f: number) {
|
||||
let c = (5/9) * (f - 32)
|
||||
return Math.round(c*100)/100
|
||||
}
|
||||
|
||||
// Convert Celsius to Fahrenheit, round to 1 decimal place
|
||||
function convertCtoF(c: number) {
|
||||
let f = (c * 1.8) + 32
|
||||
return Math.round(f*10)/10
|
||||
}
|
||||
|
||||
function ecobeeToThermostatMode(mode: string) {
|
||||
// Values: auto, auxHeatOnly, cool, heat, off
|
||||
switch(mode) {
|
||||
case "cool":
|
||||
return ThermostatMode.Cool;
|
||||
case "heat":
|
||||
return ThermostatMode.Heat;
|
||||
case "auto":
|
||||
return ThermostatMode.Auto;
|
||||
case "off":
|
||||
return ThermostatMode.Off;
|
||||
}
|
||||
}
|
||||
|
||||
function thermostatModeToEcobee(mode: ThermostatMode) {
|
||||
// Values: auto, auxHeatOnly, cool, heat, off
|
||||
switch(mode) {
|
||||
case ThermostatMode.Cool:
|
||||
return "cool";
|
||||
case ThermostatMode.Heat:
|
||||
return "heat";
|
||||
case ThermostatMode.Auto:
|
||||
return "auto";
|
||||
case ThermostatMode.Off:
|
||||
return "off";
|
||||
}
|
||||
}
|
||||
|
||||
function humModeFromEcobee(mode: string): HumidityMode {
|
||||
// Values: auto, manual, off
|
||||
switch(mode) {
|
||||
case 'auto':
|
||||
return HumidityMode.Auto;
|
||||
case "manual":
|
||||
return HumidityMode.Humidify;
|
||||
}
|
||||
|
||||
return HumidityMode.Off
|
||||
}
|
||||
|
||||
class EcobeeThermostat extends ScryptedDeviceBase implements HumiditySensor, Thermometer, TemperatureSetting, Refresh, OnOff, HumiditySetting, Settings {
|
||||
device: any;
|
||||
revisionList: string[];
|
||||
provider: EcobeeController;
|
||||
on: boolean;
|
||||
|
||||
constructor(nativeId: string, provider: EcobeeController, info: DeviceInformation) {
|
||||
super(nativeId);
|
||||
this.provider = provider;
|
||||
this.revisionList = null;
|
||||
this.info = info;
|
||||
|
||||
this.temperatureUnit = TemperatureUnit.F
|
||||
const modes: ThermostatMode[] = [ThermostatMode.Cool, ThermostatMode.Heat, ThermostatMode.Auto, ThermostatMode.Off];
|
||||
this.thermostatAvailableModes = modes;
|
||||
|
||||
let humModes: HumidityMode[] = [HumidityMode.Auto, HumidityMode.Humidify, HumidityMode.Off];
|
||||
this.humiditySetting = {
|
||||
mode: HumidityMode.Off,
|
||||
availableModes: humModes,
|
||||
}
|
||||
|
||||
setImmediate(() => this.refresh("constructor", false));
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
title: 'Additional Devices',
|
||||
key: 'additional_devices',
|
||||
value: this.storage.getItem("additional_devices"),
|
||||
choices: ['Fan', 'Humidifier'],
|
||||
description: 'Display additional devices for components',
|
||||
multiple: true,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
}
|
||||
|
||||
/*
|
||||
* Get the recommended refresh/poll frequency in seconds for this device.
|
||||
*/
|
||||
async getRefreshFrequency(): Promise<number> {
|
||||
return 15;
|
||||
}
|
||||
|
||||
/* refresh(): Request from Scrypted to refresh data from device
|
||||
* Poll from API '/thermostatSummary' endpoint for timestamp of last changes and compare to last check
|
||||
* Updates equipmentStatus on each call
|
||||
*/
|
||||
async refresh(refreshInterface: string, userInitiated: boolean): Promise<void> {
|
||||
this.console.log(`${refreshInterface} requested refresh\n ${new Date()}`)
|
||||
|
||||
const json = {
|
||||
selection: {
|
||||
selectionType: "registered",
|
||||
selectionMatch: this.nativeId,
|
||||
includeEquipmentStatus: true,
|
||||
}
|
||||
}
|
||||
const data = await this.provider.req('get', 'thermostatSummary', json)
|
||||
|
||||
// Update equipmentStatus, trigger reload if changes detected
|
||||
this._updateEquipmentStatus(data.statusList[0].split(":")[1]);
|
||||
if (this._updateRevisionList(data.revisionList[0]))
|
||||
await this.reload()
|
||||
}
|
||||
|
||||
/*
|
||||
* Set characteristics based on equipmentStatus from API
|
||||
*
|
||||
* Possible eqipmentStatus values:
|
||||
* heatPump, heatPump[2-3], compCool[1-2], auxHeat[1-3],
|
||||
* fan, humidifier, dehumidifier, ventilator, economizer,
|
||||
* compHotWater, auxHotWater
|
||||
*/
|
||||
_updateEquipmentStatus(equipmentStatus: string): void {
|
||||
equipmentStatus = equipmentStatus.toLowerCase()
|
||||
this.console.log(` Current status: ${equipmentStatus}`);
|
||||
if (equipmentStatus.includes("heat"))
|
||||
// values: heatPump, heatPump[2-3], auxHeat[1-3]
|
||||
this.thermostatActiveMode = ThermostatMode.Heat;
|
||||
else if (equipmentStatus.includes("cool"))
|
||||
// values: compCool[1-2]
|
||||
this.thermostatActiveMode = ThermostatMode.Cool;
|
||||
else
|
||||
this.thermostatActiveMode = ThermostatMode.Off;
|
||||
|
||||
// fan status
|
||||
if (equipmentStatus.includes('fan')) {
|
||||
this.on = true;
|
||||
} else {
|
||||
this.on = false;
|
||||
}
|
||||
|
||||
// humidifier status
|
||||
let activeMode = HumidityMode.Off
|
||||
if (equipmentStatus.includes('humidifier')) {
|
||||
activeMode = HumidityMode.Humidify
|
||||
}
|
||||
this.humiditySetting = Object.assign(this.humiditySetting, { activeMode });
|
||||
}
|
||||
|
||||
/* revisionListChanged(): Compare a new revision list to the stored list, return true if changed
|
||||
*
|
||||
*/
|
||||
_updateRevisionList(listStr: string): boolean {
|
||||
const listItems = ["tId", "tName", "connected", "thermostat", "alerts", "runtime", "interval"];
|
||||
const oldList = this.revisionList;
|
||||
this.revisionList = listStr.split(':');
|
||||
|
||||
if (!oldList)
|
||||
return true;
|
||||
|
||||
// Compare each element, skip first 3
|
||||
for (let i = 3; i < listItems.length; i++) {
|
||||
if (this.revisionList[i] !== oldList[i]) {
|
||||
this.console.log(` Changes detected: ${listItems[i]}`)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
this.console.log(" Changes detected: none");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* reload(): Reload all thermostat data from API '/thermostat' endpoint
|
||||
*
|
||||
*/
|
||||
async reload(): Promise<void> {
|
||||
const json = {
|
||||
selection: {
|
||||
selectionType: "registered",
|
||||
selectionMatch: this.nativeId,
|
||||
includeSettings: true,
|
||||
includeRuntime: true,
|
||||
includeEquipmentStatus: true,
|
||||
}
|
||||
}
|
||||
const data = (await this.provider.req('get', 'thermostat', json)).thermostatList[0];
|
||||
|
||||
// Set runtime values
|
||||
this.temperature = convertFtoC(Number(data.runtime.actualTemperature)/10)
|
||||
this.humidity = Number(data.runtime.actualHumidity);
|
||||
|
||||
// Set current equipment status values
|
||||
this._updateEquipmentStatus(data.equipmentStatus);
|
||||
|
||||
// update based on mode
|
||||
this.thermostatMode = ecobeeToThermostatMode(data.settings.hvacMode);
|
||||
|
||||
this.thermostatSetpointHigh = convertFtoC(Number(data.runtime.desiredCool)/10)
|
||||
this.thermostatSetpointLow = convertFtoC(Number(data.runtime.desiredHeat)/10)
|
||||
switch(data.settings.hvacMode) {
|
||||
case 'cool':
|
||||
this.thermostatSetpoint = convertFtoC(Number(data.runtime.desiredCool)/10)
|
||||
break;
|
||||
case 'heat':
|
||||
this.thermostatSetpoint = convertFtoC(Number(data.runtime.desiredHeat)/10)
|
||||
break;
|
||||
}
|
||||
|
||||
// update humidifier based on mode
|
||||
this.humiditySetting = Object.assign(this.humiditySetting, {
|
||||
mode: humModeFromEcobee(data.settings.humidifierMode),
|
||||
humidifierSetpoint: Number(data.settings.humidity),
|
||||
});
|
||||
}
|
||||
|
||||
async setHumidity(humidity: HumidityCommand): Promise<void> {
|
||||
this.console.log(`setHumidity ${humidity.mode} ${humidity.humidifierSetpoint}: not yet supported`);
|
||||
}
|
||||
|
||||
async setThermostatMode(mode: ThermostatMode): Promise<void> {
|
||||
this.console.log(`setThermostatMode ${mode}`)
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType:"registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
thermostat: {
|
||||
settings:{
|
||||
hvacMode: thermostatModeToEcobee(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data);
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("setThermostatMode success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`setThermostatMode failed: ${resp}`)
|
||||
}
|
||||
|
||||
async setThermostatSetpoint(degrees: number): Promise<void> {
|
||||
const degF = convertCtoF(degrees);
|
||||
this.console.log(`setThermostatSetpoint ${degrees}C/${degF}F`)
|
||||
|
||||
if (this.thermostatMode === ThermostatMode.Auto) {
|
||||
this.console.log(`setThermostatSetpoint not running in auto mode`)
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType:"registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
functions: [
|
||||
{
|
||||
type:"setHold",
|
||||
params:{
|
||||
holdType: "nextTransition",
|
||||
heatHoldTemp: degF*10,
|
||||
coolHoldTemp: degF*10,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data)
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("setThermostatSetpoint success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`setThermostatSetpoint failed: ${resp}`)
|
||||
}
|
||||
|
||||
async setThermostatSetpointHigh(high: number): Promise<void> {
|
||||
const degFLow = convertCtoF(this.thermostatSetpointLow);
|
||||
const degFHigh = convertCtoF(high);
|
||||
this.console.log(`setThermostatSetpointHigh ${high}C/${degFHigh}F`)
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType:"registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
functions: [
|
||||
{
|
||||
type:"setHold",
|
||||
params:{
|
||||
holdType: "nextTransition",
|
||||
heatHoldTemp: degFLow*10,
|
||||
coolHoldTemp: degFHigh*10,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data)
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("setThermostatSetpointHigh success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`setThermostatSetpointHigh failed: ${resp}`)
|
||||
}
|
||||
|
||||
async setThermostatSetpointLow(low: number): Promise<void> {
|
||||
const degFLow = convertCtoF(low);
|
||||
const degFHigh = convertCtoF(this.thermostatSetpointHigh);
|
||||
this.console.log(`setThermostatSetpointLow ${low}C/${degFLow}F`)
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType:"registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
functions: [
|
||||
{
|
||||
type:"setHold",
|
||||
params:{
|
||||
holdType: "nextTransition",
|
||||
heatHoldTemp: degFLow*10,
|
||||
coolHoldTemp: degFHigh*10,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data)
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("setThermostatSetpointLow success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`setThermostatSetpointLow failed: ${resp}`)
|
||||
}
|
||||
|
||||
async turnOff(): Promise<void> {
|
||||
this.console.log(`fanOff: setting fan to auto`)
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType: "registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
functions: [
|
||||
{
|
||||
type: "setHold",
|
||||
params: {
|
||||
coolHoldTemp: 900,
|
||||
heatHoldTemp: 550,
|
||||
holdType: "nextTransition",
|
||||
fan: "auto",
|
||||
isTemperatureAbsolute: "false",
|
||||
isTemperatureRelative: "false",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data);
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("fanOff success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`fanOff failed: ${resp}`)
|
||||
}
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
this.console.log(`fanOn: setting fan to on`)
|
||||
|
||||
const data = {
|
||||
selection: {
|
||||
selectionType: "registered",
|
||||
selectionMatch: this.nativeId,
|
||||
},
|
||||
functions: [
|
||||
{
|
||||
type:"setHold",
|
||||
params: {
|
||||
coolHoldTemp: 900,
|
||||
heatHoldTemp: 550,
|
||||
holdType: "nextTransition",
|
||||
fan: "on",
|
||||
isTemperatureAbsolute: "false",
|
||||
isTemperatureRelative: "false",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const resp = await this.provider.req('post', 'thermostat', undefined, data);
|
||||
if (resp.status.code == 0) {
|
||||
this.console.log("fanOn success")
|
||||
await this.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
this.console.log(`fanOn failed: ${resp}`)
|
||||
}
|
||||
}
|
||||
|
||||
class EcobeeController extends ScryptedDeviceBase implements DeviceProvider, Settings {
|
||||
devices = new Map<string, any>();
|
||||
access_token: string;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.log.clearAlerts();
|
||||
if (!this.storage.getItem("api_base"))
|
||||
this.storage.setItem("api_base", "api.ecobee.com");
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
// If no clientId, request clientId to start authentication process
|
||||
if (!this.storage.getItem("client_id")) {
|
||||
this.log.a("You must specify a client ID.")
|
||||
this.console.log("Enter a client ID for this app from the Ecobee developer portal. Then, collect the PIN and enter in Ecobee 'My Apps'. Restart this app to complete.")
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.storage.getItem("refresh_token"))
|
||||
// If no refresh_token, try to get token
|
||||
await this.getToken();
|
||||
else if (!this.access_token)
|
||||
await this.refreshToken();
|
||||
|
||||
this.discoverDevices();
|
||||
}
|
||||
|
||||
async getSettings(): Promise<Setting[]> {
|
||||
return [
|
||||
{
|
||||
group: "API",
|
||||
title: "API Base URL",
|
||||
key: "api_base",
|
||||
description: "Customize the API base URL",
|
||||
value: this.storage.getItem("api_base"),
|
||||
},
|
||||
{
|
||||
group: "API",
|
||||
title: "API Client ID",
|
||||
key: "client_id",
|
||||
description: "Your Client ID from the Ecboee developer portal",
|
||||
value: this.storage.getItem("client_id"),
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async putSetting(key: string, value: string): Promise<void> {
|
||||
this.storage.setItem(key, value.toString());
|
||||
|
||||
// Try to get a code when a client ID is saved
|
||||
if (key === "client_id") {
|
||||
await this.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
// Get a code from Ecobee API for user verification
|
||||
async getCode() {
|
||||
// GET https://api.ecobee.com/authorize?response_type=ecobeePin&client_id=APP_KEY&scope=SCOPE
|
||||
const authUrl = `https://${this.storage.getItem("api_base")}/authorize`
|
||||
const authParams = {
|
||||
response_type:'ecobeePin',
|
||||
scope: "smartWrite",
|
||||
client_id: this.storage.getItem("client_id"),
|
||||
}
|
||||
let authData = (await axios.get(authUrl, {
|
||||
params: authParams,
|
||||
})).data
|
||||
|
||||
this.log.clearAlerts();
|
||||
this.log.a(`Got code ${authData.ecobeePin}. Enter this in 'My Apps' Ecobee portal. Then restart this app.`)
|
||||
this.storage.setItem("ecobee_code", authData.code);
|
||||
}
|
||||
|
||||
// Trade the validated code for an access token
|
||||
async getToken() {
|
||||
// POST https://api.ecobee.com/token?grant_type=ecobeePin&code=AUTHORIZATION_TOKEN&client_id=APP_KEY&ecobee_type=jwt
|
||||
const tokenUrl = `https://${this.storage.getItem("api_base")}/token`
|
||||
const tokenParams = {
|
||||
grant_type:'ecobeePin',
|
||||
code: this.storage.getItem("ecobee_code"),
|
||||
client_id: this.storage.getItem("client_id"),
|
||||
ecobee_type: "jwt",
|
||||
};
|
||||
let tokenData = (await axios.post(tokenUrl, null, {
|
||||
params: tokenParams
|
||||
})).data;
|
||||
this.access_token = tokenData.access_token;
|
||||
this.storage.setItem("refresh_token", tokenData.refresh_token);
|
||||
console.log(`Stored access/refresh token`)
|
||||
}
|
||||
|
||||
// Refresh the tokens
|
||||
async refreshToken() {
|
||||
// POST https://api.ecobee.com/token?grant_type=refresh_token&refresh_token=REFRESH_TOKEN&client_id=APP_KEY&ecobee_type=jwt
|
||||
const tokenUrl = `https://${this.storage.getItem("api_base")}/token`
|
||||
const tokenParams = {
|
||||
grant_type:'refresh_token',
|
||||
refresh_token: this.storage.getItem("refresh_token"),
|
||||
client_id: this.storage.getItem("client_id"),
|
||||
ecobee_type: "jwt",
|
||||
};
|
||||
let tokenData = (await axios.post(tokenUrl, null, {
|
||||
params: tokenParams
|
||||
})).data;
|
||||
this.access_token = tokenData.access_token;
|
||||
this.storage.setItem("refresh_token", tokenData.refresh_token);
|
||||
console.log(`Refreshed access/refresh token`)
|
||||
}
|
||||
|
||||
// Generic API request
|
||||
async req(
|
||||
method: string,
|
||||
endpoint: string,
|
||||
json?: any,
|
||||
data?: any,
|
||||
attempt?: number,
|
||||
): Promise<any> {
|
||||
if (attempt > API_RETRY) {
|
||||
throw new Error(` request to ${method}:${endpoint} failed after ${attempt} retries`);
|
||||
}
|
||||
|
||||
// Configure API request
|
||||
const config: AxiosRequestConfig = {
|
||||
method,
|
||||
baseURL: `https://${this.storage.getItem("api_base")}/api/1/`,
|
||||
url: endpoint,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.access_token}`,
|
||||
},
|
||||
data,
|
||||
timeout: 10000,
|
||||
}
|
||||
if (json)
|
||||
config.params = { json };
|
||||
|
||||
// Make API request, recursively retry after token refresh
|
||||
try {
|
||||
return (await axios.request(config)).data;
|
||||
} catch (e) {
|
||||
this.console.log(`req failed ${e}`)
|
||||
// refresh token and retry request
|
||||
await this.refreshToken();
|
||||
return await this.req(method, endpoint, json, data, attempt++);
|
||||
}
|
||||
}
|
||||
|
||||
async discoverDevices(): Promise<void> {
|
||||
// Get a list of all accessible devices
|
||||
const json = {
|
||||
selection: {
|
||||
selectionType: "registered",
|
||||
selectionMatch: "",
|
||||
includeSettings: true,
|
||||
}
|
||||
}
|
||||
const apiDevices = (await this.req('get', 'thermostat', json)).thermostatList;
|
||||
this.console.log(`Discovered ${apiDevices.length} devices.`);
|
||||
|
||||
// Create a list of devices found from the API
|
||||
const devices: Device[] = [];
|
||||
for (let apiDevice of apiDevices) {
|
||||
this.console.log(` Discovered ${apiDevice.brand} ${apiDevice.modelNumber} ${apiDevice.name} (${apiDevice.identifier})`);
|
||||
|
||||
const interfaces: ScryptedInterface[] = [
|
||||
ScryptedInterface.Thermometer,
|
||||
ScryptedInterface.TemperatureSetting,
|
||||
ScryptedInterface.Refresh,
|
||||
ScryptedInterface.HumiditySensor,
|
||||
ScryptedInterface.OnOff,
|
||||
ScryptedInterface.Settings,
|
||||
]
|
||||
if (apiDevice.settings.hasHumidifier)
|
||||
interfaces.push(ScryptedInterface.HumiditySetting);
|
||||
|
||||
const device: Device = {
|
||||
nativeId: apiDevice.identifier,
|
||||
name: `${apiDevice.modelNumber} thermostat`,
|
||||
type: ScryptedDeviceType.Thermostat,
|
||||
info: {
|
||||
model: apiDevice.brand,
|
||||
manufacturer: apiDevice.modelNumber,
|
||||
serialNumber: apiDevice.identifier,
|
||||
},
|
||||
interfaces,
|
||||
}
|
||||
devices.push(device);
|
||||
}
|
||||
|
||||
// Sync full device list
|
||||
await deviceManager.onDevicesChanged({
|
||||
devices,
|
||||
});
|
||||
|
||||
for (let device of devices) {
|
||||
let providerDevice = this.devices.get(device.nativeId);
|
||||
if (!providerDevice) {
|
||||
providerDevice = new EcobeeThermostat(device.nativeId, this, device.info)
|
||||
this.devices.set(device.nativeId, providerDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDevice(nativeId: string) {
|
||||
return this.devices.get(nativeId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default new EcobeeController();
|
||||
Reference in New Issue
Block a user