Compare commits

...

90 Commits

Author SHA1 Message Date
4e1dce3e32 Removing drone yaml
All checks were successful
Build homebridge-harmony-control / Version (push) Successful in 5s
Build homebridge-harmony-control / Build (push) Successful in 30s
Build homebridge-harmony-control / Publish Latest (push) Successful in 34s
Build homebridge-harmony-control / Deploy (push) Successful in 19s
2024-06-06 10:42:16 -05:00
2f030fa08c Adding actions
Some checks failed
Build homebridge-harmony-control / Version (push) Successful in 7s
Build homebridge-harmony-control / Build (push) Successful in 30s
continuous-integration/drone/push Build is passing
Build homebridge-harmony-control / Publish Latest (push) Failing after 34s
Build homebridge-harmony-control / Deploy (push) Has been skipped
2024-06-06 10:39:22 -05:00
Brandon Watson
bfba6e47d1 Fixing pipeline
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-30 12:21:23 -05:00
Brandon Watson
28abf136b7 Updating to use gitea for npm registry
All checks were successful
continuous-integration/drone Build is passing
2023-01-27 16:17:58 -06:00
Brandon Watson
ed652b2f76 Updating drone
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-06 09:04:46 -05:00
Brandon Watson
0846a72931 Update
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:50:16 -05:00
Brandon Watson
f1d1da0945 Adding step to remove old packages
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:48:45 -05:00
Brandon Watson
76438d6811 Separating restart step
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:38:49 -05:00
Brandon Watson
7971fd3ec1 Updating deployment step
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:22:38 -05:00
Brandon Watson
3fe902c50b Adding deploy step
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-07 16:16:15 -05:00
Brandon Watson
52a259dafd Fixing restart issue
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 13:24:54 -05:00
Brandon Watson
128bb933bd Fixing bug where volume would restart the first activity. ControlUnit now starts with last used activity
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 22:08:59 -05:00
Brandon Watson
4519edf679 Updating publish creds
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:22:18 -05:00
Brandon Watson
6794d58594 Bumping version
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:18:44 -05:00
Brandon Watson
5470b6185d #6 Adding overrides feature. Merge branch 'feature/refactor'
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:17:33 -05:00
Brandon Watson
51441a706d Cleanup
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:16:43 -05:00
Brandon Watson
e6e3d45b5b Removing business logic from models. First pass at overrides feature
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 20:11:13 -05:00
Brandon Watson
c22a8a0325 Successfully split up data provider
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 18:38:08 -05:00
Brandon Watson
506c170746 Renaming files
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 18:14:23 -05:00
Brandon Watson
4fd9341cea Update files
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-28 18:03:51 -05:00
Brandon Watson
c65853220e Renamed files. Refactored control unit (untested)
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-28 16:37:40 -05:00
Brandon Watson
1a51e4d8c5 Successfully added tsyringe to project
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-28 14:48:02 -05:00
Brandon Watson
503b4c9d59 Removing deployment step
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-22 15:51:54 -05:00
Brandon Watson
63cd6ce3ce Test
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 15:49:21 -05:00
Brandon Watson
ff1f500fa1 Update
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 15:44:37 -05:00
Brandon Watson
686054e9b3 Updating all instance of registry
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 15:41:50 -05:00
Brandon Watson
d7e241b381 Updating registry
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 15:40:27 -05:00
Brandon Watson
3a2db8397b Updating deploy command
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 15:32:25 -05:00
Brandon Watson
f0c4cd7824 Updating package lock
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 14:38:12 -05:00
Brandon Watson
1d8d483e8f Adding scope to package
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 14:28:14 -05:00
Brandon Watson
904b8c4fab Adding a deploy step
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 12:42:15 -05:00
Brandon Watson
a01ae86794 Update registry in package.json
All checks were successful
continuous-integration/drone/push Build is passing
2021-12-22 00:31:04 -05:00
Brandon Watson
f28b6429c5 Update npm creds
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 00:29:44 -05:00
Brandon Watson
5eff708b05 Removing build step dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-22 00:25:14 -05:00
Brandon Watson
ece54b54e7 Updating drone yaml
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-12-22 00:24:18 -05:00
Brandon Watson
6b3ea14520 Fixing issue where active accessories were pruned
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-09 19:36:49 -04:00
Brandon Watson
5dc28b2409 Successfully adding sequences
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-09 17:10:49 -04:00
Brandon Watson
cdab6327f5 Created sequence accessory
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-07 22:10:50 -04:00
Brandon Watson
1121dbc52c Fixing stateless device buttons to not error out when pressed
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-07 20:51:19 -04:00
Brandon Watson
bc2208b5cb Squashed commit of the following:
All checks were successful
continuous-integration/drone/push Build is passing
commit bccadde36f8df34ce9bfd96f73f39428af5c4b14
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:48:46 2021 -0400

    Using correct branch inclusion

commit 93132ab8684436e48ae05165193d88ee22f40ced
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:40:00 2021 -0400

    excluding publishing on PRs

commit 496dd42cfda82308185eb7cd52a300a7484d9053
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:38:37 2021 -0400

    Only publishing master branch

commit 57e82789fbe3de9a0da2d87a30dd01b3736d1478
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:36:16 2021 -0400

    WIP

commit ecc54ad3c30a0f6e781561787f74bdd2b556fc17
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:35:29 2021 -0400

    WIP

commit 6d3cd651be3a5aeae97a3aa94901c8eb97c05591
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:30:18 2021 -0400

    Attempting to use drone commit in preid

commit c60387a98746f1fb5dbd4244fedfed95e065a1ef
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:16:46 2021 -0400

    Adding sequence file

commit d8a5e7541d437d8208c75a4ba80f34bfc572194a
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:16:11 2021 -0400

    Prettier

commit 34fcc1df1aa2aa954e647e62e3ccfebbc0b9dc9e
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:14:33 2021 -0400

    Updating pre-release command

commit 6c8ce2116477214ba1e09a9a97a190b0b4633dc8
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:03:41 2021 -0400

    Updating drone config

commit b6d9317a1e4ff2056fb5efd48a8f02a7b1d35a64
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:58:35 2021 -0400

    Revert "Using email alias"

    This reverts commit d1a3b802938ee213c8d03867730ec260688b20e0.

commit d1a3b802938ee213c8d03867730ec260688b20e0
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:57:22 2021 -0400

    Using email alias

commit 027d1711ada5485bf7091f217bb5ce6e967c89b4
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:36:07 2021 -0400

    Removing bash_profile

commit ed06d7ecd0ccf8fb9651c8ba2a879c2abe1d2828
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:23:46 2021 -0400

    WIP

commit d3c37b18b9576b9d485d80c7923e92dfcb2fe808
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:22:35 2021 -0400

    Updating registry path
2021-06-07 20:49:45 -04:00
Brandon Watson
845071c274 Squashed commit of the following:
All checks were successful
continuous-integration/drone/push Build is passing
commit 93132ab8684436e48ae05165193d88ee22f40ced
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:40:00 2021 -0400

    excluding publishing on PRs

commit 496dd42cfda82308185eb7cd52a300a7484d9053
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:38:37 2021 -0400

    Only publishing master branch

commit 57e82789fbe3de9a0da2d87a30dd01b3736d1478
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:36:16 2021 -0400

    WIP

commit ecc54ad3c30a0f6e781561787f74bdd2b556fc17
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:35:29 2021 -0400

    WIP

commit 6d3cd651be3a5aeae97a3aa94901c8eb97c05591
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:30:18 2021 -0400

    Attempting to use drone commit in preid

commit c60387a98746f1fb5dbd4244fedfed95e065a1ef
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:16:46 2021 -0400

    Adding sequence file

commit d8a5e7541d437d8208c75a4ba80f34bfc572194a
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:16:11 2021 -0400

    Prettier

commit 34fcc1df1aa2aa954e647e62e3ccfebbc0b9dc9e
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:14:33 2021 -0400

    Updating pre-release command

commit 6c8ce2116477214ba1e09a9a97a190b0b4633dc8
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:03:41 2021 -0400

    Updating drone config

commit b6d9317a1e4ff2056fb5efd48a8f02a7b1d35a64
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:58:35 2021 -0400

    Revert "Using email alias"

    This reverts commit d1a3b802938ee213c8d03867730ec260688b20e0.

commit d1a3b802938ee213c8d03867730ec260688b20e0
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:57:22 2021 -0400

    Using email alias

commit 027d1711ada5485bf7091f217bb5ce6e967c89b4
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:36:07 2021 -0400

    Removing bash_profile

commit ed06d7ecd0ccf8fb9651c8ba2a879c2abe1d2828
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:23:46 2021 -0400

    WIP

commit d3c37b18b9576b9d485d80c7923e92dfcb2fe808
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Wed Jun 2 21:22:35 2021 -0400

    Updating registry path
2021-06-07 20:46:30 -04:00
Brandon Watson
2612f61082 feature/drone (#4)
All checks were successful
continuous-integration/drone/push Build is passing
Revert "Using email alias"

This reverts commit d1a3b802938ee213c8d03867730ec260688b20e0.

Using email alias

Removing bash_profile

WIP

Updating registry path

Co-authored-by: Brandon Watson <watsonb8133@gmail.com>
Reviewed-on: http://gitea.watsonlabs.net/watsonb8/homebridge-harmony-control/pulls/4
2021-06-02 21:59:52 -04:00
Brandon Watson
5a8b03a038 Adding notifier
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 21:20:09 -04:00
Brandon Watson
6c363f8d8c Adding official npm plugin
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 21:14:12 -04:00
Brandon Watson
cea0c3f1cf WIP
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 21:09:06 -04:00
Brandon Watson
2b13f78a0d WIP
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 21:05:29 -04:00
Brandon Watson
810431d82b Adding publish step
All checks were successful
continuous-integration/drone/push Build is passing
2021-06-02 20:54:21 -04:00
Brandon Watson
118a8912f6 Disabling default clone step
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-31 18:36:50 -05:00
Brandon Watson
f78c9d961c using https
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-05-31 18:32:58 -05:00
Brandon Watson
ae78cb2358 Updating clone rules
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2021-05-31 18:32:06 -05:00
Brandon Watson
7d89f6b945 Adding drone yml
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-31 18:15:28 -05:00
watsonb8
94dc6a37b9 Adding README 2020-12-20 12:07:02 -05:00
watsonb8
38ffdec2db Update to use homebridge types 2020-12-11 21:59:29 -05:00
watsonb8
c28de00928 Using TS types. Not building 2020-05-03 17:31:40 -04:00
watsonb8
9d76b8297e Bump homebridge rev 2020-05-03 15:43:54 -04:00
watsonb8
31c0a0856a Merge branch 'feature/multi_hub' 2020-01-24 23:18:30 -05:00
watsonb8
7907497360 Bump rev 2020-01-24 23:18:01 -05:00
watsonb8
3ac6ffb8a3 Worked around bug where receiver was not turning off 2020-01-24 23:17:33 -05:00
watsonb8
6f7fc4c2af Fixed device buttons 2020-01-24 21:47:13 -05:00
watsonb8
97c0f625db Successful multi hub 2020-01-24 21:31:43 -05:00
watsonb8
ec0c857c97 First pass done but untested 2020-01-23 09:28:59 -05:00
watsonb8
ba1a1685ae Using harmony device
Still need to look up devices specific to hubs (In the case of the same device on multiple hubs)
2020-01-23 09:01:14 -05:00
watsonb8
5ddd1e1af0 Progress commit 2020-01-23 08:44:11 -05:00
watsonb8
e9693435ce Added missing logic 2020-01-03 19:57:47 -05:00
watsonb8
a8fddae8c5 Bump rev 2020-01-03 19:51:11 -05:00
watsonb8
3759d91d1f Setting button state correctly for stateful buttons 2020-01-03 19:50:38 -05:00
watsonb8
b004e3a966 Added information service 2020-01-03 16:26:55 -05:00
watsonb8
0b08302656 Bump rev 2020-01-03 16:14:51 -05:00
watsonb8
7ec92f28cb Fixed bug where state wasn't being remembered 2020-01-03 16:14:12 -05:00
watsonb8
3a1428aa47 Bump rev 2020-01-01 23:58:00 -05:00
watsonb8
672362a9a4 Code re-organization 2020-01-01 23:39:07 -05:00
watsonb8
020a2fc240 Added device emit option for debugging purposes 2020-01-01 23:29:44 -05:00
watsonb8
588205e507 Added device buttons 2020-01-01 22:31:25 -05:00
watsonb8
80ac6423e5 Simplified config parsing 2020-01-01 00:17:37 -05:00
watsonb8
fb260c5532 Bump rev 2019-08-15 16:02:29 -04:00
watsonb8
f903c40f9c Getting rid of externalAccessories.
Rolling back external accessories specifically for remote because this was causing an unpredictable crash. (Likely an unhandled exception within either apple framework or HAPNodeJS
2019-08-13 21:02:40 -04:00
watsonb8
5c69e7b11f Cleaned up unused code. Corrected issue with getActive 2019-08-09 17:25:24 -04:00
watsonb8
a67674b3d3 deploy script now builds 2019-08-09 17:25:03 -04:00
watsonb8
f682c0e380 Added deployment script 2019-08-01 19:21:46 -04:00
watsonb8
14312908bf Added redundant matrix calls 2019-08-01 18:57:19 -04:00
watsonb8
152d73e9f9 Fixed UseMatrix being a string instead of boolean. This fixed listen to music activity. 2019-07-31 17:34:23 -04:00
watsonb8
727c8ceba7 Fixed issue where inputs weren't getting set. Possibly fixed incorrect tv issue. 2019-07-31 17:14:02 -04:00
watsonb8
39f4a7c1bc Enabled debugging support
# Conflicts:
#	src/DataProviders/HarmonyDataProvider.ts
2019-07-31 16:33:31 -04:00
watsonb8
73cba09edb Disabled features for default remote 2019-07-26 16:30:57 -04:00
watsonb8
9b48614dd1 Merge branch 'feature/neeo_control' 2019-07-26 15:44:53 -04:00
watsonb8
04024d565d Fixed issue with Tv's turning on while using remote 2019-07-26 15:44:26 -04:00
watsonb8
7638644935 Modifying to publish new external accessory to fix remote problem (untested) 2019-07-26 13:07:50 -04:00
watsonb8
f0eb57e4dc Commands only send if the activity has already been started. 2019-07-24 16:19:23 -04:00
watsonb8
0ea4188487 Added a delay between commands. Fixed issue where inputs were not being set correctly. 2019-06-23 11:42:10 -04:00
watsonb8
8bb65b5b3c Code cleanup. Added comments. 2019-06-17 15:59:15 -04:00
47 changed files with 3123 additions and 2661 deletions

View File

@ -1 +0,0 @@
source /nvm.sh

82
.gitea/workflows/ci.yaml Normal file
View File

@ -0,0 +1,82 @@
name: Build homebridge-harmony-control
on:
workflow_dispatch:
push:
branches:
- master
jobs:
build:
name: Build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: |
npm ci
npm run build
version:
name: Version
outputs:
version: ${{ steps.get_version.outputs.version }}
commit: ${{ steps.get_version.outputs.commit }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- id: get_version
name: Set Version
run: |
export version=`node -p "require('./package.json').version"`
export commit=`echo $GITHUB_SHA | cut -c1-5`
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "commit=$commit" >> "$GITHUB_OUTPUT"
publish_tagged:
name: Publish Latest
needs:
- build
- version
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm config set @watsonb8:registry https://gitea.watsonlabs.net/api/packages/watsonb8/npm/
- name: Publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
COMMIT: ${{ needs.version.outputs.commit }}
run: |
npm ci
npm config set -- '//gitea.watsonlabs.net/api/packages/watsonb8/npm/:_authToken' "$NPM_TOKEN"
npm version prerelease --preid="$COMMIT" --git-tag-version=false --allow-same-version=true
npm publish
deploy:
runs-on:
- ubuntu-latest
- internal
name: Deploy
needs: publish_tagged
steps:
- name: Set up SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.ELEVATED_HOMEBRIDGE_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -p 22 homebridge.me >> ~/.ssh/known_hosts
sudo apt update
sudo apt install sshpass
- name: Remove old Package
run: |
sshpass -p '${{ secrets.ELEVATED_HOMEBRIDGE_PASSWORD }}' ssh -v -o StrictHostKeyChecking=no ${{ secrets.ELEVATED_HOMEBRIDGE_USER }}@${{ secrets.HOMEBRIDGE_HOST }} <<'ENDSSH'
rm -r /home/${{ secrets.HOMEBRIDGE_USER }}/.npm-global/lib/node_modules/@watsonb8/homebridge-harmony-control
ENDSSH
- name: Deploy
run: |
sshpass -p '${{ secrets.ELEVATED_HOMEBRIDGE_PASSWORD }}' ssh -v -o StrictHostKeyChecking=no ${{ secrets.ELEVATED_HOMEBRIDGE_USER }}@${{ secrets.HOMEBRIDGE_HOST }} <<'ENDSSH'
npm install -g @watsonb8/homebridge-harmony-control
ENDSSH

19
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"preLaunchTask": "build",
"program": "/Users/brandonwatson/.nvm/versions/node/v14.15.0/lib/node_modules/homebridge/bin/homebridge",
"env": {
"HOMEBRIDGE_OPTS": "/Users/brandonwatson/.homebridge"
},
"sourceMaps": true
}
]
}

18
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,18 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"label": "build",
"problemMatcher": []
},
{
"type": "shell",
"label": "build and install",
"command": "npm run build&&sudo npm install -g --unsafe-perm ."
}
]
}

178
README.md Normal file
View File

@ -0,0 +1,178 @@
# homebridge-harmony-control
A convenient way to control your home through homekit utilizing one or many Logitech Harmony remotes.
## Description
This plugin not only allows for control of your one or many entertainment consoles, but it also supports use of a more complicated matrix switch if applicable. In addition, this plugin can expose any entertainment devices directly to homekit via stateless or stateful switch.
## Installation
1. Clone the repository by running `git clone ssh://git@thebword.ddns.net:3122/watsonb8/homebridge-harmony-control.git`
2. Run `npm install` to install required modules
3. Run `npm run build` to build the module
4. Run `npm link` or install globally to link this instance to your global homebridge instance
## Configuration
```
{
"platform": "HarmonyHubMatrix",
"EmitDevicesOnStartup": false,
"Hubs": [
{
"Name": "LivingRoomHub",
"Ip": "exampleOne.com"
},
{
"Name": "RackHub",
"Ip": "exampleTwo.com"
}
],
"Devices": [
{
"Name": "LG TV",
"Hub": "LivingRoomHub"
},
{
"Name": "JVC AV Receiver",
"Hub": "LivingRoomHub"
},
{
"Name": "Microsoft Xbox One",
"Hub": "RackHub"
},
{
"Name": "Apple TV Gen 4",
"Hub": "RackHub"
},
{
"Name": "Gefen AV Switch",
"Hub": "RackHub"
}
],
"Matrix": {
"DeviceName": "Gefen AV Switch",
"Inputs": [
{
"InputNumber": "1",
"InputDevice": "Microsoft Xbox One"
},
{
"InputNumber": "5",
"InputDevice": "Apple TV Gen 4"
},
],
"Outputs": [
{
"OutputLetter": "B",
"OutputDevice": "LG TV"
},
]
},
"ControlUnits": [
{
"DisplayName": "Living Room",
"Activities": [
{
"DisplayName": "Xbox One",
"DeviceSetupList": [
{
"DeviceName": "Microsoft Xbox One",
"Input": []
},
{
"DeviceName": "LG TV",
"Input": "Hdmi1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "Dvd"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Microsoft Xbox One",
"OutputDevice": "LG TV",
"UseMatrix": true
},
{
"DisplayName": "Apple TV",
"DeviceSetupList": [
{
"DeviceName": "Apple TV Gen 4",
"Input": []
},
{
"DeviceName": "LG TV",
"Input": "Hdmi1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "Dvd"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Apple TV Gen 4",
"OutputDevice": "LG TV",
"UseMatrix": true
},
]
},
],
"DeviceButtons": [
{
"DeviceName": "JVC AV Receiver",
"ButtonName": "SurroundToggle",
"DisplayName": "Surround Toggle",
"IsStateful": true
},
]
}
```
#### Platform
- `EmitDevicesOnStartup`: If true, each device frome each hub will be written to the console on startup
- `Hubs`: A list of harmony hub configurations
- `Devices`: A list of devices to be used and corresponding hubs
- `Matrix`: An optional configuration for an hdmi matrix
- `ControlUnits`: Configures one logical control unit. This is usually a single TV in a single room.
#### Devices
- `Name`: The name of the device to use from an existing harmony hub configuration
> Note: Use `EmitDevicesOnStartup` if you are unsure of which devices are available.
- `Hub`: The name of the hub to look the device on
#### Matrix
- `DeviceName`: The name of the matrix device as seen in the harmony configuration
> Note: This device must appear in the device list
- `Inputs`: A list of input devices and their corresponding input number as it relates to the matrix switch
- `Outputs`: A list of output devices and their corresponding output letter as it relates to the matrix switch
#### ControlUnits
This is the logical display unit. It will appear as a tv accessory in the home app.
- `DisplayName`: The name of the control unit
- `Activities`: A list of activity configurations. These will be the inputs associated with the tv accessory
- `DeviceSetupList`: A list of devices associated with an activity and their corresponding input setting
- `VolumeDevice`: The device that should be used to control the volume of the activity. This is usually a stereo or a tv
- `ControlDevice`: The device that is used to control UI navigation
- `OutputDevice`: The device that is used to display the content. This is usually a TV
- `UseMatrix`: If true, corresponding commands will be issued to the matrix device to route input to output

View File

@ -1,303 +0,0 @@
{
"bridge": {
"name": "Homebridge",
"username": "CC:22:3D:E3:CE:42",
"port": 51826,
"pin": "031-45-154"
},
"platforms": [
{
"platform": "websocket",
"name": "websocket",
"port": 4050
},
{
"platform": "HarmonyHubMatrix",
"hubIp": "192.168.1.14",
"Matrix": {
"DeviceName": "Gefen AV Switch",
"Inputs": [
{
"InputNumber": "1",
"InputDevice": "Microsoft Xbox One"
},
{
"InputNumber": "5",
"InputDevice": "Apple TV Gen 4"
},
{
"InputNumber": "3",
"InputDevice": "Chromecast"
},
{
"InputNumber": "2",
"InputDevice": "Sony PS4"
}
],
"Outputs": [
{
"OutputLetter": "B",
"OutputDevice": "LG TV"
},
{
"OutputLetter": "C",
"OutputDevice": "Vizio TV"
},
{
"OutputLetter": "D",
"OutputDevice": "Westinghouse TV"
}
]
},
"ControlUnits": [
{
"DisplayName": "Living Room",
"Activities": [
{
"DisplayName": "Chromecast",
"DeviceSetupList": [
{
"DeviceName": "LG TV",
"Input": "HDMI1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "DVD"
},
{
"DeviceName": "Chromecast",
"Input": []
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Chromecast",
"OutputDevice": "LG TV",
"UseMatrix": "true"
},
{
"DisplayName": "Play Xbox One",
"DeviceSetupList": [
{
"DeviceName": "Microsoft Xbox One",
"Input": []
},
{
"DeviceName": "LG TV",
"Input": "HDMI1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "DVD"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Microsoft Xbox One",
"OutputDevice": "LG TV",
"UseMatrix": "true"
},
{
"DisplayName": "Play PS4",
"DeviceSetupList": [
{
"DeviceName": "Sony PS4",
"Input": []
},
{
"DeviceName": "LG TV",
"Input": "HDMI1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "DVD"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Sony PS4",
"OutputDevice": "LG TV",
"UseMatrix": "true"
},
{
"DisplayName": "Apple TV",
"DeviceSetupList": [
{
"DeviceName": "Apple TV Gen 4",
"Input": []
},
{
"DeviceName": "LG TV",
"Input": "HDMI1"
},
{
"DeviceName": "JVC AV Receiver",
"Input": "DVD"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": "Apple TV Gen 4",
"OutputDevice": "LG TV",
"UseMatrix": "true"
},
{
"DisplayName": "Listen to Music",
"DeviceSetupList": [
{
"DeviceName": "JVC AV Receiver",
"Input": "DVD"
}
],
"VolumeDevice": "JVC AV Receiver",
"ControlDevice": [],
"OutputDevice": [],
"UseMatrix": "false"
}
]
},
{
"DisplayName": "Bedroom",
"Activities": [
{
"DisplayName": "Chromecast",
"DeviceSetupList": [
{
"DeviceName": "Vizio TV",
"Input": "HDMI1"
},
{
"DeviceName": "Harman Kardon AV Receiver",
"Input": "Game"
},
{
"DeviceName": "Chromecast",
"Input": []
}
],
"VolumeDevice": "Harman Kardon AV Receiver",
"ControlDevice": "Chromecast",
"OutputDevice": "Vizio TV",
"UseMatrix": "true"
},
{
"DisplayName": "Play Xbox One",
"DeviceSetupList": [
{
"DeviceName": "Microsoft Xbox One",
"Input": []
},
{
"DeviceName": "Vizio TV",
"Input": "HDMI1"
},
{
"DeviceName": "Harman Kardon AV Receiver",
"Input": "Game"
}
],
"VolumeDevice": "Harman Kardon AV Receiver",
"ControlDevice": "Microsoft Xbox One",
"OutputDevice": "Vizio TV",
"UseMatrix": "true"
},
{
"DisplayName": "Play PS4",
"DeviceSetupList": [
{
"DeviceName": "Sony PS4",
"Input": []
},
{
"DeviceName": "Vizio TV",
"Input": "HDMI1"
},
{
"DeviceName": "Harman Kardon AV Receiver",
"Input": "DVD"
}
],
"VolumeDevice": "Harman Kardon AV Receiver",
"ControlDevice": "Sony PS4",
"OutputDevice": "LG TV",
"UseMatrix": "true"
},
{
"DisplayName": "Apple TV",
"DeviceSetupList": [
{
"DeviceName": "Apple TV Gen 4",
"Input": []
},
{
"DeviceName": "Vizio TV",
"Input": "HDMI1"
},
{
"DeviceName": "Harman Kardon AV Receiver",
"Input": "Game"
}
],
"VolumeDevice": "Harman Kardon AV Receiver",
"ControlDevice": "Apple TV Gen 4",
"OutputDevice": "Vizio TV",
"UseMatrix": "true"
}
]
},
{
"DisplayName": "Kitchen",
"Activities": [
{
"DisplayName": "Chromecast",
"DeviceSetupList": [
{
"DeviceName": "Chromecast",
"Input": []
},
{
"DeviceName": "Westinghouse TV",
"Input": "HDMI1"
}
],
"VolumeDevice": "Westinghouse TV",
"ControlDevice": "Chromecast",
"OutputDevice": "Westinghouse TV",
"UseMatrix": "true"
},
{
"DisplayName": "Watch Apple Tv",
"DeviceSetupList": [
{
"DeviceName": "Apple TV Gen 4",
"Input": []
},
{
"DeviceName": "Westinghouse TV",
"Input": "HDMI1"
}
],
"VolumeDevice": "Westinghouse TV",
"ControlDevice": "Apple TV Gen 4",
"OutputDevice": "Westinghouse TV",
"UseMatrix": "true"
},
{
"DisplayName": "Play Xbox One",
"DeviceSetupList": [
{
"DeviceName": "Westinghouse TV",
"Input": "HDMI1"
},
{
"DeviceName": "Microsoft Xbox One",
"Input": []
}
],
"VolumeDevice": "Westinghouse TV",
"ControlDevice": "Microsoft Xbox One",
"OutputDevice": "Westinghouse TV",
"UseMatrix": "true"
}
]
}
]
}
]
}

View File

@ -3,5 +3,8 @@
{ {
"path": "." "path": "."
} }
] ],
"settings": {
"editor.tabSize": 2
}
} }

2065
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,13 @@
{ {
"name": "homebridge-harmony-control", "name": "@watsonb8/homebridge-harmony-control",
"version": "1.0.0", "version": "1.3.0",
"description": "Homebridge platform to control smart home equipment by room.", "description": "Homebridge platform to control smart home equipment by room.",
"main": "bin/index.js", "main": "bin/index.js",
"publishConfig": {
"registry": "https://gitea.watsonlabs.net"
},
"scripts": { "scripts": {
"build": "tsc", "build": "tsc --build",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"
}, },
"repository": { "repository": {
@ -21,14 +24,15 @@
"homekit" "homekit"
], ],
"engines": { "engines": {
"homebridge": ">=0.4.21", "homebridge": ">=1.1.6",
"node": ">=7.6.0" "node": ">=7.6.0"
}, },
"devDependencies": { "devDependencies": {
"homebridge": "^1.2.5",
"rollup-plugin-typescript": "^1.0.1", "rollup-plugin-typescript": "^1.0.1",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"tslint": "^5.17.0", "tslint": "^5.17.0",
"typescript": "^3.5.1" "typescript": "^3.9.10"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.4.5", "@babel/core": "^7.4.5",
@ -36,7 +40,8 @@
"@babel/preset-typescript": "^7.3.3", "@babel/preset-typescript": "^7.3.3",
"@types/node": "^12.0.7", "@types/node": "^12.0.7",
"harmony-websocket": "^1.1.0", "harmony-websocket": "^1.1.0",
"homebridge": "^0.4.50", "reflect-metadata": "^0.1.13",
"request": "^2.88.0" "request": "^2.88.0",
"tsyringe": "^4.6.0"
} }
} }

View File

@ -1,281 +0,0 @@
import { Activity } from '../Models/Activity';
import { Matrix } from '../Models/Matrix';
import { IAccessory } from './IAccessory';
import callbackify from '../Util/Callbackify';
import HarmonyDataProvider from '../DataProviders/HarmonyDataProvider';
let Service: HAPNodeJS.Service;
let Characteristic: HAPNodeJS.Characteristic;
let Api: any;
let homebridge: any;
export interface IControlUnitProps {
dataProvider: HarmonyDataProvider,
displayName: string,
activities: Array<Activity>,
api: any,
log: any,
homebridge: any,
}
/**
* ControlUnit accessory
*/
export class ControlUnit implements IAccessory {
//Required by homebridge
name: string = "";
//fields
private log: any = {};
private displayName: string = "";
//Service fields
private televisionService: HAPNodeJS.Service | undefined;
private televisionSpeakerService: HAPNodeJS.Service | undefined;
private informationService: HAPNodeJS.Service | undefined;
private inputServices: Array<HAPNodeJS.Service | undefined> = [];
//Harmony fields
private activities: Array<Activity> = [];
private dataProvider: HarmonyDataProvider;
public platformAccessory: any;
/**
* Constructor
* @param props Input properties
*/
constructor(props: IControlUnitProps) {
//Assign class variables
this.log = props.log;
Api = props.api;
Service = props.api.hap.Service;
Characteristic = props.api.hap.Characteristic;
this.displayName = props.displayName;
this.name = this.displayName;
this.activities = props.activities;
this.dataProvider = props.dataProvider;
homebridge = props.homebridge;
this.platformAccessory = new homebridge.platformAccessory(this.name, this.generateUUID(), homebridge.hap.Accessory.Categories.TELEVISION);
//Configure services
this.configureTvService();
this.configureTvSpeakerService();
// this.configureAccessoryInformation();
this.configureInputSourceService();
//Configure external services
this.getServices().forEach(service => {
try {
this.platformAccessory.addService(service);
} catch (error) { }
//@ts-ignore
if (service.linked) {
//@ts-ignore
this.televisionService!.addLinkedService(service);
}
});
}
/*************
*
* Tv Service
*
*************/
/**
* Configure television service
*/
private configureTvService(): void {
this.televisionService = new Service.Television(
this.displayName,
'tvService'
)
this.televisionService.setCharacteristic(Characteristic.ConfiguredName, this.displayName);
//@ts-ignore
this.televisionService.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE);
this.televisionService.setCharacteristic(Characteristic.ActiveIdentifier, 1);
this.televisionService.setCharacteristic(Characteristic.Active, false);
//setup listeners
this.televisionService.getCharacteristic(Characteristic.Active)
//@ts-ignore
.on("set", callbackify(this.onSetAccessoryActive))
//@ts-ignore
.on("get", callbackify(this.onGetAccessoryActive));
this.televisionService.getCharacteristic(Characteristic.RemoteKey)
//@ts-ignore
.on("set", callbackify(this.onSetRemoteKey));
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
//@ts-ignore
.on("set", callbackify(this.onSetActiveIdentifier))
//@ts-ignore
.on("get", callbackify(this.onGetActiveIdentifier));
}
/**
* Event handler for SET active characteristic
*/
private onSetAccessoryActive = async (value: any) => {
switch (value) {
case 0: this.dataProvider.powerOff(this.name); break;
//Turn on with first activity
case 1: this.dataProvider.powerOn(this.name, this.activities[0]); break;
}
}
/**
* Event handler for GET active characteristic
*/
private onGetAccessoryActive = async () => {
//@ts-ignore
return this.dataProvider.getIsActive ? Characteristic.Active.Active : Characteristic.Active.Inactive
}
/**
* Event handler for SET remote key
*/
private onSetRemoteKey = async (key: any) => {
this.dataProvider.sendKeyPress(this.name, key);
}
/**
* Event handler for SET active identifier characteristic
*/
private onSetActiveIdentifier = async (identifier: any) => {
this.dataProvider.startActivity(this.name, this.activities[identifier]);
}
/**
* Event handler for GET active identifier characteristic
*/
private onGetActiveIdentifier = async () => {
let currentActivity: Activity = this.dataProvider.getIsActive(this.name)!;
let identifier: number = 0;
if (currentActivity) {
identifier = this.activities.findIndex(e => e.displayName === currentActivity.displayName);
}
return identifier;
}
/******************
*
* Speaker Service
*
*****************/
/**
* Configure Speaker Service
*/
private configureTvSpeakerService(): void {
this.televisionSpeakerService = new Service.TelevisionSpeaker(
this.displayName,
'tvSpeakerService'
);
this.televisionSpeakerService.setCharacteristic(Characteristic.Name, this.displayName);
//@ts-ignore
this.televisionSpeakerService.setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE);
//@ts-ignore
this.televisionSpeakerService.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE);
this.televisionSpeakerService.subtype = this.displayName + "Volume";
if (this.televisionService) {
//@ts-ignore
this.televisionService.addLinkedService(this.televisionSpeakerService);
}
//Setup listeners
this.televisionSpeakerService.getCharacteristic(Characteristic.VolumeSelector)
//@ts-ignore
.on("set", callbackify(this.onSetVolumeSelector));
}
/**
* Event handler for SET volume characteristic
*/
private onSetVolumeSelector = async (value: any) => {
switch (value) {
case 0: this.dataProvider.volumeUp(this.name); break;
case 1: this.dataProvider.volumeDown(this.name); break;
}
}
/*********************
*
* Information Service
*
********************/
/**
* Configure information service
*/
private configureAccessoryInformation(): void {
this.informationService = new Service.AccessoryInformation(this.displayName, 'information');
this.informationService
.setCharacteristic(Characteristic.Manufacturer, 'Loftux Carwings')
.setCharacteristic(Characteristic.Model, 'Heater-Cooler')
}
/*****************
*
* Input services
*
*****************/
/**
* Configure input service
*/
private configureInputSourceService(): void {
let inputs: Array<HAPNodeJS.Service> = [];
this.activities.forEach((activity: Activity, index: number) => {
let inputService = new Service.InputSource(activity.displayName, 'activity' + activity.displayName);
inputService
.setCharacteristic(Characteristic.Identifier, index)
.setCharacteristic(
Characteristic.ConfiguredName,
activity.displayName)
.setCharacteristic(
Characteristic.IsConfigured,
//@ts-ignore
Characteristic.IsConfigured.CONFIGURED
)
//@ts-ignore
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI);
//@ts-ignore
this.televisionService!.addLinkedService(inputService);
inputs.push(inputService);
});
this.inputServices = inputs;
}
private generateUUID(): string { // Public Domain/MIT
var d = new Date().getTime();
if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
d += performance.now(); //use high-precision timer if available
}
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
}
/**
* Called by homebridge to gather services for this accessory.
*/
getServices(): Array<HAPNodeJS.Service> {
let services: Array<HAPNodeJS.Service> = [this.televisionService!, this.televisionSpeakerService!];
this.inputServices.forEach((service: HAPNodeJS.Service | undefined) => {
services.push(service!);
});
return (services);
}
}

View File

@ -1,10 +0,0 @@
export interface IAccessory {
/**
* Required by homebridge.
*/
name: string,
/**
* Called by homebridge to gather services.
*/
getServices(): Array<HAPNodeJS.Service>,
}

View File

@ -1,382 +0,0 @@
import { Activity } from "../Models/Activity";
import { DeviceSetupItem } from "../Models/DeviceSetupItem";
import { threadId } from "worker_threads";
import { Input, Matrix, Output } from "../Models/Matrix";
let Characteristic: HAPNodeJS.Characteristic;
const Harmony = require("harmony-websocket");
interface IDevice {
id: string,
name: string,
supportsCommand(commandName: string): boolean,
getCommand(commandName: string): string,
commands: { [name: string]: string };
on: boolean;
}
interface IActivityState {
currentActivity: Activity
}
export enum RemoteKey {
REWIND = 0,
FAST_FORWARD = 1,
NEXT_TRACK = 2,
PREVIOUS_TRACK = 3,
ARROW_UP = 4,
ARROW_DOWN = 5,
ARROW_LEFT = 6,
ARROW_RIGHT = 7,
SELECT = 8,
BACK = 9,
EXIT = 10,
PLAY_PAUSE = 11,
INFORMATION = 15,
}
interface IHarmonyDataProviderProps {
hubAddress: string,
log: any,
matrix: Matrix
}
class HarmonyDataProvider {
private harmony: any;
private log: any;
private hubAddress: string = "";
private connected: boolean = false;
private devices: { [name: string]: IDevice; } = {};
private states: { [controlUnitName: string]: (IActivityState | undefined) } = {};
private matrix: Matrix;
constructor(props: IHarmonyDataProviderProps) {
this.log = props.log;
this.hubAddress = props.hubAddress;
this.matrix = props.matrix;
this.harmony = new Harmony();
//Listeners
this.harmony.on('open', () => {
this.connected = true;
});
this.harmony.on('close', () => {
this.connected = false;
});
this.connect();
}
public powerOn = async (controlUnitName: string, activity: Activity) => {
//Only power on if not alread on
let currentActivity = this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined;
if (!currentActivity) {
await this.startActivity(controlUnitName, activity);
}
}
public powerOff = async (controlUnitName: string) => {
if (!this.states[controlUnitName]) {
return;
}
//Build potential list of devices to turn off
let devicesToTurnOff: Array<IDevice> = this.states[controlUnitName]!.currentActivity.deviceSetupItems
.map((value: DeviceSetupItem): IDevice => {
return this.getDeviceFromName(value.deviceName);
});
//Resolve device conflicts with other controlUnits
devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName);
//Turn off devices
devicesToTurnOff.forEach((device: IDevice) => {
this.powerOffDevice(device);
});
this.states[controlUnitName] = undefined;
}
public startActivity = async (controlUnitName: string, activity: Activity) => {
this.log(`Starting activity ${activity.displayName} for controlUnit: ${controlUnitName}`)
let lastActivity: Activity | undefined = undefined;
if (this.states[controlUnitName]) {
lastActivity = this.states[controlUnitName]!.currentActivity;
}
//Build potential list of devices to to turn on
let devicesToTurnOn: Array<IDevice> = activity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => {
return this.getDeviceFromName(value.deviceName);
});
//Resolve device conflicts with other controlUnits
devicesToTurnOn = this.sanitizeDeviceList(devicesToTurnOn, controlUnitName);
//Turn on devices
await Promise.all(devicesToTurnOn.map(async (device: IDevice) => {
if (device && device.name && this.devices[device.name]) {
if (!device.on) {
this.log(`Turning on device ${device.name}`)
await this.powerOnDevice(device);
}
}
}));
//Assign correct input
await Promise.all(
activity.deviceSetupItems.map(async (value: DeviceSetupItem) => {
let device: IDevice = this.getDeviceFromName(value.deviceName);
if (device && device.supportsCommand(`Input${value.input}`)) {
let command: string = device.getCommand(`Input${value.input}`);
await this.sendCommand(command);
}
})
);
if (activity.useMatrix) {
//get input and output
let input: Input = this.matrix.inputs.filter(e => e.inputDevice === activity.controlDeviceId)[0];
let output: Output = this.matrix.outputs.filter(e => e.outputDevice === activity.outputDeviceId)[0];
let inputCommandName: string = `In ${input.inputNumber}`;
let outputCommandName: string = `Out ${output.outputLetter}`;
let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName);
//Route hdmi
if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) {
await this.sendCommand(matrixDevice.getCommand(inputCommandName));
await this.sendCommand(matrixDevice.getCommand(outputCommandName));
}
}
//Build potential list of devices to turn off
if (lastActivity) {
let devicesToTurnOff: Array<IDevice> = lastActivity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => {
return this.getDeviceFromName(value.deviceName);
});
//remove devices that will be used for next activity from list
//delete array[index] is stupid because it just nulls out the index. But now i have to deal with nulls
devicesToTurnOff.forEach((device: IDevice, index: number) => {
if (device && device.name && activity.deviceSetupItems.some(e => {
return (e && e.deviceName === device.name)
})) {
delete devicesToTurnOff[index];
}
})
//Resolve device conflicts with other controlUnits
devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName);
this.log(`Sanatized devices to turn off: ${JSON.stringify(devicesToTurnOff.map(e => e ? e.name : ""))}`);
await Promise.all(
//Turn off devices
devicesToTurnOff.map(async (device: IDevice) => {
if (device) {
if (device.on) {
this.log(`Turning off device ${device.name}`)
await this.powerOffDevice(device);
}
}
})
);
}
//Assign current activity
this.states[controlUnitName] = { currentActivity: activity };
}
public volumeUp = async (controlUnitName: string) => {
let volumeUpCommand: string = "Volume Up"
if (this.states[controlUnitName]) {
let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId);
if (volumeDevice.supportsCommand(volumeUpCommand)) {
this.sendCommand(volumeDevice.getCommand(volumeUpCommand));
}
}
}
public volumeDown = async (controlUnitName: string) => {
let volumeDownCommand: string = "Volume Down"
if (this.states[controlUnitName]) {
let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId);
if (volumeDevice.supportsCommand(volumeDownCommand)) {
this.sendCommand(volumeDevice.getCommand(volumeDownCommand));
}
}
}
public sendKeyPress = async (controlUnitName: string, key: any) => {
if (this.states[controlUnitName]) {
let commandName: string = "";
let device: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.controlDeviceId);
switch (key) {
//@ts-ignore
case RemoteKey.ARROW_UP: {
commandName = "Direction Up";
break;
}
//@ts-ignore
case RemoteKey.ARROW_DOWN: {
commandName = "Direction Down";
break;
}
//@ts-ignore
case RemoteKey.ARROW_LEFT: {
commandName = "Direction Left";
break;
}
//@ts-ignore
case RemoteKey.ARROW_RIGHT: {
commandName = "Direction Right";
break;
}
//@ts-ignore
case RemoteKey.SELECT: {
commandName = "Select";
break;
}
//@ts-ignore
case RemoteKey.PLAY_PAUSE: {
commandName = "Pause";
break;
}
//@ts-ignore
case RemoteKey.INFORMATION: {
commandName = "Menu";
break;
}
//@ts-ignore
case RemoteKey.BACK: {
commandName = "Back";
break;
}
//@ts-ignore
case RemoteKey.EXIT: {
commandName = "Back";
break;
}
}
if (device && device.supportsCommand(commandName)) {
this.sendCommand(device.getCommand(commandName));
}
}
}
public getIsActive(controlUnitName: string): Activity | undefined {
return this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined;
}
/**
* Connect to harmony and receive device info
*/
private connect = async () => {
await this.harmony.connect(this.hubAddress);
let self = this;
setTimeout(async function () {
if (self.connected) {
let devices: any = await self.harmony.getDevices();
try {
await Promise.all(
//Add each to dictionary
devices.map(async (dev: any) => {
//get commands
let commands: { [name: string]: string } = {};
let deviceCommands: any = await self.harmony.getDeviceCommands(dev.id);
deviceCommands.forEach((command: any) => {
commands[command.label] = command.action;
});
self.devices[dev.label] = {
id: dev.id,
name: dev.label,
commands: commands,
on: false,
//Define device methods
supportsCommand(commandName: string): boolean {
let command = commands[commandName];
return (command) ? true : false;
},
getCommand(commandName: string): string {
return commands[commandName];
}
}
}));
self.log(`Harmony data provider ready`);
} catch (err) {
self.log(`ERROR - error connecting to harmony: ${err}`);
}
}
}, 1000);
}
private powerOffDevice = async (device: IDevice) => {
let powerOffCommand: string = "Power Off";
let powerToggleCommand: string = "Power Toggle";
if (device && device.supportsCommand(powerOffCommand)) {
await this.sendCommand(device.getCommand(powerOffCommand));
device.on = false;
} else if (device && device.supportsCommand(powerToggleCommand)) {
await this.sendCommand(device.getCommand(powerToggleCommand));
device.on = false;
}
}
private powerOnDevice = async (device: IDevice) => {
let powerOnCommand: string = "Power On";
let powerToggleCommand: string = "Power Toggle";
if (device && device.supportsCommand(powerOnCommand)) {
await this.sendCommand(device.getCommand(powerOnCommand));
device.on = true;
} else if (device && device.supportsCommand(powerToggleCommand)) {
await this.sendCommand(device.getCommand(powerToggleCommand));
device.on = true;
}
}
private getDeviceFromName(deviceName: string): IDevice {
return this.devices[deviceName];
}
private sanitizeDeviceList(devicesToTurnOn: Array<IDevice>, controlUnitName: string): Array<IDevice> {
for (let controlUnitKey in this.states) {
//Skip self
if (controlUnitKey === controlUnitName) {
continue;
}
let currentOtherState: IActivityState = this.states[controlUnitKey]!;
if (currentOtherState) {
currentOtherState.currentActivity.deviceSetupItems.forEach((value: DeviceSetupItem) => {
//there are devices to remove
if (devicesToTurnOn.some(e => e && e.name === value.deviceName)) {
let deviceToRemove: IDevice = devicesToTurnOn.filter(i => i.name === value.deviceName)[0];
delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)];
}
});
}
}
return devicesToTurnOn;
}
private sendCommand = async (command: string) => {
try {
let response = await this.harmony.sendCommand(JSON.stringify(command));
// this.log(`Sent command: ${JSON.stringify(command)} response: ${JSON.stringify(response)}`);
} catch (err) {
this.log(`ERROR - error sending command to harmony: ${err}`);
}
}
}
export default HarmonyDataProvider;

View File

@ -1,69 +0,0 @@
import { DeviceSetupItem } from './DeviceSetupItem';
/**
* Input properties.
*/
export interface IActivityProps {
deviceList: Array<DeviceSetupItem>,
controlDeviceId: string,
volumeDeviceId: string,
outputDeviceId: string,
displayName: string
useMatrix: boolean,
}
/**
* Data model class to hold activity related information.
*/
export class Activity {
private _volumeDeviceId: string = "";
private _outputDeviceId: string = "";
private _controlDeviceId: string = "";
private _displayName: string = "";
private _deviceSetupItems: Array<DeviceSetupItem>;
private _useMatrix: boolean = false;
constructor(props: IActivityProps) {
this._controlDeviceId = props.controlDeviceId;
this._outputDeviceId = props.outputDeviceId;
this._volumeDeviceId = props.volumeDeviceId;
this._displayName = props.displayName;
this._deviceSetupItems = props.deviceList;
this._useMatrix = props.useMatrix
}
/**
* The device associated with main control.
*/
public get controlDeviceId(): string {
return this._controlDeviceId;
};
/**
* The device associated with the volume control.
*/
public get volumeDeviceId(): string {
return this._volumeDeviceId
};
/**
* The device associated with output.
*/
public get outputDeviceId(): string {
return this._outputDeviceId;
};
/**
* The display name of the activity.
*/
public get displayName(): string {
return this._displayName;
}
public get deviceSetupItems(): Array<DeviceSetupItem> {
return this._deviceSetupItems
}
public get useMatrix(): boolean {
return this._useMatrix;
}
}

View File

@ -1,22 +0,0 @@
export interface IDeviceSetupItemProps {
deviceName: string,
input: string
}
export class DeviceSetupItem {
private _deviceId: string = "";
private _input: string = "";
constructor(props: IDeviceSetupItemProps) {
this._deviceId = props.deviceName;
this._input = props.input;
}
public get deviceName() {
return this._deviceId;
}
public get input() {
return this._input;
}
}

View File

@ -1,39 +0,0 @@
export interface IMatrixProps {
inputs: Array<Input>,
outputs: Array<Output>,
deviceName: string,
}
export interface Input {
inputNumber: string,
inputDevice: string,
}
export interface Output {
outputLetter: string,
outputDevice: string,
}
export class Matrix {
private _inputs: Array<Input> = [];
private _outputs: Array<Output> = [];
private _deviceName: string;
constructor(props: IMatrixProps) {
this._inputs = props.inputs;
this._outputs = props.outputs;
this._deviceName = props.deviceName;
}
public get inputs(): Array<Input> {
return this._inputs
}
public get outputs(): Array<Output> {
return this._outputs;
}
public get deviceName(): string {
return this._deviceName;
}
}

View File

@ -1,456 +0,0 @@
declare namespace HAPNodeJS {
export interface uuid {
generate(data: string): string;
isValid(UUID: string): boolean;
unparse(bug: string, offset: number): string;
}
type EventService = "characteristic-change" | "service-configurationChange"
export interface IEventEmitterAccessory {
addListener(event: EventService, listener: Function): this;
on(event: EventService, listener: Function): this;
once(event: EventService, listener: Function): this;
removeListener(event: EventService, listener: Function): this;
removeAllListeners(event?: EventService): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventService): Function[];
emit(event: EventService, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface Service extends IEventEmitterAccessory {
new(displayName: string, UUID: string, subtype: string): Service;
displayName: string;
UUID: string;
subtype: string;
iid: string;
characteristics: Characteristic[];
optionalCharacteristics: Characteristic[];
addCharacteristic(characteristic: Characteristic | Function): Characteristic;
removeCharacteristic(characteristic: Characteristic): void;
getCharacteristic(name: string | Function): Characteristic;
testCharacteristic(name: string | Function): boolean;
setCharacteristic(name: string | Function, value: CharacteristicValue): Service;
updateCharacteristic(name: string | Function, value: CharacteristicValue): Service;
addOptionalCharacteristic(characteristic: Characteristic | Function): void;
getCharacteristicByIID(iid: string): Characteristic;
toHAP(opt: any): JSON;
AccessoryInformation: PredefinedService;
AirPurifier: PredefinedService;
AirQualitySensor: PredefinedService;
BatteryService: PredefinedService;
BridgeConfiguration: PredefinedService;
BridgingState: PredefinedService;
CameraControl: PredefinedService;
CameraRTPStreamManagement: PredefinedService;
CarbonDioxideSensor: PredefinedService;
CarbonMonoxideSensor: PredefinedService;
ContactSensor: PredefinedService;
Door: PredefinedService;
Doorbell: PredefinedService;
Fan: PredefinedService;
Fanv2: PredefinedService;
Faucet: PredefinedService;
FilterMaintenance: PredefinedService;
GarageDoorOpener: PredefinedService;
HeaterCooler: PredefinedService;
HumidifierDehumidifier: PredefinedService;
HumiditySensor: PredefinedService;
InputSource: PredefinedService;
IrrigationSystem: PredefinedService;
LeakSensor: PredefinedService;
LightSensor: PredefinedService;
Lightbulb: PredefinedService;
LockManagement: PredefinedService;
LockMechanism: PredefinedService;
Microphone: PredefinedService;
MotionSensor: PredefinedService;
OccupancySensor: PredefinedService;
Outlet: PredefinedService;
Pairing: PredefinedService;
ProtocolInformation: PredefinedService;
Relay: PredefinedService;
SecuritySystem: PredefinedService;
ServiceLabel: PredefinedService;
Slat: PredefinedService;
SmokeSensor: PredefinedService;
Speaker: PredefinedService;
StatefulProgrammableSwitch: PredefinedService;
StatelessProgrammableSwitch: PredefinedService;
Switch: PredefinedService;
Television: PredefinedService;
TelevisionSpeaker: PredefinedService;
TemperatureSensor: PredefinedService;
Thermostat: PredefinedService;
TimeInformation: PredefinedService;
TunneledBTLEAccessoryService: PredefinedService;
Valve: PredefinedService;
Window: PredefinedService;
WindowCovering: PredefinedService;
}
export interface PredefinedService {
new(displayName: string, subtype: string): Service;
}
export interface CameraSource {
}
type EventAccessory = "service-configurationChange" | "service-characteristic-change" | "identify"
export interface IEventEmitterAccessory {
addListener(event: EventAccessory, listener: Function): this;
on(event: EventAccessory, listener: Function): this;
once(event: EventAccessory, listener: Function): this;
removeListener(event: EventAccessory, listener: Function): this;
removeAllListeners(event?: EventAccessory): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventAccessory): Function[];
emit(event: EventAccessory, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface CharacteristicProps {
format: Characteristic.Formats;
unit: Characteristic.Units,
minValue: number,
maxValue: number,
minStep: number,
perms: Characteristic.Perms[]
}
type EventCharacteristic = "get" | "set"
type CharacteristicValue = boolean | string | number
export type CharacteristicGetCallback<T = CharacteristicValue> = (error: Error | null, value: T) => void
export type CharacteristicSetCallback = (error?: Error | null) => void
export type CharacteristicCallback = CharacteristicGetCallback | CharacteristicSetCallback
export interface IEventEmitterCharacteristic {
addListener(event: EventCharacteristic, listener: CharacteristicCallback): this;
on(event: EventCharacteristic, listener: CharacteristicCallback): this;
once(event: EventCharacteristic, listener: CharacteristicCallback): this;
removeListener(event: EventCharacteristic, listener: CharacteristicCallback): this;
removeAllListeners(event?: EventCharacteristic): this;
setMaxListeners(n: number): this;
getMaxListeners(): number;
listeners(event: EventCharacteristic): CharacteristicCallback[];
emit(event: EventCharacteristic, ...args: any[]): boolean;
listenerCount(type: string): number;
}
export interface Characteristic extends IEventEmitterCharacteristic {
new(displayName: string, UUID: string, props?: CharacteristicProps): Characteristic;
Formats: typeof Characteristic.Formats;
Units: typeof Characteristic.Units;
Perms: typeof Characteristic.Perms;
setProps(props: CharacteristicProps): Characteristic
getValue(callback?: CharacteristicGetCallback, context?: any, connectionID?: string): void;
setValue(newValue: CharacteristicValue, callback?: CharacteristicSetCallback, context?: any, connectionID?: string): Characteristic;
updateValue(newValue: CharacteristicValue, callback?: () => void, context?: any): Characteristic;
getDefaultValue(): CharacteristicValue;
toHAP(opt: any): JSON;
AccessoryFlags: Characteristic;
AccessoryIdentifier: Characteristic;
Active: Characteristic;
ActiveIdentifier: Characteristic;
AdministratorOnlyAccess: Characteristic;
AirParticulateDensity: Characteristic;
AirParticulateSize: Characteristic;
AirQuality: Characteristic;
AppMatchingIdentifier: Characteristic;
AudioFeedback: Characteristic;
BatteryLevel: Characteristic;
Brightness: Characteristic;
CarbonDioxideDetected: Characteristic;
CarbonDioxideLevel: Characteristic;
CarbonDioxidePeakLevel: Characteristic;
CarbonMonoxideDetected: Characteristic;
CarbonMonoxideLevel: Characteristic;
CarbonMonoxidePeakLevel: Characteristic;
Category: Characteristic;
ChargingState: Characteristic;
ClosedCaptions: Characteristic;
ColorTemperature: Characteristic;
ConfigureBridgedAccessory: Characteristic;
ConfigureBridgedAccessoryStatus: Characteristic;
ConfiguredName: Characteristic;
ContactSensorState: Characteristic;
CoolingThresholdTemperature: Characteristic;
CurrentAirPurifierState: Characteristic;
CurrentAmbientLightLevel: Characteristic;
CurrentDoorState: Characteristic;
CurrentFanState: Characteristic;
CurrentHeaterCoolerState: Characteristic;
CurrentHeatingCoolingState: Characteristic;
CurrentHorizontalTiltAngle: Characteristic;
CurrentHumidifierDehumidifierState: Characteristic;
CurrentMediaState: Characteristic;
CurrentPosition: Characteristic;
CurrentRelativeHumidity: Characteristic;
CurrentSlatState: Characteristic;
CurrentTemperature: Characteristic;
CurrentTiltAngle: Characteristic;
CurrentTime: Characteristic;
CurrentVerticalTiltAngle: Characteristic;
CurrentVisibilityState: Characteristic;
DayoftheWeek: Characteristic;
DigitalZoom: Characteristic;
DiscoverBridgedAccessories: Characteristic;
DiscoveredBridgedAccessories: Characteristic;
DisplayOrder: Characteristic;
FilterChangeIndication: Characteristic;
FilterLifeLevel: Characteristic;
FirmwareRevision: Characteristic;
HardwareRevision: Characteristic;
HeatingThresholdTemperature: Characteristic;
HoldPosition: Characteristic;
Hue: Characteristic;
Identifier: Characteristic;
Identify: Characteristic;
ImageMirroring: Characteristic;
ImageRotation: Characteristic;
InUse: Characteristic;
InputDeviceType: Characteristic;
InputSourceType: Characteristic;
IsConfigured: Characteristic;
LeakDetected: Characteristic;
LinkQuality: Characteristic;
LockControlPoint: Characteristic;
LockCurrentState: Characteristic;
LockLastKnownAction: Characteristic;
LockManagementAutoSecurityTimeout: Characteristic;
LockPhysicalControls: Characteristic;
LockTargetState: Characteristic;
Logs: Characteristic;
Manufacturer: Characteristic;
Model: Characteristic;
MotionDetected: Characteristic;
Mute: Characteristic;
Name: Characteristic;
NightVision: Characteristic;
NitrogenDioxideDensity: Characteristic;
ObstructionDetected: Characteristic;
OccupancyDetected: Characteristic;
On: Characteristic;
OpticalZoom: Characteristic;
OutletInUse: Characteristic;
OzoneDensity: Characteristic;
PM10Density: Characteristic;
PM2_5Density: Characteristic;
PairSetup: Characteristic;
PairVerify: Characteristic;
PairingFeatures: Characteristic;
PairingPairings: Characteristic;
PictureMode: Characteristic;
PositionState: Characteristic;
PowerModeSelection: Characteristic;
ProgramMode: Characteristic;
ProgrammableSwitchEvent: Characteristic;
ProgrammableSwitchOutputState: Characteristic;
Reachable: Characteristic;
RelativeHumidityDehumidifierThreshold: Characteristic;
RelativeHumidityHumidifierThreshold: Characteristic;
RelayControlPoint: Characteristic;
RelayEnabled: Characteristic;
RelayState: Characteristic;
RemainingDuration: Characteristic;
RemoteKey: Characteristic;
ResetFilterIndication: Characteristic;
RotationDirection: Characteristic;
RotationSpeed: Characteristic;
Saturation: Characteristic;
SecuritySystemAlarmType: Characteristic;
SecuritySystemCurrentState: Characteristic;
SecuritySystemTargetState: Characteristic;
SelectedRTPStreamConfiguration: Characteristic;
SerialNumber: Characteristic;
ServiceLabelIndex: Characteristic;
ServiceLabelNamespace: Characteristic;
SetDuration: Characteristic;
SetupEndpoints: Characteristic;
SlatType: Characteristic;
SleepDiscoveryMode: Characteristic;
SmokeDetected: Characteristic;
SoftwareRevision: Characteristic;
StatusActive: Characteristic;
StatusFault: Characteristic;
StatusJammed: Characteristic;
StatusLowBattery: Characteristic;
StatusTampered: Characteristic;
StreamingStatus: Characteristic;
SulphurDioxideDensity: Characteristic;
SupportedAudioStreamConfiguration: Characteristic;
SupportedRTPConfiguration: Characteristic;
SupportedVideoStreamConfiguration: Characteristic;
SwingMode: Characteristic;
TargetAirPurifierState: Characteristic;
TargetAirQuality: Characteristic;
TargetDoorState: Characteristic;
TargetFanState: Characteristic;
TargetHeaterCoolerState: Characteristic;
TargetHeatingCoolingState: Characteristic;
TargetHorizontalTiltAngle: Characteristic;
TargetHumidifierDehumidifierState: Characteristic;
TargetMediaState: Characteristic;
TargetPosition: Characteristic;
TargetRelativeHumidity: Characteristic;
TargetSlatState: Characteristic;
TargetTemperature: Characteristic;
TargetTiltAngle: Characteristic;
TargetVerticalTiltAngle: Characteristic;
TargetVisibilityState: Characteristic;
TemperatureDisplayUnits: Characteristic;
TimeUpdate: Characteristic;
TunnelConnectionTimeout: Characteristic;
TunneledAccessoryAdvertising: Characteristic;
TunneledAccessoryConnected: Characteristic;
TunneledAccessoryStateNumber: Characteristic;
VOCDensity: Characteristic;
ValveType: Characteristic;
Version: Characteristic;
Volume: Characteristic;
VolumeControlType: Characteristic;
VolumeSelector: Characteristic;
WaterLevel: Characteristic;
}
module Characteristic {
export enum Formats {
BOOL,
INT,
FLOAT,
STRING,
ARRAY, // unconfirmed
DICTIONARY, // unconfirmed
UINT8,
UINT16,
UINT32,
UINT64,
DATA, // unconfirmed
TLV8
}
export enum Units {
// HomeKit only defines Celsius, for Fahrenheit, it requires iOS app to do the conversion.
CELSIUS,
PERCENTAGE,
ARC_DEGREE,
LUX,
SECONDS
}
export enum Perms {
READ,
WRITE,
NOTIFY,
HIDDEN
}
}
export interface PublishInfo {
port: number;
username: string;
pincode: string;
category: number;
}
export interface Accessory extends IEventEmitterAccessory {
new(displayName: string, UUID: string): Accessory;
displayName: string;
username: string;
pincode: string;
UUID: string;
aid: string;
bridged: boolean;
bridgedAccessories: Accessory[];
reachable: boolean;
category: Accessory.Categories;
services: Service[];
cameraSource: CameraSource;
Categories: typeof Accessory.Categories
addService(service: Service | Function): Service;
removeService(service: Service): void;
getService(name: string | Function): Service;
updateReachability(reachable: boolean): void;
addBridgedAccessory(accessory: Accessory, deferUpdate: boolean): Accessory;
addBridgedAccessories(accessories: Accessory[]): void
removeBridgedAccessory(accessory: Accessory, deferUpdate: boolean): void;
removeBridgedAccessories(accessories: Accessory[]): void;
getCharacteristicByIID(iid: string): Characteristic;
getBridgedAccessoryByAID(aid: string): Accessory;
findCharacteristic(aid: string, iid: string): Accessory;
configureCameraSource(cameraSource: CameraSource): void;
toHAP(opt: any): JSON;
publish(info: PublishInfo, allowInsecureRequest: boolean): void;
destroy(): void;
setupURI(): string;
}
module Accessory {
export enum Categories {
OTHER = 1,
BRIDGE = 2,
FAN = 3,
GARAGE_DOOR_OPENER = 4,
LIGHTBULB = 5,
DOOR_LOCK = 6,
OUTLET = 7,
SWITCH = 8,
THERMOSTAT = 9,
SENSOR = 10,
ALARM_SYSTEM = 11,
SECURITY_SYSTEM = 11,
DOOR = 12,
WINDOW = 13,
WINDOW_COVERING = 14,
PROGRAMMABLE_SWITCH = 15,
RANGE_EXTENDER = 16,
CAMERA = 17,
IP_CAMERA = 17,
VIDEO_DOORBELL = 18,
AIR_PURIFIER = 19,
AIR_HEATER = 20,
AIR_CONDITIONER = 21,
AIR_HUMIDIFIER = 22,
AIR_DEHUMIDIFIER = 23,
APPLE_TV = 24,
SPEAKER = 26,
AIRPORT = 27,
SPRINKLER = 28,
FAUCET = 29,
SHOWER_HEAD = 30,
TELEVISION = 31,
TARGET_CONTROLLER = 32
}
}
export interface HAPNodeJS {
init(storagePath?: string): void,
uuid: uuid,
Accessory: Accessory,
Service: Service,
Characteristic: Characteristic
}
}
declare var hapNodeJS: HAPNodeJS.HAPNodeJS;
declare module "hap-nodejs" {
export = hapNodeJS;
}

View File

@ -1,27 +0,0 @@
export default function callbackify(func: (...args: any[]) => Promise<any>): Function {
return (...args: any[]) => {
const onlyArgs: any[] = [];
let maybeCallback: Function | null = null;
for (const arg of args) {
if (typeof arg === 'function') {
maybeCallback = arg;
break;
}
onlyArgs.push(arg);
}
if (!maybeCallback) {
throw new Error("Missing callback parameter!");
}
const callback = maybeCallback;
func(...onlyArgs)
.then((data: any) => callback(null, data))
.catch((err: any) => callback(err))
}
}

View File

@ -0,0 +1,294 @@
import { PlatformAccessory, Service } from "homebridge";
import { IActivity } from "../models/config";
import { Platform } from "../platform";
import { ActivityService } from "../services/activityService";
import { CommandService } from "../services/commandService";
import { VolumeService } from "../services/volumeService";
import callbackify from "../util/callbackify";
/**
* Enum describing remote key presses from homebridge.
*/
export enum RemoteKey {
REWIND = 0,
FAST_FORWARD = 1,
NEXT_TRACK = 2,
PREVIOUS_TRACK = 3,
ARROW_UP = 4,
ARROW_DOWN = 5,
ARROW_LEFT = 6,
ARROW_RIGHT = 7,
SELECT = 8,
BACK = 9,
EXIT = 10,
PLAY_PAUSE = 11,
INFORMATION = 15,
}
export class ControlUnit {
constructor(
private readonly _platform: Platform,
private readonly _accessory: PlatformAccessory,
private _activityService: ActivityService,
private _commandService: CommandService,
private _volumeService: VolumeService,
private _activities: Array<IActivity>
) {
this._accessory
.getService(this._platform.Service.AccessoryInformation)!
.setCharacteristic(
this._platform.Characteristic.Manufacturer,
"Brandon Watson"
)
.setCharacteristic(
this._platform.Characteristic.Model,
"Matrix Output Television"
)
.setCharacteristic(
this._platform.Characteristic.SerialNumber,
"123-456-789"
);
const televisionUUID = this._platform.api.hap.uuid.generate(
`${this._accessory.displayName} Television`
);
//Init services
const televisionService =
this._accessory.getService(this._platform.Service.Television) ||
this._accessory.addService(
this._platform.Service.Television,
"Television",
televisionUUID
);
this.configureTvService(televisionService);
const televisionSpeakerUUID = this._platform.api.hap.uuid.generate(
`${this._accessory.displayName} Television Speaker`
);
const televisionSpeakerService =
this._accessory.getService(this._platform.Service.TelevisionSpeaker) ||
this._accessory.addService(
this._platform.Service.TelevisionSpeaker,
"Television Speaker",
televisionSpeakerUUID
);
this.configureTvSpeakerService(televisionService, televisionSpeakerService);
this.configureInputSourceService(televisionService);
}
/*************
*
* Tv Service
*
*************/
/**
* Configure television service
*/
private configureTvService(televisionService: Service): void {
televisionService.setCharacteristic(
this._platform.Characteristic.ConfiguredName,
this._accessory.displayName
);
//@ts-ignore
televisionService.setCharacteristic(
this._platform.Characteristic.SleepDiscoveryMode,
this._platform.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE
);
televisionService.setCharacteristic(
this._platform.Characteristic.ActiveIdentifier,
1
);
televisionService.setCharacteristic(
this._platform.Characteristic.Active,
false
);
//setup listeners
televisionService
.getCharacteristic(this._platform.Characteristic.Active)
//@ts-ignore
.on("set", callbackify(this.onSetAccessoryActive))
//@ts-ignore
.on("get", callbackify(this.onGetAccessoryActive));
//Set remote characteristics if is external
televisionService
.getCharacteristic(this._platform.Characteristic.RemoteKey)
//@ts-ignore
.on("set", callbackify(this.onSetRemoteKey));
televisionService
.getCharacteristic(this._platform.Characteristic.ActiveIdentifier)
//@ts-ignore
.on("set", callbackify(this.onSetActiveIdentifier))
//@ts-ignore
.on("get", callbackify(this.onGetActiveIdentifier));
}
/**
* Event handler for SET active characteristic
*/
private onSetAccessoryActive = async (value: any) => {
switch (value) {
case 0:
this._activityService.stopCurrentActivity(this._accessory.displayName);
break;
//Turn on with first activity
case 1:
if (this._activityService.getIsActive(this._accessory.displayName)) {
break;
}
this._activityService.startLastOrDefaultActivity(
this._accessory.displayName,
this._activities[0]
);
break;
}
};
/**
* Event handler for GET active characteristic
*/
private onGetAccessoryActive = async () => {
//@ts-ignore
return this._activityService.getCurrentActivity(this._accessory.displayName)
?.DisplayName
? this._platform.Characteristic.Active.ACTIVE
: this._platform.Characteristic.Active.INACTIVE;
};
/**
* Event handler for SET remote key
*/
private onSetRemoteKey = async (key: any) => {
this._commandService.sendKeyPress(this._accessory.displayName, key);
};
/**
* Event handler for SET active identifier characteristic
*/
private onSetActiveIdentifier = async (identifier: any) => {
this._activityService.startActivity(
this._accessory.displayName,
this._activities[identifier]
);
};
/**
* Event handler for GET active identifier characteristic
*/
private onGetActiveIdentifier = async () => {
let currentActivity: IActivity = this._activityService.getCurrentActivity(
this._accessory.displayName
)!;
let identifier: number = 0;
if (currentActivity) {
identifier = this._activities.findIndex(
(e) => e.DisplayName === currentActivity.DisplayName
);
}
return identifier;
};
/******************
*
* Speaker Service
*
*****************/
/**
* Configure Speaker Service
*/
private configureTvSpeakerService(
televisionService: Service,
televisionSpeakerService: Service
): void {
televisionSpeakerService.setCharacteristic(
this._platform.Characteristic.Name,
this._accessory.displayName
);
//@ts-ignore
televisionSpeakerService.setCharacteristic(
this._platform.Characteristic.Active,
this._platform.Characteristic.Active.ACTIVE
);
//@ts-ignore
televisionSpeakerService.setCharacteristic(
this._platform.Characteristic.VolumeControlType,
this._platform.Characteristic.VolumeControlType.ABSOLUTE
);
televisionSpeakerService.subtype = this._accessory.displayName + "Volume";
if (televisionService) {
//@ts-ignore
televisionService.addLinkedService(televisionSpeakerService);
}
//Setup listeners
televisionSpeakerService
.getCharacteristic(this._platform.Characteristic.VolumeSelector)
//@ts-ignore
.on("set", callbackify(this.onSetVolumeSelector));
}
/**
* Event handler for SET volume characteristic
*/
private onSetVolumeSelector = async (value: any) => {
switch (value) {
case 0:
this._volumeService.volumeUp(this._accessory.displayName);
break;
case 1:
this._volumeService.volumeDown(this._accessory.displayName);
break;
}
};
/*****************
*
* Input services
*
*****************/
/**
* Configure input service
*/
private configureInputSourceService(televisionService: Service): void {
let inputs: Array<Service> = [];
this._activities.forEach((activity: IActivity, index: number) => {
const inputUUID = this._platform.api.hap.uuid.generate(
`${activity.DisplayName} Input`
);
let inputService =
this._accessory.getService(activity.DisplayName) ||
this._accessory.addService(
this._platform.Service.InputSource,
activity.DisplayName,
inputUUID
);
inputService
.setCharacteristic(this._platform.Characteristic.Identifier, index)
.setCharacteristic(
this._platform.Characteristic.ConfiguredName,
activity.DisplayName
)
.setCharacteristic(
this._platform.Characteristic.IsConfigured,
//@ts-ignore
this._platform.Characteristic.IsConfigured.CONFIGURED
)
//@ts-ignore
.setCharacteristic(
this._platform.Characteristic.InputSourceType,
this._platform.Characteristic.InputSourceType.HDMI
);
//@ts-ignore
televisionService!.addLinkedService(inputService);
inputs.push(inputService);
});
}
}

View File

@ -0,0 +1,122 @@
import { PlatformAccessory, Service } from "homebridge";
import { HarmonyDataProvider } from "../dataProviders/harmonyDataProvider";
import { IDeviceButton } from "../models/config";
import { HarmonyDevice } from "../models/harmonyDevice";
import { Platform } from "../platform";
export class DeviceButton {
private _buttonState: boolean;
private _device!: HarmonyDevice;
private _switchService: Service;
constructor(
private readonly _platform: Platform,
private readonly _accessory: PlatformAccessory,
private _dataProvider: HarmonyDataProvider,
private _deviceInfo: IDeviceButton
) {
this._buttonState = false;
if (this._deviceInfo.NumberOfKeyPresses && this._deviceInfo.IsStateful) {
throw new Error(
"A button cannot be stateful and be pressed more than once"
);
}
this._accessory
.getService(this._platform.Service.AccessoryInformation)!
.setCharacteristic(
this._platform.Characteristic.Manufacturer,
"Brandon Watson"
)
.setCharacteristic(this._platform.Characteristic.Model, "Device Button")
.setCharacteristic(
this._platform.Characteristic.SerialNumber,
"123-456-789"
);
const switchUUID = this._platform.api.hap.uuid.generate(
`${this._accessory.displayName} Switch`
);
this._switchService =
this._accessory.getService(this._platform.Service.Switch) ||
this._accessory.addService(
this._platform.Service.Switch,
this._accessory.displayName,
switchUUID
);
this._switchService
.getCharacteristic(this._platform.Characteristic.On)
//@ts-ignore
.on("set", this.onSwitchSet)
.updateValue(this._buttonState)
.on("get", this.onSwitchGet);
}
/**
* Handler for switch set event
* @param callback The callback function to call when complete
*/
private onSwitchSet = async (
newState: boolean,
callback: (error?: Error | null | undefined) => void
) => {
if (!this._deviceInfo.IsStateful && newState === this._buttonState) {
return callback();
}
//Get device command if we don't have it
if (!this._device) {
this._device = this._dataProvider.getDeviceByName(
this._deviceInfo.DeviceName
);
}
//Execute command
if (!this._device) {
return callback();
}
//change state if stateful
if (this._deviceInfo.IsStateful && this._buttonState != newState) {
this._buttonState = newState;
await this._dataProvider.sendCommand(
this._deviceInfo.ButtonName,
this._device
);
return callback();
} else if (!this._deviceInfo.IsStateful) {
//Send the number of configured key presses
for (let i = 0; i < this._deviceInfo.NumberOfKeyPresses; i++) {
await this._dataProvider.sendCommand(
this._deviceInfo.ButtonName,
this._device
);
}
this._switchService
.getCharacteristic(this._platform.Characteristic.On)
.updateValue(false);
this._buttonState = false;
return callback();
}
};
/**
* Handler for switch get event
* @param callback The callback function to call when complete
*/
private onSwitchGet = (
callback: (error: Error | null, value: boolean) => void
) => {
//Only return state if button is stateful
if (this._deviceInfo.IsStateful) {
return callback(null, this._buttonState);
} else {
return callback(null, false);
}
};
}

2
src/accessories/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { ControlUnit } from "./controlUnit";
export { DeviceButton } from "./deviceButton";

105
src/accessories/sequence.ts Normal file
View File

@ -0,0 +1,105 @@
import {
CharacteristicGetCallback,
CharacteristicSetCallback,
CharacteristicValue,
PlatformAccessory,
Service,
} from "homebridge";
import { HarmonyDataProvider } from "../dataProviders/harmonyDataProvider";
import { ISequence } from "../models/config/sequence";
import { HarmonyDevice } from "../models/harmonyDevice";
import { Platform } from "../platform";
import { sleep } from "../util";
export class Sequence {
private _devices: { [deviceName: string]: HarmonyDevice };
private _switchService: Service;
constructor(
private readonly _platform: Platform,
private readonly _accessory: PlatformAccessory,
private _dataProvider: HarmonyDataProvider,
private _sequence: ISequence
) {
this._accessory
.getService(this._platform.Service.AccessoryInformation)!
.setCharacteristic(
this._platform.Characteristic.Manufacturer,
"Brandon Watson"
)
.setCharacteristic(this._platform.Characteristic.Model, "Sequence Button")
.setCharacteristic(
this._platform.Characteristic.SerialNumber,
"123-456-789"
);
const switchUUID = this._platform.api.hap.uuid.generate(
`${this._accessory.displayName} Switch`
);
this._switchService =
this._accessory.getService(this._platform.Service.Switch) ||
this._accessory.addService(
this._platform.Service.Switch,
this._accessory.displayName,
switchUUID
);
this._switchService
.getCharacteristic(this._platform.Characteristic.On)
.on("set", this.onSwitchSet)
.updateValue(false)
.on("get", (callback: CharacteristicGetCallback): void => {
return callback(null);
});
this._devices = {};
// Get devices in sequence
for (const deviceName of _sequence.Steps.map((e) => e.DeviceName)) {
if (!deviceName) {
continue;
}
const device = this._dataProvider.getDeviceByName(deviceName);
if (device) {
this._devices[deviceName] = device;
} else {
this._platform.log.warn(
`Device ${deviceName} was not found in harmony configuration`
);
}
}
}
/**
* Handler for switchSet command
* @param callback
*/
public onSwitchSet = async (
_value: CharacteristicValue,
callback: CharacteristicSetCallback
): Promise<void> => {
// Execute sequence
for (const step of this._sequence.Steps) {
await sleep(step.Delay);
const device: HarmonyDevice = this._devices[step.DeviceName ?? ""];
if (
device &&
step.DeviceCommand &&
device.supportsCommand(step.DeviceCommand)
) {
await this._dataProvider.sendCommand(step.DeviceCommand, device);
} else {
this._platform.log.warn(
`Attempted to execute command ${step.DeviceCommand} on device ${step.DeviceName} but the device or command was not found`
);
}
}
// Deactivate button
this._switchService
.getCharacteristic(this._platform.Characteristic.On)
.updateValue(false);
callback(null);
};
}

View File

@ -0,0 +1,166 @@
import { Logging } from "homebridge";
import { inject, injectable } from "tsyringe";
import { ICommand } from "../models";
import { IConfig } from "../models/config";
import { IDeviceConfig } from "../models/config/deviceConfig";
import { IHub } from "../models/config/hub";
import { HarmonyDevice } from "../models/harmonyDevice";
import { HarmonyHub } from "../models/harmonyHub";
@injectable()
export class HarmonyDataProvider {
private _hubs: { [hubName: string]: HarmonyHub } = {};
private _deviceConfigByName: { [deviceName: string]: IDeviceConfig } = {};
constructor(
@inject("IConfig") private _config: IConfig,
@inject("log") private _log: Logging
) {
_config.Devices.forEach((deviceConfig: IDeviceConfig) => {
this._deviceConfigByName[deviceConfig.Name] = deviceConfig;
});
this.connect(_config.Hubs);
this.emitInfo();
}
public async powerOnDevices(devices: Array<HarmonyDevice>): Promise<void> {
await Promise.all(
devices.map(async (device: HarmonyDevice) => {
if (device && device.name) {
if (!device.on) {
this._log.info(`Turning on device ${device.name}`);
await this.powerOnDevice(device);
}
}
})
);
}
public async powerOffDevices(devices: Array<HarmonyDevice>) {
await Promise.all(
//Turn off devices
devices.map(async (device: HarmonyDevice) => {
if (device) {
if (device.on) {
this._log.info(`Turning off device ${device.name}`);
await this.powerOffDevice(device);
}
}
})
);
}
public async sendCommand(
commandName: string,
harmonyDevice: HarmonyDevice
): Promise<void> {
let command!: ICommand;
commandName = this.getOverrideCommand(commandName, harmonyDevice);
if (harmonyDevice.supportsCommand(commandName)) {
command = harmonyDevice.getCommand(commandName);
}
const hub = this.getHubByDevice(harmonyDevice);
await hub.sendCommand(command);
}
/**
* Get the IDevice by name.
* @param deviceName The device to retrieve.
*/
public getDeviceByName(deviceName: string): HarmonyDevice {
let device: HarmonyDevice | undefined;
try {
device =
this._hubs[this._deviceConfigByName[deviceName].Hub].getDeviceByName(
deviceName
);
} catch (err) {
this._log.info(`Error retrieving device from hub: ${err}`);
}
return device!;
}
private connect = async (hubs: Array<IHub>) => {
let readyCount = 0;
await Promise.all(
hubs.map(async (hub: IHub): Promise<void> => {
const newHarmonyHub = new HarmonyHub(hub.Name, hub.Ip, this._log.info);
this._hubs[hub.Name] = newHarmonyHub;
newHarmonyHub.on("Ready", () => {
readyCount++;
if (readyCount === Object.keys(this._hubs).length) {
// this.emit("Ready");
}
});
await newHarmonyHub.initialize();
})
);
};
private emitInfo(): void {
//Emit devices if requested
this._log.info("All hubs connected");
if (this._config.EmitDevicesOnStartup) {
const hubs = this._hubs;
Object.values(hubs).forEach((hub: HarmonyHub) => {
const deviceDictionary = hub.devices;
this._log.info(`${hub.hubName}`);
Object.values(deviceDictionary).forEach((device: HarmonyDevice) => {
this._log.info(` ${device.name} : ${device.id}`);
Object.keys(device.commands).forEach((command: string) => {
this._log.info(` ${command}`);
});
});
});
}
}
private getHubByDevice(device: HarmonyDevice) {
return this._hubs[this._deviceConfigByName[device.name].Hub];
}
private async powerOnDevice(harmonyDevice: HarmonyDevice): Promise<void> {
let powerOnCommand: string = "Power On";
let powerToggleCommand: string = "Power Toggle";
if (harmonyDevice.supportsCommand(powerOnCommand)) {
await this.sendCommand(powerOnCommand, harmonyDevice);
harmonyDevice.on = true;
} else if (harmonyDevice.supportsCommand(powerToggleCommand)) {
await this.sendCommand(powerToggleCommand, harmonyDevice);
harmonyDevice.on = true;
} else {
await this.sendCommand(powerOnCommand, harmonyDevice);
}
}
private async powerOffDevice(harmonyDevice: HarmonyDevice): Promise<void> {
let powerOffCommand: string = "Power Off";
let powerToggleCommand: string = "Power Toggle";
if (harmonyDevice.supportsCommand(powerOffCommand)) {
await this.sendCommand(powerOffCommand, harmonyDevice);
harmonyDevice.on = false;
} else if (harmonyDevice.supportsCommand(powerToggleCommand)) {
await this.sendCommand(powerToggleCommand, harmonyDevice);
harmonyDevice.on = false;
}
}
private getOverrideCommand(
commandName: string,
harmonyDevice: HarmonyDevice
) {
const deviceConfig: IDeviceConfig =
this._deviceConfigByName[harmonyDevice.name];
if (!deviceConfig.Overrides) {
return commandName;
}
const overrideCommand = deviceConfig.Overrides.find(
(e) => e.Command == commandName
);
return overrideCommand ? overrideCommand.Override : commandName;
}
}

View File

@ -0,0 +1,43 @@
import { injectable } from "tsyringe";
import { IActivityState } from "../models/activityState";
import { IActivity } from "../models/config";
@injectable()
export class StateDataProvider {
private _states: {
[controlUnitName: string]: IActivityState | undefined;
} = {};
public updateState(activity: IActivity, controlUnitName: string): void {
const oldState = this._states[controlUnitName];
this._states[controlUnitName] = {
currentActivity: activity,
lastActivity: oldState?.lastActivity,
};
}
public deactivateState(controlUnitName: string): void {
const oldState = this._states[controlUnitName];
this._states[controlUnitName] = {
currentActivity: undefined,
lastActivity: oldState?.currentActivity,
};
}
public removeState(controlUnitName: string): void {
this._states[controlUnitName] = undefined;
}
public getState(controlUnitName: string): IActivityState | undefined {
if (!this._states[controlUnitName]) {
return undefined;
}
return this._states[controlUnitName];
}
public get states(): {
[controlUnitName: string]: IActivityState | undefined;
} {
return this._states;
}
}

View File

@ -1,152 +1,11 @@
import { ControlUnit } from "./Accessories/ControlUnit"; import { API } from "homebridge";
import { Activity } from "./Models/Activity";
import { DeviceSetupItem } from "./Models/DeviceSetupItem";
import { Input, Output, Matrix } from "./Models/Matrix";
import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider";
let Accessory: any; import { PLATFORM_NAME } from "./settings";
let Homebridge: any; import { Platform } from "./platform";
export default function (homebridge: any) { /**
Homebridge = homebridge; * This method registers the platform with Homebridge
Accessory = homebridge.platformAccessory; */
homebridge.registerPlatform( export = (api: API) => {
'homebridge-harmony-watson', api.registerPlatform(PLATFORM_NAME, Platform);
'HarmonyHubMatrix',
HarmonyMatrixPlatform,
true
);
}; };
class HarmonyMatrixPlatform {
log: any = {};
config: any = {};
api: any;
externalAccessories: Array<any> = [];
constructor(log: any, config: any, api: any) {
this.log = log;
this.config = config;
this.api = api;
this.log('INFO - Registering Harmony Matrix Platform');
this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this));
}
didFinishLaunching() {
this.log(`Publishing external accessories`);
this.externalAccessories.forEach((accessory: ControlUnit) => {
this.api.publishExternalAccessories("HarmonyMatrixPlatform", [accessory.platformAccessory]);
})
}
/**
* Called by homebridge to gather accessories.
* @param callback
*/
accessories(callback: (accessories: Array<ControlUnit>) => void) {
//Parse ip
let hubIp: string = this.config["hubIp"];
//Parse matrix
let configInputs: any = this.config["Matrix"]["Inputs"];
let configOutputs: any = this.config["Matrix"]["Outputs"];
let matrixName: string = this.config["Matrix"]["DeviceName"];
let inputs: Array<Input> = [];
let outputs: Array<Output> = [];
configInputs.forEach((configInput: any) => {
let inputDevice: string = configInput["InputDevice"];
let inputNumber: string = configInput["InputNumber"];
this.log(`INFO - Added input to matrix '${inputDevice}'`);
inputs.push({
inputDevice: inputDevice,
inputNumber: inputNumber
});
});
configOutputs.forEach((configOutput: any) => {
let outputDevice: string = configOutput["OutputDevice"];
let outputLetter: string = configOutput["OutputLetter"];
this.log(`INFO - Added output to matrix '${outputDevice}'`);
outputs.push({
outputDevice: outputDevice,
outputLetter: outputLetter
});
});
let matrix = new Matrix({
inputs: inputs,
outputs: outputs,
deviceName: matrixName,
});
//construct data provider
let dataProvider = new HarmonyDataProvider({
hubAddress: hubIp,
matrix: matrix,
log: this.log
});
//Parse control units
let configControlUnits: any = this.config["ControlUnits"];
let controlUnits: Array<ControlUnit> = [];
configControlUnits.forEach((configControlUnit: any) => {
//Parse activities list
let configActivities: any = configControlUnit["Activities"];
let activities: Array<Activity> = [];
configActivities.forEach((configActivity: any) => {
//parse devices
let configDevices: any = configActivity["DeviceSetupList"];
let devices: Array<DeviceSetupItem> = [];
configDevices.forEach((configDevice: any) => {
//Add device
devices.push(new DeviceSetupItem({
deviceName: configDevice["DeviceName"],
input: configDevice["Input"]
}));
this.log(`INFO - Added device '${configDevice["DeviceName"]}' for activity '${configActivity["DisplayName"]}'`);
});
//Add activity
activities.push(new Activity({
volumeDeviceId: configActivity["VolumeDevice"],
controlDeviceId: configActivity["ControlDevice"],
outputDeviceId: configActivity["OutputDevice"],
displayName: configActivity["DisplayName"],
useMatrix: configActivity["UseMatrix"],
deviceList: devices
}));
this.log(`INFO - Added activity '${configActivity["DisplayName"]}'`);
});
// let accessory = new Accessory(configControlUnit["DisplayName"],
// Homebridge.hap.uuid.generate(configControlUnit["DisplayName"], Homebridge.hap.Accessory.Categories.TELEVISION));
let controlUnit: ControlUnit = new ControlUnit({
dataProvider: dataProvider,
displayName: configControlUnit["DisplayName"],
api: this.api,
log: this.log,
activities: activities,
homebridge: Homebridge
});
//@ts-ignore
let accessory = controlUnit as homebridge.platformAccessory;
//Add control unit
controlUnits.push(accessory);
this.log(`INFO - Added ControlUnit`);
});
this.externalAccessories = controlUnits;
callback(controlUnits);
}
}

View File

@ -0,0 +1,6 @@
import { IActivity } from "./config";
export interface IActivityState {
currentActivity?: IActivity;
lastActivity?: IActivity;
}

View File

@ -0,0 +1,10 @@
import { IDeviceSetupItem } from "./deviceSetupItem";
export interface IActivity {
OutputDevice: string;
VolumeDevice: string;
ControlDevice: string;
DisplayName: string;
DeviceSetupList: Array<IDeviceSetupItem>;
UseMatrix: boolean;
}

View File

@ -0,0 +1,22 @@
import { IMatrix } from "./matrix";
import { IActivity } from "./activity";
import { IDeviceButton } from "./deviceButton";
import { IDeviceConfig } from "./deviceConfig";
import { IHub } from "./hub";
import { ISequence } from "./sequence";
export interface IControlUnit {
DisplayName: string;
Activities: Array<IActivity>;
}
export interface IConfig {
hubIp: string;
EmitDevicesOnStartup: boolean;
Matrix: IMatrix;
ControlUnits: Array<IControlUnit>;
DeviceButtons: Array<IDeviceButton>;
Sequences: Array<ISequence>;
Devices: Array<IDeviceConfig>;
Hubs: Array<IHub>;
}

View File

@ -0,0 +1,7 @@
export interface IDeviceButton {
DeviceName: string;
ButtonName: string;
DisplayName: string;
NumberOfKeyPresses: number;
IsStateful: boolean;
}

View File

@ -0,0 +1,5 @@
export interface IDeviceConfig {
Name: string;
Hub: string;
Overrides: Array<{ Command: string; Override: string }>;
}

View File

@ -0,0 +1,5 @@
export interface IDeviceSetupItem {
DeviceName: string;
Input: string;
}

5
src/models/config/hub.ts Normal file
View File

@ -0,0 +1,5 @@
export interface IHub {
Name: string;
Ip: string;
Harmony: any;
}

View File

@ -0,0 +1,5 @@
export * from "./activity";
export * from "./config";
export * from "./deviceButton";
export * from "./deviceSetupItem";
export * from "./matrix";

View File

@ -0,0 +1,15 @@
export interface IInput {
InputNumber: string,
InputDevice: string,
}
export interface IOutput {
OutputLetter: string,
OutputDevice: string,
}
export interface IMatrix {
Inputs: Array<IInput>;
Outputs: Array<IOutput>;
DeviceName: string;
}

View File

@ -0,0 +1,10 @@
export interface ISequence {
DisplayName: string;
Steps: Array<IStep>;
}
export interface IStep {
DeviceName?: string;
DeviceCommand?: string;
Delay: number;
}

19
src/models/device.ts Normal file
View File

@ -0,0 +1,19 @@
export interface ICommand {
command?: string,
deviceId?: string,
type?: string
}
export interface IDevice {
id: string;
name: string;
harmony: any;
log: any
supportsCommand(commandName: string): boolean,
getCommand(commandName: string): ICommand,
powerOn(): Promise<void>;
powerOff(): Promise<void>;
sendCommand(commandName: string): Promise<void>;
commands: { [name: string]: ICommand };
on: boolean;
}

View File

@ -0,0 +1,47 @@
import { ICommand } from "./device";
import { sleep } from "../util/sleep";
export interface IHarmonyDeviceProps {
id: string;
name: string;
harmony: any;
log: any;
commands: { [name: string]: ICommand };
}
export class HarmonyDevice {
private _commands: { [name: string]: ICommand } = {};
private _on: boolean;
constructor(props: IHarmonyDeviceProps) {
this.id = props.id;
this.name = props.name;
this._on = false;
this._commands = props.commands;
}
public id: string;
public name: string;
public get on(): boolean {
return this._on;
}
public set on(value: boolean) {
this._on = value;
}
public get commands(): { [name: string]: ICommand } {
return this._commands;
}
//Define device methods
public supportsCommand(commandName: string): boolean {
let command = this._commands[commandName];
return command ? true : false;
}
public getCommand(commandName: string): ICommand {
return this._commands[commandName];
}
}

94
src/models/harmonyHub.ts Normal file
View File

@ -0,0 +1,94 @@
import { HarmonyDevice } from "./harmonyDevice";
const Harmony = require("harmony-websocket");
import { ICommand } from "./device";
import { EventEmitter } from "events";
import { sleep } from "../util";
export class HarmonyHub extends EventEmitter {
private _devices: { [deviceName: string]: HarmonyDevice } = {};
private _ip: string;
private _harmony: any;
private _log: any;
private _name: string;
constructor(hubName: string, ipAddress: string, log: any) {
super();
this._ip = ipAddress;
this._log = log;
this._name = hubName;
}
public get devices(): { [deviceName: string]: HarmonyDevice } {
return this._devices;
}
public get hubName(): string {
return this._name;
}
public getDeviceByName = (deviceName: string): HarmonyDevice => {
return this._devices[deviceName];
};
public initialize = async () => {
this._harmony = new Harmony();
await this._harmony.connect(this._ip);
this._harmony.on("stateDigest", (data: any) => {
console.log(data);
});
this._harmony.on("automationState", (data: any) => {
console.log(data);
});
//Gather devices
let devices: any = await this._harmony.getDevices();
try {
await Promise.all(
//Add each to dictionary
devices.map(async (dev: any) => {
//get commands
let commands: { [name: string]: ICommand } = {};
let deviceCommands: any = await this._harmony.getDeviceCommands(
dev.id
);
deviceCommands.forEach((command: any) => {
commands[command.label] = command.action;
});
this._devices[dev.label] = new HarmonyDevice({
id: dev.id,
name: dev.label,
commands: commands,
log: this._log,
harmony: this._harmony,
});
})
);
this.emit("Ready");
} catch (err) {
this._log(`ERROR - error connecting to harmony: ${err}`);
}
};
private connect = async (): Promise<void> => {
await this._harmony.Connect(this._ip);
};
public async sendCommand(command: ICommand): Promise<void> {
try {
//Execute command
//HACK to fix Harmon Kardon receiver not turning off
if (command.command === "PowerOff") {
for (let i = 0; i < 2; i++) {
await this._harmony.sendCommand(JSON.stringify(command));
}
}
await this._harmony.sendCommand(JSON.stringify(command));
//Sleep
await sleep(800);
} catch (err) {
this._log(`ERROR - error sending command to harmony: ${err}`);
}
}
}

1
src/models/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./device";

228
src/platform.ts Normal file
View File

@ -0,0 +1,228 @@
import "reflect-metadata";
import {
API,
Characteristic,
DynamicPlatformPlugin,
Logger,
PlatformAccessory,
PlatformConfig,
Service,
} from "homebridge";
import { ControlUnit, DeviceButton } from "./accessories";
import { Sequence } from "./accessories/sequence";
import { IConfig, IControlUnit, IDeviceButton } from "./models/config";
import { ISequence } from "./models/config/sequence";
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings";
import { container } from "tsyringe";
import { HarmonyDataProvider } from "./dataProviders/harmonyDataProvider";
import { StateDataProvider } from "./dataProviders/stateDataProvider";
import { CommandService } from "./services/commandService";
import { ActivityService } from "./services/activityService";
import { VolumeService } from "./services/volumeService";
export class Platform implements DynamicPlatformPlugin {
constructor(
public readonly log: Logger,
config: PlatformConfig,
public readonly api: API
) {
this.log.debug("Finished initializing platform:", config.name);
this.config = config as unknown as IConfig;
this.register();
let didFinishLaunching = false;
this.api.on("didFinishLaunching", async () => {
log.debug("Executed didFinishLaunching callback");
this.discoverControlUnitAccessories();
this.discoverDeviceButtonAccessories();
this.discoverSequenceAccessories();
this.pruneAccessories();
didFinishLaunching = true;
});
}
public readonly Service: typeof Service = this.api.hap.Service;
public readonly Characteristic: typeof Characteristic =
this.api.hap.Characteristic;
// this is used to track restored cached accessories
public readonly accessories: PlatformAccessory[] = [];
public config: IConfig;
/**
* Discover new control unit accessories
* @param dataProvider
*/
private discoverControlUnitAccessories(): void {
this.config.ControlUnits.forEach((unit: IControlUnit) => {
const uuid = this.api.hap.uuid.generate(unit.DisplayName);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: " +
existingAccessory.displayName
);
new ControlUnit(
this,
existingAccessory,
container.resolve(ActivityService),
container.resolve(CommandService),
container.resolve(VolumeService),
unit.Activities
);
this.api.publishExternalAccessories(PLUGIN_NAME, [existingAccessory]);
console.log("Publishing external accessory: " + uuid);
} else {
this.log.info("Adding new accessory: " + unit.DisplayName);
const accessory = new this.api.platformAccessory(
unit.DisplayName,
uuid
);
accessory.context["DeviceName"] = unit.DisplayName;
accessory.context["Type"] = typeof ControlUnit.name;
new ControlUnit(
this,
accessory,
container.resolve(ActivityService),
container.resolve(CommandService),
container.resolve(VolumeService),
unit.Activities
);
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
console.log("Publishing external accessory: " + uuid);
}
});
}
/**
* Discover new device button accessories
* @param dataProvider
*/
private discoverDeviceButtonAccessories(): void {
this.config.DeviceButtons.forEach((button: IDeviceButton) => {
const uuid = this.api.hap.uuid.generate(button.DisplayName);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: " +
existingAccessory.displayName
);
new DeviceButton(
this,
existingAccessory,
container.resolve(HarmonyDataProvider),
button
);
this.api.updatePlatformAccessories([existingAccessory]);
} else {
this.log.info("Adding new accessory: " + button.DisplayName);
const accessory = new this.api.platformAccessory(
button.DisplayName,
uuid
);
accessory.context["DeviceName"] = button.DisplayName;
accessory.context["Type"] = typeof DeviceButton;
new DeviceButton(
this,
accessory,
container.resolve(HarmonyDataProvider),
button
);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
this.accessories.push(accessory);
}
});
}
/**
* Discover new sequence accessories
* @param dataProvider
*/
private discoverSequenceAccessories(): void {
this.config.Sequences.forEach((sequence: ISequence) => {
const uuid = this.api.hap.uuid.generate(sequence.DisplayName);
const existingAccessory = this.accessories.find((e) => e.UUID === uuid);
if (existingAccessory) {
this.log.info(
"Restoring existing accessory from cache: " +
existingAccessory.displayName
);
new Sequence(
this,
existingAccessory,
container.resolve(HarmonyDataProvider),
sequence
);
this.api.updatePlatformAccessories([existingAccessory]);
} else {
this.log.info("Adding new accessory: " + sequence.DisplayName);
const accessory = new this.api.platformAccessory(
sequence.DisplayName,
uuid
);
accessory.context["DeviceName"] = sequence.DisplayName;
accessory.context["Type"] = typeof "Sequence";
new Sequence(
this,
accessory,
container.resolve(HarmonyDataProvider),
sequence
);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
this.accessories.push(accessory);
}
});
}
private pruneAccessories(): void {
// Remove devices not discovered
const listOfConfiguredAccessories = [
...this.config.DeviceButtons.map((e) => e.DisplayName),
...this.config.Sequences.map((e) => e.DisplayName),
...this.config.ControlUnits.map((e) => e.DisplayName),
];
for (const accessory of this.accessories) {
if (
listOfConfiguredAccessories.filter(
(displayName) => displayName === accessory.displayName
).length === 0
) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
configureAccessory(accessory: PlatformAccessory<Record<string, any>>): void {
this.log.info("Loading accessory from cache:", accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
private register(): void {
container.register<Platform>(Platform, { useValue: this });
container.register<IConfig>("IConfig", { useValue: this.config });
container.register<Logger>("log", { useValue: this.log });
container.registerSingleton<HarmonyDataProvider>(HarmonyDataProvider);
container.registerSingleton<ActivityService>(ActivityService);
container.registerSingleton<StateDataProvider>(StateDataProvider);
container.registerSingleton<CommandService>(CommandService);
}
}

View File

@ -0,0 +1,243 @@
import { Logging } from "homebridge";
import { inject, injectable } from "tsyringe";
import { HarmonyDataProvider } from "../dataProviders/harmonyDataProvider";
import { StateDataProvider } from "../dataProviders/stateDataProvider";
import { IActivityState } from "../models/activityState";
import {
IActivity,
IConfig,
IDeviceSetupItem,
IInput,
IOutput,
} from "../models/config";
import { HarmonyDevice } from "../models/harmonyDevice";
@injectable()
export class ActivityService {
constructor(
@inject(HarmonyDataProvider)
private _harmonyDataProvider: HarmonyDataProvider,
@inject(StateDataProvider) private _stateDataProvider: StateDataProvider,
@inject("IConfig") private _config: IConfig,
@inject("log") private _log: Logging
) {}
public startLastOrDefaultActivity = async (
controlUnitName: string,
defaultActivity: IActivity
) => {
const lastActivity = this.getLastActivity(controlUnitName);
if (!lastActivity) {
return this.startActivity(controlUnitName, defaultActivity);
}
return this.startActivity(controlUnitName, lastActivity);
};
public startActivity = async (
controlUnitName: string,
activity: IActivity
) => {
this._log.info(
`Starting activity ${activity.DisplayName} for controlUnit: ${controlUnitName}`
);
let devicesToTurnOn: Array<HarmonyDevice> = this.getDevicesToTurnOn(
activity,
controlUnitName
);
await this._harmonyDataProvider.powerOnDevices(devicesToTurnOn);
await this.assignDeviceInput(activity);
await this.routeInputsToOutputs(activity);
let lastActivity = this.getCurrentActivity(controlUnitName);
let devicesToTurnOff: Array<HarmonyDevice> = this.getDevicesToTurnOff(
controlUnitName,
lastActivity,
activity
);
await this._harmonyDataProvider.powerOffDevices(devicesToTurnOff);
this._stateDataProvider.updateState(activity, controlUnitName);
};
public stopCurrentActivity = async (
controlUnitName: string
): Promise<void> => {
if (!this.getCurrentActivity(controlUnitName)) {
return;
}
let lastActivity: IActivity | undefined =
this.getCurrentActivity(controlUnitName);
let devicesToTurnOff: Array<HarmonyDevice> = this.getDevicesToTurnOff(
controlUnitName,
lastActivity
);
await this._harmonyDataProvider.powerOffDevices(devicesToTurnOff);
this._stateDataProvider.deactivateState(controlUnitName);
};
public getCurrentActivity(controlUnitName: string): IActivity | undefined {
return this._stateDataProvider.getState(controlUnitName)?.currentActivity;
}
public getLastActivity(controlUnitName: string): IActivity | undefined {
return this._stateDataProvider.getState(controlUnitName)?.lastActivity;
}
/**
* Return if a control unit is active
* @param controlUnitName
*/
public getIsActive(controlUnitName: string): boolean {
const state = this._stateDataProvider.getState(controlUnitName);
return state != undefined && state.currentActivity != undefined;
}
/**
* Helper function to make sure no control unit depends on device list.
* @param devicesToTurnOn The list of devices to modify.
* @param controlUnitName The name of the control unit in question.
*/
private sanitizeDeviceList(
devicesToTurnOn: Array<HarmonyDevice>,
controlUnitName: string
): Array<HarmonyDevice> {
for (let controlUnitKey in this._stateDataProvider.states) {
//Skip self
if (controlUnitKey === controlUnitName) {
continue;
}
let currentOtherState: IActivityState =
this._stateDataProvider.states[controlUnitKey]!;
if (!currentOtherState || !currentOtherState.currentActivity) {
return devicesToTurnOn;
}
currentOtherState.currentActivity!.DeviceSetupList.forEach(
(value: IDeviceSetupItem) => {
//there are devices to remove
if (devicesToTurnOn.some((e) => e && e.name === value.DeviceName)) {
let deviceToRemove: HarmonyDevice = devicesToTurnOn.filter(
(i) => i.name === value.DeviceName
)[0];
delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)];
}
}
);
}
return devicesToTurnOn;
}
private getDevicesToTurnOn(
activity: IActivity,
controlUnitName: string
): Array<HarmonyDevice> {
let potentialDevices = this.buildPotentialDeviceList(activity);
return this.sanitizeDeviceList(potentialDevices, controlUnitName);
}
private getDevicesToTurnOff(
controlUnitName: string,
lastActivity?: IActivity,
nextActivity?: IActivity
) {
let potentialDevices = lastActivity
? this.buildPotentialDeviceList(lastActivity)
: [];
//remove devices that will be used for next activity from list
//delete array[index] is stupid because it just nulls out the index. But now i have to deal with nulls
if (nextActivity) {
potentialDevices.forEach((device: HarmonyDevice, index: number) => {
if (
device &&
device.name &&
nextActivity.DeviceSetupList.some((e) => {
return e && e.DeviceName === device.name;
})
) {
delete potentialDevices[index];
}
});
}
return this.sanitizeDeviceList(potentialDevices, controlUnitName);
}
private buildPotentialDeviceList(activity: IActivity): HarmonyDevice[] {
return activity.DeviceSetupList.map(
(value: IDeviceSetupItem): HarmonyDevice => {
return this._harmonyDataProvider.getDeviceByName(value.DeviceName);
}
);
}
private async assignDeviceInput(activity: IActivity): Promise<void> {
await Promise.all(
activity.DeviceSetupList.map(async (value: IDeviceSetupItem) => {
let device: HarmonyDevice = this._harmonyDataProvider.getDeviceByName(
value.DeviceName
);
if (device && device.supportsCommand(`Input${value.Input}`)) {
await this._harmonyDataProvider.sendCommand(
`Input${value.Input}`,
device
);
}
})
);
}
private async routeInputsToOutputs(activity: IActivity) {
if (activity.UseMatrix) {
//get input and output
let input: IInput = this._config.Matrix.Inputs.filter(
(e) => e.InputDevice === activity.ControlDevice
)[0];
let output: IOutput = this._config.Matrix.Outputs.filter(
(e) => e.OutputDevice === activity.OutputDevice
)[0];
let inputCommandName: string = `In ${input.InputNumber}`;
let outputCommandName: string = `Out ${output.OutputLetter}`;
let matrixDevice: HarmonyDevice =
this._harmonyDataProvider.getDeviceByName(
this._config.Matrix.DeviceName
);
//Route hdmi
if (
matrixDevice.supportsCommand(inputCommandName) &&
matrixDevice.supportsCommand(outputCommandName)
) {
await this._harmonyDataProvider.sendCommand(
outputCommandName,
matrixDevice
);
await this._harmonyDataProvider.sendCommand(
inputCommandName,
matrixDevice
);
await this._harmonyDataProvider.sendCommand(
outputCommandName,
matrixDevice
);
await this._harmonyDataProvider.sendCommand(
inputCommandName,
matrixDevice
);
}
}
}
}

View File

@ -0,0 +1,71 @@
import { inject, injectable } from "tsyringe";
import { RemoteKey } from "../accessories/controlUnit";
import { HarmonyDataProvider } from "../dataProviders/harmonyDataProvider";
import { StateDataProvider } from "../dataProviders/stateDataProvider";
import { HarmonyDevice } from "../models/harmonyDevice";
@injectable()
export class CommandService {
constructor(
@inject(StateDataProvider) private _stateDataProvider: StateDataProvider,
@inject(HarmonyDataProvider)
private _harmonyDataProvider: HarmonyDataProvider
) {}
/**
* Send key press for current activity.
*
* @param controlUnitName The name of the control unit to act on.
* @param key The key to send.
*/
public sendKeyPress = async (controlUnitName: string, key: any) => {
let currentActivity = this._stateDataProvider.getState(controlUnitName);
if (currentActivity) {
let commandName: string = "";
let device: HarmonyDevice = this._harmonyDataProvider.getDeviceByName(
currentActivity.currentActivity!.ControlDevice
);
switch (key) {
case RemoteKey.ARROW_UP: {
commandName = "Direction Up";
break;
}
case RemoteKey.ARROW_DOWN: {
commandName = "Direction Down";
break;
}
case RemoteKey.ARROW_LEFT: {
commandName = "Direction Left";
break;
}
case RemoteKey.ARROW_RIGHT: {
commandName = "Direction Right";
break;
}
case RemoteKey.SELECT: {
commandName = "Select";
break;
}
case RemoteKey.PLAY_PAUSE: {
commandName = "Pause";
break;
}
case RemoteKey.INFORMATION: {
commandName = "Menu";
break;
}
case RemoteKey.BACK: {
commandName = "Back";
break;
}
case RemoteKey.EXIT: {
commandName = "Back";
break;
}
}
await this._harmonyDataProvider.sendCommand(commandName, device);
}
};
}

View File

@ -0,0 +1,49 @@
import { inject, injectable } from "tsyringe";
import { HarmonyDataProvider } from "../dataProviders/harmonyDataProvider";
import { StateDataProvider } from "../dataProviders/stateDataProvider";
import { HarmonyDevice } from "../models/harmonyDevice";
@injectable()
export class VolumeService {
constructor(
@inject(StateDataProvider) private _stateDataProvider: StateDataProvider,
@inject(HarmonyDataProvider)
private _harmonyDataProvider: HarmonyDataProvider
) {}
/**
* Turn the volume up for the current running activity.
*/
public volumeUp = async (controlUnitName: string) => {
let volumeUpCommand: string = "Volume Up";
let currentState = this._stateDataProvider.getState(controlUnitName);
if (currentState) {
let volumeDevice: HarmonyDevice =
this._harmonyDataProvider.getDeviceByName(
currentState.currentActivity!.VolumeDevice
);
await this._harmonyDataProvider.sendCommand(
volumeUpCommand,
volumeDevice
);
}
};
/**
* Volume down for current running activity.
*/
public volumeDown = async (controlUnitName: string) => {
let volumeDownCommand: string = "Volume Down";
let currentState = this._stateDataProvider.getState(controlUnitName);
if (currentState) {
let volumeDevice: HarmonyDevice =
this._harmonyDataProvider.getDeviceByName(
currentState.currentActivity!.VolumeDevice
);
await this._harmonyDataProvider.sendCommand(
volumeDownCommand,
volumeDevice
);
}
};
}

9
src/settings.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* This is the name of the platform that users will use to register the plugin in the Homebridge config.json
*/
export const PLATFORM_NAME = "HarmonyHubMatrix";
/**
* This must match the name of your plugin as defined the package.json
*/
export const PLUGIN_NAME = "@watsonb8/homebridge-harmony-control";

32
src/util/callbackify.ts Normal file
View File

@ -0,0 +1,32 @@
/**
* Helper function to convert callbacks into promises
* @param func
*/
export default function callbackify(
func: (...args: any[]) => Promise<any>
): Function {
return (...args: any[]) => {
const onlyArgs: any[] = [];
let maybeCallback: Function | null = null;
for (const arg of args) {
if (typeof arg === "function") {
maybeCallback = arg;
break;
}
onlyArgs.push(arg);
}
if (!maybeCallback) {
throw new Error("Missing callback parameter!");
}
const callback = maybeCallback;
func(...onlyArgs)
.then((data: any) => callback(null, data))
.catch((err: any) => callback(err));
};
}

2
src/util/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from "./callbackify";
export * from "./sleep";

3
src/util/sleep.ts Normal file
View File

@ -0,0 +1,3 @@
export function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}

View File

@ -1,16 +1,15 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6", "target": "es6",
"module": "commonjs", "module": "commonjs",
"declaration": true, "declaration": true,
"outDir": "./bin", "outDir": "./bin",
"strict": true, "strict": true,
}, "sourceMap": true,
"include": [ "skipLibCheck": true,
"src" "experimentalDecorators": true,
], "emitDecoratorMetadata": true
"exclude": [ },
"node_modules", "include": ["src"],
"**/__tests__/*" "exclude": ["**/node_modules", "**/__tests__/*"]
] }
}