54 Commits

Author SHA1 Message Date
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 93132ab868
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:40:00 2021 -0400

    excluding publishing on PRs

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

    Only publishing master branch

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

    WIP

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

    WIP

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

    Attempting to use drone commit in preid

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

    Adding sequence file

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

    Prettier

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

    Updating pre-release command

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

    Updating drone config

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

    Revert "Using email alias"

    This reverts commit d1a3b80293.

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

    Using email alias

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

    Removing bash_profile

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

    WIP

commit d3c37b18b9
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 93132ab868
Author: Brandon Watson <watsonb8133@gmail.com>
Date:   Mon Jun 7 20:40:00 2021 -0400

    excluding publishing on PRs

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

    Only publishing master branch

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

    WIP

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

    WIP

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

    Attempting to use drone commit in preid

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

    Adding sequence file

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

    Prettier

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

    Updating pre-release command

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

    Updating drone config

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

    Revert "Using email alias"

    This reverts commit d1a3b80293.

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

    Using email alias

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

    Removing bash_profile

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

    WIP

commit d3c37b18b9
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 d1a3b80293.

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
39 changed files with 2908 additions and 2620 deletions

View File

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

75
.drone.yml Normal file
View File

@@ -0,0 +1,75 @@
kind: pipeline
type: docker
name: default
clone:
disable: true
steps:
- name: clone
image: alpine/git
commands:
- git clone https://gitea.watsonlabs.net/watsonb8/homebridge-harmony-control.git .
- git checkout $DRONE_COMMIT
- name: build
image: node
commands:
- npm install
- npm run build
- name: version
image: node
commands:
- export version=`node -p "require('./package.json').version"`
- export commit=`echo $DRONE_COMMIT | cut -c1-5`
- npm version prerelease --preid=$commit --git-tag-version=false --allow-same-version=true
when:
event:
exclude:
- tag
- pull_request
branch:
include:
- master
- name: publish pre
image: plugins/npm
settings:
username: admin
password:
from_secret: npm_password
email: brandon@watsonlabs.net
registry: "http://linuxhost.me:4873/"
when:
event:
exclude:
- tag
- pull_request
branch:
include:
- master
- name: publish tagged version
image: plugins/npm
settings:
username: admin
password:
from_secret: npm_password
email: brandon@watsonlabs.net
registry: "http://linuxhost.me:4873/"
when:
event:
- tag
exclude:
- pull_request
notify:
image: drillster/drone-email
host: smtp.watsonlabs.net
username: srvGitea
password:
from_secret: smtp_password
from: drone@watsonlabs.net
when:
status: [failure]

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
}
} }

2035
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,42 +1,45 @@
{ {
"name": "homebridge-harmony-control", "name": "homebridge-harmony-control",
"version": "1.0.0", "version": "1.1.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",
"scripts": { "publishConfig": {
"build": "tsc", "registry": "http://linuxhost.me:4873/"
"prepublishOnly": "npm run build" },
}, "scripts": {
"repository": { "build": "tsc --build",
"type": "git", "prepublishOnly": "npm run build"
"url": "ssh://git@watsonb8.ddns.net:2122/misc/homebridge-harmony-control.git" },
}, "repository": {
"author": "Brandon Watson", "type": "git",
"license": "ISC", "url": "ssh://git@watsonb8.ddns.net:2122/misc/homebridge-harmony-control.git"
"keywords": [ },
"homebridge-plugin", "author": "Brandon Watson",
"harmony", "license": "ISC",
"websocket", "keywords": [
"harmonyhub", "homebridge-plugin",
"homekit" "harmony",
], "websocket",
"engines": { "harmonyhub",
"homebridge": ">=0.4.21", "homekit"
"node": ">=7.6.0" ],
}, "engines": {
"devDependencies": { "homebridge": ">=1.1.6",
"rollup-plugin-typescript": "^1.0.1", "node": ">=7.6.0"
"tslib": "^1.10.0", },
"tslint": "^5.17.0", "devDependencies": {
"typescript": "^3.5.1" "rollup-plugin-typescript": "^1.0.1",
}, "tslib": "^1.10.0",
"dependencies": { "tslint": "^5.17.0",
"@babel/core": "^7.4.5", "typescript": "^3.5.1"
"@babel/preset-env": "^7.4.5", },
"@babel/preset-typescript": "^7.3.3", "dependencies": {
"@types/node": "^12.0.7", "@babel/core": "^7.4.5",
"harmony-websocket": "^1.1.0", "@babel/preset-env": "^7.4.5",
"homebridge": "^0.4.50", "@babel/preset-typescript": "^7.3.3",
"request": "^2.88.0" "@types/node": "^12.0.7",
} "harmony-websocket": "^1.1.0",
"homebridge": "^1.1.16",
"request": "^2.88.0"
}
} }

View File

@@ -1,281 +1,286 @@
import { Activity } from '../Models/Activity'; import { PlatformAccessory, RemoteController, Service } from "homebridge";
import { Matrix } from '../Models/Matrix'; import HarmonyDataProvider from "../DataProviders/HarmonyDataProvider";
import { IAccessory } from './IAccessory'; import { IActivity } from "../Models/Config";
import callbackify from '../Util/Callbackify'; import { Platform } from "../platform";
import HarmonyDataProvider from '../DataProviders/HarmonyDataProvider'; import callbackify from "../Util/Callbackify";
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 * Enum describing remote key presses from homebridge.
*/ */
export class ControlUnit implements IAccessory { export enum RemoteKey {
//Required by homebridge REWIND = 0,
name: string = ""; FAST_FORWARD = 1,
NEXT_TRACK = 2,
//fields PREVIOUS_TRACK = 3,
private log: any = {}; ARROW_UP = 4,
private displayName: string = ""; ARROW_DOWN = 5,
ARROW_LEFT = 6,
//Service fields ARROW_RIGHT = 7,
private televisionService: HAPNodeJS.Service | undefined; SELECT = 8,
private televisionSpeakerService: HAPNodeJS.Service | undefined; BACK = 9,
private informationService: HAPNodeJS.Service | undefined; EXIT = 10,
private inputServices: Array<HAPNodeJS.Service | undefined> = []; PLAY_PAUSE = 11,
INFORMATION = 15,
//Harmony fields }
private activities: Array<Activity> = [];
private dataProvider: HarmonyDataProvider; export class ControlUnit {
constructor(
public platformAccessory: any; private readonly _platform: Platform,
private readonly _accessory: PlatformAccessory,
/** private _dataProvider: HarmonyDataProvider,
* Constructor private _activities: Array<IActivity>
* @param props Input properties ) {
*/ this._accessory
constructor(props: IControlUnitProps) { .getService(this._platform.Service.AccessoryInformation)!
//Assign class variables .setCharacteristic(
this.log = props.log; this._platform.Characteristic.Manufacturer,
Api = props.api; "Brandon Watson"
Service = props.api.hap.Service; )
Characteristic = props.api.hap.Characteristic; .setCharacteristic(
this.displayName = props.displayName; this._platform.Characteristic.Model,
this.name = this.displayName; "Matrix Output Television"
)
this.activities = props.activities; .setCharacteristic(
this._platform.Characteristic.SerialNumber,
this.dataProvider = props.dataProvider; "123-456-789"
homebridge = props.homebridge; );
this.platformAccessory = new homebridge.platformAccessory(this.name, this.generateUUID(), homebridge.hap.Accessory.Categories.TELEVISION); const televisionUUID = this._platform.api.hap.uuid.generate(
`${this._accessory.displayName} Television`
//Configure services );
this.configureTvService(); //Init services
this.configureTvSpeakerService(); const televisionService =
// this.configureAccessoryInformation(); this._accessory.getService(this._platform.Service.Television) ||
this.configureInputSourceService(); this._accessory.addService(
this._platform.Service.Television,
//Configure external services "Television",
this.getServices().forEach(service => { televisionUUID
try { );
this.platformAccessory.addService(service); this.configureTvService(televisionService);
} catch (error) { }
const televisionSpeakerUUID = this._platform.api.hap.uuid.generate(
//@ts-ignore `${this._accessory.displayName} Television Speaker`
if (service.linked) { );
//@ts-ignore const televisionSpeakerService =
this.televisionService!.addLinkedService(service); this._accessory.getService(this._platform.Service.TelevisionSpeaker) ||
} this._accessory.addService(
}); this._platform.Service.TelevisionSpeaker,
} "Television Speaker",
/************* televisionSpeakerUUID
* );
* Tv Service this.configureTvSpeakerService(televisionService, televisionSpeakerService);
*
*************/ this.configureInputSourceService(televisionService);
}
/**
* Configure television service /*************
*/ *
private configureTvService(): void { * Tv Service
this.televisionService = new Service.Television( *
this.displayName, *************/
'tvService'
) /**
* Configure television service
this.televisionService.setCharacteristic(Characteristic.ConfiguredName, this.displayName); */
//@ts-ignore private configureTvService(televisionService: Service): void {
this.televisionService.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE); televisionService.setCharacteristic(
this.televisionService.setCharacteristic(Characteristic.ActiveIdentifier, 1); this._platform.Characteristic.ConfiguredName,
this.televisionService.setCharacteristic(Characteristic.Active, false); this._accessory.displayName
);
//setup listeners //@ts-ignore
this.televisionService.getCharacteristic(Characteristic.Active) televisionService.setCharacteristic(
//@ts-ignore this._platform.Characteristic.SleepDiscoveryMode,
.on("set", callbackify(this.onSetAccessoryActive)) this._platform.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE
//@ts-ignore );
.on("get", callbackify(this.onGetAccessoryActive)); televisionService.setCharacteristic(
this._platform.Characteristic.ActiveIdentifier,
this.televisionService.getCharacteristic(Characteristic.RemoteKey) 1
//@ts-ignore );
.on("set", callbackify(this.onSetRemoteKey)); televisionService.setCharacteristic(
this._platform.Characteristic.Active,
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier) false
//@ts-ignore );
.on("set", callbackify(this.onSetActiveIdentifier))
//@ts-ignore //setup listeners
.on("get", callbackify(this.onGetActiveIdentifier)); televisionService
} .getCharacteristic(this._platform.Characteristic.Active)
//@ts-ignore
/** .on("set", callbackify(this.onSetAccessoryActive))
* Event handler for SET active characteristic //@ts-ignore
*/ .on("get", callbackify(this.onGetAccessoryActive));
private onSetAccessoryActive = async (value: any) => {
switch (value) { //Set remote characteristics if is external
case 0: this.dataProvider.powerOff(this.name); break; televisionService
//Turn on with first activity .getCharacteristic(this._platform.Characteristic.RemoteKey)
case 1: this.dataProvider.powerOn(this.name, this.activities[0]); break; //@ts-ignore
} .on("set", callbackify(this.onSetRemoteKey));
}
televisionService
/** .getCharacteristic(this._platform.Characteristic.ActiveIdentifier)
* Event handler for GET active characteristic //@ts-ignore
*/ .on("set", callbackify(this.onSetActiveIdentifier))
private onGetAccessoryActive = async () => { //@ts-ignore
//@ts-ignore .on("get", callbackify(this.onGetActiveIdentifier));
return this.dataProvider.getIsActive ? Characteristic.Active.Active : Characteristic.Active.Inactive }
}
/**
/** * Event handler for SET active characteristic
* Event handler for SET remote key */
*/ private onSetAccessoryActive = async (value: any) => {
private onSetRemoteKey = async (key: any) => { switch (value) {
this.dataProvider.sendKeyPress(this.name, key); case 0:
} this._dataProvider.powerOff(this._accessory.displayName);
break;
/** //Turn on with first activity
* Event handler for SET active identifier characteristic case 1:
*/ this._dataProvider.powerOn(
private onSetActiveIdentifier = async (identifier: any) => { this._accessory.displayName,
this.dataProvider.startActivity(this.name, this.activities[identifier]); this._activities[0]
} );
break;
/** }
* Event handler for GET active identifier characteristic };
*/
private onGetActiveIdentifier = async () => { /**
let currentActivity: Activity = this.dataProvider.getIsActive(this.name)!; * Event handler for GET active characteristic
let identifier: number = 0; */
if (currentActivity) { private onGetAccessoryActive = async () => {
identifier = this.activities.findIndex(e => e.displayName === currentActivity.displayName); //@ts-ignore
} return this._dataProvider.getIsActive(this._accessory.displayName)
return identifier; ? this._platform.Characteristic.Active.ACTIVE
} : this._platform.Characteristic.Active.INACTIVE;
};
/******************
* /**
* Speaker Service * Event handler for SET remote key
* */
*****************/ private onSetRemoteKey = async (key: any) => {
this._dataProvider.sendKeyPress(this._accessory.displayName, key);
/** };
* Configure Speaker Service
*/ /**
private configureTvSpeakerService(): void { * Event handler for SET active identifier characteristic
this.televisionSpeakerService = new Service.TelevisionSpeaker( */
this.displayName, private onSetActiveIdentifier = async (identifier: any) => {
'tvSpeakerService' this._dataProvider.startActivity(
); this._accessory.displayName,
this.televisionSpeakerService.setCharacteristic(Characteristic.Name, this.displayName); this._activities[identifier]
//@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"; * Event handler for GET active identifier characteristic
if (this.televisionService) { */
//@ts-ignore private onGetActiveIdentifier = async () => {
this.televisionService.addLinkedService(this.televisionSpeakerService); let currentActivity: IActivity = this._dataProvider.getIsActive(
} this._accessory.displayName
)!;
//Setup listeners let identifier: number = 0;
this.televisionSpeakerService.getCharacteristic(Characteristic.VolumeSelector) if (currentActivity) {
//@ts-ignore identifier = this._activities.findIndex(
.on("set", callbackify(this.onSetVolumeSelector)); (e) => e.DisplayName === currentActivity.DisplayName
} );
}
/** return identifier;
* Event handler for SET volume characteristic };
*/
private onSetVolumeSelector = async (value: any) => { /******************
switch (value) { *
case 0: this.dataProvider.volumeUp(this.name); break; * Speaker Service
case 1: this.dataProvider.volumeDown(this.name); break; *
} *****************/
}
/**
/********************* * Configure Speaker Service
* */
* Information Service private configureTvSpeakerService(
* televisionService: Service,
********************/ televisionSpeakerService: Service
): void {
/** televisionSpeakerService.setCharacteristic(
* Configure information service this._platform.Characteristic.Name,
*/ this._accessory.displayName
private configureAccessoryInformation(): void { );
this.informationService = new Service.AccessoryInformation(this.displayName, 'information'); //@ts-ignore
this.informationService televisionSpeakerService.setCharacteristic(
.setCharacteristic(Characteristic.Manufacturer, 'Loftux Carwings') this._platform.Characteristic.Active,
.setCharacteristic(Characteristic.Model, 'Heater-Cooler') this._platform.Characteristic.Active.ACTIVE
} );
//@ts-ignore
/***************** televisionSpeakerService.setCharacteristic(
* this._platform.Characteristic.VolumeControlType,
* Input services this._platform.Characteristic.VolumeControlType.ABSOLUTE
* );
*****************/ televisionSpeakerService.subtype = this._accessory.displayName + "Volume";
if (televisionService) {
/** //@ts-ignore
* Configure input service televisionService.addLinkedService(televisionSpeakerService);
*/ }
private configureInputSourceService(): void {
let inputs: Array<HAPNodeJS.Service> = []; //Setup listeners
this.activities.forEach((activity: Activity, index: number) => { televisionSpeakerService
let inputService = new Service.InputSource(activity.displayName, 'activity' + activity.displayName); .getCharacteristic(this._platform.Characteristic.VolumeSelector)
inputService //@ts-ignore
.setCharacteristic(Characteristic.Identifier, index) .on("set", callbackify(this.onSetVolumeSelector));
.setCharacteristic( }
Characteristic.ConfiguredName,
activity.displayName) /**
.setCharacteristic( * Event handler for SET volume characteristic
Characteristic.IsConfigured, */
//@ts-ignore private onSetVolumeSelector = async (value: any) => {
Characteristic.IsConfigured.CONFIGURED switch (value) {
) case 0:
//@ts-ignore this._dataProvider.volumeUp(this._accessory.displayName);
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI); break;
case 1:
//@ts-ignore this._dataProvider.volumeDown(this._accessory.displayName);
this.televisionService!.addLinkedService(inputService); break;
inputs.push(inputService); }
}); };
this.inputServices = inputs;
} /*****************
*
private generateUUID(): string { // Public Domain/MIT * Input services
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) { * Configure input service
var r = (d + Math.random() * 16) % 16 | 0; */
d = Math.floor(d / 16); private configureInputSourceService(televisionService: Service): void {
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); 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) ||
* Called by homebridge to gather services for this accessory. this._accessory.addService(
*/ this._platform.Service.InputSource,
getServices(): Array<HAPNodeJS.Service> { activity.DisplayName,
let services: Array<HAPNodeJS.Service> = [this.televisionService!, this.televisionSpeakerService!]; inputUUID
this.inputServices.forEach((service: HAPNodeJS.Service | undefined) => { );
services.push(service!);
}); inputService
return (services); .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,116 @@
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.getDeviceFromName(
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._device.sendCommand(this._deviceInfo.ButtonName);
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._device.sendCommand(this._deviceInfo.ButtonName);
}
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);
}
};
}

View File

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

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/ISequence";
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.getDeviceFromName(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 device.sendCommand(step.DeviceCommand);
} 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);
};
}

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

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

View File

@@ -1,382 +1,404 @@
import { Activity } from "../Models/Activity"; import { IActivity } from "../Models/Config/IActivity";
import { DeviceSetupItem } from "../Models/DeviceSetupItem"; import { IDeviceSetupItem } from "../Models/Config/IDeviceSetupItem";
import { threadId } from "worker_threads"; import { IInput, IMatrix, IOutput } from "../Models/Config/IMatrix";
import { Input, Matrix, Output } from "../Models/Matrix"; import { RemoteKey } from "../Accessories/ControlUnit";
import { EventEmitter } from "events";
let Characteristic: HAPNodeJS.Characteristic; import { IHub } from "../Models/Config/IHub";
import { IDeviceConfig } from "../Models/Config/IDeviceConfig";
import { HarmonyDevice } from "../Models/HarmonyDevice";
import { HarmonyHub } from "../Models/HarmonyHub";
const Harmony = require("harmony-websocket"); 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 { interface IActivityState {
currentActivity: Activity currentActivity: IActivity;
}
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 { interface IHarmonyDataProviderProps {
hubAddress: string, hubs: Array<IHub>;
log: any, deviceConfigs: Array<IDeviceConfig>;
matrix: Matrix log: any;
matrix: IMatrix;
} }
class HarmonyDataProvider { class HarmonyDataProvider extends EventEmitter {
private harmony: any; private _log: any;
private log: any; private _hubsByDevice: { [deviceName: string]: string } = {};
private hubAddress: string = ""; private _hubs: { [hubName: string]: HarmonyHub } = {};
private connected: boolean = false; // private _devicesByHub: { [hubName: string]: { [deviceName: string]: HarmonyDevice } } = {};
private _states: {
[controlUnitName: string]: IActivityState | undefined;
} = {};
private devices: { [name: string]: IDevice; } = {}; private _matrix: IMatrix;
private states: { [controlUnitName: string]: (IActivityState | undefined) } = {};
private matrix: Matrix; constructor(props: IHarmonyDataProviderProps) {
super();
this._log = props.log;
this._matrix = props.matrix;
props.deviceConfigs.forEach((deviceConfig: IDeviceConfig) => {
this._hubsByDevice[deviceConfig.Name] = deviceConfig.Hub;
});
// this._deviceConfigs = props.deviceConfigs;
constructor(props: IHarmonyDataProviderProps) { this.connect(props.hubs);
this.log = props.log; }
this.hubAddress = props.hubAddress;
this.matrix = props.matrix;
this.harmony = new Harmony(); // public get devicesByHub(): { [hubName: string]: { [deviceName: string]: HarmonyDevice } } {
// return this._devicesByHub;
// }
//Listeners public get hubs(): { [hubName: string]: HarmonyHub } {
this.harmony.on('open', () => { return this._hubs;
this.connected = true; }
});
this.harmony.on('close', () => {
this.connected = false;
});
this.connect(); /**
* Power on all devices in an activity.
*/
public powerOn = async (controlUnitName: string, activity: IActivity) => {
//Only power on if not alread on
let currentActivity = this._states[controlUnitName]
? this._states[controlUnitName]!.currentActivity
: undefined;
if (!currentActivity) {
await this.startActivity(controlUnitName, activity);
}
};
/**
* Power off all devices in an activity that aren't being used.
*/
public powerOff = async (controlUnitName: string) => {
if (!this._states[controlUnitName]) {
return;
}
//Build potential list of devices to turn off
let devicesToTurnOff: Array<HarmonyDevice> = this._states[
controlUnitName
]!.currentActivity.DeviceSetupList.map(
(value: IDeviceSetupItem): HarmonyDevice => {
return this.getDeviceFromName(value.DeviceName);
}
);
//Resolve device conflicts with other controlUnits
devicesToTurnOff = this.sanitizeDeviceList(
devicesToTurnOff,
controlUnitName
);
//Turn off devices
devicesToTurnOff.forEach(async (device: HarmonyDevice) => {
if (device) {
await device.powerOff();
}
});
this._states[controlUnitName] = undefined;
};
/**
* Start an activity
*/
public startActivity = async (
controlUnitName: string,
activity: IActivity
) => {
this._log(
`Starting activity ${activity.DisplayName} for controlUnit: ${controlUnitName}`
);
let lastActivity: IActivity | undefined = undefined;
if (this._states[controlUnitName]) {
lastActivity = this._states[controlUnitName]!.currentActivity;
} }
public powerOn = async (controlUnitName: string, activity: Activity) => { //Build potential list of devices to to turn on
//Only power on if not alread on let devicesToTurnOn: Array<HarmonyDevice> = activity.DeviceSetupList.map(
let currentActivity = this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined; (value: IDeviceSetupItem): HarmonyDevice => {
if (!currentActivity) { return this.getDeviceFromName(value.DeviceName);
await this.startActivity(controlUnitName, activity); }
);
//Resolve device conflicts with other controlUnits
devicesToTurnOn = this.sanitizeDeviceList(devicesToTurnOn, controlUnitName);
//Turn on devices
await Promise.all(
devicesToTurnOn.map(async (device: HarmonyDevice) => {
if (device && device.name) {
if (!device.on) {
this._log(`Turning on device ${device.name}`);
await device.powerOn();
}
} }
})
);
//Assign correct input
await Promise.all(
activity.DeviceSetupList.map(async (value: IDeviceSetupItem) => {
let device: HarmonyDevice = this.getDeviceFromName(value.DeviceName);
if (device && device.supportsCommand(`Input${value.Input}`)) {
await device.sendCommand(`Input${value.Input}`);
}
})
);
if (activity.UseMatrix) {
//get input and output
let input: IInput = this._matrix.Inputs.filter(
(e) => e.InputDevice === activity.ControlDevice
)[0];
let output: IOutput = this._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.getDeviceFromName(
this._matrix.DeviceName
);
//Route hdmi
if (
matrixDevice.supportsCommand(inputCommandName) &&
matrixDevice.supportsCommand(outputCommandName)
) {
await matrixDevice.sendCommand(outputCommandName);
await matrixDevice.sendCommand(inputCommandName);
await matrixDevice.sendCommand(outputCommandName);
await matrixDevice.sendCommand(inputCommandName);
}
} }
public powerOff = async (controlUnitName: string) => { //Build potential list of devices to turn off
if (!this.states[controlUnitName]) { if (lastActivity) {
return; let devicesToTurnOff: Array<HarmonyDevice> = lastActivity.DeviceSetupList.map(
(value: IDeviceSetupItem): HarmonyDevice => {
return this.getDeviceFromName(value.DeviceName);
} }
//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 //remove devices that will be used for next activity from list
devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName); //delete array[index] is stupid because it just nulls out the index. But now i have to deal with nulls
devicesToTurnOff.forEach((device: HarmonyDevice, index: number) => {
if (
device &&
device.name &&
activity.DeviceSetupList.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 //Turn off devices
devicesToTurnOff.forEach((device: IDevice) => { devicesToTurnOff.map(async (device: HarmonyDevice) => {
this.powerOffDevice(device); if (device) {
}); if (device.on) {
this._log(`Turning off device ${device.name}`);
this.states[controlUnitName] = undefined; await device.powerOff();
}
}
})
);
} }
public startActivity = async (controlUnitName: string, activity: Activity) => { //Assign current activity
this.log(`Starting activity ${activity.displayName} for controlUnit: ${controlUnitName}`) this._states[controlUnitName] = { currentActivity: activity };
let lastActivity: Activity | undefined = undefined; };
if (this.states[controlUnitName]) {
lastActivity = this.states[controlUnitName]!.currentActivity; /**
* Turn the volume up for the current running activity.
*/
public volumeUp = async (controlUnitName: string) => {
let volumeUpCommand: string = "Volume Up";
if (this._states[controlUnitName]) {
let volumeDevice: HarmonyDevice = this.getDeviceFromName(
this._states[controlUnitName]!.currentActivity.VolumeDevice
);
await volumeDevice.sendCommand(volumeUpCommand);
}
};
/**
* Volume down for current running activity.
*/
public volumeDown = async (controlUnitName: string) => {
let volumeDownCommand: string = "Volume Down";
if (this._states[controlUnitName]) {
let volumeDevice: HarmonyDevice = this.getDeviceFromName(
this._states[controlUnitName]!.currentActivity.VolumeDevice
);
await volumeDevice.sendCommand(volumeDownCommand);
}
};
/**
* 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) => {
if (this._states[controlUnitName]) {
let commandName: string = "";
let device: HarmonyDevice = this.getDeviceFromName(
this._states[controlUnitName]!.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;
}
}
//Build potential list of devices to to turn on await device.sendCommand(commandName);
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); * Return if a control unit is active
* @param controlUnitName
*/
public getIsActive(controlUnitName: string): IActivity | undefined {
return this._states[controlUnitName]
? this._states[controlUnitName]!.currentActivity
: undefined;
}
//Turn on devices /**
await Promise.all(devicesToTurnOn.map(async (device: IDevice) => { * Get the IDevice by name.
if (device && device.name && this.devices[device.name]) { * @param deviceName The device to retrieve.
if (!device.on) { */
this.log(`Turning on device ${device.name}`) public getDeviceFromName(deviceName: string): HarmonyDevice {
await this.powerOnDevice(device); let device: HarmonyDevice | undefined;
} try {
device = this._hubs[this._hubsByDevice[deviceName]].getDeviceByName(
deviceName
);
} catch (err) {
this._log(`Error retrieving device from hub: ${err}`);
}
return device!;
}
// /**
// * Gets device button commands
// * @param deviceCommandName The device command name
// * @param deviceName The device name
// */
// public getCommand(deviceCommandName: string, deviceName: string): ICommand | undefined {
// const device: HarmonyDevice = this.getDeviceFromName(deviceName);
// if (device && device.supportsCommand(deviceCommandName)) {
// return device.getCommand(deviceCommandName);
// } else {
// return undefined;
// }
// }
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);
this._hubs[hub.Name] = newHarmonyHub;
newHarmonyHub.on("Ready", () => {
readyCount++;
if (readyCount === Object.keys(this._hubs).length) {
this.emit("Ready");
} }
})); });
await newHarmonyHub.initialize();
}
)
);
};
//Assign correct input /**
await Promise.all( * Helper function to make sure no control unit depends on device list.
activity.deviceSetupItems.map(async (value: DeviceSetupItem) => { * @param devicesToTurnOn The list of devices to modify.
let device: IDevice = this.getDeviceFromName(value.deviceName); * @param controlUnitName The name of the control unit in question.
*/
private sanitizeDeviceList(
devicesToTurnOn: Array<HarmonyDevice>,
controlUnitName: string
): Array<HarmonyDevice> {
for (let controlUnitKey in this._states) {
//Skip self
if (controlUnitKey === controlUnitName) {
continue;
}
let currentOtherState: IActivityState = this._states[controlUnitKey]!;
if (device && device.supportsCommand(`Input${value.input}`)) { if (currentOtherState) {
let command: string = device.getCommand(`Input${value.input}`); currentOtherState.currentActivity.DeviceSetupList.forEach(
await this.sendCommand(command); (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)];
}
}
); );
}
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) => { return devicesToTurnOn;
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; 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

@@ -0,0 +1,11 @@
import { IDeviceSetupItem } from './IDeviceSetupItem';
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 "./IMatrix";
import { IActivity } from "./IActivity";
import { IDeviceButton } from "./IDeviceButton";
import { IDeviceConfig } from "./IDeviceConfig";
import { IHub } from "./IHub";
import { ISequence } from "./ISequence";
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,4 @@
export interface IDeviceConfig {
Name: string;
Hub: string;
}

View File

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

View File

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

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;
}

View File

@@ -0,0 +1,5 @@
export * from './IActivity';
export * from './IConfig';
export * from './IDeviceButton';
export * from './IDeviceSetupItem';
export * from './IMatrix';

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

@@ -0,0 +1,94 @@
import { ICommand } from "./IDevice";
import { sleep } from "../Util/Sleep";
export interface IHarmonyDeviceProps {
id: string;
name: string;
harmony: any;
log: any;
commands: { [name: string]: ICommand };
}
export class HarmonyDevice {
private _harmony: any;
private _log: any;
private _commands: { [name: string]: ICommand } = {};
private _on: boolean;
constructor(props: IHarmonyDeviceProps) {
this.id = props.id;
this.name = props.name;
this._harmony = props.harmony;
this._on = false;
this._commands = props.commands;
}
public id: string;
public name: string;
public get on(): boolean {
return this._on;
}
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];
}
public async powerOn(): Promise<void> {
let powerOnCommand: string = "Power On";
let powerToggleCommand: string = "Power Toggle";
if (this.supportsCommand(powerOnCommand)) {
await this.sendCommand(powerOnCommand);
this._on = true;
} else if (this.supportsCommand(powerToggleCommand)) {
await this.sendCommand(powerToggleCommand);
this._on = true;
}
}
public async powerOff(): Promise<void> {
let powerOffCommand: string = "Power Off";
let powerToggleCommand: string = "Power Toggle";
if (this.supportsCommand(powerOffCommand)) {
await this.sendCommand(powerOffCommand);
this._on = false;
} else if (this.supportsCommand(powerToggleCommand)) {
await this.sendCommand(powerToggleCommand);
this._on = false;
}
}
public async sendCommand(commandName: string): Promise<void> {
let command!: ICommand;
if (this.supportsCommand(commandName)) {
command = this.getCommand(commandName);
}
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}`);
}
}
}

75
src/Models/HarmonyHub.ts Normal file
View File

@@ -0,0 +1,75 @@
import { HarmonyDevice } from './HarmonyDevice';
const Harmony = require("harmony-websocket");
import { ICommand } from './IDevice';
import { EventEmitter } from 'events';
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);
}
}

19
src/Models/IDevice.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

@@ -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;
}
}

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

@@ -0,0 +1 @@
export * from './IDevice';

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

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));
}

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

@@ -0,0 +1,2 @@
export * from "./Callbackify";
export * from "./Sleep";

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);
}
}

225
src/platform.ts Normal file
View File

@@ -0,0 +1,225 @@
import {
API,
Characteristic,
DynamicPlatformPlugin,
Logger,
PlatformAccessory,
PlatformConfig,
Service,
} from "homebridge";
import { ControlUnit, DeviceButton } from "./Accessories";
import { Sequence } from "./Accessories/Sequence";
import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider";
import { IConfig, IControlUnit, IDeviceButton } from "./Models/Config";
import { ISequence } from "./Models/Config/ISequence";
import { HarmonyDevice } from "./Models/HarmonyDevice";
import { HarmonyHub } from "./Models/HarmonyHub";
import { PLATFORM_NAME, PLUGIN_NAME } from "./settings";
import { sleep } from "./Util";
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;
//construct data provider
const dataProvider = new HarmonyDataProvider({
hubs: this.config.Hubs,
deviceConfigs: this.config.Devices,
matrix: this.config.Matrix,
log: this.log,
});
let didFinishLaunching = false;
this.api.on("didFinishLaunching", async () => {
log.debug("Executed didFinishLaunching callback");
didFinishLaunching = true;
});
this.dataProvider = null;
if (this.config) {
//construct data provider
this.dataProvider = new HarmonyDataProvider({
hubs: this.config.Hubs,
deviceConfigs: this.config.Devices,
matrix: this.config.Matrix,
log: this.log,
});
//Emit devices if requested
this.dataProvider.on("Ready", () => {
this.log.info("All hubs connected");
this.discoverControlUnitAccessories(dataProvider);
this.discoverDeviceButtonAccessories(dataProvider);
this.discoverSequenceAccessories(dataProvider);
if (this.config.EmitDevicesOnStartup) {
const hubs = this.dataProvider!.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}`);
});
});
});
}
});
}
}
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;
public dataProvider: HarmonyDataProvider | null;
/**
* Discover new control unit accessories
* @param dataProvider
*/
private discoverControlUnitAccessories(
dataProvider: HarmonyDataProvider
): 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, dataProvider, 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;
new ControlUnit(this, accessory, dataProvider, unit.Activities);
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]);
console.log("Publishing external accessory: " + uuid);
}
});
}
/**
* Discover new device button accessories
* @param dataProvider
*/
private discoverDeviceButtonAccessories(
dataProvider: HarmonyDataProvider
): 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, dataProvider, 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;
new DeviceButton(this, accessory, dataProvider, button);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
});
// Remove old device buttons
for (const accessory of this.accessories) {
if (
this.config.DeviceButtons.filter(
(button) => button.DisplayName === accessory.displayName
).length === 0
) {
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
}
}
/**
* Discover new sequence accessories
* @param dataProvider
*/
public discoverSequenceAccessories(dataProvider: HarmonyDataProvider): 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, dataProvider, 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;
new Sequence(this, accessory, dataProvider, sequence);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [
accessory,
]);
}
});
// Remove old device buttons
for (const accessory of this.accessories) {
if (
this.config.Sequences.filter(
(sequence) => sequence.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);
}
}

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 = "homebridge-harmony-control";

View File

@@ -1,16 +1,13 @@
{ {
"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" },
], "include": ["src"],
"exclude": [ "exclude": ["**/node_modules", "**/__tests__/*"]
"node_modules",
"**/__tests__/*"
]
} }