Showing preview only (400K chars total). Download the full file or copy to clipboard to get everything.
Repository: tonesto7/homebridge-smartthings
Branch: master
Commit: 157eb9f92e2f
Files: 34
Total size: 384.5 KB
Directory structure:
gitextract_mihrh1sf/
├── .esformatter
├── .eslintrc.json
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── stale.yml
│ └── workflows/
│ └── nodejs.yml
├── .gitignore
├── .snyk
├── CHANGELOG-app.md
├── CHANGELOG.md
├── README.md
├── appData.json
├── config.schema.json
├── installerManifest.json
├── jsconfig.json
├── package.json
├── platform.schema.json
├── prettierrc
├── smartapps/
│ └── tonesto7/
│ └── homebridge-v2.src/
│ └── homebridge-v2.groovy
└── src/
├── ST_Accessories.js
├── ST_Client.js
├── ST_DeviceCharacteristics.js
├── ST_Platform.js
├── ST_ServiceTypes.js
├── ST_Transforms.js
├── index.js
└── libs/
├── CommunityTypes.js
├── Constants.js
├── HomeKitTypes-Bridge.js
├── HomeKitTypes.js
├── Logger.js
└── MyUtils.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .esformatter
================================================
{
"plugins": [
"esformatter-quotes",
"esformatter-braces",
"esformatter-semicolons"
],
"quotes": {
"type": "single",
"avoidEscape": false
},
"whiteSpace": {
"before": {
"ParameterList": -1,
"ParameterComma": -1,
"FunctionDeclarationOpeningBrace": -1,
"FunctionDeclarationClosingBrace": -1,
"ForStatementExpressionOpening": -1
},
"after": {
"FunctionName": -1,
"ParameterComma": 1,
"FunctionReservedWord": -1,
"ParameterList": -1,
"FunctionDeclarationOpeningBrace": -1,
"PropertyName": -1
}
},
"lineBreak": {
"before": {
"EndOfFile": 1
}
}
}
================================================
FILE: .eslintrc.json
================================================
{
"root": true,
"env": {
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 9,
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"semi": "error",
"eqeqeq": "error",
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn"
},
"extends": [
"eslint:recommended",
"prettier"
]
}
================================================
FILE: .github/FUNDING.yml
================================================
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVFJTG8H86SK8&source=url
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug Report
about: Create a report to help us improve
title: '(BUG) {ENTER TITLE HERE} '
labels: 'bug'
assignees: 'tonesto7'
---
## Verify the following before opening an trouble issue
**Go over all the following points, and put an `x` in all the boxes that apply.
If you're unsure about any of these, don't hesitate to ask. We're here to help!**
- [ ] That _OAuth_ is Enabled for the SmartApp under the IDE.
- [ ] The SmartApp and Device Handler are using the latest code available.
- [ ] That Both the SmartApps and Device Handlers have been _Published for You_ in the IDE.
---
## About Your Setup
- How many devices are detected?:
- Mobile App Version(Not required):
- SmartApp Version:
- Device Handler Version:
- Homebridge Version:
- NodeJS Version:
## Expected Behavior
**Tell us what you think should be happening**
## Current Behavior
**What happens instead of the expected behavior?**
## Steps to Reproduce (for bugs)
**Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant**
1.
2.
3.
4.
## Context
**How has this issue affected you? What are you trying to accomplish?
**Providing context helps us come up with a solution that is most useful in the real world**
---
Please include a copy of any relevant log output to assist in tracking down the bug
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: Suggest an idea for this project
title: '[Feature Request]'
labels: 'enhancement'
assignees: 'tonesto7'
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--- Provide a general summary of your changes in the Title above -->
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Description of Changes
<!--- Describe your changes in detail -->
## Reason for Change
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
## How Has This Been Tested?
<!--- Please describe how you tested your changes. -->
## Screenshots (if appropriate):
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: true
================================================
FILE: .github/workflows/nodejs.yml
================================================
name: Node-CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
node-version: [10.x, 12.x, 13.x]
os: [ubuntu-latest, macOS-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v1
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- name: npm install, build and test
run: |
npm ci
npm run build --if-present
npm test
env:
CI: true
publish-npm:
# publish should only ran on 'push' event and if a version tag was created
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
needs: build # only run if build succeeds
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: 10 # use the minimum required version
registry-url: https://registry.npmjs.org/
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
================================================
FILE: .gitignore
================================================
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
tempCodeRunnerFile.js
AWS_Files/Testing/input.json
.vscode/settings.json
website/
config.json
accessories
auth.json
persist
.uix-secrets
auth.json
.uix-secrets
smartthings_rsa.pub
logaudit.json
homebridge-smartthings-v2-logaudit.json
accessories/cachedAccessories
================================================
FILE: .snyk
================================================
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.1
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
SNYK-JS-LODASH-567746:
- lodash:
patched: '2020-05-01T01:44:59.218Z'
- winston > async > lodash:
patched: '2020-05-01T01:44:59.218Z'
- portfinder-sync > portfinder > async > lodash:
patched: '2020-05-01T01:44:59.218Z'
================================================
FILE: CHANGELOG-app.md
================================================
# Changelog
## v2.3.3
- [FIX] Minor bugs and icons squashed.
## v2.3.2
- [NEW] Added support for bringing acceleration sensors into homekit as motion sensors.
- [FIX] Fixed issue with new Garage and Thermostat define type inputs from actually bringing in the devices.
## v2.3.1
- [FIX] Typo `?.` in code preventing saving in IDE.
- [NEW] Fixed new version info when using beta version of plugin.
## v2.3.0
- [REMOVE] Support for Local Commands removed (It doesn't really speed up anything anyways)
- [NEW] Added garage door, thermostat inputs to define device types
- [FIX] Minor bugfixes and optimizations.
## v2.2.1
- [FIX] Minor tweaks to support shades fixes in the plugin.
## v2.2.0
- [UPDATE] Added support for passing the pressed button number when provided.
- [FIX] Other minor bugfixes and optimizations.
- [REMOVE] Support for Energy and Power capabilities removed (for now).
## v2.1.1
- [UPDATE] The app now validates the appId on all local commands made to ST app so if you have more than one instance of the homebridge smartapp it doesn't start sending events to wrong plugin.
## v2.1.0
- [NEW] Added a Device Event and Command history page to review events and commands sent and received by the plugin.
- [UPDATE] Cleaned up some of the unnecessary attributes from the subscription logic.
- [FIX] Refactored the accessToken logic to be more consistent. #38
- [UPDATE] Modified the device event subscription process to reduce timeouts.
- [FIX] Other bug fixes, cleanups, and optimizations.
## v2.0.3
- [NEW] Added a new device data input where you can select a device and see all available attributes, capabilities, commands, and the last 30 events.
- [FIX] Other bug fixes and cleanups.
## v2.0.1
- [UPDATE] Reworked and cleaned up the UI so it's now more organized and easier to follow.
- [NEW] Added new capability filter options.
- [UPDATE] Optimized the command/event streaming system to perform faster and more reliably.
- [NEW] Added duplicate device detection cleanups so Homekit doesn't try to create duplicate devices and throw an error.
- [FIX] Many, many other bug fixes and cleanups.
================================================
FILE: CHANGELOG.md
================================================
## v2.3.8
- [UPDATE] Updated dependencies.
## v2.3.4
- [REMOVE] Removing Sentry error reporting module prior to submitting plugin for verification under homebridge.
## v2.3.3
- [FIX] Packages updates.
## v2.3.2
- [NEW] Added support for bringing acceleration sensors into homekit as motion sensors.
## v2.3.1
- [FIX] Plugin wasn't sending pluginstatus and enable direct messages to SmartApp, so device events weren't being sent to the plugin
- [NEW] Changed some of the plugin version check logic. I also runs after every device refresh (~1 hour)
## v2.3.0
- [REMOVE] Support for Local Commands removed (It doesn't really speed up anything anyways)
- [NEW] Rate-Limiting of commands (debounce)
- [UPDATE] Command optimizations.
- [NEW] Switched web request library from Request-Promise to Axios.
- [FIX] StatusActive characteristic now reports correctly.
- [FIX] Minor bugfixes and optimizations.
## v2.2.1
- [FIX] Resolved the issue with Window Shades not working #71.
- [FIX] Resolve null Service types issue #74.
## v2.2.0
- [UPDATE] Button logic now generates the push/held actions for every button available on the remote now. Meaning you can select the parent remote and have it show actions for each button on the remote. NOTE: I've noticed that I need to open the home app once after adding the buttons to create the event connection.
- [FIX] Buttons should now work 100% again. Sorry about the issues.
- [FIX] Fixed the cannot read property of 'includes' and '\_events' errors.
- [FIX] Fixed some rare issues with requestPromises on device commands.
- [FIX] Lot's of other minor cleanup.
- [NEW] Direct port is now selected automatically using the direct_port config value as the start point for available port detection.
- [NEW] Logs now alert you when your local ST hub endpoint can't be reached.
- [NEW] Added a new config item to define your ST Community username in the error reporting so if you want me to be able to review your issues.
- [NEW] Added config item to allow you to disable error reporting.
- [UPDATE] Modified the point when the Sentry IO Error collector is loaded so it doesn't collect other plugin exceptions.replace
- [UPDATE] Updated Sentry.IO library to v5.11.1.
- [UPDATE] Changed the plugin to not list every single device loaded from cache and every device updated in the logs. They are only visible when debug option is enabled.
- [REMOVE] Support for Energy and Power capabilities removed (for now).
## v2.1.14 - v2.1.16
- [FIX] Thermostats should now update the state correctly and also auto mode is working again.
## v2.1.13
- [NEW] Added Sentry library to help collect/report anonymous error/exception data (absolutely no person data is shared with the exception of maybe a device label in the logs).
- [FIX] Thermostats should now update the state correctly.
- [FIX] Resolved the issue with Buttons crashing your entire HomeKit Instance.
## v2.1.1 - v2.1.12
- [UPDATE] Updated winston logger from v2 to v3 to help with issues running on Hoobs.
- [UPDATE] Added app id header to all local commands made to ST app so if you have more than one instance of the homebridge smartapp it doesn't start sending events to wrong plugin.
- [UPDATE] Updated the app config to allow setting the local_commands value.
## v2.1.0
- [UPDATE] Refactored the device service and characteristic logic so it's cleaner, more modular, and easier to maintain.
- [NEW] Device services and characteristics are now cleaned up when they are no longer used.
- [FIX] Lot's of fixes for device state updates and device commands.
- [FIX] Button events should now work again.
- [FIX] Updated the Hoobs config file (Plugin will be undergoing review by Hoobs to be certified soon) (@mkellsy)
- [FIX] Added support for AirPurifier & AirQuality (@danielskowronski)
- [FIX] Delays on device event updates resolved. (@devarshi) #33 #40
- [FIX] Thermostat Mode fixes (@torandreroland)
- [FIX] Dozens of other minor bugfixes and tweaks.
## v2.0.5 - v2.0.10
- [FIX] Fixed thermostat temp unit error.
- [FIX] removed token/id validation by default to prevent error with mismatched access_token | app_id.
- [FIX] Other minor bugfixes and tweaks.
## v2.0.4
- [FIX] Fixed AlarmStatus updates not being shown in the Home app when changed from ST side.
- [FIX] Fixed issues with local_commands option.
- [FIX] Fix for Celcius temperature conversions.
- [NEW] Added support for new 'temperature_unit' config option using either the smartapp or config.json file.
- [FIX] Other minor bugfixes and tweaks.
## v2.0.1
- [NEW] Completely rewrote the entire plugin using modern javascript structure.
- [NEW] The code is now much cleaner, easier to update/maintain, and easier for others to follow.
- [NEW] This translates into a faster/leaner and way more stable plugin than previous versions.
- [NEW] The plugin now uses the Homebridge Dynamic platform API, meaning it no longer requires a restart of the Homebridge service for device changes to occur.
- [NEW] The plugin now utilizes the device cache on service restart to prevent losing all of your devices when the plugin fails to start for an extended period of time.
- [NEW] It will now remove devices no longer selected under SmartThings.
- [NEW] Introduced an all-new logging system to provide more insight into issues and status, as well as write them to a file.
- [NEW] I used all of the issues from my existing plugin to repair this new version.
- [NEW] Many, many other bug fixes for devices, commands and many other items.
- [NEW] **_Important NOTICE:_**
- **Due to the changes in the plugin API you can not directly update the plugin from v1, you will need to add as a new accessory and setup your devices/automations/scenes again.
On a positive note, you can use the same SmartApp instance though as long as you update to the latest code.**
================================================
FILE: README.md
================================================
This Plugin is no longer being maintained. The ST platform removed all of the greatness that made it fun to develop for and I will not rewrite my years of code to adapt.
I have moved to Hubitat and all of my code has lived on.
I recommend you move over if possible. You won't regret it!
Thanks for the many years of support.
================================================
FILE: appData.json
================================================
{
"appDataVer": "1.1",
"versions": {
"mainApp": "2.3.3",
"plugin": "2.3.4"
},
"settings": {
"disableSentry": true,
"allowLocalCmds": false
}
}
================================================
FILE: config.schema.json
================================================
{
"pluginAlias": "SmartThings-v2",
"pluginType": "platform",
"singular": true,
"footerDisplay": "If you need help or have issues visit: [issues](https://github.com/tonesto7/homebridge-smartthings-v2/issues)",
"schema": {
"name": {
"title": "Name",
"description": "This should default to SmartThings-v2",
"type": "string",
"default": "SmartThings-v2",
"required": true
},
"app_url": {
"title": "App Url",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"app_id": {
"title": "App ID",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"access_token": {
"title": "App Token",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"communityUserName": {
"title": "SmartThings Community Username",
"description": "Only need to set this when you are having issues with the plugin and you want me to be able to identify your reported exception errors.",
"type": "string",
"required": false
},
"direct_ip": {
"title": "Direct IP",
"description": "Most installations won't need this, but if for any reason it can't identify your ip address correctly, use this setting to force the IP presented to SmartThings for the hub to send to.",
"type": "string",
"required": false
},
"direct_port": {
"title": "Direct Port",
"description": "This is the port that the plugin will listen on for traffic from your hub. Make sure your firewall allows incoming traffic on this port from your hub's IP address. (This is now a dynamic port selection)",
"type": "integer",
"maximum": 65535,
"default": 8000,
"required": false
},
"temperature_unit": {
"title": "Define Temperature Unit",
"type": "string",
"default": "F",
"oneOf": [{
"title": "Fahrenheit",
"enum": [
"F"
]
},
{
"title": "Celcius",
"enum": [
"C"
]
}
]
},
"validateTokenId": {
"title": "Validate SmartApp Access Token and AppID?",
"description": "This will help secure your plugin by validating that the plugin is receiving data from the correct smartapp if you have multiple instances of the SmartApp.",
"type": "boolean",
"required": true,
"default": false
},
"logConfig": {
"type": "object",
"properties": {
"debug": {
"title": "Enable Debug logging?",
"description": "This will show just about every log output available.",
"type": "boolean",
"required": false,
"default": false
},
"showChanges": {
"title": "Show Device Events in the Logs?",
"description": "This will log device event changes received by SmartThings.",
"type": "boolean",
"required": false,
"default": true
},
"hideTimestamp": {
"title": "Hide TimeStamp Prefix inLogs?",
"description": "This will remove the prefix from all console log output.",
"type": "boolean",
"required": false,
"default": true
},
"hideNamePrefix": {
"title": "Hide Plugin Name Prefix in Logs?",
"description": "This will remove the prefix from all console log output.",
"type": "boolean",
"required": false,
"default": true
},
"file": {
"type": "object",
"properties": {
"enabled": {
"title": "Enable Logging to file",
"description": "This log will be created as homebridge-smartthings-v2.log in the same folder as this config.json file.",
"type": "boolean",
"required": false,
"default": true
},
"level": {
"title": "Log File Output Level",
"type": "string",
"default": "good",
"oneOf": [{
"title": "Debug",
"enum": [
"debug"
]
},
{
"title": "Good",
"enum": [
"good"
]
},
{
"title": "Notice",
"enum": [
"pink"
]
},
{
"title": "Alert",
"enum": [
"alert"
]
},
{
"title": "Warnings",
"enum": [
"warn"
]
},
{
"title": "Errors",
"enum": [
"error"
]
}
],
"required": false
}
}
}
}
}
}
}
================================================
FILE: installerManifest.json
================================================
{
"namespace": "tonesto7",
"repoOwner": "tonesto7",
"repoName": "homebridge-smartthings",
"repoBranch": "master",
"name": "Homebridge",
"author": "Anthony S.",
"description": "Provides the API interface between Homebridge (HomeKit) and SmartThings",
"category": "My Apps",
"bannerUrl": "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings/master/images/hb_tonesto7@2x.png",
"forumUrl": "https://community.smartthings.com/t/release-homebridge-smartthings-v2-0/",
"docUrl": "https://github.com/tonesto7/homebridge-smartthings/#readme",
"releaseType": "production",
"keywords": [
"smartthings",
"homekit",
"homebridge"
],
"notes": "Nothing to show here (yet)",
"smartApps": {
"parent": {
"name": "Homebridge",
"iconUrl": "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings/master/images/hb_tonesto7@2x.png",
"published": true,
"oAuth": true,
"version": "2.3.4",
"appSettings": {},
"appUrl": "smartapps/tonesto7/homebridge-v2.src/homebridge.groovy"
},
"children": []
},
"deviceHandlers": []
}
================================================
FILE: jsconfig.json
================================================
{
"compilerOptions": {
"target": "ES6"
},
"exclude": [
"node_modules"
]
}
================================================
FILE: package.json
================================================
{
"dependencies": {
"axios": "^0.19.2",
"body-parser": "^1.19.0",
"chalk": "^4.1.0",
"compare-versions": "^3.6.0",
"express": "^4.17.1",
"hap-nodejs-community-types": "0.3.1",
"lodash": "^4.17.20",
"node-machine-id": "^1.1.12",
"os": "0.1.1",
"portfinder-sync": "^0.0.2",
"request": "^2.88.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.0",
"snyk": "^1.380.0"
},
"devDependencies": {
"eslint": "^7.7.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"lint-staged": "^10.2.11",
"prettier": "^2.0.5"
},
"description": "SmartThings plugin for HomeBridge",
"engines": {
"homebridge": ">=0.4.46",
"node": ">=0.12.0"
},
"lint-staged": {
"*.js": [
"prettier --write",
"git add"
]
},
"homepage": "https://github.com/tonesto7/homebridge-smartthings/#readme",
"keywords": [
"homebridge-plugin",
"smartthings",
"homekit",
"homebridge",
"category_climate",
"category_hubs",
"category_lighting"
],
"funding": {
"type": "paypal",
"url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVFJTG8H86SK8&source=url"
},
"scripts": {
"test": "eslint .",
"prettier": "prettier --write src/**/*.js",
"freeport": "npx kill-port 8000",
"start": "homebridge -D -P . -U .",
"snyk-protect": "snyk protect",
"prepare": "npm run snyk-protect"
},
"main": "src/index.js",
"license": "ISC",
"name": "homebridge-smartthings",
"preferGlobal": true,
"repository": "github:https://github.com/tonesto7/homebridge-smartthings",
"bugs": {
"url": "http://github.com/tonesto7/homebridge-smartthings/issues"
},
"version": "2.3.8",
"snyk": true
}
================================================
FILE: platform.schema.json
================================================
{
"plugin_alias": "SmartThings-v2",
"schema": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"description": "This should default to SmartThings-v2",
"type": "string",
"default": "SmartThings-v2",
"required": true,
"readOnly": true
},
"app_url": {
"title": "App Url",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"app_id": {
"title": "App ID",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"access_token": {
"title": "App Token",
"description": "To get this information, open Homebridge (SmartThings) SmartApp in your SmartThings Classic Mobile App, and tap on 'View Configuration Data for Homebridge'",
"type": "string",
"required": true
},
"communityUserName": {
"title": "SmartThings Community Username",
"description": "Only need to set this when you are having issues with the plugin and you want me to be able to identify your reported exception errors.",
"type": "string",
"required": false
},
"direct_port": {
"title": "Direct Port",
"description": "This is the port that the plugin will listen on for traffic from your hub. Make sure your firewall allows incoming traffic on this port from your hub's IP address. (This is now a dynamic port selection)",
"type": "integer",
"maximum": 65535,
"default": 8000,
"required": false
},
"temperature_unit": {
"title": "Define Temperature Unit",
"type": "string",
"default": "F",
"enum": [{
"text": "Fahrenheit",
"value": "F"
},
{
"text": "Celcius",
"value": "C"
}
],
"required": true
},
"validateTokenId": {
"title": "Validate SmartApp Access Token and AppID?",
"description": "This will help make sure your plugin is receiving data from the correct smartapp if you have multiple instances of the SmartApp.",
"type": "boolean",
"required": false,
"default": false
},
"logConfig": {
"type": "object",
"properties": {
"debug": {
"title": "Enable Debug logging?",
"description": "This will show just about every log output available.",
"type": "boolean",
"required": false,
"default": false
},
"showChanges": {
"title": "Show Device Events in the Logs?",
"description": "This will log device event changes received by SmartThings.",
"type": "boolean",
"required": false,
"default": true
},
"hideTimestamp": {
"title": "Hide TimeStamp Prefix inLogs?",
"description": "This will remove the prefix from all console log output.",
"type": "boolean",
"required": false,
"default": true
},
"hideNamePrefix": {
"title": "Hide Plugin Name Prefix in Logs?",
"description": "This will remove the prefix from all console log output.",
"type": "boolean",
"required": false,
"default": true
},
"file": {
"type": "object",
"properties": {
"enabled": {
"title": "Enable Logging to file",
"description": "This log will be created as homebridge-smartthings-v2.log in the same folder as this config.json file.",
"type": "boolean",
"required": false,
"default": true
},
"level": {
"title": "Log File Output Level",
"type": "string",
"default": "Good",
"enum": [{
"text": "Debug",
"value": "debug"
},
{
"text": "Good",
"value": "good"
},
{
"text": "Notice",
"value": "pink"
},
{
"text": "Alert",
"value": "alert"
},
{
"text": "Warnings",
"value": "warn"
},
{
"text": "Errors",
"value": "error"
}
],
"required": false
}
}
}
}
}
}
}
}
================================================
FILE: prettierrc
================================================
{
"trailingComma": "all"
}
================================================
FILE: smartapps/tonesto7/homebridge-v2.src/homebridge-v2.groovy
================================================
/**
* Homebridge SmartThing Interface
* Loosely Modelled off of Paul Lovelace's JSON API
* Copyright 2018, 2019, 2020 Anthony Santilli
*/
String appVersion() { return "2.3.3" }
String appModified() { return "04-28-2020" }
String branch() { return "master" }
String platform() { return "SmartThings" }
String pluginName() { return "${platform()}-v2" }
String appIconUrl() { return "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/${branch()}/images/hb_tonesto7@2x.png" }
String getAppImg(imgName, ext=".png") { return "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/${branch()}/images/${imgName}${ext}" }
Map minVersions() { return [plugin: 233] }
definition(
name: "Homebridge v2",
namespace: "tonesto7",
author: "Anthony Santilli",
description: "Provides the API interface between Homebridge (HomeKit) and ${platform()}",
category: "My Apps",
iconUrl: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/master/images/hb_tonesto7@1x.png",
iconX2Url: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/master/images/hb_tonesto7@2x.png",
iconX3Url: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/master/images/hb_tonesto7@3x.png",
oauth: true)
{
appSetting "devMode"
appSetting "log_address"
}
preferences {
page(name: "startPage")
page(name: "mainPage")
page(name: "defineDevicesPage")
page(name: "deviceSelectPage")
page(name: "changeLogPage")
page(name: "capFilterPage")
page(name: "virtDevicePage")
page(name: "developmentPage")
page(name: "donationPage")
page(name: "historyPage")
page(name: "deviceDebugPage")
page(name: "settingsPage")
page(name: "confirmPage")
}
private Map ignoreLists() {
Boolean noPwr = true
Boolean noEnr = true
Map o = [
commands: ["indicatorWhenOn", "indicatorWhenOff", "ping", "refresh", "indicatorNever", "configure", "poll", "reset"],
attributes: [
'DeviceWatch-Enroll', 'DeviceWatch-Status', "checkInterval", "LchildVer", "FchildVer", "LchildCurr", "FchildCurr", "lightStatus", "lastFanMode", "lightLevel",
"coolingSetpointRange", "heatingSetpointRange", "thermostatSetpointRange"
],
evt_attributes: [
'DeviceWatch-DeviceStatus', "DeviceWatch-Enroll", 'checkInterval', 'devTypeVer', 'dayPowerAvg', 'apiStatus', 'yearCost', 'yearUsage','monthUsage', 'monthEst', 'weekCost', 'todayUsage',
'maxCodeLength', 'maxCodes', 'readingUpdated', 'maxEnergyReading', 'monthCost', 'maxPowerReading', 'minPowerReading', 'monthCost', 'weekUsage', 'minEnergyReading',
'codeReport', 'scanCodes', 'verticalAccuracy', 'horizontalAccuracyMetric', 'altitudeMetric', 'latitude', 'distanceMetric', 'closestPlaceDistanceMetric',
'closestPlaceDistance', 'leavingPlace', 'currentPlace', 'codeChanged', 'codeLength', 'lockCodes', 'healthStatus', 'horizontalAccuracy', 'bearing', 'speedMetric',
'speed', 'verticalAccuracyMetric', 'altitude', 'indicatorStatus', 'todayCost', 'longitude', 'distance', 'previousPlace','closestPlace', 'places', 'minCodeLength',
'arrivingAtPlace', 'lastUpdatedDt', 'scheduleType', 'zoneStartDate', 'zoneElapsed', 'zoneDuration', 'watering', 'eventTime', 'eventSummary', 'endOffset', 'startOffset',
'closeTime', 'endMsgTime', 'endMsg', 'openTime', 'startMsgTime', 'startMsg', 'calName', "deleteInfo", "eventTitle", "floor", "sleeping", "powerSource", "batteryStatus",
"LchildVer", "FchildVer", "LchildCurr", "FchildCurr", "lightStatus", "lastFanMode", "lightLevel", "coolingSetpointRange", "heatingSetpointRange", "thermostatSetpointRange",
"colorName", "locationForURL", "location", "offsetNotify"
],
capabilities: ["Health Check", "Ultraviolet Index", "Indicator", "Window Shade Preset"]
]
if(noPwr) { o?.attributes?.push("power"); o?.evt_attributes?.push("power"); o?.capabilities?.push("Power Meter") }
if(noEnr) { o?.attributes?.push("energy"); o?.evt_attributes?.push("energy"); o?.capabilities?.push("Energy Meter") }
return o
}
def startPage() {
if(!getAccessToken()) { return dynamicPage(name: "mainPage", install: false, uninstall: true) { section() { paragraph title: "OAuth Error", "OAuth is not Enabled for ${app?.getName()}!.\n\nPlease click remove and Enable Oauth under the SmartApp App Settings in the IDE", required: true, state: null } } }
else {
if(!state?.installData) { state?.installData = [initVer: appVersion(), dt: getDtNow().toString(), updatedDt: getDtNow().toString(), shownDonation: false] }
checkVersionData(true)
if(showChgLogOk()) { return changeLogPage() }
if(showDonationOk()) { return donationPage() }
return mainPage()
}
}
def mainPage() {
Boolean isInst = (state?.isInstalled == true)
return dynamicPage(name: "mainPage", nextPage: (isInst ? "confirmPage" : ""), install: !isInst, uninstall: true) {
appInfoSect()
section("Define Specific Categories:") {
paragraph "Each category below will adjust the device attributes to make sure they are recognized as the desired device type under HomeKit.\nNOTICE: Don't select the same devices used here in the Select Your Devices Input below.", state: "complete"
Boolean conf = (lightList || buttonList || fanList || fan3SpdList || fan4SpdList || speakerList || shadesList || garageList || tstatList || tstatHeatList)
Integer fansize = (fanList?.size() ?: 0) + (fan3SpdList?.size() ?: 0) + (fan4SpdList?.size() ?: 0)
String desc = "Tap to configure"
if(conf) {
desc = ""
desc += lightList ? "(${lightList?.size()}) Light Devices\n" : ""
desc += buttonList ? "(${buttonList?.size()}) Button Devices\n" : ""
desc += (fanList || fan3SpdList || fan4SpdList) ? "(${fansize}) Fan Devices\n" : ""
desc += speakerList ? "(${speakerList?.size()}) Speaker Devices\n" : ""
desc += shadesList ? "(${shadesList?.size()}) Shade Devices\n" : ""
desc += garageList ? "(${garageList?.size()}) Garage Door Devices\n" : ""
desc += tstatList ? "(${tstatList?.size()}) Tstat Devices\n" : ""
desc += tstatHeatList ? "(${tstatHeatList?.size()}) Tstat Heat Devices\n" : ""
desc += "\nTap to modify..."
}
href "defineDevicesPage", title: "Define Device Types", required: false, image: getAppImg("devices2"), state: (conf ? "complete" : null), description: desc
}
section("All Other Devices:") {
Boolean conf = (sensorList || switchList || deviceList)
String desc = "Tap to configure"
if(conf) {
desc = ""
desc += sensorList ? "(${sensorList?.size()}) Sensor Devices\n" : ""
desc += switchList ? "(${switchList?.size()}) Switch Devices\n" : ""
desc += deviceList ? "(${deviceList?.size()}) Other Devices\n" : ""
desc += "\nTap to modify..."
}
href "deviceSelectPage", title: "Select your Devices", required: false, image: getAppImg("devices"), state: (conf ? "complete" : null), description: desc
}
inputDupeValidation()
section("Capability Filtering:") {
Boolean conf = (
removeAcceleration || removeBattery || removeButton || removeContact || removeEnergy || removeHumidity || removeIlluminance || removeLevel || removeLock || removeMotion ||
removePower || removePresence || removeSwitch || removeTamper || removeTemp || removeValve
)
href "capFilterPage", title: "Filter out capabilities from your devices", required: false, image: getAppImg("filter"), state: (conf ? "complete" : null), description: (conf ? "Tap to modify..." : "Tap to configure")
}
section("Virtual Devices:") {
Boolean conf = (modeList || routineList)
String desc = "Create virtual (mode, routine) devices\n\nTap to Configure..."
if(conf) {
desc = ""
desc += modeList ? "(${modeList?.size()}) Mode Devices\n" : ""
desc += routineList ? "(${routineList?.size()}) Routine Devices\n" : ""
desc += "\nTap to modify..."
}
href "virtDevicePage", title: "Configure Virtual Devices", required: false, image: getAppImg("devices"), state: (conf ? "complete" : null), description: desc
}
section("Smart Home Monitor (SHM):") {
paragraph title:"NOTICE:", "This will not work with the New SmartThings Home Monitor (Under the new mobile app). If you are using the new STHM please disable the setting below."
input "addSecurityDevice", "bool", title: "Allow SHM Control in HomeKit?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("alarm_home")
}
section("Plugin Options & Review:") {
// paragraph "Turn off if you are having issues sending commands"
// input "sendCmdViaHubaction", "bool", title: "Send HomeKit Commands Locally?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("command")
input "temp_unit", "enum", title: "Temperature Unit?", required: true, defaultValue: location?.temperatureScale, options: ["F":"Fahrenheit", "C":"Celcius"], submitOnChange: true, image: getAppImg("command")
Integer devCnt = getDeviceCnt()
href url: getAppEndpointUrl("config"), style: "embedded", required: false, title: "Render the platform data for Homebridge config.json", description: "Tap, select, copy, then click \"Done\"", state: "complete", image: getAppImg("info")
if(devCnt > 148) {
paragraph "Notice:\nHomebridge Allows for 149 Devices per Bridge!!!", image: getAppImg("error"), state: null, required: true
}
paragraph "Devices Selected: (${devCnt})", image: getAppImg("info"), state: "complete"
}
section("History and Device Data:") {
href "historyPage", title: "Command and Event History", image: getAppImg("backup")
href "deviceDebugPage", title: "Device Data Viewer", image: getAppImg("debug")
}
section("App Preferences:") {
def sDesc = getSetDesc()
href "settingsPage", title: "App Settings", description: sDesc, state: (sDesc?.endsWith("modify...") ? "complete" : null), required: false, image: getAppImg("settings")
label title: "App Label (optional)", description: "Rename this App", defaultValue: app?.name, required: false, image: getAppImg("name_tag")
}
if(devMode()) {
section("Dev Mode Options") {
input "sendViaNgrok", "bool", title: "Communicate with Plugin via Ngrok Http?", defaultValue: false, submitOnChange: true, image: getAppImg("command")
if(sendViaNgrok) { input "ngrokHttpUrl", "text", title: "Enter the ngrok code from the url", required: true, submitOnChange: true }
}
section("Other Settings:") {
input "restartService", "bool", title: "Restart Homebridge plugin when you press Save?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("reset")
}
}
clearTestDeviceItems()
}
}
def deviceValidationErrors() {
/*
NOTE: Determine what we require to determine the thermostat a thermostat so we can support devices like Flair which are custom heat-only thermostats.
*/
Map reqs = [
tstat: [ c:["Thermostat Operating State"], a: [r: ["thermostatOperatingState"], o: ["heatingSetpoint", "coolingSetpoint"]] ],
tstat_heat: [
c: ["Thermostat Operating State"],
a: [
r: ["thermostatOperatingState", "heatingSetpoint"],
o: []
]
]
]
// if(tstatHeatList || tstatList) {}
return reqs
}
def defineDevicesPage() {
return dynamicPage(name: "defineDevicesPage", title: "", install: false, uninstall: false) {
section("Define Specific Categories:") {
paragraph "NOTE: Please do not select a device here and then again in another input below."
paragraph "Each category below will adjust the device attributes to make sure they are recognized as the desired device type under HomeKit", state: "complete"
input "lightList", "capability.switch", title: "Lights: (${lightList ? lightList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("light_on")
input "garageList", "capability.garageDoorControl", title: "Garage Doors: (${garageList ? garageList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("garage_door")
input "buttonList", "capability.button", title: "Buttons: (${buttonList ? buttonList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("button")
input "speakerList", "capability.switch", title: "Speakers: (${speakerList ? speakerList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("media_player")
input "shadesList", "capability.windowShade", title: "Window Shades: (${shadesList ? shadesList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("window_shade")
}
section("Fans") {
input "fanList", "capability.switch", title: "Fans: (${fanList ? fanList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("fan_on")
input "fan3SpdList", "capability.switch", title: "Fans (3 Speeds): (${fan3SpdList ? fan3SpdList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("fan_on")
input "fan4SpdList", "capability.switch", title: "Fans (4 Speeds): (${fan4SpdList ? fan4SpdList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("fan_on")
}
section("Thermostats") {
input "tstatList", "capability.thermostat", title: "Thermostats: (${tstatList ? tstatList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("thermostat")
input "tstatHeatList", "capability.thermostat", title: "Heat Only Thermostats: (${tstatHeatList ? tstatHeatList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("thermostat")
}
}
}
def deviceSelectPage() {
return dynamicPage(name: "deviceSelectPage", title: "", install: false, uninstall: false) {
section("All Other Devices:") {
input "sensorList", "capability.sensor", title: "Sensors: (${sensorList ? sensorList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("sensors")
input "switchList", "capability.switch", title: "Switches: (${switchList ? switchList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("switch")
input "deviceList", "capability.refresh", title: "Others: (${deviceList ? deviceList?.size() : 0} Selected)", multiple: true, submitOnChange: true, required: false, image: getAppImg("devices2")
}
}
}
def settingsPage() {
return dynamicPage(name: "settingsPage", title: "", install: false, uninstall: false) {
section("Logging:") {
input "showEventLogs", "bool", title: "Show Events in Live Logs?", required: false, defaultValue: true, submitOnChange: true, image: getAppImg("debug")
input "showDebugLogs", "bool", title: "Debug Logging?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("debug")
}
section("Reset Token:") {
paragraph title: "What This?", "This will allow you to clear you existing and auth_token and force a new one to be created"
input "resetAppToken", "bool", title: "Revoke and Recreate Access Token?", defaultValue: false, submitOnChange: true, image: getAppImg("reset")
if(settings?.resetAppToken) { settingUpdate("resetAppToken", "false", "bool"); resetAppToken() }
}
}
}
private resetAppToken() {
logWarn("resetAppToken | Current Access Token Removed...")
state.remove("accessToken")
if(getAccessToken()) {
logInfo("resetAppToken | New Access Token Created...")
}
}
private resetCapFilters() {
List items = settings?.each?.findAll { it?.key?.startsWith("remove") }?.collect { it?.key as String }
if(items?.size()) {
items?.each { item->
settingRemove(item)
}
}
}
private inputDupeValidation() {
Map clnUp = [d: [:], o: [:]]
Map items = [
d: ["fanList": "Fans", "fan3SpdList": "Fans (3-Speed)", "fan4SpdList": "Fans (4-Speed)", "buttonList": "Buttons", "lightList": "Lights", "shadesList": "Window Shadse", "speakerList": "Speakers",
"garageList": "Garage Doors", "tstatList": "Thermostat", "tstatHeatList": "Thermostat (Heat Only)"
],
o: ["deviceList": "Other", "sensorList": "Sensor", "switchList": "Switch"]
]
items?.d?.each { k, v->
List priItems = (settings?."${k}"?.size()) ? settings?."${k}"?.collect { it?.getLabel() } : null
if(priItems) {
items?.d?.each { k2, v2->
List secItems = (settings?."${k2}"?.size()) ? settings?."${k2}"?.collect { it?.getLabel() } : null
if(k != k2 && secItems) {
secItems?.retainAll(priItems)
if(secItems?.size()) {
clnUp?.d[k2] = clnUp?.d[k2] ?: []
clnUp?.d[k2] = (clnUp?.d[k2] + secItems)?.unique()
}
}
}
items?.o?.each { k2, v2->
List secItems = (settings?."${k2}"?.size()) ? settings?."${k2}"?.collect { it?.getLabel() } : null
if(secItems) {
secItems?.retainAll(priItems)
if(secItems?.size()) {
clnUp?.o[k2] = clnUp?.o[k2] ?: []
clnUp?.o[k2] = (clnUp?.o[k2] + secItems)?.unique()
}
}
}
}
}
String out = ""
Boolean show = false
Boolean first = true
if(clnUp?.d?.size()) {
show=true
clnUp?.d?.each { k,v->
out += "${first ? "" : "\n"}${items?.d[k]}:\n "
out += v?.join("\n ") + "\n"
first = false
}
}
if(clnUp?.o?.size()) {
show=true
clnUp?.o?.each { k,v->
out += "${first ? "" : "\n"}${items?.o[k]}:\n "
out += v?.join("\n ") + "\n"
first = false
}
}
if(show && out) {
section("Duplicate Device Validation:") {
paragraph title: "Duplicate Devices Found in these Inputs:", out + "\nPlease remove these duplicate items!", required: true, state: null
}
}
}
String getSetDesc() {
def s = []
if(settings?.showEventLogs == true) s?.push("\u2022 Device Event Logs")
if(settings?.showDebugLogs == true) s?.push("\u2022 Debug Logging")
return s?.size() ? "${s?.join("\n")}\n\nTap to modify..." : "Tap to configure..."
}
def historyPage() {
return dynamicPage(name: "historyPage", title: "", install: false, uninstall: false) {
List cHist = getCmdHistory()?.sort {it?.dt}?.reverse()
List eHist = getEvtHistory()?.sort {it?.dt}?.reverse()
section("Last (${cHist?.size()}) Commands:") {
if(cHist?.size()) {
cHist?.each { c-> paragraph title: c?.dt, "Device: ${c?.data?.device}\nCommand: (${c?.data?.cmd})${c?.data?.value1 ? "\nValue1: (${c?.data?.value1})" : ""}${c?.data?.value2 ? "\nValue2: (${c?.data?.value2})" : ""}", state: "complete" }
} else {paragraph "No Command History Found..." }
}
section("Last (${eHist?.size()}) Events:") {
if(eHist?.size()) {
eHist?.each { h-> paragraph title: h?.dt, "Device: ${h?.data?.device}\nEvent: (${h?.data?.name})${h?.data?.value ? "\nValue: (${h?.data?.value})" : ""}", state: "complete" }
} else {paragraph "No Event History Found..." }
}
}
}
def capFilterPage() {
return dynamicPage(name: "capFilterPage", title: "Filter out capabilities", install: false, uninstall: false) {
section("Restrict Temp Device Creation") {
input "noTemp", "bool", title: "Remove Temp from All Contacts and Water Sensors?", required: false, defaultValue: false, submitOnChange: true
if(settings?.noTemp) {
input "sensorAllowTemp", "capability.sensor", title: "Allow Temp on these Sensors", multiple: true, submitOnChange: true, required: false, image: getAppImg("temperature")
}
}
section("Remove Capabilities from Devices") {
paragraph "This will allow you to filter out certain capabilities from creating unwanted devices under HomeKit"
input "removeAcceleration", "capability.accelerationSensor", title: "Remove Acceleration from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("acceleration")
input "removeBattery", "capability.battery", title: "Remove Battery from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("battery")
input "removeButton", "capability.button", title: "Remove Buttons from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("button")
input "removeContact", "capability.contactSensor", title: "Remove Contact from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("contact")
// input "removeEnergy", "capability.energyMeter", title: "Remove Energy Meter from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("power")
input "removeHumidity", "capability.relativeHumidityMeasurement", title: "Remove Humidity from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("humidity")
input "removeIlluminance", "capability.illuminanceMeasurement", title: "Remove Illuminance from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("illuminance")
input "removeLevel", "capability.switchLevel", title: "Remove Level from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("speed_knob")
input "removeLock", "capability.lock", title: "Remove Lock from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("lock")
input "removeMotion", "capability.motionSensor", title: "Remove Motion from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("motion")
// input "removePower", "capability.powerMeter", title: "Remove Power Meter from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("power")
input "removePresence", "capability.presenceSensor", title: "Remove Presence from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("presence")
input "removeSwitch", "capability.switch", title: "Remove Switch from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("switch")
input "removeTamper", "capability.tamperAlert", title: "Remove Tamper from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("tamper")
input "removeTemp", "capability.temperatureMeasurement", title: "Remove Temperature from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("temperature")
input "removeValve", "capability.valve", title: "Remove Valve from these Devices", multiple: true, submitOnChange: true, required: false, image: getAppImg("valve")
}
section("Filter Reset:", hideable: true, hidden: true) {
input "resetCapFilters", "bool", title: "Clear All Selected Removal Filters?", required: false, defaultValue: false, submitOnChange: true, image: getAppImg("reset")
if(settings?.resetCapFilters) { settingUpdate("resetCapFilters", "false", "bool"); resetCapFilters() }
}
}
}
def virtDevicePage() {
return dynamicPage(name: "virtDevicePage", title: "", install: false, uninstall: false) {
section("Create Devices for Modes in HomeKit?") {
paragraph title: "What are these for?", "A virtual switch will be created for each mode in HomeKit.\nThe switch will be ON when that mode is active.", state: "complete", image: getAppImg("info")
def modes = location?.modes?.sort{it?.name}?.collect { [(it?.id):it?.name] }
input "modeList", "enum", title: "Create Devices for these Modes", required: false, multiple: true, options: modes, submitOnChange: true, image: getAppImg("mode")
}
section("Create Devices for Routines in HomeKit?") {
paragraph title: "What are these?", "A virtual device will be created for each routine in HomeKit.\nThese are very useful for use in Home Kit scenes", state: "complete", image: getAppImg("info")
def routines = location.helloHome?.getPhrases()?.sort { it?.label }?.collect { [(it?.id):it?.label] }
input "routineList", "enum", title: "Create Devices for these Routines", required: false, multiple: true, options: routines, submitOnChange: true, image: getAppImg("routine")
}
}
}
def donationPage() {
return dynamicPage(name: "donationPage", title: "", nextPage: "mainPage", install: false, uninstall: false) {
section("") {
def str = ""
str += "Hello User, \n\nPlease forgive the interuption but it's been 30 days since you installed/updated this SmartApp and I wanted to present you with this one time reminder that donations are accepted (We do not require them)."
str += "\n\nIf you have been enjoying the software and devices please remember that we have spent thousand's of hours of our spare time working on features and stability for those applications and devices."
str += "\n\nIf you have already donated, thank you very much for your support!"
str += "\n\nIf you are just not interested or have already donated please ignore this message and toggle the setting below"
str += "\n\nThanks again for using Homebridge SmartThings"
paragraph str, required: true, state: null
input "sentDonation", "bool", title: "Already Donated?", defaultValue: false, submitOnChange: true
href url: textDonateLink(), style: "external", required: false, title: "Donations", description: "Tap to open in browser", state: "complete", image: getAppImg("donate")
}
updInstData("shownDonation", true)
}
}
def confirmPage() {
return dynamicPage(name: "confirmPage", title: "Confirmation Page", install: true, uninstall:true) {
section() {
paragraph "Restarting the service is no longer required to apply any device changes under homekit.\n\nThe service will refresh your devices about 15-20 seconds after Pressing Done/Save.", state: "complete", image: getAppImg("info")
}
}
}
def deviceDebugPage() {
return dynamicPage(name: "deviceDebugPage", title: "", install: false, uninstall: false) {
section("All Other Devices:") {
paragraph "Have a device that's not working under homekit like you want?\nSelect a device from one of the inputs below and it will show you all data about the device.", state: "complete", image: getAppImg("info")
if(!debug_switch && !debug_other && !debug_garage && !debug_tstat)
input "debug_sensor", "capability.sensor", title: "Sensors: ", multiple: false, submitOnChange: true, required: false, image: getAppImg("sensors")
if(!debug_sensor && !debug_other && !debug_garage && !debug_tstat)
input "debug_switch", "capability.actuator", title: "Switches: ", multiple: false, submitOnChange: true, required: false, image: getAppImg("switch")
if(!debug_switch && !debug_sensor && !debug_garage && !debug_tstat)
input "debug_other", "capability.refresh", title: "Others Devices: ", multiple: false, submitOnChange: true, required: false, image: getAppImg("devices2")
if(!debug_sensor && !debug_other && !debug_switch)
input "debug_garage", "capability.garageDoorControl", title: "Garage Doors: ", multiple: false, submitOnChange: true, required: false, image: getAppImg("garage_door")
if(!debug_sensor && !debug_other && !debug_switch && !debug_garage)
input "debug_tstat", "capability.thermostat", title: "Thermostats: ", multiple: false, submitOnChange: true, required: false, image: getAppImg("thermostat")
if(debug_other || debug_sensor || debug_switch || debug_garage || debug_tstat) {
href url: getAppEndpointUrl("deviceDebug"), style: "embedded", required: false, title: "Tap here to view Device Data...", description: "", state: "complete", image: getAppImg("info")
}
}
}
}
public clearTestDeviceItems() {
settingRemove("debug_sensor")
settingRemove("debug_switch")
settingRemove("debug_other")
settingRemove("debug_garage")
settingRemove("debug_tstat")
}
def viewDeviceDebug() {
def sDev = null;
if(debug_other) sDev = debug_other
if(debug_sensor) sDev = debug_sensor
if(debug_switch) sDev = debug_switch
if(debug_garage) sDev = debug_garage
if(debug_tstat) sDev = debug_tstat
def json = new groovy.json.JsonOutput().toJson(getDeviceDebugMap(sDev))
def jsonStr = new groovy.json.JsonOutput().prettyPrint(json)
render contentType: "application/json", data: jsonStr
}
def getDeviceDebugMap(dev) {
def r = "No Data Returned"
if(dev) {
try {
r = [:]
r?.name = dev?.displayName?.toString()?.replaceAll("[#\$()!%&@^']", "");
r?.basename = dev?.getName();
r?.deviceid = dev?.getId();
r?.status = dev?.getStatus();
r?.manufacturer = dev?.getManufacturerName() ?: "Unknown";
r?.model = dev?.getModelName() ?: dev?.getTypeName();
r?.deviceNetworkId = dev?.getDeviceNetworkId();
r?.lastActivity = dev?.getLastActivity() ?: null;
r?.capabilities = dev?.capabilities?.collect { it?.name as String }?.unique()?.sort() ?: [];
r?.commands = dev?.supportedCommands?.collect { it?.name as String }?.unique()?.sort() ?: [];
r?.customflags = getDeviceFlags(dev) ?: [];
r?.attributes = [:];
r?.eventHistory = dev?.eventsSince(new Date() - 1, [max: 20])?.collect { "${it?.date} | [${it?.name}] | (${it?.value}${it?.unit ? " ${it?.unit}" : ""})" };
dev?.supportedAttributes?.collect { it?.name as String }?.unique()?.sort()?.each { r?.attributes[it] = dev?.currentValue(it as String); };
} catch(ex) {
logError("Error while generating device data: ", ex);
}
}
return r
}
def getDeviceCnt(phyOnly=false) {
List devices = []
List items = deviceSettingKeys()?.collect { it?.key as String }
items?.each { item -> if(settings[item]?.size() > 0) devices = devices + settings[item] }
if(!phyOnly) {
["modeList", "routineList"]?.each { item->
if(settings[item]?.size() > 0) devices = devices + settings[item]
}
}
return devices?.unique()?.size() ?: 0
}
def installed() {
logDebug("${app.name} | installed() has been called...")
state?.isInstalled = true
state?.installData = [initVer: appVersion(), dt: getDtNow().toString(), updatedDt: "Not Set", showDonation: false, shownChgLog: true]
initialize()
}
def updated() {
logDebug("${app.name} | updated() has been called...")
state?.isInstalled = true
if(!state?.installData) { state?.installData = [initVer: appVersion(), dt: getDtNow().toString(), updatedDt: getDtNow().toString(), shownDonation: false] }
unsubscribe()
stateCleanup()
initialize()
}
def initialize() {
if(getAccessToken()) {
subscribeToEvts()
runEvery5Minutes("healthCheck")
} else { logError("initialize error: Unable to get or generate smartapp access token") }
}
def getAccessToken() {
try {
if(!atomicState?.accessToken) {
atomicState?.accessToken = createAccessToken();
logWarn("SmartApp Access Token Missing... Generating New Token!!!")
return true;
}
return true
} catch (ex) {
def msg = "Error: OAuth is not Enabled for ${appName()}!. Please click remove and Enable Oauth under the SmartApp App Settings in the IDE"
logError("getAccessToken Exception: ${msg}")
return false
}
}
private subscribeToEvts() {
runIn(4, "registerDevices")
logInfo("Starting Device Subscription Process")
if(settings?.addSecurityDevice) {
subscribe(location, "alarmSystemStatus", changeHandler)
}
if(settings?.modeList) {
logDebug("Registering (${settings?.modeList?.size() ?: 0}) Virtual Mode Devices")
subscribe(location, "mode", changeHandler)
if(state?.lastMode == null) { state?.lastMode = location?.mode?.toString() }
}
state?.subscriptionRenewed = 0
subscribe(app, onAppTouch)
if(settings?.sendCmdViaHubaction != false) { subscribe(location, null, lanEventHandler, [filterEvents:false]) }
if(settings?.routineList) {
logDebug("Registering (${settings?.routineList?.size() ?: 0}) Virtual Routine Devices")
subscribe(location, "routineExecuted", changeHandler)
}
}
private healthCheck() {
checkVersionData()
if(checkIfCodeUpdated()) {
logWarn("Code Version Change Detected... Health Check will occur on next cycle.")
return
}
}
private checkIfCodeUpdated() {
logDebug("Code versions: ${state?.codeVersions}")
if(state?.codeVersions) {
if(state?.codeVersions?.mainApp != appVersion()) {
checkVersionData(true)
state?.pollBlocked = true
updCodeVerMap("mainApp", appVersion())
Map iData = atomicState?.installData ?: [:]
iData["updatedDt"] = getDtNow().toString()
iData["shownChgLog"] = false
if(iData?.shownDonation == null) {
iData["shownDonation"] = false
}
atomicState?.installData = iData
logInfo("Code Version Change Detected... | Re-Initializing SmartApp in 5 seconds")
return true
}
}
return false
}
private stateCleanup() {
List removeItems = []
if(state?.directIP && state?.directPort) {
state?.pluginDetails = [
directIP: state?.directIP,
directPort: state?.directPort
]
removeItems?.push("directIP")
removeItems?.push("directPort")
}
removeItems?.each { if(state?.containsKey(it)) state?.remove(it) }
}
def onAppTouch(event) {
updated()
}
def renderDevices() {
Map devMap = [:]
List devList = []
List items = deviceSettingKeys()?.collect { it?.key as String }
items = items+["modeList", "routineList"]
items?.each { item ->
if(settings[item]?.size()) {
settings[item]?.each { dev->
try {
Map devObj = getDeviceData(item, dev) ?: [:]
if(devObj?.size()) { devMap[dev] = devObj }
} catch (e) {
logError("Device (${dev?.displayName}) Render Exception: ${ex.message}")
}
}
}
}
if(settings?.addSecurityDevice == true) { devList?.push(getSecurityDevice()) }
if(devMap?.size()) { devMap?.sort{ it?.value?.name }?.each { k,v-> devList?.push(v) } }
return devList
}
def getDeviceData(type, sItem) {
// log.debug "getDeviceData($type, $sItem)"
String curType = null
String devId = sItem
Boolean isVirtual = false
String firmware = null
String name = null
Map optFlags = [:]
def attrVal = null
def obj = null
switch(type) {
case "routineList":
isVirtual = true
curType = "Routine"
optFlags["virtual_routine"] = 1
obj = getRoutineById(sItem)
if(obj) {
name = "Routine - " + obj?.label
attrVal = "off"
}
break
case "modeList":
isVirtual = true
curType = "Mode"
optFlags["virtual_mode"] = 1
obj = getModeById(sItem)
if(obj) {
name = "Mode - " + obj?.name
attrVal = modeSwitchState(obj?.name)
}
break
default:
curType = "device"
obj = sItem
// Define firmware variable and initialize it out of device handler attribute`
try {
if (sItem?.hasAttribute("firmware")) { firmware = sItem?.currentValue("firmware")?.toString() }
} catch (ex) { firmware = null }
break
}
if(curType && obj) {
return [
name: !isVirtual ? sItem?.displayName?.toString()?.replaceAll("[#\$()!%&@^']", "") : name?.toString()?.replaceAll("[#\$()!%&@^']", ""),
basename: !isVirtual ? sItem?.name : name,
deviceid: !isVirtual ? sItem?.id : devId,
status: !isVirtual ? sItem?.status : "Online",
manufacturerName: (!isVirtual ? sItem?.getManufacturerName() : pluginName()) ?: pluginName(),
modelName: !isVirtual ? (sItem?.getModelName() ?: sItem?.getTypeName()) : "${curType} Device",
serialNumber: !isVirtual ? sItem?.getDeviceNetworkId() : "${curType}${devId}",
firmwareVersion: firmware ?: "1.0.0",
lastTime: !isVirtual ? (sItem?.getLastActivity() ?: null) : now(),
capabilities: !isVirtual ? deviceCapabilityList(sItem) : ["${curType}": 1],
commands: !isVirtual ? deviceCommandList(sItem) : [on: 1],
deviceflags: !isVirtual ? getDeviceFlags(sItem) : optFlags,
attributes: !isVirtual ? deviceAttributeList(sItem) : ["switch": attrVal]
]
}
return null
}
String modeSwitchState(String mode) {
return location?.mode?.toString() == mode ? "on" : "off"
}
def getSecurityDevice() {
return [
name: "Security Alarm",
basename: "Security Alarm",
deviceid: "alarmSystemStatus_${location?.id}",
status: "ACTIVE",
manufacturerName: pluginName(),
modelName: "Security System",
serialNumber: "SHM",
firmwareVersion: "1.0.0",
lastTime: null,
capabilities: ["Alarm System Status": 1, "Alarm": 1],
commands: [],
attributes: ["alarmSystemStatus": getSecurityStatus()]
]
}
def getDeviceFlags(device) {
Map opts = [:]
if(settings?.fan3SpdList?.find { it?.id == device?.id }) {
opts["fan_3_spd"] = 1
}
if(settings?.fan4SpdList?.find { it?.id == device?.id }) {
opts["fan_4_spd"] = 1
}
// if(opts?.size()) log.debug "opts: ${opts}"
return opts
}
def findDevice(dev_id) {
List allDevs = []
deviceSettingKeys()?.collect { it?.key as String }?.each { key-> allDevs = allDevs + (settings?."${key}" ?: []) }
return allDevs?.find { it?.id == dev_id } ?: null
}
def authError() {
return [error: "Permission denied"]
}
def getSecurityStatus(retInt=false) {
def cur = location.currentState("alarmSystemStatus")?.value
def inc = getShmIncidents()
if(inc != null && inc?.size()) { cur = 'alarm_active' }
if(retInt) {
switch (cur) {
case 'stay':
return 0
case 'away':
return 1
case 'night':
return 2
case 'off':
return 3
case 'alarm_active':
return 4
}
} else { return cur ?: "disarmed" }
}
private setSecurityMode(mode) {
logInfo("Setting the Smart Home Monitor Mode to (${mode})...")
sendLocationEvent(name: 'alarmSystemStatus', value: mode.toString())
}
def renderConfig() {
Map jsonMap = [
platform: pluginName(),
name: pluginName(),
app_url: apiServerUrl("/api/smartapps/installations/"),
app_id: app?.getId(),
access_token: atomicState?.accessToken,
temperature_unit: settings?.temp_unit ?: location?.temperatureScale,
validateTokenId: false,
logConfig: [
debug: false,
showChanges: true,
hideTimestamp: false,
hideNamePrefix: false,
file: [
enabled: true
]
]
]
def configJson = new groovy.json.JsonOutput().toJson(jsonMap)
def configString = new groovy.json.JsonOutput().prettyPrint(configJson)
render contentType: "text/plain", data: configString
}
def renderLocation() {
return [
latitude: location?.latitude,
longitude: location?.longitude,
mode: location?.mode,
name: location?.name,
temperature_scale: settings?.temp_unit ?: location?.temperatureScale,
zip_code: location?.zipCode,
hubIP: location?.hubs[0]?.localIP,
local_commands: false, //(settings?.sendCmdViaHubaction != false),
app_version: appVersion()
]
}
def CommandReply(statusOut, messageOut) {
def replyJson = new groovy.json.JsonOutput().toJson([status: statusOut, message: messageOut])
render contentType: "application/json", data: replyJson
}
private getHttpHeaders(headers) {
def obj = [:]
new String(headers.decodeBase64()).split("\r\n")?.each {param ->
def nameAndValue = param.split(":")
obj[nameAndValue[0]] = (nameAndValue.length == 1) ? "" : nameAndValue[1].trim()
}
return obj
}
def lanEventHandler(evt) {
// log.trace "lanEventHandler..."
try {
def evtData = parseLanMessage(evt?.description?.toString())
Map headers = evtData?.headers
def slurper = new groovy.json.JsonSlurper()
def body = evtData?.body ? slurper?.parseText(evtData?.body as String) : null
// log.trace "lanEventHandler... | headers: ${headerMap}"
// log.debug "headers: $headers"
// log.debug "body: $body"
Map msgData = [:]
if (headers?.size()) {
String evtSrc = (headers?.evtsource || body?.evtsource) ? (headers?.evtsource ?: body?.evtsource) : null
if (evtSrc && evtSrc?.startsWith("Homebridge_${pluginName()}_${app?.getId()}")) {
String evtType = (headers?.evttype || body?.evttype) ? (headers?.evttype ?: body?.evttype) : null
if (body && evtType) {
switch(evtType) {
case "hkCommand":
// log.trace "hkCommand($msgData)"
def val1 = body?.values?.value1 ?: null
def val2 = body?.values?.value2 ?: null
processCmd(body?.deviceid, body?.command, val1, val2, true)
break
case "enableDirect":
// log.trace "enableDirect($msgData)"
state?.pluginDetails = [
directIP: body?.ip,
directPort: body?.port,
version: body?.version ?: null
]
updCodeVerMap("plugin", body?.version ?: null)
activateDirectUpdates(true)
break
case "attrUpdStatus":
// if(body?.evtStatus && body?.evtStatus != "OK") { log.warn "Attribute Update Failed | Device: ${body?.evtDevice} | Attribute: ${body?.evtAttr}" }
break
default:
break
}
}
}
}
} catch (ex) {
logError "lanEventHandler Exception:", ex
}
}
def deviceCommand() {
// log.info("Command Request: $params")
def val1 = request?.JSON?.value1 ?: null
def val2 = request?.JSON?.value2 ?: null
processCmd(params?.id, params?.command, val1, val2)
}
private processCmd(devId, cmd, value1, value2, local=false) {
logInfo("Process Command${local ? "(LOCAL)" : ""} | DeviceId: $devId | Command: ($cmd)${value1 ? " | Param1: ($value1)" : ""}${value2 ? " | Param2: ($value2)" : ""}")
def device = findDevice(devId)
def command = cmd
if(settings?.addSecurityDevice != false && devId == "alarmSystemStatus_${location?.id}") {
setSecurityMode(command)
CommandReply("Success", "Security Alarm, Command $command")
} else if (settings?.modeList && command == "mode" && devId) {
logDebug("Virtual Mode Received: ${devId}")
changeMode(devId)
CommandReply("Success", "Mode Device, Command $command")
} else if (settings?.routineList && command == "routine" && devId) {
logDebug("Virtual Routine Received: ${devId}")
runRoutine(devId)
CommandReply("Success", "Routine Device, Command $command")
} else {
if (!device) {
logError("Device Not Found")
CommandReply("Failure", "Device Not Found")
} else if (!device?.hasCommand(command as String)) {
logError("Device ${device.displayName} does not have the command $command")
CommandReply("Failure", "Device ${device.displayName} does not have the command $command")
} else {
try {
if (value2 != null) {
device?."$command"(value1,value2)
logInfo("Command Successful for Device ${device.displayName} | Command ${command}($value1, $value2)")
} else if (value1 != null) {
device?."$command"(value1)
logInfo("Command Successful for Device ${device.displayName} | Command ${command}($value1)")
} else {
device?."$command"()
logInfo("Command Successful for Device ${device.displayName} | Command ${command}()")
}
CommandReply("Success", "Device ${device.displayName} | Command ${command}()")
logCmd([cmd: command, device: device?.displayName, value1: value1, value2: value2])
} catch (e) {
logError("Error Occurred for Device ${device.displayName} | Command ${command}()")
CommandReply("Failure", "Error Occurred For Device ${device.displayName} | Command ${command}()")
}
}
}
}
def changeMode(modeId) {
if(modeId) {
def mode = findVirtModeDevice(modeId)
if(mode) {
logInfo("Setting the Location Mode to (${mode})...")
setLocationMode(mode)
state?.lastMode = mode
} else { logError("Unable to find a matching mode for the id: ${modeId}") }
}
}
def runRoutine(rtId) {
if(rtId) {
def rt = findVirtRoutineDevice(rtId)
if(rt?.label) {
logInfo("Executing the (${rt?.label}) Routine...")
location?.helloHome?.execute(rt?.label)
} else { logError("Unable to find a matching routine for the id: ${rtId}") }
}
}
def deviceAttribute() {
def device = findDevice(params?.id)
def attribute = params?.attribute
if (!device) {
httpError(404, "Device not found")
} else {
return [currentValue: device?.currentValue(attribute)]
}
}
def findVirtModeDevice(id) {
return getModeById(id) ?: null
}
def findVirtRoutineDevice(id) {
return getRoutineById(id) ?: null
}
def deviceQuery() {
log.trace "deviceQuery(${params?.id}"
def device = findDevice(params?.id)
if (!device) {
def mode = findVirtModeDevice(params?.id)
def routine = findVirtModeDevice(params?.id)
def obj = mode ? mode : routine ?: null
if(!obj) {
device = null
httpError(404, "Device not found")
} else {
def name = routine ? obj?.label : obj?.name
def type = routine ? "Routine" : "Mode"
def attrVal = routine ? "off" : modeSwitchState(obj?.name)
try {
deviceData?.push([
name: name,
deviceid: params?.id,
capabilities: ["${type}": 1],
commands: [on:1],
attributes: ["switch": attrVal]
])
} catch (e) {
logError("Error Occurred Parsing ${item} ${type} ${name}, Error: ${ex}")
}
}
}
if (result) {
def jsonData = [
name: device.displayName,
deviceid: device.id,
capabilities: deviceCapabilityList(device),
commands: deviceCommandList(device),
attributes: deviceAttributeList(device)
]
def resultJson = new groovy.json.JsonOutput().toJson(jsonData)
render contentType: "application/json", data: resultJson
}
}
def deviceCapabilityList(device) {
String devId = device?.getId()
def capItems = device?.capabilities?.findAll{ !(it?.name in ignoreLists()?.capabilities) }?.collectEntries { capability-> [ (capability?.name as String):1 ] }
if(isDeviceInInput("lightList", device?.id)) {
capItems["LightBulb"] = 1
}
if(isDeviceInInput("buttonList", device?.id)) {
capItems["Button"] = 1
}
if(isDeviceInInput("fanList", device?.id)) {
capItems["Fan"] = 1
}
if(isDeviceInInput("speakerList", device?.id)) {
capItems["Speaker"] = 1
}
if(isDeviceInInput("shadesList", device?.id)) {
capItems["Window Shade"] = 1
}
if(isDeviceInInput("garageList", device?.id)) {
capItems["Garage Door Control"] = 1
}
if(isDeviceInInput("tstatList", device?.id)) {
capItems["Thermostat"] = 1
capItems["Thermostat Operating State"] = 1
}
if(isDeviceInInput("tstatHeatList", device?.id)) {
capItems["Thermostat"] = 1
capItems["Thermostat Operating State"] = 1
capItems?.remove("Thermostat Cooling Setpoint")
}
if(settings?.noTemp && capItems["Temperature Measurement"] && (capItems["Contact Sensor"] || capItems["Water Sensor"])) {
Boolean remTemp = true
if(settings?.sensorAllowTemp && isDeviceInInput("sensorAllowTemp", device?.id)) remTemp = false
if(remTemp) { capItems?.remove("Temperature Measurement") }
}
//This will filter out selected capabilities from the devices selected in filtering inputs.
Map remCaps = [
"Acceleration": "Acceleration Sensor", "Battery": "Battery", "Button": "Button", "Contact": "Contact Sensor", "Energy": "Energy Meter", "Humidity": "Relative Humidity Measurement",
"Illuminance": "Illuminance Measurement", "Level": "Switch Level", "Lock": "Lock", "Motion": "Motion Sensor", "Power": "Power Meter", "Presence": "Presence Sensor", "Switch": "Switch",
"Tamper": "Tamper Alert", "Temp": "Temperature Measurement", "Valve": "Valve"
]
List remKeys = settings?.findAll { it?.key?.toString()?.startsWith("remove") && it?.value != null }?.collect { it?.key as String } ?: []
remKeys?.each { k->
String capName = k?.replaceAll("remove", "")
if(remCaps[capName] && capItems[remCaps[capName]] && isDeviceInInput(k, device?.id)) { capItems?.remove(remCaps[capName]); if(showDebugLogs) { logDebug("Filtering ${capName}"); } }
}
return capItems
}
def deviceCommandList(device) {
def cmds = device?.supportedCommands?.findAll { !(it?.name in ignoreLists()?.commands) }?.collectEntries { c-> [ (c?.name): 1 ] }
if(isDeviceInInput("tstatHeatList", device?.id)) { cmds?.remove("setCoolingSetpoint"); cmds?.remove("auto"); cmds?.remove("cool"); }
return cmds
}
def deviceAttributeList(device) {
def atts = device?.supportedAttributes?.findAll { !(it?.name in ignoreLists()?.attributes) }?.collectEntries { attribute->
try {
[(attribute?.name): device?.currentValue(attribute?.name)]
} catch(e) {
[(attribute?.name): null]
}
}
if(isDeviceInInput("tstatHeatList", device?.id)) { atts?.remove("coolingSetpoint"); atts?.remove("coolingSetpointRange"); }
return atts
}
String getAppEndpointUrl(subPath) { return "${apiServerUrl("/api/smartapps/installations/${app.id}${subPath ? "/${subPath}" : ""}?access_token=${atomicState?.accessToken}")}" }
def getAllData() {
state?.subscriptionRenewed = now()
state?.devchanges = []
def deviceJson = new groovy.json.JsonOutput().toJson([location: renderLocation(), deviceList: renderDevices()])
updTsVal("lastDeviceDataQueryDt")
render contentType: "application/json", data: deviceJson
}
def checkForMissedRegistration() {
def mr = atomicState?.pendingDeviceRegistrations ?: []
}
Map deviceSettingKeys() {
return [
"fanList": "Fan Devices", "fan3SpdList": "Fans (3Spd) Devices", "fan4SpdList": "Fans (4Spd) Devices", "buttonList": "Button Devices", "deviceList": "Other Devices",
"sensorList": "Sensor Devices", "speakerList": "Speaker Devices", "switchList": "Switch Devices", "lightList": "Light Devices", "shadesList": "Window Shade Devices",
"garageList": "Garage Devices", "tstatList":"T-Stat Devices", "tstatHeatList": "T-Stat Devices (Heat)"
]
}
private registerDevicesTest() {
def strtDt = now()
Boolean done = false
Boolean sched = false
List keysToRegister = atomicState?.pendingDeviceRegistrations ?: []
Integer regRnd = atomicState?.pendingDeviceRegistrationRnd ?: 1
if(!keysToRegister?.size()) {
deviceSettingKeys()?.each { k,v ->
if(settings?."${k}"?.size()>0) keysToRegister?.push(k)
}
}
if(keysToRegister?.size()) {
List keyToRemove = []
List devItems = []
log.trace "(${keysToRegister?.size()}) Device Groups Pending Event Registration..."
keysToRegister?.each { key->
if(devItems?.size() > 30) {
sched = true
} else {
settings?."${key}"?.each { dev->
devItems?.push(dev)
registerChangeHandler(dev)
}
keyToRemove?.push(key)
}
}
keysToRegister -= keyToRemove
if(sched) {
log.trace "Device Registration Round (${regRnd}) Completed | Registered (${devItems?.size()}) Devices | Starting Next Round in 4 seconds... | Process Time: (${now()-strtDt}ms)"
atomicState?.pendingDeviceRegistrations = keysToRegister
atomicState?.pendingDeviceRegistrationRnd = regRnd+1
runIn(3, "registerDevices")
} else {
done = true
log.trace "Device Registration Round (${regRnd}) Completed | Registered (${devItems?.size()}) Devices... | Process Time: (${now()-strtDt}ms)"
}
}
if(done) {
log.trace "Device Registration Process Completed | Registered (${getDeviceCnt(true)} Devices) | Process Time: (${now()-strtDt}ms) | Rounds: ${atomicState?.pendingDeviceRegistrationRnd}"
log.info "-----------------------------------------------"
unschedule("registerDevices")
state?.remove("pendingDeviceRegistrations");
state?.remove("pendingDeviceRegistrationRnd")
if(settings?.restartService == true) {
logWarn("Sent Request to Homebridge Service to Stop... Service should restart automatically")
attemptServiceRestart()
settingUpdate("restartService", "false", "bool")
}
runIn(10, "updateServicePrefs")
runIn(15, "sendDeviceRefreshCmd")
}
}
def registerDevices() {
//This has to be done at startup because it takes too long for a normal command.
["lightList": "Light Devices", "fanList": "Fan Devices", "fan3SpdList": "Fans (3SPD) Devices", "fan4SpdList": "Fans (4SPD) Devices", "buttonList": "Button Devices"]?.each { k,v->
logDebug("Registering (${settings?."${k}"?.size() ?: 0}) ${v}")
registerChangeHandler(settings?."${k}")
}
runIn(3, "registerDevices2")
}
def registerDevices2() {
//This has to be done at startup because it takes too long for a normal command.
["sensorList": "Sensor Devices", "speakerList": "Speaker Devices", "deviceList": "Other Devices"]?.each { k,v->
logDebug("Registering (${settings?."${k}"?.size() ?: 0}) ${v}")
registerChangeHandler(settings?."${k}")
}
runIn(3, "registerDevices3")
}
def registerDevices3() {
//This has to be done at startup because it takes too long for a normal command.
["switchList": "Switch Devices", "shadesList": "Window Shade Devices", "garageList": "Garage Door Devices", "tstatList": "Thermostat Devices", "tstatHeatList": "Thermostat (HeatOnly) Devices"]?.each { k,v->
logDebug("Registering (${settings?."${k}"?.size() ?: 0}) ${v}")
registerChangeHandler(settings?."${k}")
}
logDebug("Registered (${getDeviceCnt(true)} Devices)")
logDebug("-----------------------------------------------")
if(settings?.restartService == true) {
logWarn("Sent Request to Homebridge Service to Stop... Service should restart automatically")
attemptServiceRestart()
settingUpdate("restartService", "false", "bool")
}
runIn(5, "updateServicePrefs")
runIn(8, "sendDeviceRefreshCmd")
}
Boolean isDeviceInInput(setKey, devId) {
if(settings[setKey]) {
return (settings[setKey]?.find { it?.getId() == devId })
}
return false
}
def registerChangeHandler(devices, showlog=false) {
devices?.each { device ->
List theAtts = device?.supportedAttributes?.collect { it?.name as String }?.unique()
if(showlog) { log.debug "atts: ${theAtts}" }
theAtts?.each {att ->
if(!(ignoreLists()?.evt_attributes?.contains(att))) {
if(settings?.noTemp && att == "temperature" && (device?.hasAttribute("contact") || device?.hasAttribute("water"))) {
Boolean skipAtt = true
if(settings?.sensorAllowTemp) {
skipAtt = isDeviceInInput('sensorAllowTemp', device?.id)
}
if(skipAtt) { return }
}
Map attMap = [
"acceleration": "Acceleration", "battery": "Battery", "button": "Button", "contact": "Contact", "energy": "Energy", "humidity": "Humidity", "illuminance": "Illuminance",
"level": "Level", "lock": "Lock", "motion": "Motion", "power": "Power", "presence": "Presence", "switch": "Switch", "tamper": "Tamper",
"temperature": "Temp", "valve": "Valve"
]?.each { k,v -> if(att == k && isDeviceInInput('remove${v}', device?.id)) { return } }
subscribe(device, att, "changeHandler")
if(showlog) { log.debug "Registering ${device?.displayName} for ${att} events" }
}
}
}
}
def changeHandler(evt) {
def sendItems = []
def sendNum = 1
def src = evt?.source
def deviceid = evt?.deviceId
def deviceName = evt?.displayName
def attr = evt?.name
def value = evt?.value
def dt = evt?.date
def sendEvt = true
switch(evt?.name) {
case "hsmStatus":
deviceid = "alarmSystemStatus_${location?.id}"
attr = "alarmSystemStatus"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
break
case "hsmAlert":
if(evt?.value == "intrusion") {
deviceid = "alarmSystemStatus_${location?.id}"
attr = "alarmSystemStatus"
value = "alarm_active"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
} else { sendEvt = false }
break
case "hsmRules":
case "hsmSetArm":
sendEvt = false
break
case "alarmSystemStatus":
deviceid = "alarmSystemStatus_${location?.id}"
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt])
break
case "mode":
settings?.modeList?.each { id->
def md = getModeById(id)
if(md && md?.id) { sendItems?.push([evtSource: "MODE", evtDeviceName: "Mode - ${md?.name}", evtDeviceId: md?.id, evtAttr: "switch", evtValue: modeSwitchState(md?.name), evtUnit: "", evtDate: dt]) }
}
break
case "routineExecuted":
settings?.routineList?.each { id->
def rt = getRoutineById(id)
if(rt && rt?.id) {
sendItems?.push([evtSource: "ROUTINE", evtDeviceName: "Routine - ${rt?.label}", evtDeviceId: rt?.id, evtAttr: "switch", evtValue: "off", evtUnit: "", evtDate: dt])
}
}
break
default:
def evtData = null
if(attr == "button") { evtData = parseJson(evt?.data) }
sendItems?.push([evtSource: src, evtDeviceName: deviceName, evtDeviceId: deviceid, evtAttr: attr, evtValue: value, evtUnit: evt?.unit ?: "", evtDate: dt, evtData: evtData])
break
}
if (sendEvt && state?.pluginDetails?.directIP != "" && sendItems?.size()) {
//Send Using the Direct Mechanism
sendItems?.each { send->
if(settings?.showEventLogs) {
String unitStr = ""
switch(send?.evtAttr as String) {
case "temperature":
unitStr = "\u00b0${send?.evtUnit}"
break
case "humidity":
case "level":
case "battery":
unitStr = "%"
break
case "power":
unitStr = "W"
break
case "illuminance":
unitStr = " Lux"
break
default:
unitStr = "${send?.evtUnit}"
break
}
logDebug("Sending${" ${send?.evtSource}" ?: ""} Event (${send?.evtDeviceName} | ${send?.evtAttr.toUpperCase()}: ${send?.evtValue}${unitStr}) ${send?.evtData ? "Data: ${send?.evtData}" : ""} to Homebridge at (${state?.pluginDetails?.directIP}:${state?.pluginDetails?.directPort})")
}
sendHttpPost("update", [
change_name: send?.evtDeviceName,
change_device: send?.evtDeviceId,
change_attribute: send?.evtAttr,
change_value: send?.evtValue,
change_data: send?.evtData,
change_date: send?.evtDate,
app_id: app?.getId(),
access_token: atomicState?.accessToken
])
logEvt([name: send?.evtAttr, value: send?.evtValue, device: send?.evtDeviceName])
}
}
}
private sendHttpGet(path, contentType) {
if(settings?.sendViaNgrok && settings?.ngrokHttpUrl) {
httpGet([
uri: "https://${settings?.ngrokHttpUrl}.ngrok.io",
path: "/${path}",
contentType: contentType
])
} else { sendHubCommand(new physicalgraph.device.HubAction(method: "GET", path: "/${path}", headers: [HOST: getServerAddress()])) }
}
private sendHttpPost(path, body, contentType = "application/json") {
if(settings?.sendViaNgrok && settings?.ngrokHttpUrl) {
Map params = [
uri: "https://${settings?.ngrokHttpUrl}.ngrok.io",
path: "/${path}",
contentType: contentType,
body: body
]
httpPost(params)
} else {
Map params = [
method: "POST",
path: "/${path}",
headers: [
HOST: getServerAddress(),
'Content-Type': contentType
],
body: body
]
def result = new physicalgraph.device.HubAction(params)
sendHubCommand(result)
}
}
private getServerAddress() { return "${state?.pluginDetails?.directIP}:${state?.pluginDetails?.directPort}" }
def getModeById(String mId) {
return location?.modes?.find{it?.id?.toString() == mId}
}
def getRoutineById(String rId) {
return location?.helloHome?.getPhrases()?.find{it?.id == rId}
}
def getModeByName(String name) {
return location?.modes?.find{it?.name?.toString() == name}
}
def getRoutineByName(String name) {
return location?.helloHome?.getPhrases()?.find{it?.label == name}
}
def getShmIncidents() {
//Thanks Adrian
def incidentThreshold = now() - 604800000
return location.activeIncidents.collect{[date: it?.date?.time, title: it?.getTitle(), message: it?.getMessage(), args: it?.getMessageArgs(), sourceType: it?.getSourceType()]}.findAll{ it?.date >= incidentThreshold } ?: null
}
void settingUpdate(name, value, type=null) {
if(name && type) {
app?.updateSetting("$name", [type: "$type", value: value])
}
else if (name && type == null){ app?.updateSetting(name.toString(), value) }
}
void settingRemove(String name) {
if(name && settings?.containsKey(name as String)) { app?.deleteSetting(name as String) }
}
Boolean devMode() {
return (appSettings?.devMode?.toString() == "true")
}
private activateDirectUpdates(isLocal=false) {
logTrace("activateDirectUpdates: ${getServerAddress()}${isLocal ? " | (Local)" : ""}")
sendHttpPost("initial", [
app_id: app?.getId(),
access_token: atomicState?.accessToken
])
}
private attemptServiceRestart(isLocal=false) {
logTrace("attemptServiceRestart: ${getServerAddress()}${isLocal ? " | (Local)" : ""}")
sendHttpPost("restart", [
app_id: app?.getId(),
access_token: atomicState?.accessToken
])
}
private sendDeviceRefreshCmd(isLocal=false) {
logTrace("sendDeviceRefreshCmd: ${getServerAddress()}${isLocal ? " | (Local)" : ""}")
sendHttpPost("refreshDevices", [
app_id: app?.getId(),
access_token: atomicState?.accessToken
])
}
private updateServicePrefs(isLocal=false) {
logTrace("updateServicePrefs: ${getServerAddress()}${isLocal ? " | (Local)" : ""}")
sendHttpPost("updateprefs", [
app_id: app?.getId(),
access_token: atomicState?.accessToken,
local_commands: false, //(settings?.sendCmdViaHubaction != false),
local_hub_ip: location?.hubs[0]?.localIP
])
}
def pluginStatus() {
def body = request?.JSON;
state?.pluginUpdates = [hasUpdate: (body?.hasUpdate == true), newVersion: (body?.newVersion ?: null)]
if(body?.version) { updCodeVerMap("plugin", body?.version)}
def resultJson = new groovy.json.JsonOutput().toJson([status: 'OK'])
render contentType: "application/json", data: resultJson
}
def enableDirectUpdates() {
// log.trace "enableDirectUpdates: ($params)"
state?.pluginDetails = [
directIP: params?.ip,
directPort: params?.port,
version: params?.version ?: null
]
updCodeVerMap("plugin", params?.version ?: null)
activateDirectUpdates()
updTsVal("lastDirectUpdsEnabled")
def resultJson = new groovy.json.JsonOutput().toJson([status: 'OK'])
render contentType: "application/json", data: resultJson
}
mappings {
if (!params?.access_token || (params?.access_token && params?.access_token != atomicState?.accessToken)) {
path("/devices") { action: [GET: "authError"] }
path("/config") { action: [GET: "authError"] }
path("/location") { action: [GET: "authError"] }
path("/pluginStatus") { action: [POST: "authError"] }
path("/:id/command/:command") { action: [POST: "authError"] }
path("/:id/query") { action: [GET: "authError"] }
path("/:id/attribute/:attribute") { action: [GET: "authError"] }
path("/startDirect/:ip/:port/:version") { action: [GET: "authError"] }
} else {
path("/devices") { action: [GET: "getAllData"] }
path("/config") { action: [GET: "renderConfig"] }
path("/deviceDebug") { action: [GET: "viewDeviceDebug"] }
path("/location") { action: [GET: "renderLocation"] }
path("/pluginStatus") { action: [POST: "pluginStatus"] }
path("/:id/command/:command") { action: [POST: "deviceCommand"] }
path("/:id/query") { action: [GET: "deviceQuery"] }
path("/:id/attribute/:attribute") { action: [GET: "deviceAttribute"] }
path("/startDirect/:ip/:port/:version") { action: [POST: "enableDirectUpdates"] }
}
}
def appInfoSect() {
Map codeVer = state?.codeVersions ?: null
Boolean isNote = false
section() {
String str = "Version: v${appVersion()}"
str += state?.pluginDetails?.version ? "\nPlugin: v${state?.pluginDetails?.version}" : ""
str += (state?.pluginDetails?.version && state?.pluginUpdates) ? ((state?.pluginUpdates?.hasUpdate == true) ? "\nUpdate Available: (v${state?.pluginUpdates?.newVersion})" : "") : ""
href "changeLogPage", title: "${app?.name}", description: str, image: appIconUrl()
Map minUpdMap = getMinVerUpdsRequired()
List codeUpdItems = codeUpdateItems(true)
if(minUpdMap?.updRequired && minUpdMap?.updItems?.size()) {
isNote=true
String str3 = "Updates Required:"
minUpdMap?.updItems?.each { item-> str3 += bulletItem(str3, item) }
paragraph str3, required: true, state: null
paragraph "If you just updated the code please press Done/Save to let the app process the changes.", required: true, state: null
} else if(codeUpdItems?.size()) {
isNote=true
String str2 = "Code Updates Available:"
codeUpdItems?.each { item-> str2 += bulletItem(str2, item) }
paragraph str2, required: true, state: null
}
if(!isNote) { paragraph "No Issues to Report" }
}
}
/**********************************************
APP HELPER FUNCTIONS
***********************************************/
String bulletItem(String inStr, String strVal) { return "${inStr == "" ? "" : "\n"} \u2022 ${strVal}" }
String dashItem(String inStr, String strVal, newLine=false) { return "${(inStr == "" && !newLine) ? "" : "\n"} - ${strVal}" }
String textDonateLink() { return "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVFJTG8H86SK8&source=url" }
Integer versionStr2Int(str) { return str ? str?.toString()?.tokenize("-")[0]?.replaceAll("\\.", "")?.toInteger() : null }
String versionCleanup(str) { return str ? str?.toString()?.tokenize("-")[0] : null }
Boolean codeUpdIsAvail(String newVer, String curVer, String type) {
Boolean result = false
def latestVer
if(newVer && curVer) {
newVer = versionCleanup(newVer)
curVer = versionCleanup(curVer)
List versions = [newVer, curVer]
if(newVer != curVer) {
latestVer = versions?.max { a, b ->
List verA = a?.tokenize('.'); List verB = b?.tokenize('.'); Integer commonIndices = Math.min(verA?.size(), verB?.size());
for (int i = 0; i < commonIndices; ++i) { if(verA[i]?.toInteger() != verB[i]?.toInteger()) { return verA[i]?.toInteger() <=> verB[i]?.toInteger() }; }
verA?.size() <=> verB?.size()
}
result = (latestVer == newVer)
}
}
return result
}
Boolean appUpdAvail() { return (state?.appData?.versions && state?.codeVersions?.mainApp && codeUpdIsAvail(state?.appData?.versions?.mainApp, appVersion(), "main_app")) }
Boolean pluginUpdAvail() { return (state?.appData?.versions && state?.codeVersions?.plugin && codeUpdIsAvail(state?.appData?.versions?.plugin, state?.codeVersions?.plugin, "plugin")) }
private Map getMinVerUpdsRequired() {
Boolean updRequired = false
List updItems = []
Map codeItems = [plugin: "Homebridge Plugin"]
Map codeVers = state?.codeVersions ?: [:]
codeVers?.each { k,v->
try {
if(codeItems?.containsKey(k as String) && v != null && (versionStr2Int(v) < minVersions()[k as String])) { updRequired = true; updItems?.push(codeItems[k]); }
} catch (ex) {
logError("getMinVerUpdsRequired Error: ${ex}")
}
}
return [updRequired: updRequired, updItems: updItems]
}
private List codeUpdateItems(shrt=false) {
Boolean appUpd = appUpdAvail()
Boolean plugUpd = pluginUpdAvail()
List updItems = []
if(appUpd || servUpd) {
if(appUpd) updItems.push("${!shrt ? "\nHomebridge " : ""}App: (v${state?.appData?.versions?.mainApp?.toString()})")
if(plugUpd) updItems.push("${!shrt ? "\n" : ""}Plugin: (v${state?.appData?.versions?.server?.toString()})")
}
return updItems
}
Integer getLastTsValSecs(val, nullVal=1000000) {
def tsMap = atomicState?.tsDtMap
return (val && tsMap && tsMap[val]) ? GetTimeDiffSeconds(tsMap[val]).toInteger() : nullVal
}
private updTsVal(key, dt=null) {
def data = atomicState?.tsDtMap ?: [:]
if(key) { data[key] = dt ?: getDtNow() }
atomicState?.tsDtMap = data
}
private remTsVal(key) {
def data = atomicState?.tsDtMap ?: [:]
if(key) {
if(key instanceof List) {
key?.each { k-> if(data?.containsKey(k)) { data?.remove(k) } }
} else { if(data?.containsKey(key)) { data?.remove(key) } }
atomicState?.tsDtMap = data
}
}
private getTsVal(val) {
def tsMap = atomicState?.tsDtMap
if(val && tsMap && tsMap[val]) { return tsMap[val] }
return null
}
private updCodeVerMap(key, val) {
Map cv = atomicState?.codeVersions ?: [:]
if(val && (!cv.containsKey(key) || (cv?.containsKey(key) && cv[key] != val))) { cv[key as String] = val }
if (cv?.containsKey(key) && val == null) { cv?.remove(key) }
atomicState?.codeVersions = cv
}
private cleanUpdVerMap() {
Map cv = atomicState?.codeVersions ?: [:]
cv?.each { k, v-> if(v == null) ri?.push(k) }
ri?.each { cv?.remove(it) }
atomicState?.codeVersions = cv
}
private updInstData(key, val) {
Map iData = atomicState?.installData ?: [:]
iData[key] = val
atomicState?.installData = iData
}
private getInstData(key) {
def iMap = atomicState?.installData
if(val && iMap && iMap[val]) { return iMap[val] }
return null
}
private checkVersionData(now = false) { //This reads a JSON file from GitHub with version numbers
def lastUpd = getLastTsValSecs("lastAppDataUpdDt")
if (now || !state?.appData || (lastUpd > (3600*6))) {
if(now && (lastUpd < 300)) { return }
getConfigData()
}
}
private getConfigData() {
Map params = [
uri: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/master/appData.json",
contentType: "application/json"
]
def data = getWebData(params, "appData", false)
if(data) {
state?.appData = data
updTsVal("lastAppDataUpdDt")
logDebug("Successfully Retrieved (v${data?.appDataVer}) of AppData Content from GitHub Repo...")
}
}
private getWebData(params, desc, text=true) {
try {
httpGet(params) { resp ->
if(resp?.data) {
if(text) { return resp?.data?.text.toString() }
return resp?.data
}
}
} catch (ex) {
incrementCntByKey("appErrorCnt")
if(ex instanceof groovyx.net.http.HttpResponseException) {
logWarn("${desc} file not found")
} else { logError("getWebData(params: $params, desc: $desc, text: $text) Exception: ${ex}") }
return "${label} info not found"
}
}
/******************************************
| DATE | TIME HELPERS
******************************************/
def formatDt(dt, tzChg=true) {
def tf = new java.text.SimpleDateFormat("E MMM dd HH:mm:ss z yyyy")
if(tzChg) { if(location.timeZone) { tf.setTimeZone(location?.timeZone) } }
return tf?.format(dt)
}
def getDtNow() {
def now = new Date()
return formatDt(now)
}
def GetTimeDiffSeconds(lastDate, sender=null) {
try {
if(lastDate?.contains("dtNow")) { return 10000 }
def now = new Date()
def lastDt = Date.parse("E MMM dd HH:mm:ss z yyyy", lastDate)
def start = Date.parse("E MMM dd HH:mm:ss z yyyy", formatDt(lastDt)).getTime()
def stop = Date.parse("E MMM dd HH:mm:ss z yyyy", formatDt(now)).getTime()
def diff = (int) (long) (stop - start) / 1000
return diff?.abs()
} catch (ex) {
logError("GetTimeDiffSeconds Exception: (${sender ? "$sender | " : ""}lastDate: $lastDate): ${ex}")
return 10000
}
}
/******************************************
| Changelog Logic
******************************************/
Boolean showDonationOk() { return (state?.isInstalled && !atomicState?.installData?.shownDonation && getDaysSinceUpdated() >= 30 && !settings?.sentDonation) }
Integer getDaysSinceUpdated() {
def updDt = atomicState?.installData?.updatedDt ?: null
if(updDt == null || updDt == "Not Set") {
updInstData("updatedDt", getDtNow().toString())
return 0
}
def start = Date.parse("E MMM dd HH:mm:ss z yyyy", updDt)
def stop = new Date()
if(start && stop) { return (stop - start) }
return 0
}
String changeLogData() { return getWebData([uri: "https://raw.githubusercontent.com/tonesto7/homebridge-smartthings-v2/master/CHANGELOG-app.md", contentType: "text/plain; charset=UTF-8"], "changelog") }
Boolean showChgLogOk() { return (state?.isInstalled && (state?.curAppVer != appVersion() || state?.installData?.shownChgLog != true)) }
def changeLogPage() {
def execTime = now()
return dynamicPage(name: "changeLogPage", title: "", nextPage: "mainPage", install: false) {
section() {
paragraph title: "Release Notes", "", state: "complete", image: getAppImg("change_log")
paragraph changeLogData()
}
state?.curAppVer = appVersion()
updInstData("shownChgLog", true)
}
}
Integer stateSize() { def j = new groovy.json.JsonOutput().toJson(state); return j?.toString().length(); }
Integer stateSizePerc() { return (int) ((stateSize() / 100000)*100).toDouble().round(0); }
private addToHistory(String logKey, data, Integer max=10) {
Boolean ssOk = (stateSizePerc() > 70)
List eData = atomicState[logKey as String] ?: []
if(eData?.find { it?.data == data }) { return; }
eData?.push([dt: getDtNow(), data: data])
if(!ssOk || eData?.size() > max) { eData = eData?.drop( (eData?.size()-max) ) }
atomicState[logKey as String] = eData
}
private logDebug(msg) { if(showDebugLogs) { logToServer(msg, "debug"); log.debug "Homebridge (v${appVersion()}) | ${msg}"; } }
private logInfo(msg) { logToServer(msg, "info"); log.info " Homebridge (v${appVersion()}) | ${msg}"; }
private logTrace(msg) { logToServer(msg, "trace"); log.trace "Homebridge (v${appVersion()}) | ${msg}"; }
private logWarn(msg) { logToServer(msg, "warn"); log.warn " Homebridge (v${appVersion()}) | ${msg}"; }
private logError(msg) { logToServer(msg, "error"); log.error "Homebridge (v${appVersion()}) | ${msg}"; }
public String getLogServerAddr() { return appSettings?.log_address ?: null }
public logToServer(msg, lvl) {
String addr = parent ? parent?.getLogServerAddr() : getLogServerAddr()
if(addr) {
Map params = [
method: "POST",
path: "/gelf",
headers: [
HOST: addr,
'Content-Type': "application/json"
],
body: [short_message: msg, logLevel: lvl, host: "SmartThings (HomebridgeV2)"]
]
params?.body?.appVersion = appVersion(); params?.body?.appName = app?.getName(); params?.body?.appLabel = app?.getLabel();
// params?.body?.devVersion = devVersion(); params?.body?.deviceHandler = device?.getName(); params?.body?.deviceName = device?.displayName;
def result = new physicalgraph.device.HubAction(params)
sendHubCommand(result)
}
}
List getCmdHistory() { return atomicState?.cmdHistory ?: [] }
List getEvtHistory() { return atomicState?.evtHistory ?: [] }
void clearHistory() {
atomicState?.cmdHistory = []
atomicState?.evtHistory = []
}
private logEvt(evtData) { addToHistory("evtHistory", evtData, 15) }
private logCmd(cmdData) { addToHistory("cmdHistory", cmdData, 15) }
================================================
FILE: src/ST_Accessories.js
================================================
const knownCapabilities = require("./libs/Constants").knownCapabilities,
pluginVersion = require("./libs/Constants").pluginVersion,
_ = require("lodash"),
ServiceTypes = require("./ST_ServiceTypes"),
Transforms = require("./ST_Transforms"),
DeviceTypes = require("./ST_DeviceCharacteristics");
var Service, Characteristic, appEvts;
module.exports = class ST_Accessories {
constructor(platform) {
this.mainPlatform = platform;
appEvts = platform.appEvts;
this.logConfig = platform.logConfig;
this.configItems = platform.getConfigItems();
this.myUtils = platform.myUtils;
this.log = platform.log;
this.hap = platform.hap;
this.uuid = platform.uuid;
Service = platform.Service;
Characteristic = platform.Characteristic;
this.CommunityTypes = require("./libs/CommunityTypes")(Service, Characteristic);
this.client = platform.client;
this.comparator = this.comparator.bind(this);
this.transforms = new Transforms(this, Characteristic);
this.serviceTypes = new ServiceTypes(this, Service);
this.device_types = new DeviceTypes(this, Characteristic);
this._accessories = {};
this._buttonMap = {};
this._attributeLookup = {};
}
initializeAccessory(accessory, fromCache = false) {
if (!fromCache) {
accessory.deviceid = accessory.context.deviceData.deviceid;
accessory.name = accessory.context.deviceData.name;
accessory.context.deviceData.excludedCapabilities.forEach((cap) => {
if (cap !== undefined) {
this.log.debug(`Removing capability: ${cap} from Device: ${accessory.context.deviceData.name}`);
delete accessory.context.deviceData.capabilities[cap];
}
});
accessory.context.name = accessory.context.deviceData.name;
accessory.context.deviceid = accessory.context.deviceData.deviceid;
} else {
this.log.debug(`Initializing Cached Device ${accessory.context.deviceid}`);
accessory.deviceid = accessory.context.deviceid;
accessory.name = accessory.context.name;
}
try {
accessory.commandTimers = {};
accessory.commandTimersTS = {};
accessory.context.uuid = accessory.UUID || this.uuid.generate(`smartthings_v2_${accessory.deviceid}`);
accessory.getOrAddService = this.getOrAddService.bind(accessory);
accessory.getOrAddServiceByName = this.getOrAddServiceByName.bind(accessory);
accessory.getOrAddCharacteristic = this.getOrAddCharacteristic.bind(accessory);
accessory.hasCapability = this.hasCapability.bind(accessory);
accessory.getCapabilities = this.getCapabilities.bind(accessory);
accessory.hasAttribute = this.hasAttribute.bind(accessory);
accessory.hasCommand = this.hasCommand.bind(accessory);
accessory.hasDeviceFlag = this.hasDeviceFlag.bind(accessory);
accessory.hasService = this.hasService.bind(accessory);
accessory.hasCharacteristic = this.hasCharacteristic.bind(accessory);
accessory.updateDeviceAttr = this.updateDeviceAttr.bind(accessory);
accessory.updateCharacteristicVal = this.updateCharacteristicVal.bind(accessory);
accessory.manageGetCharacteristic = this.device_types.manageGetCharacteristic.bind(accessory);
accessory.manageGetSetCharacteristic = this.device_types.manageGetSetCharacteristic.bind(accessory);
accessory.sendCommand = this.sendCommand.bind(accessory);
return this.configureCharacteristics(accessory);
} catch (err) {
this.log.error(`initializeAccessory (fromCache: ${fromCache}) Error:`, err);
// console.error(err);
return accessory;
}
}
configureCharacteristics(accessory) {
for (let index in accessory.context.deviceData.capabilities) {
if (knownCapabilities.indexOf(index) === -1 && this.mainPlatform.unknownCapabilities.indexOf(index) === -1) this.mainPlatform.unknownCapabilities.push(index);
}
accessory.context.deviceGroups = [];
accessory.servicesToKeep = [];
accessory.reachable = true;
accessory.context.lastUpdate = new Date();
let accessoryInformation = accessory
.getOrAddService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.FirmwareRevision, accessory.context.deviceData.firmwareVersion)
.setCharacteristic(Characteristic.Manufacturer, accessory.context.deviceData.manufacturerName)
.setCharacteristic(Characteristic.Model, accessory.context.deviceData.modelName ? `${this.myUtils.toTitleCase(accessory.context.deviceData.modelName)}` : "Unknown")
.setCharacteristic(Characteristic.Name, accessory.context.deviceData.name)
.setCharacteristic(Characteristic.HardwareRevision, pluginVersion);
accessory.servicesToKeep.push(Service.AccessoryInformation.UUID);
if (!accessoryInformation.listeners("identify")) {
accessoryInformation.on("identify", function(paired, callback) {
this.log.info("%s - identify", accessory.displayName);
callback();
});
}
let svcTypes = this.serviceTypes.getServiceTypes(accessory);
if (svcTypes) {
svcTypes.forEach((svc) => {
if (svc.name && svc.type) {
this.log.debug(accessory.name, " | ", svc.name);
accessory.servicesToKeep.push(svc.type.UUID);
this.device_types[svc.name](accessory, svc.type);
}
});
} else {
throw "Unable to determine the service type of " + accessory.deviceid;
}
return this.removeUnusedServices(accessory);
}
processDeviceAttributeUpdate(change) {
// let that = this;
return new Promise((resolve) => {
let characteristics = this.getAttributeStoreItem(change.attribute, change.deviceid);
let accessory = this.getAccessoryFromCache(change);
// console.log(characteristics);
if (!characteristics || !accessory) resolve(false);
if (characteristics instanceof Array) {
characteristics.forEach((char) => {
accessory.context.deviceData.attributes[change.attribute] = change.value;
accessory.context.lastUpdate = new Date().toLocaleString();
switch (change.attribute) {
case "thermostatSetpoint":
char.getValue();
break;
case "button":
// console.log(characteristics);
var btnNum = change.data && change.data.buttonNumber ? change.data.buttonNumber : 1;
if (btnNum && accessory.buttonEvent !== undefined) {
accessory.buttonEvent(btnNum, change.value, change.deviceid, this._buttonMap);
}
break;
default:
char.updateValue(this.transforms.transformAttributeState(change.attribute, change.value, char.displayName));
break;
}
});
resolve(this.addAccessoryToCache(accessory));
} else {
resolve(false);
}
});
}
sendCommand(callback, acc, dev, cmd, vals) {
const id = `${cmd}`;
const tsNow = Date.now();
let d = 0;
let b = false;
let d2;
let o = {};
switch (cmd) {
case "setLevel":
case "setVolume":
case "setFanSpeed":
case "setSaturation":
case "setHue":
case "setColorTemperature":
case "setHeatingSetpoint":
case "setCoolingSetpoint":
case "setThermostatSetpoint":
d = 600;
d2 = 1500;
o.trailing = true;
break;
case "setThermostatMode":
d = 600;
d2 = 1500;
o.trailing = true;
break;
default:
b = true;
break;
}
if (b) {
appEvts.emit("event:device_command", dev, cmd, vals);
} else {
let lastTS = acc.commandTimersTS[id] && tsNow ? tsNow - acc.commandTimersTS[id] : undefined;
// console.log("lastTS: " + lastTS, ' | ts:', acc.commandTimersTS[id]);
if (acc.commandTimers[id] && acc.commandTimers[id] !== null) {
acc.commandTimers[id].cancel();
acc.commandTimers[id] = null;
// console.log('lastTS: ', lastTS, ' | now:', tsNow, ' | last: ', acc.commandTimersTS[id]);
// console.log(`Existing Command Found | Command: ${cmd} | Vals: ${vals} | Executing in (${d}ms) | Last Cmd: (${lastTS ? (lastTS/1000).toFixed(1) : "unknown"}sec) | Id: ${id} `);
if (lastTS && lastTS < d) {
d = d2 || 0;
}
}
acc.commandTimers[id] = _.debounce(
async() => {
acc.commandTimersTS[id] = tsNow;
appEvts.emit("event:device_command", dev, cmd, vals);
},
d,
o,
);
acc.commandTimers[id]();
}
if (callback) {
callback();
callback = undefined;
}
}
log_change(attr, char, acc, chgObj) {
if (this.logConfig.debug === true) this.log.notice(`[CHARACTERISTIC (${char}) CHANGE] ${attr} (${acc.displayName}) | LastUpdate: (${acc.context.lastUpdate}) | NewValue: (${chgObj.newValue}) | OldValue: (${chgObj.oldValue})`);
}
log_get(attr, char, acc, val) {
if (this.logConfig.debug === true) this.log.good(`[CHARACTERISTIC (${char}) GET] ${attr} (${acc.displayName}) | LastUpdate: (${acc.context.lastUpdate}) | Value: (${val})`);
}
log_set(attr, char, acc, val) {
if (this.logConfig.debug === true) this.log.warn(`[CHARACTERISTIC (${char}) SET] ${attr} (${acc.displayName}) | LastUpdate: (${acc.context.lastUpdate}) | Value: (${val})`);
}
hasCapability(obj) {
let keys = Object.keys(this.context.deviceData.capabilities);
if (keys.includes(obj) || keys.includes(obj.toString().replace(/\s/g, ""))) return true;
return false;
}
getCapabilities() {
return Object.keys(this.context.deviceData.capabilities);
}
hasAttribute(attr) {
return Object.keys(this.context.deviceData.attributes).includes(attr) || false;
}
hasCommand(cmd) {
return Object.keys(this.context.deviceData.commands).includes(cmd) || false;
}
getCommands() {
return Object.keys(this.context.deviceData.commands);
}
hasService(service) {
return this.services.map((s) => s.UUID).includes(service.UUID) || false;
}
hasCharacteristic(svc, char) {
let s = this.getService(svc) || undefined;
return (s && s.getCharacteristic(char) !== undefined) || false;
}
updateCharacteristicVal(svc, char, val) {
this.getOrAddService(svc).setCharacteristic(char, val);
}
updateCharacteristicProps(svc, char, props) {
this.getOrAddService(svc).getCharacteristic(char).setProps(props);
}
hasDeviceFlag(flag) {
return (this.context && this.context.deviceData && this.context.deviceData.deviceflags && Object.keys(this.context.deviceData.deviceflags).includes(flag)) || false;
}
updateDeviceAttr(attr, val) {
this.context.deviceData.attributes[attr] = val;
}
getOrAddService(svc) {
return this.getService(svc) || this.addService(svc);
}
getOrAddServiceByName(service, dName, sType) {
let svc = this.services.find((s) => s.displayName === dName);
if (svc) {
// console.log('service found');
return svc;
} else {
// console.log('service not found adding new one...');
svc = this.addService(new service(dName, sType));
return svc;
}
}
getOrAddCharacteristic(service, characteristic) {
return service.getCharacteristic(characteristic) || service.addCharacteristic(characteristic);
}
getServices() {
return this.services;
}
removeUnusedServices(acc) {
// console.log('servicesToKeep:', acc.servicesToKeep);
let newSvcUuids = acc.servicesToKeep || [];
let svcs2rmv = acc.services.filter((s) => !newSvcUuids.includes(s.UUID));
if (Object.keys(svcs2rmv).length) {
svcs2rmv.forEach((s) => {
acc.removeService(s);
this.log.info("Removing Unused Service:", s.UUID);
});
}
return acc;
}
storeCharacteristicItem(attr, devid, char) {
// console.log('storeCharacteristicItem: ', attr, devid, char);
if (!this._attributeLookup[attr]) {
this._attributeLookup[attr] = {};
}
if (!this._attributeLookup[attr][devid]) {
this._attributeLookup[attr][devid] = [];
}
this._attributeLookup[attr][devid].push(char);
}
getAttributeStoreItem(attr, devid) {
if (!this._attributeLookup[attr] || !this._attributeLookup[attr][devid]) {
return undefined;
}
return this._attributeLookup[attr][devid] || undefined;
}
removeAttributeStoreItem(attr, devid) {
if (!this._attributeLookup[attr] || !this._attributeLookup[attr][devid]) return;
delete this._attributeLookup[attr][devid];
}
getDeviceAttributeValueFromCache(device, attr) {
const key = this.getAccessoryId(device);
let result = this._accessories[key] ? this._accessories[key].context.deviceData.attributes[attr] : undefined;
this.log.info(`Attribute (${attr}) Value From Cache: [${result}]`);
return result;
}
getAccessoryId(accessory) {
const id = accessory.deviceid || accessory.context.deviceid || undefined;
return id;
}
getAccessoryFromCache(device) {
const key = this.getAccessoryId(device);
return this._accessories[key];
}
getAllAccessoriesFromCache() {
return this._accessories;
}
clearAccessoryCache() {
this.log.alert("CLEARING ACCESSORY CACHE AND FORCING DEVICE RELOAD");
this._accessories = {};
}
addAccessoryToCache(accessory) {
const key = this.getAccessoryId(accessory);
this._accessories[key] = accessory;
return true;
}
removeAccessoryFromCache(accessory) {
const key = this.getAccessoryId(accessory);
const _accessory = this._accessories[key];
delete this._accessories[key];
return _accessory;
}
forEach(fn) {
return _.forEach(this._accessories, fn);
}
intersection(devices) {
const accessories = _.values(this._accessories);
return _.intersectionWith(devices, accessories, this.comparator);
}
diffAdd(devices) {
const accessories = _.values(this._accessories);
return _.differenceWith(devices, accessories, this.comparator);
}
diffRemove(devices) {
const accessories = _.values(this._accessories);
return _.differenceWith(accessories, devices, this.comparator);
}
comparator(accessory1, accessory2) {
return this.getAccessoryId(accessory1) === this.getAccessoryId(accessory2);
}
clearAndSetTimeout(timeoutReference, fn, timeoutMs) {
if (timeoutReference) clearTimeout(timeoutReference);
return setTimeout(fn, timeoutMs);
}
};
================================================
FILE: src/ST_Client.js
================================================
const {
platformName,
platformDesc,
pluginVersion
} = require("./libs/Constants"),
axios = require('axios').default,
url = require("url");
module.exports = class ST_Client {
constructor(platform) {
this.platform = platform;
this.log = platform.log;
this.appEvts = platform.appEvts;
this.useLocal = false; //platform.local_commands;
this.hubIp = platform.local_hub_ip;
this.configItems = platform.getConfigItems();
let appURL = url.parse(this.configItems.app_url);
this.urlItems = {
app_host: appURL.hostname || "graph.api.smartthings.com",
app_port: appURL.port || 443,
app_path: `${(appURL.path || "/api/smartapps/installations/")}${this.configItems.app_id}/`
};
this.localErrCnt = 0;
this.localDisabled = false;
this.registerEvtListeners();
}
registerEvtListeners() {
this.appEvts.on("event:device_command", async(devData, cmd, vals) => {
await this.sendDeviceCommand(devData, cmd, vals);
});
this.appEvts.on("event:plugin_upd_status", async() => {
await this.sendUpdateStatus();
});
this.appEvts.on("event:plugin_start_direct", async() => {
await this.sendStartDirect();
});
}
sendAsLocalCmd() {
return (this.useLocal === true && this.hubIp !== undefined);
}
localHubErr(hasErr) {
if (hasErr) {
if (this.useLocal && !this.localDisabled) {
this.log.error(`Unable to reach your SmartThing Hub Locally... You will not receive device events!!!`);
this.useLocal = false;
this.localDisabled = true;
}
} else {
if (this.localDisabled) {
this.useLocal = true;
this.localDisabled = false;
this.log.good(`Now able to reach local Hub... Restoring Local Commands!!!`);
this.sendStartDirect();
}
}
}
updateGlobals(hubIp, useLocal = false) {
this.log.notice(`Updating Global Values | HubIP: ${hubIp} | UseLocal: ${useLocal}`);
this.hubIp = hubIp;
this.useLocal = false; //(useLocal === true);
}
handleError(src, err, allowLocal = false) {
switch (err.status) {
case 401:
this.log.error(`${src} Error | SmartThings Token Error: ${err.response} | Message: ${err.message}`);
break;
case 403:
this.log.error(`${src} Error | SmartThings Authentication Error: ${err.response} | Message: ${err.message}`);
break;
default:
if (err.message.startsWith('getaddrinfo EAI_AGAIN')) {
this.log.error(`${src} Error | Possible Internet/Network/DNS Error | Unable to reach the uri | Message ${err.message}`);
} else if (allowLocal && err.message.startsWith('Error: connect ETIMEDOUT ')) {
this.localHubErr(true);
} else {
// console.error(err);
this.log.error(`${src} Error: ${err.response} | Message: ${err.message}`);
}
break;
}
}
getDevices() {
let that = this;
return new Promise((resolve) => {
axios({
method: 'get',
url: `${that.configItems.app_url}${that.configItems.app_id}/devices`,
params: {
access_token: that.configItems.access_token
},
timeout: 10000
})
.then((response) => {
resolve(response.data);
})
.catch((err) => {
this.handleError('getDevices', err);
resolve(undefined);
});
});
}
getDevice(deviceid) {
let that = this;
return new Promise((resolve) => {
axios({
method: 'get',
url: `${that.configItems.app_url}${that.configItems.app_id}/${deviceid}/query`,
params: {
access_token: that.configItems.access_token
},
timeout: 10000
})
.then((response) => {
resolve(response.data);
})
.catch((err) => {
this.handleError('getDevice', err);
resolve(undefined);
});
});
}
sendDeviceCommand(devData, cmd, vals) {
return new Promise((resolve) => {
let that = this;
let sendLocal = this.sendAsLocalCmd();
let config = {
method: 'post',
url: `${this.configItems.app_url}${this.configItems.app_id}/${devData.deviceid}/command/${cmd}`,
params: {
access_token: this.configItems.access_token
},
headers: {
evtsource: `Homebridge_${platformName}_${this.configItems.app_id}`,
evttype: 'hkCommand'
},
data: vals,
timeout: 5000
};
if (sendLocal) {
config.url = `http://${this.hubIp}:39500/event`;
delete config.params;
config.data = {
deviceid: devData.deviceid,
command: cmd,
values: vals,
evtsource: `Homebridge_${platformName}_${this.configItems.app_id}`,
evttype: 'hkCommand'
};
}
try {
that.log.notice(`Sending Device Command: ${cmd}${vals ? ' | Value: ' + JSON.stringify(vals) : ''} | Name: (${devData.name}) | DeviceID: (${devData.deviceid}) | SendToLocalHub: (${sendLocal})`);
axios(config)
.then((response) => {
// console.log('command response:', response.data);
this.log.debug(`sendDeviceCommand | Response: ${JSON.stringify(response.data)}`);
that.localHubErr(false);
resolve(true);
})
.catch((err) => {
that.handleError('sendDeviceCommand', err, true);
resolve(false);
});
} catch (err) {
resolve(false);
}
});
}
sendUpdateStatus() {
return new Promise((resolve) => {
this.platform.myUtils.checkVersion()
.then((res) => {
this.log.notice(`Sending Plugin Status to SmartThings | UpdateAvailable: ${res.hasUpdate}${res.newVersion ? ' | newVersion: ' + res.newVersion : ''}`);
axios({
method: 'post',
url: `${this.configItems.app_url}${this.configItems.app_id}/pluginStatus`,
params: {
access_token: this.configItems.access_token
},
data: {
hasUpdate: res.hasUpdate,
newVersion: res.newVersion,
version: pluginVersion
},
timeout: 10000
})
.then((response) => {
// console.log(response.data);
if (response.data) {
this.log.debug(`sendUpdateStatus Resp: ${JSON.stringify(response.data)}`);
resolve(response.data);
} else {
resolve(null);
}
})
.catch((err) => {
this.handleError('sendUpdateStatus', err, true);
resolve(undefined);
});
});
});
}
sendStartDirect() {
let that = this;
return new Promise((resolve) => {
let sendLocal = this.sendAsLocalCmd();
let config = {
method: 'post',
url: `${this.configItems.app_url}${this.configItems.app_id}/startDirect/${this.configItems.direct_ip}/${this.configItems.direct_port}/${pluginVersion}`,
params: {
access_token: this.configItems.access_token
},
headers: {
evtsource: `Homebridge_${platformName}_${this.configItems.app_id}`,
evttype: 'enableDirect'
},
data: {
ip: that.configItems.direct_ip,
port: that.configItems.direct_port,
version: pluginVersion,
evtsource: `Homebridge_${platformName}_${this.configItems.app_id}`,
evttype: 'enableDirect'
},
timeout: 10000
};
if (sendLocal) {
config.url = `http://${this.hubIp}:39500/event`;
delete config.params;
}
that.log.info(`Sending StartDirect Request to ${platformDesc} | SendToLocalHub: (${sendLocal})`);
try {
axios(config)
.then((response) => {
// that.log.info('sendStartDirect Resp:', body);
if (response.data) {
this.log.debug(`sendStartDirect Resp: ${JSON.stringify(response.data)}`);
resolve(response.data);
that.localHubErr(false);
} else {
resolve(null);
}
})
.catch((err) => {
that.handleError("sendStartDirect", err, true);
resolve(undefined);
});
} catch (err) {
resolve(err);
}
});
}
};
================================================
FILE: src/ST_DeviceCharacteristics.js
================================================
var Characteristic, CommunityTypes, accClass;
module.exports = class DeviceCharacteristics {
constructor(accessories, char) {
this.platform = accessories.mainPlatform;
// this.appEvts = accessories.mainPlatform.appEvts;
Characteristic = char;
CommunityTypes = accessories.CommunityTypes;
accClass = accessories;
this.log = accessories.log;
this.logConfig = accessories.logConfig;
this.accessories = accessories;
this.client = accessories.client;
this.myUtils = accessories.myUtils;
this.transforms = accessories.transforms;
this.homebridge = accessories.homebridge;
}
manageGetCharacteristic(svc, acc, char, attr, opts = {}) {
let c = this.getOrAddService(svc).getCharacteristic(char);
if (!c._events.get) {
c.on("get", (callback) => {
if (attr === 'status' && char === Characteristic.StatusActive) {
callback(null, accClass.transforms.transformStatus(this.context.deviceData.status));
} else {
callback(null, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
accClass.log_get(attr, char, acc, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
}
});
if (opts.props && Object.keys(opts.props).length) c.setProps(opts.props);
if (opts.evtOnly && opts.evtOnly === true) c.eventOnlyCharacteristic = opts.evtOnly;
c.getValue();
accClass.storeCharacteristicItem(attr, this.context.deviceData.deviceid, c);
} else {
if (attr === 'status' && char === Characteristic.StatusActive) {
c.updateValue(accClass.transforms.transformStatus(this.context.deviceData.status));
} else {
c.updateValue(accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
accClass.log_get(attr, char, acc, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
}
}
if (!c._events.change) {
c.on("change", (chg) => {
accClass.log_change(attr, char, acc, chg);
});
}
}
manageGetSetCharacteristic(svc, acc, char, attr, opts = {}) {
let c = this.getOrAddService(svc).getCharacteristic(char);
if (!c._events.get || !c._events.set) {
if (!c._events.get) {
c.on("get", (callback) => {
callback(null, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
accClass.log_get(attr, char, acc, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
});
}
if (!c._events.set) {
c.on("set", async(value, callback) => {
let cmdName = accClass.transforms.transformCommandName(opts.set_altAttr || attr, value);
let cmdVal = accClass.transforms.transformCommandValue(opts.set_altAttr || attr, value);
if (opts.cmdHasVal === true) {
acc.sendCommand(callback, acc, this.context.deviceData, cmdName, {
value1: cmdVal
});
} else {
acc.sendCommand(callback, acc, this.context.deviceData, cmdVal);
}
if (opts.updAttrVal) this.context.deviceData.attributes[attr] = accClass.transforms.transformAttributeState(opts.set_altAttr || attr, this.context.deviceData.attributes[opts.set_altValAttr || attr], c.displayName);
});
if (opts.props && Object.keys(opts.props).length) c.setProps(opts.props);
if (opts.evtOnly && opts.evtOnly === true) c.eventOnlyCharacteristic = opts.evtOnly;
c.getValue();
}
c.getValue();
accClass.storeCharacteristicItem(attr, this.context.deviceData.deviceid, c);
} else {
c.updateValue(accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
accClass.log_get(attr, char, acc, accClass.transforms.transformAttributeState(opts.get_altAttr || attr, this.context.deviceData.attributes[opts.get_altValAttr || attr], c.displayName));
}
if (!c._events.change) {
c.on("change", (chg) => {
accClass.log_change(attr, char, acc, chg);
});
}
}
acceleration_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.MotionDetected, 'acceleration');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("acceleration_sensor");
return _accessory;
}
air_purifier(_accessory, _service) {
let actState = (_accessory.context.deviceData.attributes.switch === "on") ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE;
let c = this.getOrAddService(_service).getCharacteristic(Characteristic.Active);
if (!c.events.get || !c.events.set) {
if (!c.events.get) {
c.on('get', (callback) => {
callback(null, actState);
});
}
if (!c.events.set) {
c.on('set', (value, callback) => {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, value ? 'on' : 'off');
});
}
c.getValue();
accClass.storeCharacteristicItem("switch", _accessory.context.deviceData.deviceid, c);
} else {
c.updateValue(actState);
}
c = this.getaddService(_service).getCharacteristic(Characteristic.CurrentAirPurifierState);
let apState = (actState === Characteristic.Active.INACTIVE) ? Characteristic.CurrentAirPurifierState.INACTIVE : Characteristic.CurrentAirPurifierState.PURIFYING_AIR;
if (!c.events.get) {
c.on('get', (callback) => {
callback(null, apState);
});
}
c.updateValue(apState);
c = this.getaddService(CommunityTypes.NewAirPurifierService).getCharacteristic(CommunityTypes.FanOscilationMode);
if (!c.events.get || !c.events.set) {
if (!c.events.get) {
c.on('get', (callback) => {
callback(null, this.transforms.transformAttributeState('fanMode', _accessory.context.deviceData.attributes.fanMode));
});
}
if (!c.events.set) {
c.on('set', (value, callback) => {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, 'setFanMode', {
value1: this.transforms.transformCommandValue('fanMode', value)
});
});
}
}
this.accessories.storeCharacteristicItem("fanMode", _accessory.context.deviceData.deviceid, c);
_accessory.context.deviceGroups.push("air_purifier");
return _accessory;
}
air_quality(_accessory, _service) {
let c = _accessory.getOrAddService(_service).getCharacteristic(Characteristic.AirQuality);
if (!c._events.get) {
c.on("get", (callback) => {
callback(null, Characteristic.AirQuality);
});
}
this.accessories.storeCharacteristicItem("airQuality", _accessory.context.deviceData.deviceid, c);
_accessory.context.deviceGroups.push("airQuality");
return _accessory;
}
alarm_system(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.SecuritySystemCurrentState, 'alarmSystemStatus');
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.SecuritySystemTargetState, 'alarmSystemStatus');
_accessory.context.deviceGroups.push("alarm_system");
return _accessory;
}
battery(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.BatteryLevel, 'battery');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusLowBattery, 'battery');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.ChargingState, 'batteryStatus');
_accessory.context.deviceGroups.push("battery");
return _accessory;
}
button(_accessory, _service) {
let that = this;
let validValues = this.transforms.transformAttributeState('supportedButtonValues', _accessory.context.deviceData.attributes.supportedButtonValues) || [0, 2];
const btnCnt = _accessory.context.deviceData.attributes.numberOfButtons || 1;
// console.log('btnCnt: ', btnCnt);
if (btnCnt >= 1) {
for (let bNum = 1; bNum <= btnCnt; bNum++) {
const svc = _accessory.getOrAddServiceByName(_service, `${_accessory.context.deviceData.deviceid}_${bNum}`, bNum);
let c = svc.getCharacteristic(Characteristic.ProgrammableSwitchEvent);
c.setProps({
validValues: validValues
});
c.eventOnlyCharacteristic = false;
if (!c._events.get) {
that.accessories._buttonMap[`${_accessory.context.deviceData.deviceid}_${bNum}`] = svc;
c.on("get", (callback) => {
this.value = -1;
callback(null, that.transforms.transformAttributeState('button', _accessory.context.deviceData.attributes.button));
});
_accessory.buttonEvent = this.buttonEvent.bind(_accessory);
this.accessories.storeCharacteristicItem("button", _accessory.context.deviceData.deviceid, c);
}
svc.getCharacteristic(Characteristic.ServiceLabelIndex).setValue(bNum);
}
_accessory.context.deviceGroups.push("button");
}
return _accessory;
}
buttonEvent(btnNum, btnVal, devId, btnMap) {
console.log('Button Press Event... | Button Number: (' + btnNum + ') | Button Value: ' + btnVal);
let bSvc = btnMap[`${devId}_${btnNum}`];
// console.log(bSvc);
if (bSvc) {
bSvc.getCharacteristic(Characteristic.ProgrammableSwitchEvent).getValue();
}
}
carbon_dioxide(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CarbonDioxideDetected, 'carbonDioxideMeasurement');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CarbonDioxideLevel, 'carbonDioxideMeasurement');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("carbon_dioxide");
return _accessory;
}
carbon_monoxide(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CarbonMonoxideDetected, 'carbonMonoxide');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("carbon_monoxide");
return _accessory;
}
contact_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.ContactSensorState, 'contact');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("contact_sensor");
return _accessory;
}
energy_meter(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, CommunityTypes.KilowattHours, 'energy');
_accessory.context.deviceGroups.push("energy_meter");
return _accessory;
}
fan(_accessory, _service) {
if (_accessory.hasAttribute('switch')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Active, 'switch');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentFanState, 'switch', {
get_altAttr: "fanState"
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.CurrentFanState);
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.Active);
}
let spdSteps = 1;
if (_accessory.hasDeviceFlag('fan_3_spd')) spdSteps = 32;
if (_accessory.hasDeviceFlag('fan_4_spd')) spdSteps = 25;
let spdAttr = (_accessory.hasAttribute('level')) ? "level" : (_accessory.hasAttribute('fanSpeed') && _accessory.hasCommand('setFanSpeed')) ? 'fanSpeed' : undefined;
if (_accessory.hasAttribute('level') || _accessory.hasAttribute('fanSpeed')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.RotationSpeed, spdAttr, {
cmdHasVal: true,
props: {
minStep: spdSteps
}
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.RotationSpeed);
}
_accessory.context.deviceGroups.push("fan");
return _accessory;
}
garage_door(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentDoorState, 'door');
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.TargetDoorState, 'door');
_accessory.getOrAddService(_service).getCharacteristic(Characteristic.ObstructionDetected).updateValue(false);
return _accessory;
}
humidity_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentRelativeHumidity, 'humidity');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("humidity_sensor");
return _accessory;
}
illuminance_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentAmbientLightLevel, 'illuminance', {
props: {
minValue: 0,
maxValue: 100000
}
});
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("illuminance_sensor");
return _accessory;
}
light(_accessory, _service) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.On, 'switch');
if (_accessory.hasAttribute('level')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Brightness, 'level', {
cmdHasVal: true
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.Brightness);
}
if (_accessory.hasAttribute('hue')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Hue, 'hue', {
cmdHasVal: true,
props: {
minValue: 1,
maxValue: 30000
}
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.Hue);
}
if (_accessory.hasAttribute('saturation')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Saturation, 'saturation', {
cmdHasVal: true
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.Saturation);
}
if (_accessory.hasAttribute('colorTemperature')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.ColorTemperature, 'colorTemperature', {
cmdHasVal: true
});
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.ColorTemperature);
}
_accessory.context.deviceGroups.push("light_bulb");
return _accessory;
}
lock(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.LockCurrentState, 'lock');
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.LockTargetState, 'lock');
_accessory.context.deviceGroups.push("lock");
return _accessory;
}
motion_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.MotionDetected, 'motion');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("motion_sensor");
return _accessory;
}
power_meter(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, CommunityTypes.Watts, 'power');
_accessory.context.deviceGroups.push("power_meter");
return _accessory;
}
presence_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.OccupancyDetected, 'presence');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("presence_sensor");
return _accessory;
}
smoke_detector(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.SmokeDetected, 'smoke');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("smoke_detector");
return _accessory;
}
speaker(_accessory, _service) {
let isSonos = (_accessory.context.deviceData.manufacturerName === "Sonos");
let lvlAttr = (isSonos || _accessory.hasAttribute('volume')) ? 'volume' : _accessory.hasAttribute('level') ? 'level' : undefined;
let c = _accessory.getOrAddService(_service).getCharacteristic(Characteristic.Volume);
let lastVolumeWriteValue = null;
if (!c._events.get || !c._events.set) {
if (!c._events.get) {
c.on("get", (callback) => {
callback(null, this.transforms.transformAttributeState(lvlAttr, _accessory.context.deviceData.attributes[lvlAttr]) || 0);
});
}
if (!c._events.set) {
c.on("set", (value, callback) => {
if (isSonos) {
if (value > 0 && value !== lastVolumeWriteValue) {
lastVolumeWriteValue = value;
this.log.debug(`Existing volume: ${_accessory.context.deviceData.attributes.volume}, set to ${lastVolumeWriteValue}`);
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "setVolume", {
value1: lastVolumeWriteValue
});
}
}
if (value > 0) {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, this.accessories.transformCommandName(lvlAttr, value), {
value1: this.transforms.transformAttributeState(lvlAttr, value)
});
}
});
}
this.accessories.storeCharacteristicItem("volume", _accessory.context.deviceData.deviceid, c);
}
_accessory.getOrAddService(_service).getCharacteristic(Characteristic.Volume).updateValue(this.transforms.transformAttributeState(lvlAttr, _accessory.context.deviceData.attributes[lvlAttr]) || 0);
if (_accessory.hasCapability('Audio Mute')) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Mute, 'mute');
}
_accessory.context.deviceGroups.push("speaker_device");
return _accessory;
}
switch_device(_accessory, _service) {
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.On, 'switch');
_accessory.context.deviceGroups.push("switch");
return _accessory;
}
temperature_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentTemperature, 'temperature', {
props: {
minValue: -100,
maxValue: 200
}
});
if (_accessory.hasCapability('Tamper Alert')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
} else {
_accessory.getOrAddService(_service).removeCharacteristic(Characteristic.StatusTampered);
}
_accessory.context.deviceGroups.push("temperature_sensor");
return _accessory;
}
thermostat(_accessory, _service) {
//TODO: Still seeing an issue when setting mode from OFF to HEAT. It's setting the temp to 40 but if I change to cool then back to heat it sets the correct value.
const tstatService = _accessory.getOrAddService(_service);
let curTempChar = tstatService.getCharacteristic(Characteristic.CurrentTemperature);
let curHeatCoolStateChar = tstatService.getCharacteristic(Characteristic.CurrentHeatingCoolingState);
let targetHeatCoolStateChar = tstatService.getCharacteristic(Characteristic.TargetHeatingCoolingState);
let targetTempChar = tstatService.getCharacteristic(Characteristic.TargetTemperature);
// CURRENT HEATING/COOLING STATE
if (!curHeatCoolStateChar._events.get) {
curHeatCoolStateChar.on("get", (callback) => {
const state = this.transforms.transformAttributeState('thermostatOperatingState', _accessory.context.deviceData.attributes.thermostatOperatingState);
callback(null, state);
});
this.accessories.storeCharacteristicItem("thermostatOperatingState", _accessory.context.deviceData.deviceid, curHeatCoolStateChar);
} else {
curHeatCoolStateChar.updateValue(this.transforms.transformAttributeState("thermostatOperatingState", _accessory.context.deviceData.attributes.thermostatOperatingState));
}
// TARGET HEATING/COOLING STATE
if (!targetHeatCoolStateChar._events.get || !targetHeatCoolStateChar._events.set) {
targetHeatCoolStateChar.setProps({
validValues: this.transforms.thermostatSupportedModes(_accessory.context.deviceData)
});
if (!targetHeatCoolStateChar._events.get) {
targetHeatCoolStateChar.on("get", (callback) => {
// console.log('thermostatMode(get): ', this.transforms.transformAttributeState('thermostatMode', _accessory.context.deviceData.attributes.thermostatMode));
callback(null, this.transforms.transformAttributeState('thermostatMode', _accessory.context.deviceData.attributes.thermostatMode));
});
}
if (!targetHeatCoolStateChar._events.set) {
targetHeatCoolStateChar.on("set", async(value, callback) => {
let state = this.transforms.transformCommandValue('thermostatMode', value);
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, this.transforms.transformCommandName('thermostatMode', value), {
value1: state
});
_accessory.context.deviceData.attributes.thermostatMode = state;
// targetTempChar.updateValue(this.transforms.thermostatTargetTemp(_accessory.context.deviceData));
});
}
this.accessories.storeCharacteristicItem("thermostatMode", _accessory.context.deviceData.deviceid, targetHeatCoolStateChar);
} else {
targetHeatCoolStateChar.updateValue(this.transforms.transformAttributeState("thermostatMode", _accessory.context.deviceData.attributes.thermostatMode));
}
// CURRENT RELATIVE HUMIDITY
if (_accessory.hasCapability('Relative Humidity Measurement')) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentRelativeHumidity, 'humidity');
}
// CURRENT TEMPERATURE
if (!curTempChar._events.get) {
curTempChar.on("get", (callback) => {
// targetTempChar.updateValue(this.transforms.thermostatTargetTemp(_accessory.context.deviceData));
callback(null, this.transforms.thermostatTempConversion(_accessory.context.deviceData.attributes.temperature));
});
this.accessories.storeCharacteristicItem("temperature", _accessory.context.deviceData.deviceid, curTempChar);
this.accessories.storeCharacteristicItem("thermostatSetpoint", _accessory.context.deviceData.deviceid, targetTempChar);
} else {
curTempChar.updateValue(this.transforms.transformAttributeState("temperature", _accessory.context.deviceData.attributes.temperature));
}
// TARGET TEMPERATURE
if (!targetTempChar._events.get || !targetTempChar._events.set) {
if (!targetTempChar._events.get) {
targetTempChar.on("get", (callback) => {
const targetTemp = this.transforms.thermostatTargetTemp(_accessory.context.deviceData);
// console.log('targetTemp:', targetTemp);
callback(null, targetTemp ? this.transforms.thermostatTempConversion(targetTemp) : null);
});
}
if (!targetTempChar._events.set) {
targetTempChar.on("set", (value, callback) => {
// Convert the Celsius value to the appropriate unit for Smartthings
let temp = this.transforms.thermostatTempConversion(value, true);
const targetObj = this.transforms.thermostatTargetTemp_set(_accessory.context.deviceData);
if (targetObj && targetObj.cmdName && targetObj.attrName && temp) {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, targetObj.cmdName, {
value1: temp
});
_accessory.context.deviceData.attributes[targetObj.attrName] = temp;
}
});
}
this.accessories.storeCharacteristicItem("coolingSetpoint", _accessory.context.deviceData.deviceid, targetTempChar);
this.accessories.storeCharacteristicItem("heatingSetpoint", _accessory.context.deviceData.deviceid, targetTempChar);
this.accessories.storeCharacteristicItem("thermostatSetpoint", _accessory.context.deviceData.deviceid, targetTempChar);
} else {
const targetTemp = this.transforms.thermostatTargetTemp(_accessory.context.deviceData);
targetTempChar.updateValue(targetTemp ? this.transforms.thermostatTempConversion(targetTemp) : null);
}
// TEMPERATURE DISPLAY UNITS
let tempUnitChar = tstatService.getCharacteristic(Characteristic.TemperatureDisplayUnits);
tempUnitChar.updateValue((this.platform.getTempUnit() === 'F') ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS);
// HEATING THRESHOLD TEMPERATURE
if (targetHeatCoolStateChar.props.validValues.includes(3)) {
// console.log('test', targetHeatCoolStateChar.props);
let heatThreshTempChar = tstatService.getCharacteristic(Characteristic.HeatingThresholdTemperature);
let coolThreshTempChar = tstatService.getCharacteristic(Characteristic.CoolingThresholdTemperature);
if (!heatThreshTempChar._events.get || !heatThreshTempChar._events.set) {
if (!heatThreshTempChar._events.get) {
heatThreshTempChar.on("get", (callback) => {
console.log('heatingSetpoint: ', _accessory.context.deviceData.attributes.heatingSetpoint);
callback(null, this.transforms.thermostatTempConversion(_accessory.context.deviceData.attributes.heatingSetpoint));
});
}
if (!heatThreshTempChar._events.set) {
heatThreshTempChar.on("set", (value, callback) => {
// Convert the Celsius value to the appropriate unit for Smartthings
let temp = this.transforms.thermostatTempConversion(value, true);
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "setHeatingSetpoint", {
value1: temp
});
_accessory.context.deviceData.attributes.heatingSetpoint = temp;
});
}
this.accessories.storeCharacteristicItem("heatingSetpoint", _accessory.context.deviceData.deviceid, heatThreshTempChar);
this.accessories.storeCharacteristicItem("thermostatSetpoint", _accessory.context.deviceData.deviceid, heatThreshTempChar);
} else {
heatThreshTempChar.updateValue(this.transforms.thermostatTempConversion(_accessory.context.deviceData.attributes.heatingSetpoint));
}
// COOLING THRESHOLD TEMPERATURE
if (!coolThreshTempChar._events.get || !coolThreshTempChar._events.set) {
if (!coolThreshTempChar._events.get) {
coolThreshTempChar.on("get", (callback) => {
console.log('coolingSetpoint: ', _accessory.context.deviceData.attributes.coolingSetpoint);
callback(null, this.transforms.thermostatTempConversion(_accessory.context.deviceData.attributes.coolingSetpoint));
});
}
if (!coolThreshTempChar._events.set) {
coolThreshTempChar.on("set", (value, callback) => {
// Convert the Celsius value to the appropriate unit for Smartthings
let temp = this.transforms.thermostatTempConversion(value, true);
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "setCoolingSetpoint", {
value1: temp
});
_accessory.context.deviceData.attributes.coolingSetpoint = temp;
});
}
this.accessories.storeCharacteristicItem("coolingSetpoint", _accessory.context.deviceData.deviceid, coolThreshTempChar);
this.accessories.storeCharacteristicItem("thermostatSetpoint", _accessory.context.deviceData.deviceid, coolThreshTempChar);
} else {
coolThreshTempChar.updateValue(this.transforms.thermostatTempConversion(_accessory.context.deviceData.attributes.coolingSetpoint));
}
} else {
tstatService.removeCharacteristic(Characteristic.HeatingThresholdTemperature);
tstatService.removeCharacteristic(Characteristic.CoolingThresholdTemperature);
}
_accessory.context.deviceGroups.push("thermostat");
return _accessory;
}
valve(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.InUse, 'valve');
_accessory.manageGetSetCharacteristic(_service, _accessory, Characteristic.Active, 'valve');
if (!_accessory.hasCharacteristic(_service, Characteristic.ValveType))
_accessory.getOrAddService(_service).setCharacteristic(Characteristic.ValveType, 0);
_accessory.context.deviceGroups.push("valve");
return _accessory;
}
virtual_mode(_accessory, _service) {
let c = _accessory.getOrAddService(_service).getCharacteristic(Characteristic.On);
if (!c._events.get || !c._events.set) {
if (!c._events.get)
c.on("get", (callback) => {
callback(null, this.transforms.transformAttributeState('switch', _accessory.context.deviceData.attributes.switch));
});
if (!c._events.set)
c.on("set", (value, callback) => {
if (value && (_accessory.context.deviceData.attributes.switch === "off")) {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "mode");
}
});
this.accessories.storeCharacteristicItem("switch", _accessory.context.deviceData.deviceid, c);
} else {
c.updateValue(this.transforms.transformAttributeState('switch', _accessory.context.deviceData.attributes.switch));
}
_accessory.context.deviceGroups.push("virtual_mode");
return _accessory;
}
virtual_routine(_accessory, _service) {
let c = _accessory.getOrAddService(_service).getCharacteristic(Characteristic.On);
if (!c._events.get || !c._events.set) {
if (!c._events.get)
c.on("get", (callback) => {
callback(null, false);
});
if (!c._events.set)
c.on("set", (value, callback) => {
if (value) {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "routine");
setTimeout(() => {
console.log("routineOff...");
_accessory.context.deviceData.attributes.switch = "off";
c.updateValue(false);
}, 1000);
}
});
this.accessories.storeCharacteristicItem("switch", _accessory.context.deviceData.deviceid, c);
} else {
c.updateValue(this.transforms.transformAttributeState('switch', _accessory.context.deviceData.attributes.switch));
}
_accessory.context.deviceGroups.push("virtual_routine");
return _accessory;
}
water_sensor(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.LeakDetected, 'water');
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusActive, 'status');
if (_accessory.hasCapability('Tamper Alert'))
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.StatusTampered, 'tamper');
_accessory.context.deviceGroups.push("window_shade");
return _accessory;
}
window_shade(_accessory, _service) {
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.CurrentPosition, 'level', {
props: {
steps: 10
}
});
let c = _accessory.getOrAddService(_service).getCharacteristic(Characteristic.TargetPosition);
if (!c._events.get || !c._events.set) {
if (!c._events.get) {
c.on("get", (callback) => {
callback(null, parseInt(_accessory.context.deviceData.attributes.level));
});
}
if (!c._events.set) {
c.on("set", (value, callback) => {
if (_accessory.hasCommand('close') && value <= 2) {
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "close");
} else {
let v = value;
if (value <= 2) v = 0;
if (value >= 98) v = 100;
_accessory.sendCommand(callback, _accessory, _accessory.context.deviceData, "setLevel", {
value1: v
});
}
});
}
this.accessories.storeCharacteristicItem("level", _accessory.context.deviceData.deviceid, c);
} else {
c.updateValue(this.transforms.transformAttributeState('level', _accessory.context.deviceData.attributes.level));
}
_accessory.manageGetCharacteristic(_service, _accessory, Characteristic.PositionState, 'windowShade');
_accessory.getOrAddService(_service).getCharacteristic(Characteristic.ObstructionDetected).updateValue(false);
_accessory.getOrAddService(_service).getCharacteristic(Characteristic.HoldPosition).updateValue(false);
_accessory.context.deviceGroups.push("window_shade");
return _accessory;
}
};
================================================
FILE: src/ST_Platform.js
================================================
const {
pluginName,
platformName,
platformDesc,
pluginVersion
} = require("./libs/Constants"),
events = require('events'),
myUtils = require("./libs/MyUtils"),
SmartThingsClient = require("./ST_Client"),
SmartThingsAccessories = require("./ST_Accessories"),
express = require("express"),
bodyParser = require("body-parser"),
chalk = require('chalk'),
Logging = require("./libs/Logger"),
webApp = express(),
// os = require('os'),
portFinderSync = require('portfinder-sync');
var PlatformAccessory;
module.exports = class ST_Platform {
constructor(log, config, api) {
this.config = config;
this.homebridge = api;
this.Service = api.hap.Service;
this.Characteristic = api.hap.Characteristic;
PlatformAccessory = api.platformAccessory;
this.uuid = api.hap.uuid;
if (config === undefined || config === null || config.app_url === undefined || config.app_url === null || config.app_id === undefined || config.app_id === null) {
log(`${platformName} Plugin is not Configured | Skipping...`);
return;
}
this.ok2Run = true;
this.direct_port = this.findDirectPort();
this.logConfig = this.getLogConfig();
this.appEvts = new events.EventEmitter();
this.logging = new Logging(this, this.config["name"], this.logConfig);
this.log = this.logging.getLogger();
this.log.info(`Homebridge Version: ${api.version}`);
this.log.info(`${platformName} Plugin Version: ${pluginVersion}`);
this.polling_seconds = config.polling_seconds || 3600;
this.excludedAttributes = this.config.excluded_attributes || [];
this.excludedCapabilities = this.config.excluded_capabilities || [];
this.update_method = this.config.update_method || "direct";
this.temperature_unit = this.config.temperature_unit || "F";
this.local_commands = this.config.local_commands || false;
this.local_hub_ip = undefined;
this.myUtils = new myUtils(this);
this.configItems = this.getConfigItems();
this.unknownCapabilities = [];
this.client = new SmartThingsClient(this);
this.SmartThingsAccessories = new SmartThingsAccessories(this);
this.homebridge.on("didFinishLaunching", this.didFinishLaunching.bind(this));
this.appEvts.emit('event:plugin_upd_status');
}
getLogConfig() {
let config = this.config;
return (config.logConfig) ? {
debug: (config.logConfig.debug === true),
showChanges: (config.logConfig.showChanges === true),
hideTimestamp: (config.logConfig.hideTimestamp === true),
hideNamePrefix: (config.logConfig.hideNamePrefix === true),
file: {
enabled: (config.logConfig.file.enabled === true),
level: (config.logConfig.file.level || 'good')
}
} : {
debug: false,
showChanges: true,
hideTimestamp: false,
hideNamePrefix: false
};
}
findDirectPort() {
let port = this.config.direct_port || 8000;
if (port)
port = portFinderSync.getPort(port);
return this.direct_port = port;
}
getConfigItems() {
return {
app_url: this.config.app_url,
app_id: this.config.app_id,
access_token: this.config.access_token,
update_seconds: this.config.update_seconds || 30,
direct_port: this.direct_port,
direct_ip: this.config.direct_ip || this.myUtils.getIPAddress(),
debug: (this.config.debug === true),
local_commands: (this.config.local_commands === true),
validateTokenId: (this.config.validateTokenId === true)
};
}
updateTempUnit(unit) {
this.log.notice(`Temperature Unit is Now: (${unit})`);
this.temperature_unit = unit;
}
getTempUnit() {
return this.temperature_unit;
}
didFinishLaunching() {
this.log.info(`Fetching ${platformName} Devices. NOTICE: This may take a moment if you have a large number of device data is being loaded!`);
setInterval(this.refreshDevices.bind(this), this.polling_seconds * 1000);
let that = this;
this.refreshDevices('First Launch')
.then(() => {
that.WebServerInit(that)
.catch(err => that.log.error("WebServerInit Error: ", err))
.then(resp => {
if (resp && resp.status === "OK") this.appEvts.emit('event:plugin_start_direct');;
});
})
.catch(err => {
that.log.error(`didFinishLaunching | refreshDevices Exception:`, err);
});
}
refreshDevices(src = undefined) {
let that = this;
let starttime = new Date();
return new Promise((resolve, reject) => {
try {
that.log.good(`Refreshing All Device Data${src ? ' | Source: (' + src + ')' : ""}`);
this.client.getDevices()
.catch(err => {
that.log.error('getDevices Exception:', err);
reject(err.message);
})
.then(resp => {
if (resp && resp.location) {
that.updateTempUnit(resp.location.temperature_scale);
if (resp.location.hubIP) {
that.local_hub_ip = resp.location.hubIP;
that.local_commands = resp.location.local_commands === true;
that.client.updateGlobals(that.local_hub_ip, that.local_commands);
}
}
if (resp && resp.deviceList && resp.deviceList instanceof Array) {
// that.log.debug("Received All Device Data");
const toCreate = this.SmartThingsAccessories.diffAdd(resp.deviceList);
const toUpdate = this.SmartThingsAccessories.intersection(resp.deviceList);
const toRemove = this.SmartThingsAccessories.diffRemove(resp.deviceList);
that.log.warn(`Devices to Remove: (${Object.keys(toRemove).length})`, toRemove.map(i => i.name));
that.log.info(`Devices to Update: (${Object.keys(toUpdate).length})`);
that.log.good(`Devices to Create: (${Object.keys(toCreate).length})`, toCreate.map(i => i.name));
toRemove.forEach(accessory => this.removeAccessory(accessory));
toUpdate.forEach(device => this.updateDevice(device));
toCreate.forEach(device => this.addDevice(device));
}
that.log.alert(`Total Initialization Time: (${Math.round((new Date() - starttime) / 1000)} seconds)`);
that.log.notice(`Unknown Capabilities: ${JSON.stringify(that.unknownCapabilities)}`);
that.log.info(`${platformDesc} DeviceCache Size: (${Object.keys(this.SmartThingsAccessories.getAllAccessoriesFromCache()).length})`);
if (src !== 'First Launch') this.appEvts.emit('event:plugin_upd_status');
resolve(true);
});
} catch (ex) {
this.log.error("refreshDevices Error: ", ex);
resolve(false);
}
});
}
getNewAccessory(device, UUID) {
let accessory = new PlatformAccessory(device.name, UUID);
accessory.context.deviceData = device;
this.SmartThingsAccessories.initializeAccessory(accessory);
return accessory;
}
addDevice(device) {
let accessory;
const new_uuid = this.uuid.generate(`smartthings_v2_${device.deviceid}`);
device.excludedCapabilities = this.excludedCapabilities[device.deviceid] || [];
this.log.debug(`Initializing New Device (${device.name} | ${device.deviceid})`);
accessory = this.getNewAccessory(device, new_uuid);
this.homebridge.registerPlatformAccessories(pluginName, platformName, [accessory]);
this.SmartThingsAccessories.addAccessoryToCache(accessory);
this.log.info(`Added Device: (${accessory.name} | ${accessory.deviceid})`);
}
updateDevice(device) {
let cachedAccessory = this.SmartThingsAccessories.getAccessoryFromCache(device);
device.excludedCapabilities = this.excludedCapabilities[device.deviceid] || [];
cachedAccessory.context.deviceData = device;
this.log.debug(`Loading Existing Device (${device.name}) | (${device.deviceid})`);
cachedAccessory = this.SmartThingsAccessories.initializeAccessory(cachedAccessory);
this.SmartThingsAccessories.addAccessoryToCache(cachedAccessory);
}
removeAccessory(accessory) {
if (this.SmartThingsAccessories.removeAccessoryFromCache(accessory)) {
this.homebridge.unregisterPlatformAccessories(pluginName, platformName, [accessory]);
this.log.info(`Removed: ${accessory.context.name} (${accessory.context.deviceid})`);
}
}
configureAccessory(accessory) {
if (!this.ok2Run) return;
this.log.debug(`Configure Cached Accessory: ${accessory.displayName}, UUID: ${accessory.UUID}`);
let cachedAccessory = this.SmartThingsAccessories.initializeAccessory(accessory, true);
this.SmartThingsAccessories.addAccessoryToCache(cachedAccessory);
}
processIncrementalUpdate(data, that) {
that.log.debug("new data: " + data);
if (data && data.attributes && data.attributes instanceof Array) {
for (let i = 0; i < data.attributes.length; i++) {
that.processDeviceAttributeUpdate(data.attributes[i], that);
}
}
}
isValidRequestor(access_token, app_id, src) {
if (this.configItems.validateTokenId !== true) {
return true;
}
if (app_id && access_token && (access_token === this.getConfigItems().access_token) && (app_id === this.getConfigItems().app_id)) return true;
this.log.error(`(${src}) | We received a request from a client that didn't provide a valid access_token and app_id`);
return false;
}
WebServerInit() {
let that = this;
// Get the IP address that we will send to the SmartApp. This can be overridden in the config file.
return new Promise(resolve => {
try {
let ip = that.configItems.direct_ip || that.myUtils.getIPAddress();
that.log.info("WebServer Initiated...");
// Start the HTTP Server
webApp.listen(that.configItems.direct_port, () => {
that.log.info(`Direct Connect Active | Listening at ${ip}:${that.configItems.direct_port}`);
});
webApp.use(bodyParser.urlencoded({
extended: false
}));
webApp.use(bodyParser.json());
webApp.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
webApp.get("/", (req, res) => {
res.send("WebApp is running...");
});
webApp.post("/initial", (req, res) => {
let body = JSON.parse(JSON.stringify(req.body));
if (body && that.isValidRequestor(body.access_token, body.app_id, 'initial')) {
that.log.info(`${platformName} Hub Communication Established`);
res.send({
status: "OK"
});
} else {
res.send({
status: "Failed: Missing access_token or app_id"
});
}
});
webApp.get("/debugOpts", (req, res) => {
that.log.info(`${platformName} Debug Option Request(${req.query.option})...`);
if (req.query && req.query.option) {
let accs = this.SmartThingsAccessories.getAllAccessoriesFromCache();
// let accsKeys = Object.keys(accs);
// console.log(accsKeys);
switch (req.query.option) {
case 'allAccData':
res.send(JSON.stringify(accs));
break;
// case 'accServices':
// var o = accsKeys.forEach(s => s.services.forEach(s1 => s1.UUID));
// res.send(JSON.stringify(o));
// break;
// case 'accCharacteristics':
// var o = accsKeys.forEach(s => s.services.forEach(s1 => s1.characteristics.forEach(c => c.displayName)));
// res.send(JSON.stringify(o));
// break;
// case 'accContext':
// res.send(JSON.stringify(this.SmartThingsAccessories.getAllAccessoriesFromCache()));
// break;
default:
res.send(`Error: Invalid Option Parameter Received | Option: ${req.query.option}`);
break;
}
} else {
res.send('Error: Missing Valid Debug Query Parameter');
gitextract_mihrh1sf/
├── .esformatter
├── .eslintrc.json
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── stale.yml
│ └── workflows/
│ └── nodejs.yml
├── .gitignore
├── .snyk
├── CHANGELOG-app.md
├── CHANGELOG.md
├── README.md
├── appData.json
├── config.schema.json
├── installerManifest.json
├── jsconfig.json
├── package.json
├── platform.schema.json
├── prettierrc
├── smartapps/
│ └── tonesto7/
│ └── homebridge-v2.src/
│ └── homebridge-v2.groovy
└── src/
├── ST_Accessories.js
├── ST_Client.js
├── ST_DeviceCharacteristics.js
├── ST_Platform.js
├── ST_ServiceTypes.js
├── ST_Transforms.js
├── index.js
└── libs/
├── CommunityTypes.js
├── Constants.js
├── HomeKitTypes-Bridge.js
├── HomeKitTypes.js
├── Logger.js
└── MyUtils.js
SYMBOL INDEX (142 symbols across 8 files)
FILE: src/ST_Accessories.js
method constructor (line 10) | constructor(platform) {
method initializeAccessory (line 32) | initializeAccessory(accessory, fromCache = false) {
method configureCharacteristics (line 76) | configureCharacteristics(accessory) {
method processDeviceAttributeUpdate (line 116) | processDeviceAttributeUpdate(change) {
method sendCommand (line 150) | sendCommand(callback, acc, dev, cmd, vals) {
method log_change (line 211) | log_change(attr, char, acc, chgObj) {
method log_get (line 215) | log_get(attr, char, acc, val) {
method log_set (line 219) | log_set(attr, char, acc, val) {
method hasCapability (line 223) | hasCapability(obj) {
method getCapabilities (line 229) | getCapabilities() {
method hasAttribute (line 233) | hasAttribute(attr) {
method hasCommand (line 237) | hasCommand(cmd) {
method getCommands (line 241) | getCommands() {
method hasService (line 245) | hasService(service) {
method hasCharacteristic (line 249) | hasCharacteristic(svc, char) {
method updateCharacteristicVal (line 254) | updateCharacteristicVal(svc, char, val) {
method updateCharacteristicProps (line 258) | updateCharacteristicProps(svc, char, props) {
method hasDeviceFlag (line 262) | hasDeviceFlag(flag) {
method updateDeviceAttr (line 266) | updateDeviceAttr(attr, val) {
method getOrAddService (line 270) | getOrAddService(svc) {
method getOrAddServiceByName (line 274) | getOrAddServiceByName(service, dName, sType) {
method getOrAddCharacteristic (line 286) | getOrAddCharacteristic(service, characteristic) {
method getServices (line 290) | getServices() {
method removeUnusedServices (line 294) | removeUnusedServices(acc) {
method storeCharacteristicItem (line 307) | storeCharacteristicItem(attr, devid, char) {
method getAttributeStoreItem (line 318) | getAttributeStoreItem(attr, devid) {
method removeAttributeStoreItem (line 325) | removeAttributeStoreItem(attr, devid) {
method getDeviceAttributeValueFromCache (line 330) | getDeviceAttributeValueFromCache(device, attr) {
method getAccessoryId (line 337) | getAccessoryId(accessory) {
method getAccessoryFromCache (line 342) | getAccessoryFromCache(device) {
method getAllAccessoriesFromCache (line 347) | getAllAccessoriesFromCache() {
method clearAccessoryCache (line 351) | clearAccessoryCache() {
method addAccessoryToCache (line 356) | addAccessoryToCache(accessory) {
method removeAccessoryFromCache (line 362) | removeAccessoryFromCache(accessory) {
method forEach (line 369) | forEach(fn) {
method intersection (line 373) | intersection(devices) {
method diffAdd (line 378) | diffAdd(devices) {
method diffRemove (line 383) | diffRemove(devices) {
method comparator (line 388) | comparator(accessory1, accessory2) {
method clearAndSetTimeout (line 392) | clearAndSetTimeout(timeoutReference, fn, timeoutMs) {
FILE: src/ST_Client.js
method constructor (line 10) | constructor(platform) {
method registerEvtListeners (line 28) | registerEvtListeners() {
method sendAsLocalCmd (line 40) | sendAsLocalCmd() {
method localHubErr (line 44) | localHubErr(hasErr) {
method updateGlobals (line 61) | updateGlobals(hubIp, useLocal = false) {
method handleError (line 67) | handleError(src, err, allowLocal = false) {
method getDevices (line 88) | getDevices() {
method getDevice (line 109) | getDevice(deviceid) {
method sendDeviceCommand (line 130) | sendDeviceCommand(devData, cmd, vals) {
method sendUpdateStatus (line 178) | sendUpdateStatus() {
method sendStartDirect (line 213) | sendStartDirect() {
FILE: src/ST_DeviceCharacteristics.js
method constructor (line 4) | constructor(accessories, char) {
method manageGetCharacteristic (line 19) | manageGetCharacteristic(svc, acc, char, attr, opts = {}) {
method manageGetSetCharacteristic (line 49) | manageGetSetCharacteristic(svc, acc, char, attr, opts = {}) {
method acceleration_sensor (line 88) | acceleration_sensor(_accessory, _service) {
method air_purifier (line 100) | air_purifier(_accessory, _service) {
method air_quality (line 149) | air_quality(_accessory, _service) {
method alarm_system (line 161) | alarm_system(_accessory, _service) {
method battery (line 168) | battery(_accessory, _service) {
method button (line 176) | button(_accessory, _service) {
method buttonEvent (line 205) | buttonEvent(btnNum, btnVal, devId, btnMap) {
method carbon_dioxide (line 214) | carbon_dioxide(_accessory, _service) {
method carbon_monoxide (line 227) | carbon_monoxide(_accessory, _service) {
method contact_sensor (line 240) | contact_sensor(_accessory, _service) {
method energy_meter (line 252) | energy_meter(_accessory, _service) {
method fan (line 258) | fan(_accessory, _service) {
method garage_door (line 286) | garage_door(_accessory, _service) {
method humidity_sensor (line 293) | humidity_sensor(_accessory, _service) {
method illuminance_sensor (line 305) | illuminance_sensor(_accessory, _service) {
method light (line 322) | light(_accessory, _service) {
method lock (line 360) | lock(_accessory, _service) {
method motion_sensor (line 367) | motion_sensor(_accessory, _service) {
method power_meter (line 379) | power_meter(_accessory, _service) {
method presence_sensor (line 385) | presence_sensor(_accessory, _service) {
method smoke_detector (line 397) | smoke_detector(_accessory, _service) {
method speaker (line 409) | speaker(_accessory, _service) {
method switch_device (line 449) | switch_device(_accessory, _service) {
method temperature_sensor (line 455) | temperature_sensor(_accessory, _service) {
method thermostat (line 471) | thermostat(_accessory, _service) {
method valve (line 627) | valve(_accessory, _service) {
method virtual_mode (line 637) | virtual_mode(_accessory, _service) {
method virtual_routine (line 658) | virtual_routine(_accessory, _service) {
method water_sensor (line 684) | water_sensor(_accessory, _service) {
method window_shade (line 693) | window_shade(_accessory, _service) {
FILE: src/ST_Platform.js
method constructor (line 22) | constructor(log, config, api) {
method getLogConfig (line 57) | getLogConfig() {
method findDirectPort (line 76) | findDirectPort() {
method getConfigItems (line 83) | getConfigItems() {
method updateTempUnit (line 97) | updateTempUnit(unit) {
method getTempUnit (line 102) | getTempUnit() {
method didFinishLaunching (line 106) | didFinishLaunching() {
method refreshDevices (line 123) | refreshDevices(src = undefined) {
method getNewAccessory (line 170) | getNewAccessory(device, UUID) {
method addDevice (line 177) | addDevice(device) {
method updateDevice (line 188) | updateDevice(device) {
method removeAccessory (line 197) | removeAccessory(accessory) {
method configureAccessory (line 204) | configureAccessory(accessory) {
method processIncrementalUpdate (line 211) | processIncrementalUpdate(data, that) {
method isValidRequestor (line 220) | isValidRequestor(access_token, app_id, src) {
method WebServerInit (line 229) | WebServerInit() {
FILE: src/ST_ServiceTypes.js
method constructor (line 5) | constructor(accessories, srvc) {
method getServiceTypes (line 48) | getServiceTypes(accessory) {
method lookupServiceType (line 74) | lookupServiceType(name) {
class ServiceTest (line 82) | class ServiceTest {
method constructor (line 83) | constructor(name, testfn, onlyOnNoGrps = false) {
FILE: src/ST_Transforms.js
method constructor (line 5) | constructor(platform, char) {
method transformStatus (line 14) | transformStatus(val) {
method transformAttributeState (line 25) | transformAttributeState(attr, val, charName) {
method transformCommandName (line 207) | transformCommandName(attr, val) {
method transformCommandValue (line 244) | transformCommandValue(attr, val) {
method colorTempFromK (line 308) | colorTempFromK(temp) {
method colorTempToK (line 312) | colorTempToK(temp) {
method thermostatTempConversion (line 316) | thermostatTempConversion(temp, isSet = false) {
method thermostatTargetTemp (line 324) | thermostatTargetTemp(devData) {
method thermostatSupportedModes (line 350) | thermostatSupportedModes(devData) {
method thermostatTargetTemp_set (line 366) | thermostatTargetTemp_set(devData) {
method tempConversion (line 402) | tempConversion(temp, onlyC = false) {
method cToF (line 410) | cToF(temp) {
method fToC (line 414) | fToC(temp) {
method fanSpeedConversion (line 418) | fanSpeedConversion(speedVal, has4Spd = false) {
method fanSpeedConversionInt (line 443) | fanSpeedConversionInt(speedVal) {
method fanSpeedIntToLevel (line 455) | fanSpeedIntToLevel(speedVal) {
method fanSpeedLevelToInt (line 470) | fanSpeedLevelToInt(val) {
method convertAlarmState (line 482) | convertAlarmState(value) {
method convertAlarmCmd (line 496) | convertAlarmCmd(value) {
FILE: src/libs/Logger.js
method constructor (line 15) | constructor(platform, prefix, config) {
method getLogger (line 43) | getLogger() {
method removeAnsi (line 97) | removeAnsi(msg) {
method getLogLevel (line 102) | getLogLevel(lvl) {
method levelColor (line 106) | levelColor(lvl) {
method colorMsgLevel (line 131) | colorMsgLevel(lvl, msg) {
method enabledDebug (line 157) | enabledDebug() {
method disableDebug (line 161) | disableDebug() {
method enabledTimestamp (line 165) | enabledTimestamp() {
method disableTimestamp (line 169) | disableTimestamp() {
FILE: src/libs/MyUtils.js
method constructor (line 13) | constructor(platform) {
method cleanSpaces (line 20) | cleanSpaces(str) {
method toTitleCase (line 24) | toTitleCase(str) {
method debounce (line 28) | debounce(a, b, c) {
method getIPAddress (line 41) | getIPAddress() {
method updateConfig (line 55) | updateConfig(newConfig) {
method checkVersion (line 66) | checkVersion() {
Condensed preview — 34 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (410K chars).
[
{
"path": ".esformatter",
"chars": 673,
"preview": "{\n \"plugins\": [\n \"esformatter-quotes\",\n \"esformatter-braces\",\n \"esformatter-semicolons\"\n ],\n \"quotes\": {\n "
},
{
"path": ".eslintrc.json",
"chars": 617,
"preview": "{\n \"root\": true,\n \"env\": {\n \"es6\": true,\n \"node\": true\n },\n \"parserOptions\": {\n \"ecmaVe"
},
{
"path": ".github/FUNDING.yml",
"chars": 102,
"preview": "custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVFJTG8H86SK8&source=url\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1362,
"preview": "---\nname: Bug Report\nabout: Create a report to help us improve\ntitle: '(BUG) {ENTER TITLE HERE} '\nlabels: 'bug'\nassignee"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 631,
"preview": "---\nname: Feature Request\nabout: Suggest an idea for this project\ntitle: '[Feature Request]'\nlabels: 'enhancement'\nassig"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1066,
"preview": "<!--- Provide a general summary of your changes in the Title above -->\n\n## Checklist:\n\n<!--- Go over all the following p"
},
{
"path": ".github/stale.yml",
"chars": 685,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 60\n# Number of days of inactivity before a "
},
{
"path": ".github/workflows/nodejs.yml",
"chars": 1128,
"preview": "name: Node-CI\n\non: [push, pull_request]\n\njobs:\n build:\n\n strategy:\n matrix:\n node-version: [10.x, 12.x, "
},
{
"path": ".gitignore",
"chars": 1629,
"preview": ".vscode/*\n!.vscode/tasks.json\n!.vscode/launch.json\n!.vscode/extensions.json\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug"
},
{
"path": ".snyk",
"chars": 439,
"preview": "# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.\nversion: v1.14.1\nignore: {}\n# patches ap"
},
{
"path": "CHANGELOG-app.md",
"chars": 2130,
"preview": "# Changelog\n\n## v2.3.3\n\n- [FIX] Minor bugs and icons squashed.\n\n## v2.3.2\n\n- [NEW] Added support for bringing accelerati"
},
{
"path": "CHANGELOG.md",
"chars": 5822,
"preview": "## v2.3.8\n\n- [UPDATE] Updated dependencies.\n\n## v2.3.4\n\n- [REMOVE] Removing Sentry error reporting module prior to submi"
},
{
"path": "README.md",
"chars": 330,
"preview": "This Plugin is no longer being maintained. The ST platform removed all of the greatness that made it fun to develop for"
},
{
"path": "appData.json",
"chars": 194,
"preview": "{\n \"appDataVer\": \"1.1\",\n \"versions\": {\n \"mainApp\": \"2.3.3\",\n \"plugin\": \"2.3.4\"\n },\n \"settings\""
},
{
"path": "config.schema.json",
"chars": 7141,
"preview": "{\n \"pluginAlias\": \"SmartThings-v2\",\n \"pluginType\": \"platform\",\n \"singular\": true,\n \"footerDisplay\": \"If you "
},
{
"path": "installerManifest.json",
"chars": 1221,
"preview": "{\n \"namespace\": \"tonesto7\",\n \"repoOwner\": \"tonesto7\",\n \"repoName\": \"homebridge-smartthings\",\n \"repoBranch\": "
},
{
"path": "jsconfig.json",
"chars": 105,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES6\"\n },\n \"exclude\": [\n \"node_modules\"\n ]\n}"
},
{
"path": "package.json",
"chars": 2001,
"preview": "{\n \"dependencies\": {\n \"axios\": \"^0.19.2\",\n \"body-parser\": \"^1.19.0\",\n \"chalk\": \"^4.1.0\",\n "
},
{
"path": "platform.schema.json",
"chars": 6679,
"preview": "{\n \"plugin_alias\": \"SmartThings-v2\",\n \"schema\": {\n \"type\": \"object\",\n \"properties\": {\n \"n"
},
{
"path": "prettierrc",
"chars": 29,
"preview": "{\n \"trailingComma\": \"all\"\n}\n"
},
{
"path": "smartapps/tonesto7/homebridge-v2.src/homebridge-v2.groovy",
"chars": 81909,
"preview": "/**\n * Homebridge SmartThing Interface\n * Loosely Modelled off of Paul Lovelace's JSON API\n * Copyright 2018, 2019, 2"
},
{
"path": "src/ST_Accessories.js",
"chars": 16141,
"preview": "const knownCapabilities = require(\"./libs/Constants\").knownCapabilities,\n pluginVersion = require(\"./libs/Constants\")"
},
{
"path": "src/ST_Client.js",
"chars": 10397,
"preview": "const {\n platformName,\n platformDesc,\n pluginVersion\n} = require(\"./libs/Constants\"),\n axios = require('axio"
},
{
"path": "src/ST_DeviceCharacteristics.js",
"chars": 40286,
"preview": "var Characteristic, CommunityTypes, accClass;\n\nmodule.exports = class DeviceCharacteristics {\n constructor(accessorie"
},
{
"path": "src/ST_Platform.js",
"chars": 20281,
"preview": "const {\n pluginName,\n platformName,\n platformDesc,\n pluginVersion\n} = require(\"./libs/Constants\"),\n event"
},
{
"path": "src/ST_ServiceTypes.js",
"chars": 8931,
"preview": "// const debounce = require('debounce-promise');\nvar Service;\n\nmodule.exports = class ServiceTypes {\n constructor(acc"
},
{
"path": "src/ST_Transforms.js",
"chars": 20580,
"preview": "var Characteristic;\nvar CommunityTypes;\n\nmodule.exports = class Transforms {\n constructor(platform, char) {\n t"
},
{
"path": "src/index.js",
"chars": 232,
"preview": "const {\n pluginName,\n platformName\n} = require(\"./libs/Constants\"),\n StPlatform = require(\"./ST_Platform\");\n\nmo"
},
{
"path": "src/libs/CommunityTypes.js",
"chars": 17533,
"preview": "const inherits = require('util').inherits;\n\nmodule.exports = function(Service, Characteristic) {\n var CommunityTypes "
},
{
"path": "src/libs/Constants.js",
"chars": 1672,
"preview": "module.exports = {\n pluginName: \"homebridge-smartthings\",\n platformDesc: \"SmartThings\",\n platformName: \"SmartTh"
},
{
"path": "src/libs/HomeKitTypes-Bridge.js",
"chars": 18362,
"preview": "'use strict';\n// Removed from new HAS\n\nvar inherits = require('util').inherits;\nvar Characteristic = require('../Charact"
},
{
"path": "src/libs/HomeKitTypes.js",
"chars": 114138,
"preview": "/* eslint-disable no-unused-vars */\n'use strict';\n// THIS FILE IS AUTO-GENERATED - DO NOT MODIFY\n\nvar inherits = require"
},
{
"path": "src/libs/Logger.js",
"chars": 6102,
"preview": "const pluginName = require(\"./Constants\").pluginName,\n chalk = require('chalk'),\n { createLogger, format, transpor"
},
{
"path": "src/libs/MyUtils.js",
"chars": 3204,
"preview": "const {\n // platformName,\n // platformDesc,\n packageFile\n} = require(\"./Constants\"),\n _ = require(\"lodash\"),"
}
]
About this extraction
This page contains the full source code of the tonesto7/homebridge-smartthings GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 34 files (384.5 KB), approximately 90.5k tokens, and a symbol index with 142 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.