Merge pull request #150 from SweVictor/feature/code-style
Feature/code style
This commit is contained in:
commit
61d16fc07c
27 changed files with 1498 additions and 2498 deletions
|
|
@ -1,29 +1,27 @@
|
||||||
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
|
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or the definition README at
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/javascript-node-12
|
// https://github.com/microsoft/vscode-dev-containers/tree/master/containers/javascript-node-12
|
||||||
{
|
{
|
||||||
"name": "Node.js 12",
|
"name": "Node.js 12",
|
||||||
"dockerFile": "Dockerfile",
|
"dockerFile": "Dockerfile",
|
||||||
|
|
||||||
// Use 'settings' to set *default* container specific settings.json values on container create.
|
// Use 'settings' to set *default* container specific settings.json values on container create.
|
||||||
// You can edit these settings after create using File > Preferences > Settings > Remote.
|
// You can edit these settings after create using File > Preferences > Settings > Remote.
|
||||||
"settings": {
|
"settings": {
|
||||||
"terminal.integrated.shell.linux": "/bin/bash"
|
"terminal.integrated.shell.linux": "/bin/bash"
|
||||||
},
|
},
|
||||||
|
|
||||||
// Use 'appPort' to create a container with published ports. If the port isn't working, be sure
|
// Use 'appPort' to create a container with published ports. If the port isn't working, be sure
|
||||||
// your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost.
|
// your server accepts connections from all interfaces (0.0.0.0 or '*'), not just localhost.
|
||||||
// "appPort": [],
|
// "appPort": [],
|
||||||
|
|
||||||
// Uncomment the next line to run commands after the container is created.
|
// Uncomment the next line to run commands after the container is created.
|
||||||
// "postCreateCommand": "yarn install",
|
// "postCreateCommand": "yarn install",
|
||||||
|
|
||||||
// Uncomment the next line to have VS Code connect as an existing non-root user in the container.
|
// Uncomment the next line to have VS Code connect as an existing non-root user in the container.
|
||||||
// On Linux, by default, the container user's UID/GID will be updated to match your local user. See
|
// On Linux, by default, the container user's UID/GID will be updated to match your local user. See
|
||||||
// https://aka.ms/vscode-remote/containers/non-root for details on adding a non-root user if none exist.
|
// https://aka.ms/vscode-remote/containers/non-root for details on adding a non-root user if none exist.
|
||||||
// "remoteUser": "node",
|
// "remoteUser": "node",
|
||||||
|
|
||||||
// Add the IDs of extensions you want installed when the container is created in the array below.
|
// Add the IDs of extensions you want installed when the container is created in the array below.
|
||||||
"extensions": [
|
"extensions": ["dbaeumer.vscode-eslint"]
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -42,6 +42,8 @@ build/Release
|
||||||
|
|
||||||
# Dependency directories
|
# Dependency directories
|
||||||
node_modules/
|
node_modules/
|
||||||
|
# npm install should not be done on dev machines, only on installation in HA
|
||||||
|
package-lock.json
|
||||||
jspm_packages/
|
jspm_packages/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
# TypeScript v1 declaration files
|
||||||
|
|
|
||||||
16
.prettierrc.js
Normal file
16
.prettierrc.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
endOfLine: 'lf',
|
||||||
|
printWidth: 100,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'all',
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.js'],
|
||||||
|
options: {
|
||||||
|
trailingComma: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
271
README.md
271
README.md
|
|
@ -1,4 +1,5 @@
|
||||||
# Hass.io Plejd add-on
|
# Hass.io Plejd add-on
|
||||||
|
|
||||||
Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant.
|
Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant.
|
||||||
It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range.
|
It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range.
|
||||||
|
|
||||||
|
|
@ -15,56 +16,70 @@ I am in no way affiliated with Plejd and am solely doing this as a hobby project
|
||||||
[](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To get started, make sure that the following requirements are met:
|
To get started, make sure that the following requirements are met:
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
* A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
|
||||||
* An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
- A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
||||||
|
- An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
||||||
|
|
||||||
### Tested on
|
### Tested on
|
||||||
|
|
||||||
The add-on has been tested on the following platforms:
|
The add-on has been tested on the following platforms:
|
||||||
* Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
|
||||||
* Raspberry Pi 4 with Hass.io
|
- Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
||||||
* Raspberry Pi 4 with Hass.io/aarch64
|
- Raspberry Pi 4 with Hass.io
|
||||||
|
- Raspberry Pi 4 with Hass.io/aarch64
|
||||||
|
|
||||||
#### Tested Plejd devices
|
#### Tested Plejd devices
|
||||||
* DIM-01
|
|
||||||
* DIM-02
|
- DIM-01
|
||||||
* LED-10
|
- DIM-02
|
||||||
* CTR-01
|
- LED-10
|
||||||
* REL-01
|
- CTR-01
|
||||||
* REL-02
|
- REL-01
|
||||||
* WPH-01
|
- REL-02
|
||||||
|
- WPH-01
|
||||||
|
|
||||||
### Easy Installation
|
### Easy Installation
|
||||||
|
|
||||||
Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left.
|
Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left.
|
||||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
|
||||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||||
* Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||||
* Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
||||||
* Enjoy!
|
- Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
||||||
|
- Enjoy!
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc.
|
Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc.
|
||||||
* Open the `/addon` directory
|
|
||||||
* Create a new folder named `hassio-plejd`
|
- Open the `/addon` directory
|
||||||
* Copy all files from this repository into that newly created one.
|
- Create a new folder named `hassio-plejd`
|
||||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
- Copy all files from this repository into that newly created one.
|
||||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||||
* Click on the refresh button in the upper right corner.
|
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||||
* A new Local Add-on should appear named Plejd. Open that and install.
|
- Click on the refresh button in the upper right corner.
|
||||||
* Enjoy!
|
- A new Local Add-on should appear named Plejd. Open that and install.
|
||||||
|
- Enjoy!
|
||||||
|
|
||||||
### IMPORTANT INFORMATION
|
### IMPORTANT INFORMATION
|
||||||
|
|
||||||
#### Startup error message
|
#### Startup error message
|
||||||
|
|
||||||
When starting the add-on, the log displays this message:
|
When starting the add-on, the log displays this message:
|
||||||
|
|
||||||
```
|
```
|
||||||
parse error: Expected string key before ':' at line 1, column 4
|
parse error: Expected string key before ':' at line 1, column 4
|
||||||
[08:56:24] ERROR: Unknown HTTP error occured
|
[08:56:24] ERROR: Unknown HTTP error occured
|
||||||
```
|
```
|
||||||
|
|
||||||
However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though.
|
However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though.
|
||||||
|
|
||||||
#### Running the Plejd add-on outside of HassOS
|
#### Running the Plejd add-on outside of HassOS
|
||||||
|
|
||||||
If you're planning on running this add-on outside of HassOS, you might need to turn off AppArmor in the `config.json` file. This is due to missing AppArmor configuration that is performed in HassOS (if you've manually done it, ignore this).
|
If you're planning on running this add-on outside of HassOS, you might need to turn off AppArmor in the `config.json` file. This is due to missing AppArmor configuration that is performed in HassOS (if you've manually done it, ignore this).
|
||||||
|
|
||||||
Open the `config.json` file and locate `host_dbus`, after that line, insert: `"apparmor": "no",` and then restart the add-on.
|
Open the `config.json` file and locate `host_dbus`, after that line, insert: `"apparmor": "no",` and then restart the add-on.
|
||||||
|
|
@ -73,10 +88,13 @@ More information about available parameters can be found here:
|
||||||
https://developers.home-assistant.io/docs/en/hassio_addon_config.html
|
https://developers.home-assistant.io/docs/en/hassio_addon_config.html
|
||||||
|
|
||||||
#### Migration from 32bit to 64 bit
|
#### Migration from 32bit to 64 bit
|
||||||
|
|
||||||
If you restore a backup from a 32bit system to a new 64bit system, use the Rebuild option in the Add-on
|
If you restore a backup from a 32bit system to a new 64bit system, use the Rebuild option in the Add-on
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
You need to add the following to your `configuration.yaml` file:
|
You need to add the following to your `configuration.yaml` file:
|
||||||
|
|
||||||
```
|
```
|
||||||
mqtt:
|
mqtt:
|
||||||
broker: [point to your broker IP eg. 'mqtt://localhost']
|
broker: [point to your broker IP eg. 'mqtt://localhost']
|
||||||
|
|
@ -91,23 +109,26 @@ mqtt:
|
||||||
topic: 'hass/status'
|
topic: 'hass/status'
|
||||||
payload: 'offline'
|
payload: 'offline'
|
||||||
```
|
```
|
||||||
|
|
||||||
The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing all devices).
|
The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing all devices).
|
||||||
|
|
||||||
The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it.
|
The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it.
|
||||||
|
|
||||||
Parameter | Value
|
| Parameter | Value |
|
||||||
--- | ---
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
site | Name of your Plejd site, the name is displayed in the Plejd app (top bar).
|
| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). |
|
||||||
username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||||
password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||||
mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost
|
| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost |
|
||||||
mqttUsername | Username of the MQTT broker
|
| mqttUsername | Username of the MQTT broker |
|
||||||
mqttPassword | Password of the MQTT broker
|
| mqttPassword | Password of the MQTT broker |
|
||||||
includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. *Added in v. 5*.
|
| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. _Added in v. 5_. |
|
||||||
connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds.
|
| logLevel | Minimim log level. Supported values are `error`, `warn`, `info`, `debug`, `verbose`, `silly` with increasing amount of logging. Do not log more than `info` for production purposes. |
|
||||||
writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50.
|
| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. |
|
||||||
|
| writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50. |
|
||||||
|
|
||||||
## Transitions
|
## Transitions
|
||||||
|
|
||||||
Transitions from Home Assistant are supported (for dimmable devices) when transition is longer than 1 second. Plejd will do a bit of internal transitioning (default soft start is 0.1 seconds).
|
Transitions from Home Assistant are supported (for dimmable devices) when transition is longer than 1 second. Plejd will do a bit of internal transitioning (default soft start is 0.1 seconds).
|
||||||
|
|
||||||
This implementation will transition each device independently, meaning that brightness change might be choppy if transitioning many devices at once or a changing brightness a lot in a limited time. Hassio-plejd's communication channel seems to handle a few updates per second, this is the combined value for all devices.
|
This implementation will transition each device independently, meaning that brightness change might be choppy if transitioning many devices at once or a changing brightness a lot in a limited time. Hassio-plejd's communication channel seems to handle a few updates per second, this is the combined value for all devices.
|
||||||
|
|
@ -115,81 +136,163 @@ This implementation will transition each device independently, meaning that brig
|
||||||
Transition points will be skipped if the queue of messages to be sent is over a certain threshold, by default equal to the number of devices in the system. Total transition time is prioritized rather than smoothness.
|
Transition points will be skipped if the queue of messages to be sent is over a certain threshold, by default equal to the number of devices in the system. Total transition time is prioritized rather than smoothness.
|
||||||
|
|
||||||
Recommendations
|
Recommendations
|
||||||
* Only transition a few devices at a time when possible
|
|
||||||
* Expect 5-10 brightness changes per second, meaning 5 devices => 1-2 updates per device per second
|
- Only transition a few devices at a time when possible
|
||||||
* ... meaning that SLOW transitions will work well (wake-up light, gradually fade over a minute, ...), but quick ones will only work well for few devices or small relative changes in brightness
|
- Expect 5-10 brightness changes per second, meaning 5 devices => 1-2 updates per device per second
|
||||||
* When experiencing choppy quick transitions, turn transitioning off and let the Plejd hardware do the work instead
|
- ... meaning that SLOW transitions will work well (wake-up light, gradually fade over a minute, ...), but quick ones will only work well for few devices or small relative changes in brightness
|
||||||
|
- When experiencing choppy quick transitions, turn transitioning off and let the Plejd hardware do the work instead
|
||||||
|
|
||||||
## I want voice control!
|
## I want voice control!
|
||||||
|
|
||||||
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
||||||
https://www.home-assistant.io/integrations/google_assistant/
|
https://www.home-assistant.io/integrations/google_assistant/
|
||||||
|
|
||||||
### I don't want voice, I want HomeKit!
|
### I don't want voice, I want HomeKit!
|
||||||
|
|
||||||
Check this out for more information on how you can get your Plejd lights controlled using HomeKit:
|
Check this out for more information on how you can get your Plejd lights controlled using HomeKit:
|
||||||
https://www.home-assistant.io/integrations/homekit/
|
https://www.home-assistant.io/integrations/homekit/
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
The code in this project follows the [Airbnb JavaScript guide](https://github.com/airbnb/javascript) with a few exceptions. Do run the `npm run lint:fix` command in the `plejd` folder (after running `npm install`) and fix any remaining issues before committing. If copying the plugin locally to your Home Assistant instance _do not include the node_modules directory_, strange errors will happen during build!
|
||||||
|
|
||||||
|
For a nice developer experience it is very convenient to have `eslint` and `prettier` installed in your favorite editor (such as VS Code) and use the "format on save" option (or invoke formatting by Alt+Shift+F in VS Code). Any code issues should appear in the problems window inside the editor, as well as when running the command above.
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
Logs are color coded and can be accessed on the Log tab of the addon. If you set log level to debug, verbose or silly you will generate a lot of log output
|
||||||
|
that will quickly scroll out of view. Logs can be exported through Docker that hosts all Home Assistant addons. To do that:
|
||||||
|
|
||||||
|
- SSH or console access the HA installation
|
||||||
|
- Identify the docker container name using `docker container ls` (NAMES column). Example name used `addon_local_plejd`
|
||||||
|
- tail logs: `tail -f addon_local_plejd`
|
||||||
|
- tail logs, strip color coding and save to file `docker logs -f addon_local_plejd | sed 's/\x1b\[[0-9;]*m//g' > /config/plejd.log` (output file might need to be adjusted)
|
||||||
|
|
||||||
|
### View logs in VS Code addon
|
||||||
|
|
||||||
|
Logs extracted as above can easily be viewed in the VS Code Home Assistant addon, which will default to using the excellent `Log File Highlighter` extension to parse the file.
|
||||||
|
Out of the box you can for example view elapsed time by selecting multiple lines and keeping an eye in the status bar. If you're feeling fancy you can get back the removed color information by adding something like below to the the `settings.json` configuration of VS Code.
|
||||||
|
|
||||||
|
```JSON
|
||||||
|
{
|
||||||
|
... other settings,
|
||||||
|
"logFileHighlighter.customPatterns": [
|
||||||
|
{
|
||||||
|
"pattern": "ERR",
|
||||||
|
"foreground": "#af1f1f",
|
||||||
|
"fontStyle": "bold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "WRN",
|
||||||
|
"foreground": "#af6f00",
|
||||||
|
"fontStyle": "bold",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "INF",
|
||||||
|
"foreground": "#44d",
|
||||||
|
"fontStyle": "bold"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "VRB",
|
||||||
|
"foreground": "#4a4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "DBG",
|
||||||
|
"foreground": "#4a4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "SIL",
|
||||||
|
"foreground": "#999"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "\\[.*\\]",
|
||||||
|
"foreground": "#666"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
*v 0.4.5*:
|
|
||||||
* FIX: Resolved a Docker build error
|
|
||||||
|
|
||||||
*v 0.4.4*:
|
_v 0.4.5_:
|
||||||
* FIX: Disabled AppArmor Policy since there's been a lot of issues with that.
|
|
||||||
|
|
||||||
*v 0.4.3*:
|
- FIX: Resolved a Docker build error
|
||||||
* FIX: Updated add-on to work with the API changes made by Plejd.
|
|
||||||
|
|
||||||
*v 0.4.0*:
|
_v 0.4.4_:
|
||||||
* NEW: Implemented support for Plejd scenes, each scene appears as a switch in Home Assistant.
|
|
||||||
* NEW: *WPH-01* is supported and generates two switches (left and right button).
|
|
||||||
* NEW: Write queues, finally able to incorporate Plejd devices in HA automations/scenes etc.
|
|
||||||
|
|
||||||
*v 0.3.4*:
|
- FIX: Disabled AppArmor Policy since there's been a lot of issues with that.
|
||||||
* NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
|
||||||
* FIX: Reworked some logging to get better understanding of what happens.
|
|
||||||
|
|
||||||
*v 0.3.0*:
|
_v 0.4.3_:
|
||||||
* NEW: New BLE manager, DBus instead of noble
|
|
||||||
* FIX: Adding entities as devices now as well
|
|
||||||
* FIX: Bug fixes
|
|
||||||
|
|
||||||
*v 0.2.8*:
|
- FIX: Updated add-on to work with the API changes made by Plejd.
|
||||||
* FIX: Reset characteristic state on disconnect
|
|
||||||
|
|
||||||
*v 0.2.7*:
|
_v 0.4.0_:
|
||||||
* FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
|
||||||
|
|
||||||
*v 0.2.6*:
|
- NEW: Implemented support for Plejd scenes, each scene appears as a switch in Home Assistant.
|
||||||
* FIX: Added null check to remove listeners for characteristics
|
- NEW: _WPH-01_ is supported and generates two switches (left and right button).
|
||||||
|
- NEW: Write queues, finally able to incorporate Plejd devices in HA automations/scenes etc.
|
||||||
|
|
||||||
*v 0.2.5*:
|
_v 0.3.4_:
|
||||||
* FIX: Invalid scene id in events/scene message
|
|
||||||
|
|
||||||
*v 0.2.4*:
|
- NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
||||||
* Stability improvements
|
- FIX: Reworked some logging to get better understanding of what happens.
|
||||||
|
|
||||||
*v 0.2.3*:
|
_v 0.3.0_:
|
||||||
* FIX: Container build error fix
|
|
||||||
|
|
||||||
*v 0.2.2*:
|
- NEW: New BLE manager, DBus instead of noble
|
||||||
* Stability improvements
|
- FIX: Adding entities as devices now as well
|
||||||
|
- FIX: Bug fixes
|
||||||
|
|
||||||
*v 0.2.1*:
|
_v 0.2.8_:
|
||||||
* Stability improvements
|
|
||||||
|
|
||||||
*v 0.2.0*:
|
- FIX: Reset characteristic state on disconnect
|
||||||
* Stability improvements
|
|
||||||
* Bugfixes
|
|
||||||
|
|
||||||
*v 0.1.1*:
|
_v 0.2.7_:
|
||||||
* FIX: Fixed missing reference on startup, preventing add-on from starting
|
|
||||||
|
|
||||||
*v 0.1.0*:
|
- FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
||||||
* NEW: Rewrote the BLE integration for more stability
|
|
||||||
* FIX: discovery wasn't always sent
|
|
||||||
|
|
||||||
*previous*:
|
_v 0.2.6_:
|
||||||
* FIX: bug preventing add-on from building
|
|
||||||
* NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
- FIX: Added null check to remove listeners for characteristics
|
||||||
|
|
||||||
|
_v 0.2.5_:
|
||||||
|
|
||||||
|
- FIX: Invalid scene id in events/scene message
|
||||||
|
|
||||||
|
_v 0.2.4_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.3_:
|
||||||
|
|
||||||
|
- FIX: Container build error fix
|
||||||
|
|
||||||
|
_v 0.2.2_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.1_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.0_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
- Bugfixes
|
||||||
|
|
||||||
|
_v 0.1.1_:
|
||||||
|
|
||||||
|
- FIX: Fixed missing reference on startup, preventing add-on from starting
|
||||||
|
|
||||||
|
_v 0.1.0_:
|
||||||
|
|
||||||
|
- NEW: Rewrote the BLE integration for more stability
|
||||||
|
- FIX: discovery wasn't always sent
|
||||||
|
|
||||||
|
_previous_:
|
||||||
|
|
||||||
|
- FIX: bug preventing add-on from building
|
||||||
|
- NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
1
plejd/.eslintignore
Normal file
1
plejd/.eslintignore
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
39
plejd/.eslintrc.js
Normal file
39
plejd/.eslintrc.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// const path = require('path');
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "extends": ["airbnb-base", "plugin:prettier/recommended"],
|
||||||
|
// "plugins": ["prettier"],
|
||||||
|
// "rules": {
|
||||||
|
// "prettier/prettier": "error"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: [
|
||||||
|
'airbnb-base',
|
||||||
|
// 'prettier',
|
||||||
|
// 'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
// plugins: ['prettier'],
|
||||||
|
rules: getRules(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function getRules() {
|
||||||
|
return {
|
||||||
|
// Allows modification of properties passed to functions.
|
||||||
|
// Notably used in array.forEach(e => {e.prop = val;})
|
||||||
|
'no-param-reassign': ['error', { props: false }],
|
||||||
|
// ++ operator widely used
|
||||||
|
'no-plusplus': ['off'],
|
||||||
|
// Hassio-Plejd team feals _ prefix is great for "private" variables.
|
||||||
|
// They will still be available for use from the outside
|
||||||
|
'no-underscore-dangle': ['off'],
|
||||||
|
// Allow function hoisting to improve code readability
|
||||||
|
'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
|
||||||
|
// Allow direct indexing of arrays only (array[0])
|
||||||
|
'prefer-destructuring': ['error', { array: false, object: true }],
|
||||||
|
};
|
||||||
|
}
|
||||||
15
plejd/Configuration.js
Normal file
15
plejd/Configuration.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
class Configuration {
|
||||||
|
static _config = null;
|
||||||
|
|
||||||
|
static getConfiguration() {
|
||||||
|
if (!Configuration._config) {
|
||||||
|
const rawData = fs.readFileSync('/data/options.json');
|
||||||
|
Configuration._config = JSON.parse(rawData);
|
||||||
|
}
|
||||||
|
return Configuration._config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Configuration;
|
||||||
|
|
@ -7,13 +7,17 @@ ENV LANG C.UTF-8
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
# Copy data for add-on
|
# Copy data for add-on
|
||||||
COPY ./api.js /plejd/
|
|
||||||
COPY ./config.json /plejd/
|
COPY ./config.json /plejd/
|
||||||
|
COPY ./Configuration.js /plejd/
|
||||||
|
COPY ./Logger.js /plejd/
|
||||||
COPY ./main.js /plejd/
|
COPY ./main.js /plejd/
|
||||||
COPY ./mqtt.js /plejd/
|
COPY ./MqttClient.js /plejd/
|
||||||
COPY ./package.json /plejd/
|
COPY ./package.json /plejd/
|
||||||
COPY ./ble.bluez.js /plejd/
|
COPY ./PlejdApi.js /plejd/
|
||||||
COPY ./scene.manager.js /plejd/
|
COPY ./PlejdService.js /plejd/
|
||||||
|
COPY ./Scene.js /plejd/
|
||||||
|
COPY ./SceneManager.js /plejd/
|
||||||
|
COPY ./SceneStep.js /plejd/
|
||||||
|
|
||||||
ARG BUILD_ARCH
|
ARG BUILD_ARCH
|
||||||
|
|
||||||
|
|
|
||||||
111
plejd/Logger.js
Normal file
111
plejd/Logger.js
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
const winston = require('winston');
|
||||||
|
|
||||||
|
const {
|
||||||
|
colorize, combine, label, printf, timestamp,
|
||||||
|
} = winston.format;
|
||||||
|
|
||||||
|
const Configuration = require('./Configuration');
|
||||||
|
|
||||||
|
const LEVELS = ['error', 'warn', 'info', 'debug', 'verbose', 'silly'];
|
||||||
|
const LEVELS_LOOKUP = {
|
||||||
|
error: 'ERR',
|
||||||
|
warn: 'WRN',
|
||||||
|
info: 'INF',
|
||||||
|
debug: 'DBG',
|
||||||
|
verbose: 'VRB',
|
||||||
|
silly: 'SLY',
|
||||||
|
};
|
||||||
|
|
||||||
|
const logFormat = printf((info) => {
|
||||||
|
if (info.stack) {
|
||||||
|
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}\n${info.stack}`;
|
||||||
|
}
|
||||||
|
return `${info.timestamp} ${info.level} [${info.label}] ${info.message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Winston-based logger */
|
||||||
|
class Logger {
|
||||||
|
constructor() {
|
||||||
|
throw new Error('Please call createLogger instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Created logger will follow Winston createLogger, but
|
||||||
|
* - add module name to logger
|
||||||
|
* - swap debug/verbose levels and omit http to mimic HA standard
|
||||||
|
* Levels (in order): error, warn, info, debug, verbose, silly
|
||||||
|
* */
|
||||||
|
static getLogger(moduleName) {
|
||||||
|
const config = Configuration.getConfiguration();
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
const level = (config.logLevel && LEVELS.find((l) => l.startsWith(config.logLevel[0].toLowerCase())))
|
||||||
|
|| 'info';
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
format: combine(
|
||||||
|
winston.format((info) => {
|
||||||
|
info.level = LEVELS_LOOKUP[info.level] || '???';
|
||||||
|
return info;
|
||||||
|
})(),
|
||||||
|
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
|
||||||
|
label({ label: moduleName }),
|
||||||
|
colorize(),
|
||||||
|
logFormat,
|
||||||
|
),
|
||||||
|
level,
|
||||||
|
levels: Logger.logLevels().levels,
|
||||||
|
transports: [new winston.transports.Console()],
|
||||||
|
});
|
||||||
|
winston.addColors(Logger.logLevels().colors);
|
||||||
|
|
||||||
|
if (moduleName === 'plejd-main') {
|
||||||
|
logger.log(level, `Log level set to ${level}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
static logLevels() {
|
||||||
|
// Default (npm) levels
|
||||||
|
// levels = {
|
||||||
|
// error: 0,
|
||||||
|
// warn: 1,
|
||||||
|
// info: 2,
|
||||||
|
// http: 3,
|
||||||
|
// verbose: 4,
|
||||||
|
// debug: 5,
|
||||||
|
// silly: 6
|
||||||
|
// }
|
||||||
|
// colors = {
|
||||||
|
// error: 'red',
|
||||||
|
// warn: 'yellow',
|
||||||
|
// info: 'green',
|
||||||
|
// http: 'green',
|
||||||
|
// verbose: 'cyan',
|
||||||
|
// debug: 'blue',
|
||||||
|
// silly: 'magenta'
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Mimic HA standard below
|
||||||
|
// Debug/verbose swapped compared to npm levels, http omitted
|
||||||
|
return {
|
||||||
|
levels: {
|
||||||
|
error: 0,
|
||||||
|
warn: 1,
|
||||||
|
info: 2,
|
||||||
|
debug: 3,
|
||||||
|
verbose: 4,
|
||||||
|
silly: 6,
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
error: 'red',
|
||||||
|
warn: 'yellow',
|
||||||
|
info: 'green',
|
||||||
|
debug: 'cyan',
|
||||||
|
verbose: 'blue',
|
||||||
|
silly: 'magenta',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Logger;
|
||||||
189
plejd/MqttClient.js
Normal file
189
plejd/MqttClient.js
Normal file
|
|
@ -0,0 +1,189 @@
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const mqtt = require('mqtt');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
|
||||||
|
const startTopic = 'hass/status';
|
||||||
|
|
||||||
|
const logger = Logger.getLogger('plejd-mqtt');
|
||||||
|
|
||||||
|
// #region discovery
|
||||||
|
|
||||||
|
const discoveryPrefix = 'homeassistant';
|
||||||
|
const nodeId = 'plejd';
|
||||||
|
|
||||||
|
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
||||||
|
const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`;
|
||||||
|
const getConfigPath = (plug) => `${getPath(plug)}/config`;
|
||||||
|
const getStateTopic = (plug) => `${getPath(plug)}/state`;
|
||||||
|
const getCommandTopic = (plug) => `${getPath(plug)}/set`;
|
||||||
|
const getSceneEventTopic = () => 'plejd/event/scene';
|
||||||
|
|
||||||
|
const getDiscoveryPayload = (device) => ({
|
||||||
|
schema: 'json',
|
||||||
|
name: device.name,
|
||||||
|
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
||||||
|
state_topic: getStateTopic(device),
|
||||||
|
command_topic: getCommandTopic(device),
|
||||||
|
optimistic: false,
|
||||||
|
brightness: `${device.dimmable}`,
|
||||||
|
device: {
|
||||||
|
identifiers: `${device.serialNumber}_${device.id}`,
|
||||||
|
manufacturer: 'Plejd',
|
||||||
|
model: device.typeName,
|
||||||
|
name: device.name,
|
||||||
|
sw_version: device.version,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSwitchPayload = (device) => ({
|
||||||
|
name: device.name,
|
||||||
|
state_topic: getStateTopic(device),
|
||||||
|
command_topic: getCommandTopic(device),
|
||||||
|
optimistic: false,
|
||||||
|
device: {
|
||||||
|
identifiers: `${device.serialNumber}_${device.id}`,
|
||||||
|
manufacturer: 'Plejd',
|
||||||
|
model: device.typeName,
|
||||||
|
name: device.name,
|
||||||
|
sw_version: device.version,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
class MqttClient extends EventEmitter {
|
||||||
|
constructor(mqttBroker, username, password) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.mqttBroker = mqttBroker;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.deviceMap = {};
|
||||||
|
this.devices = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
logger.info('Initializing MQTT connection for Plejd addon');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
this.client = mqtt.connect(this.mqttBroker, {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('connect', () => {
|
||||||
|
logger.info('Connected to MQTT.');
|
||||||
|
|
||||||
|
this.client.subscribe(startTopic, (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(`Unable to subscribe to ${startTopic}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emit('connected');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.subscribe(getSubscribePath(), (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('Unable to subscribe to control topics');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('close', () => {
|
||||||
|
logger.verbose('Warning: mqtt channel closed event, reconnecting...');
|
||||||
|
self.reconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.on('message', (topic, message) => {
|
||||||
|
// const command = message.toString();
|
||||||
|
const command = message.toString().substring(0, 1) === '{'
|
||||||
|
? JSON.parse(message.toString())
|
||||||
|
: message.toString();
|
||||||
|
|
||||||
|
if (topic === startTopic) {
|
||||||
|
logger.info('Home Assistant has started. lets do discovery.');
|
||||||
|
self.emit('connected');
|
||||||
|
} else if (topic.includes('set')) {
|
||||||
|
logger.verbose(`Got mqtt command on ${topic} - ${message}`);
|
||||||
|
const device = self.devices.find((x) => getCommandTopic(x) === topic);
|
||||||
|
if (device) {
|
||||||
|
self.emit('stateChanged', device, command);
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
`Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (topic.includes('state')) {
|
||||||
|
logger.verbose(`State update sent over mqtt to HA ${topic} - ${message}`);
|
||||||
|
} else {
|
||||||
|
logger.verbose(`Warning: Got unrecognized mqtt command on ${topic} - ${message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reconnect() {
|
||||||
|
this.client.reconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
discover(devices) {
|
||||||
|
this.devices = devices;
|
||||||
|
|
||||||
|
const self = this;
|
||||||
|
logger.debug(`Sending discovery of ${devices.length} device(s).`);
|
||||||
|
|
||||||
|
devices.forEach((device) => {
|
||||||
|
logger.debug(`Sending discovery for ${device.name}`);
|
||||||
|
|
||||||
|
const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
||||||
|
logger.info(
|
||||||
|
`Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
self.deviceMap[device.id] = payload.unique_id;
|
||||||
|
|
||||||
|
self.client.publish(getConfigPath(device), JSON.stringify(payload));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateState(deviceId, data) {
|
||||||
|
const device = this.devices.find((x) => x.id === deviceId);
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
logger.warn(`Unknown device id ${deviceId} - not handled by us.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.verbose(
|
||||||
|
`Updating state for ${device.name}: ${data.state}${
|
||||||
|
data.brightness ? `, dim: ${data.brightness}` : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
let payload = null;
|
||||||
|
|
||||||
|
if (device.type === 'switch') {
|
||||||
|
payload = data.state === 1 ? 'ON' : 'OFF';
|
||||||
|
} else {
|
||||||
|
if (device.dimmable) {
|
||||||
|
payload = {
|
||||||
|
state: data.state === 1 ? 'ON' : 'OFF',
|
||||||
|
brightness: data.brightness,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
payload = {
|
||||||
|
state: data.state === 1 ? 'ON' : 'OFF',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = JSON.stringify(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.publish(getStateTopic(device), payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
sceneTriggered(scene) {
|
||||||
|
logger.verbose(`Scene triggered: ${scene}`);
|
||||||
|
this.client.publish(getSceneEventTopic(), JSON.stringify({ scene }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MqttClient;
|
||||||
329
plejd/PlejdApi.js
Normal file
329
plejd/PlejdApi.js
Normal file
|
|
@ -0,0 +1,329 @@
|
||||||
|
const axios = require('axios');
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
|
||||||
|
const API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak';
|
||||||
|
const API_BASE_URL = 'https://cloud.plejd.com/parse/';
|
||||||
|
const API_LOGIN_URL = 'login';
|
||||||
|
const API_SITE_LIST_URL = 'functions/getSiteList';
|
||||||
|
const API_SITE_DETAILS_URL = 'functions/getSiteById';
|
||||||
|
|
||||||
|
const logger = Logger.getLogger('plejd-api');
|
||||||
|
|
||||||
|
class PlejdApi extends EventEmitter {
|
||||||
|
constructor(siteName, username, password, includeRoomsAsLights) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.includeRoomsAsLights = includeRoomsAsLights;
|
||||||
|
this.siteName = siteName;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
|
||||||
|
this.sessionToken = '';
|
||||||
|
this.site = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
login() {
|
||||||
|
logger.info('login()');
|
||||||
|
logger.info(`logging into ${this.siteName}`);
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': API_APP_ID,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logger.debug(`sending POST to ${API_BASE_URL}${API_LOGIN_URL}`);
|
||||||
|
|
||||||
|
instance
|
||||||
|
.post(API_LOGIN_URL, {
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
logger.info('got session token response');
|
||||||
|
self.sessionToken = response.data.sessionToken;
|
||||||
|
|
||||||
|
if (!self.sessionToken) {
|
||||||
|
logger.error('No session token received');
|
||||||
|
reject(new Error('no session token received.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.response.status === 400) {
|
||||||
|
logger.error(
|
||||||
|
'Server returned status 400. probably invalid credentials, please verify.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error('Unable to retrieve session token response: ', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(new Error(`unable to retrieve session token response: ${error}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSites() {
|
||||||
|
logger.info('Get all Plejd sites for account...');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': API_APP_ID,
|
||||||
|
'X-Parse-Session-Token': this.sessionToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_LIST_URL}`);
|
||||||
|
|
||||||
|
instance
|
||||||
|
.post(API_SITE_LIST_URL)
|
||||||
|
.then((response) => {
|
||||||
|
logger.info('got site list response');
|
||||||
|
const site = response.data.result.find((x) => x.site.title === self.siteName);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.error(`error: failed to find a site named ${self.siteName}`);
|
||||||
|
reject(new Error(`failed to find a site named ${self.siteName}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(site);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('error: unable to retrieve list of sites. error: ', error);
|
||||||
|
return reject(new Error(`plejd-api: unable to retrieve list of sites. error: ${error}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSite(siteId) {
|
||||||
|
logger.info('Get site details...');
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'X-Parse-Application-Id': API_APP_ID,
|
||||||
|
'X-Parse-Session-Token': this.sessionToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_DETAILS_URL}`);
|
||||||
|
|
||||||
|
instance
|
||||||
|
.post(API_SITE_DETAILS_URL, { siteId })
|
||||||
|
.then((response) => {
|
||||||
|
logger.info('got site details response');
|
||||||
|
if (response.data.result.length === 0) {
|
||||||
|
const msg = `no site with ID ${siteId} was found.`;
|
||||||
|
logger.error(`error: ${msg}`);
|
||||||
|
reject(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.site = response.data.result[0];
|
||||||
|
self.cryptoKey = self.site.plejdMesh.cryptoKey;
|
||||||
|
|
||||||
|
resolve(self.cryptoKey);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error('error: unable to retrieve the crypto key. error: ', error);
|
||||||
|
return reject(new Error(`plejd-api: unable to retrieve the crypto key. error: ${error}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDevices() {
|
||||||
|
const devices = [];
|
||||||
|
|
||||||
|
logger.verbose(JSON.stringify(this.site));
|
||||||
|
|
||||||
|
const roomDevices = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < this.site.devices.length; i++) {
|
||||||
|
const device = this.site.devices[i];
|
||||||
|
const { deviceId } = device;
|
||||||
|
|
||||||
|
const settings = this.site.outputSettings.find((x) => x.deviceParseId === device.objectId);
|
||||||
|
let deviceNum = this.site.deviceAddress[deviceId];
|
||||||
|
|
||||||
|
if (settings) {
|
||||||
|
const outputs = this.site.outputAddress[deviceId];
|
||||||
|
deviceNum = outputs[settings.output];
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if device is dimmable
|
||||||
|
const plejdDevice = this.site.plejdDevices.find((x) => x.deviceId === deviceId);
|
||||||
|
const deviceType = this._getDeviceType(plejdDevice.hardwareId);
|
||||||
|
const { name, type } = deviceType;
|
||||||
|
let { dimmable } = deviceType;
|
||||||
|
|
||||||
|
if (settings) {
|
||||||
|
dimmable = settings.dimCurve !== 'NonDimmable';
|
||||||
|
}
|
||||||
|
|
||||||
|
const newDevice = {
|
||||||
|
id: deviceNum,
|
||||||
|
name: device.title,
|
||||||
|
type,
|
||||||
|
typeName: name,
|
||||||
|
dimmable,
|
||||||
|
version: plejdDevice.firmware.version,
|
||||||
|
serialNumber: plejdDevice.deviceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (newDevice.typeName === 'WPH-01') {
|
||||||
|
// WPH-01 is special, it has two buttons which needs to be
|
||||||
|
// registered separately.
|
||||||
|
const inputs = this.site.inputAddress[deviceId];
|
||||||
|
const first = inputs[0];
|
||||||
|
const second = inputs[1];
|
||||||
|
|
||||||
|
let switchDevice = {
|
||||||
|
id: first,
|
||||||
|
name: `${device.title} knapp vä`,
|
||||||
|
type,
|
||||||
|
typeName: name,
|
||||||
|
dimmable,
|
||||||
|
version: plejdDevice.firmware.version,
|
||||||
|
serialNumber: plejdDevice.deviceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (roomDevices[device.roomId]) {
|
||||||
|
roomDevices[device.roomId].push(switchDevice);
|
||||||
|
} else {
|
||||||
|
roomDevices[device.roomId] = [switchDevice];
|
||||||
|
}
|
||||||
|
devices.push(switchDevice);
|
||||||
|
|
||||||
|
switchDevice = {
|
||||||
|
id: second,
|
||||||
|
name: `${device.title} knapp hö`,
|
||||||
|
type,
|
||||||
|
typeName: name,
|
||||||
|
dimmable,
|
||||||
|
version: plejdDevice.firmware.version,
|
||||||
|
serialNumber: plejdDevice.deviceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (roomDevices[device.roomId]) {
|
||||||
|
roomDevices[device.roomId].push(switchDevice);
|
||||||
|
} else {
|
||||||
|
roomDevices[device.roomId] = [switchDevice];
|
||||||
|
}
|
||||||
|
devices.push(switchDevice);
|
||||||
|
} else {
|
||||||
|
if (roomDevices[device.roomId]) {
|
||||||
|
roomDevices[device.roomId].push(newDevice);
|
||||||
|
} else {
|
||||||
|
roomDevices[device.roomId] = [newDevice];
|
||||||
|
}
|
||||||
|
|
||||||
|
devices.push(newDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.includeRoomsAsLights) {
|
||||||
|
logger.debug('includeRoomsAsLights is set to true, adding rooms too.');
|
||||||
|
for (let i = 0; i < this.site.rooms.length; i++) {
|
||||||
|
const room = this.site.rooms[i];
|
||||||
|
const { roomId } = room;
|
||||||
|
const roomAddress = this.site.roomAddress[roomId];
|
||||||
|
|
||||||
|
const newDevice = {
|
||||||
|
id: roomAddress,
|
||||||
|
name: room.title,
|
||||||
|
type: 'light',
|
||||||
|
typeName: 'Room',
|
||||||
|
dimmable: roomDevices[roomId].filter((x) => x.dimmable).length > 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
devices.push(newDevice);
|
||||||
|
}
|
||||||
|
logger.debug('includeRoomsAsLights done.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// add scenes as switches
|
||||||
|
const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const scene of scenes) {
|
||||||
|
const sceneNum = this.site.sceneIndex[scene.sceneId];
|
||||||
|
const newScene = {
|
||||||
|
id: sceneNum,
|
||||||
|
name: scene.title,
|
||||||
|
type: 'switch',
|
||||||
|
typeName: 'Scene',
|
||||||
|
dimmable: false,
|
||||||
|
version: '1.0',
|
||||||
|
serialNumber: scene.objectId,
|
||||||
|
};
|
||||||
|
|
||||||
|
devices.push(newScene);
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
_getDeviceType(hardwareId) {
|
||||||
|
switch (parseInt(hardwareId, 10)) {
|
||||||
|
case 1:
|
||||||
|
case 11:
|
||||||
|
return { name: 'DIM-01', type: 'light', dimmable: true };
|
||||||
|
case 2:
|
||||||
|
return { name: 'DIM-02', type: 'light', dimmable: true };
|
||||||
|
case 3:
|
||||||
|
return { name: 'CTR-01', type: 'light', dimmable: false };
|
||||||
|
case 4:
|
||||||
|
return { name: 'GWY-01', type: 'sensor', dimmable: false };
|
||||||
|
case 5:
|
||||||
|
return { name: 'LED-10', type: 'light', dimmable: true };
|
||||||
|
case 6:
|
||||||
|
return { name: 'WPH-01', type: 'switch', dimmable: false };
|
||||||
|
case 7:
|
||||||
|
return { name: 'REL-01', type: 'switch', dimmable: false };
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
// Unknown
|
||||||
|
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||||
|
case 10:
|
||||||
|
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||||
|
case 12:
|
||||||
|
// Unknown
|
||||||
|
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||||
|
case 13:
|
||||||
|
return { name: 'Generic', type: 'light', dimmable: false };
|
||||||
|
case 14:
|
||||||
|
case 15:
|
||||||
|
case 16:
|
||||||
|
// Unknown
|
||||||
|
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||||
|
case 17:
|
||||||
|
return { name: 'REL-01', type: 'switch', dimmable: false };
|
||||||
|
case 18:
|
||||||
|
return { name: 'REL-02', type: 'switch', dimmable: false };
|
||||||
|
case 19:
|
||||||
|
// Unknown
|
||||||
|
return { name: '-unknown-', type: 'light', dimmable: false };
|
||||||
|
case 20:
|
||||||
|
return { name: 'SPR-01', type: 'switch', dimmable: false };
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown device type with id ${hardwareId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PlejdApi;
|
||||||
|
|
@ -2,21 +2,9 @@ const dbus = require('dbus-next');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const xor = require('buffer-xor');
|
const xor = require('buffer-xor');
|
||||||
const EventEmitter = require('events');
|
const EventEmitter = require('events');
|
||||||
|
const Logger = require('./Logger');
|
||||||
|
|
||||||
const logInfo = true; // Normal operations
|
const logger = Logger.getLogger('plejd-ble');
|
||||||
const logDebug = false; // Chatty
|
|
||||||
const logVerbose = false; // Very chatty
|
|
||||||
|
|
||||||
const consoleLogger = (level) => (...msg) =>
|
|
||||||
console.log(new Date().toISOString().replace('T', ' ').substring(0, 19) + 'Z', level, 'plejd-ble', ...msg);
|
|
||||||
|
|
||||||
const getLogger = (level, shouldLog) => (shouldLog ? consoleLogger(level) : () => {});
|
|
||||||
|
|
||||||
const errLogger = getLogger('ERR', true);
|
|
||||||
const infLogger = getLogger('INF', logInfo);
|
|
||||||
const dbgLogger = getLogger('DBG', logDebug);
|
|
||||||
const vrbLogger = getLogger('vrb', logVerbose);
|
|
||||||
|
|
||||||
|
|
||||||
// UUIDs
|
// UUIDs
|
||||||
const PLEJD_SERVICE = '31ba0001-6085-4726-be45-040c957391b5';
|
const PLEJD_SERVICE = '31ba0001-6085-4726-be45-040c957391b5';
|
||||||
|
|
@ -43,10 +31,10 @@ const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting
|
||||||
const MAX_RETRY_COUNT = 5; // Could be made a setting
|
const MAX_RETRY_COUNT = 5; // Could be made a setting
|
||||||
|
|
||||||
class PlejdService extends EventEmitter {
|
class PlejdService extends EventEmitter {
|
||||||
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime, keepAlive = false) {
|
constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
infLogger('Starting Plejd BLE, resetting all device states.');
|
logger.info('Starting Plejd BLE, resetting all device states.');
|
||||||
|
|
||||||
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex');
|
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex');
|
||||||
|
|
||||||
|
|
@ -70,13 +58,13 @@ class PlejdService extends EventEmitter {
|
||||||
lastData: null,
|
lastData: null,
|
||||||
lastDataProperties: null,
|
lastDataProperties: null,
|
||||||
auth: null,
|
auth: null,
|
||||||
ping: null
|
ping: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.bus = dbus.systemBus();
|
this.bus = dbus.systemBus();
|
||||||
this.adapter = null;
|
this.adapter = null;
|
||||||
|
|
||||||
dbgLogger('wiring events and waiting for BLE interface to power up.');
|
logger.debug('wiring events and waiting for BLE interface to power up.');
|
||||||
this.wireEvents();
|
this.wireEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,30 +81,32 @@ class PlejdService extends EventEmitter {
|
||||||
lastData: null,
|
lastData: null,
|
||||||
lastDataProperties: null,
|
lastDataProperties: null,
|
||||||
auth: null,
|
auth: null,
|
||||||
ping: null
|
ping: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
clearInterval(this.pingRef);
|
clearInterval(this.pingRef);
|
||||||
clearTimeout(this.writeQueueRef);
|
clearTimeout(this.writeQueueRef);
|
||||||
infLogger('init()');
|
logger.info('init()');
|
||||||
|
|
||||||
const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/');
|
const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/');
|
||||||
this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE);
|
this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE);
|
||||||
|
|
||||||
// We need to find the ble interface which implements the Adapter1 interface
|
// We need to find the ble interface which implements the Adapter1 interface
|
||||||
const managedObjects = await this.objectManager.GetManagedObjects();
|
const managedObjects = await this.objectManager.GetManagedObjects();
|
||||||
let result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID);
|
const result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this.adapter = result[1];
|
this.adapter = result[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.adapter) {
|
if (!this.adapter) {
|
||||||
errLogger('unable to find a bluetooth adapter that is compatible.');
|
logger.error('Unable to find a bluetooth adapter that is compatible.');
|
||||||
return;
|
return Promise.reject(new Error('Unable to find a bluetooth adapter that is compatible.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let path of Object.keys(managedObjects)) {
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const path of Object.keys(managedObjects)) {
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
const interfaces = Object.keys(managedObjects[path]);
|
const interfaces = Object.keys(managedObjects[path]);
|
||||||
|
|
||||||
if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
||||||
|
|
@ -126,73 +116,84 @@ class PlejdService extends EventEmitter {
|
||||||
const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value;
|
const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value;
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
infLogger('disconnecting ' + path);
|
logger.info(`disconnecting ${path}`);
|
||||||
await device.Disconnect();
|
await device.Disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.adapter.RemoveDevice(path);
|
await this.adapter.RemoveDevice(path);
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-await-in-loop */
|
||||||
}
|
}
|
||||||
|
|
||||||
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this));
|
this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this));
|
||||||
|
|
||||||
this.adapter.SetDiscoveryFilter({
|
this.adapter.SetDiscoveryFilter({
|
||||||
'UUIDs': new dbus.Variant('as', [PLEJD_SERVICE]),
|
UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]),
|
||||||
'Transport': new dbus.Variant('s', 'le')
|
Transport: new dbus.Variant('s', 'le'),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.adapter.StartDiscovery();
|
await this.adapter.StartDiscovery();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger('failed to start discovery. Make sure no other add-on is currently scanning.');
|
logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.');
|
||||||
return;
|
return Promise.reject(
|
||||||
|
new Error('Failed to start discovery. Make sure no other add-on is currently scanning.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return new Promise(resolve =>
|
return new Promise((resolve) => setTimeout(
|
||||||
setTimeout(() => resolve(
|
() => resolve(
|
||||||
this._internalInit().catch((err) => { errLogger('InternalInit exception! Will rethrow.', err); throw err; })
|
this._internalInit().catch((err) => {
|
||||||
), this.connectionTimeout * 1000
|
logger.error('InternalInit exception! Will rethrow.', err);
|
||||||
)
|
throw err;
|
||||||
);
|
}),
|
||||||
|
),
|
||||||
|
this.connectionTimeout * 1000,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
async _internalInit() {
|
async _internalInit() {
|
||||||
dbgLogger(`Got ${this.bleDevices.length} device(s).`);
|
logger.debug(`Got ${this.bleDevices.length} device(s).`);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const plejd of this.bleDevices) {
|
for (const plejd of this.bleDevices) {
|
||||||
dbgLogger(`Inspecting ${plejd['path']}`);
|
/* eslint-disable no-await-in-loop */
|
||||||
|
logger.debug(`Inspecting ${plejd.path}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd['path']);
|
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd.path);
|
||||||
const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID);
|
const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID);
|
||||||
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||||
|
|
||||||
plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
|
plejd.rssi = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
|
||||||
plejd['instance'] = device;
|
plejd.instance = device;
|
||||||
|
|
||||||
const segments = plejd['path'].split('/');
|
const segments = plejd.path.split('/');
|
||||||
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
|
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
|
||||||
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
|
||||||
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
|
plejd.device = this.devices.find((x) => x.serialNumber === fixedPlejdPath);
|
||||||
|
|
||||||
dbgLogger(`Discovered ${plejd['path']} with rssi ${plejd['rssi']}`);
|
logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger(`Failed inspecting ${plejd['path']}. `, err);
|
logger.error(`Failed inspecting ${plejd.path}. `, err);
|
||||||
}
|
}
|
||||||
|
/* eslint-enable no-await-in-loop */
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedDevices = this.bleDevices.sort((a, b) => b['rssi'] - a['rssi']);
|
const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi);
|
||||||
let connectedDevice = null;
|
let connectedDevice = null;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const plejd of sortedDevices) {
|
for (const plejd of sortedDevices) {
|
||||||
try {
|
try {
|
||||||
if (plejd['instance']) {
|
if (plejd.instance) {
|
||||||
infLogger(`Connecting to ${plejd['path']}`);
|
logger.info(`Connecting to ${plejd.path}`);
|
||||||
await plejd['instance'].Connect();
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await plejd.instance.Connect();
|
||||||
connectedDevice = plejd;
|
connectedDevice = plejd;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger('Warning: unable to connect, will retry. ', err);
|
logger.error('Warning: unable to connect, will retry. ', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,15 +206,17 @@ class PlejdService extends EventEmitter {
|
||||||
async _getInterface(managedObjects, iface) {
|
async _getInterface(managedObjects, iface) {
|
||||||
const managedPaths = Object.keys(managedObjects);
|
const managedPaths = Object.keys(managedObjects);
|
||||||
|
|
||||||
for (let path of managedPaths) {
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const path of managedPaths) {
|
||||||
const pathInterfaces = Object.keys(managedObjects[path]);
|
const pathInterfaces = Object.keys(managedObjects[path]);
|
||||||
if (pathInterfaces.indexOf(iface) > -1) {
|
if (pathInterfaces.indexOf(iface) > -1) {
|
||||||
dbgLogger(`Found BLE interface '${iface}' at ${path}`);
|
logger.debug(`Found BLE interface '${iface}' at ${path}`);
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
||||||
return [path, adapterObject.getInterface(iface), adapterObject];
|
return [path, adapterObject.getInterface(iface), adapterObject];
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger(`Failed to get interface '${iface}'. `, err);
|
logger.error(`Failed to get interface '${iface}'. `, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -226,39 +229,37 @@ class PlejdService extends EventEmitter {
|
||||||
const interfaceKeys = Object.keys(interfaces);
|
const interfaceKeys = Object.keys(interfaces);
|
||||||
|
|
||||||
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) {
|
||||||
if (interfaces[BLUEZ_DEVICE_ID]['UUIDs'].value.indexOf(PLEJD_SERVICE) > -1) {
|
if (interfaces[BLUEZ_DEVICE_ID].UUIDs.value.indexOf(PLEJD_SERVICE) > -1) {
|
||||||
dbgLogger(`Found Plejd service on ${path}`);
|
logger.debug(`Found Plejd service on ${path}`);
|
||||||
this.bleDevices.push({
|
this.bleDevices.push({
|
||||||
'path': path
|
path,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
errLogger('Uh oh, no Plejd device!');
|
logger.error('Uh oh, no Plejd device!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSettings(settings) {
|
|
||||||
dbgLogger('Got new settings: ', settings);
|
|
||||||
if (settings.debug) {
|
|
||||||
debug = true;
|
|
||||||
} else {
|
|
||||||
debug = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
turnOn(deviceId, command) {
|
turnOn(deviceId, command) {
|
||||||
const deviceName = (logVerbose || logDebug) ? this._getDeviceName(deviceId) : '';
|
const deviceName = this._getDeviceName(deviceId);
|
||||||
infLogger(`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${command.transition ? `, transition: ${command.transition}` : ''}`);
|
logger.info(
|
||||||
|
`Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${
|
||||||
|
command.transition ? `, transition: ${command.transition}` : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
|
this._transitionTo(deviceId, command.brightness, command.transition, deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
turnOff(deviceId, command) {
|
turnOff(deviceId, command) {
|
||||||
const deviceName = (logVerbose || logDebug) ? this._getDeviceName(deviceId) : '';
|
const deviceName = this._getDeviceName(deviceId);
|
||||||
infLogger(`Plejd got turn off command for ${deviceName} (${deviceId}), brightness ${command.brightness}${command.transition ? `, transition: ${command.transition}` : ''}`);
|
logger.info(
|
||||||
|
`Plejd got turn off command for ${deviceName} (${deviceId})${
|
||||||
|
command.transition ? `, transition: ${command.transition}` : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
this._transitionTo(deviceId, 0, command.transition, deviceName);
|
this._transitionTo(deviceId, 0, command.transition, deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_clearDeviceTransitionTimer(deviceId) {
|
_clearDeviceTransitionTimer(deviceId) {
|
||||||
if (this.bleDeviceTransitionTimers[deviceId]) {
|
if (this.bleDeviceTransitionTimers[deviceId]) {
|
||||||
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
|
clearInterval(this.bleDeviceTransitionTimers[deviceId]);
|
||||||
|
|
@ -266,55 +267,81 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
|
_transitionTo(deviceId, targetBrightness, transition, deviceName) {
|
||||||
const initialBrightness = this.plejdDevices[deviceId] ? this.plejdDevices[deviceId].state && this.plejdDevices[deviceId].dim : null;
|
const initialBrightness = this.plejdDevices[deviceId]
|
||||||
|
? this.plejdDevices[deviceId].state && this.plejdDevices[deviceId].dim
|
||||||
|
: null;
|
||||||
this._clearDeviceTransitionTimer(deviceId);
|
this._clearDeviceTransitionTimer(deviceId);
|
||||||
|
|
||||||
const isDimmable = this.devices.find(d => d.id === deviceId).dimmable;
|
const isDimmable = this.devices.find((d) => d.id === deviceId).dimmable;
|
||||||
|
|
||||||
if (transition > 1 && isDimmable && (initialBrightness || initialBrightness === 0) && (targetBrightness || targetBrightness === 0) && targetBrightness !== initialBrightness) {
|
if (
|
||||||
|
transition > 1
|
||||||
|
&& isDimmable
|
||||||
|
&& (initialBrightness || initialBrightness === 0)
|
||||||
|
&& (targetBrightness || targetBrightness === 0)
|
||||||
|
&& targetBrightness !== initialBrightness
|
||||||
|
) {
|
||||||
// Transition time set, known initial and target brightness
|
// Transition time set, known initial and target brightness
|
||||||
// Calculate transition interval time based on delta brightness and max steps per second
|
// Calculate transition interval time based on delta brightness and max steps per second
|
||||||
// During transition, measure actual transition interval time and adjust stepping continously
|
// During transition, measure actual transition interval time and adjust stepping continously
|
||||||
// If transition <= 1 second, Plejd will do a better job than we can in transitioning so transitioning will be skipped
|
// If transition <= 1 second, Plejd will do a better job
|
||||||
|
// than we can in transitioning so transitioning will be skipped
|
||||||
|
|
||||||
const deltaBrightness = targetBrightness - initialBrightness;
|
const deltaBrightness = targetBrightness - initialBrightness;
|
||||||
const transitionSteps = Math.min(Math.abs(deltaBrightness), MAX_TRANSITION_STEPS_PER_SECOND * transition);
|
const transitionSteps = Math.min(
|
||||||
const transitionInterval = transition * 1000 / transitionSteps;
|
Math.abs(deltaBrightness),
|
||||||
|
MAX_TRANSITION_STEPS_PER_SECOND * transition,
|
||||||
|
);
|
||||||
|
const transitionInterval = (transition * 1000) / transitionSteps;
|
||||||
|
|
||||||
dbgLogger(`transitioning from ${initialBrightness} to ${targetBrightness} ${transition ? 'in ' + transition + ' seconds' : ''}.`);
|
logger.debug(
|
||||||
vrbLogger(`delta brightness ${deltaBrightness}, steps ${transitionSteps}, interval ${transitionInterval} ms`);
|
`transitioning from ${initialBrightness} to ${targetBrightness} ${
|
||||||
|
transition ? `in ${transition} seconds` : ''
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
logger.verbose(
|
||||||
|
`delta brightness ${deltaBrightness}, steps ${transitionSteps}, interval ${transitionInterval} ms`,
|
||||||
|
);
|
||||||
|
|
||||||
const dtStart = new Date();
|
const dtStart = new Date();
|
||||||
|
|
||||||
let nSteps = 0;
|
let nSteps = 0;
|
||||||
|
|
||||||
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
|
this.bleDeviceTransitionTimers[deviceId] = setInterval(() => {
|
||||||
let tElapsedMs = new Date().getTime() - dtStart.getTime();
|
const tElapsedMs = new Date().getTime() - dtStart.getTime();
|
||||||
let tElapsed = tElapsedMs / 1000;
|
let tElapsed = tElapsedMs / 1000;
|
||||||
|
|
||||||
if (tElapsed > transition || tElapsed < 0) {
|
if (tElapsed > transition || tElapsed < 0) {
|
||||||
tElapsed = transition;
|
tElapsed = transition;
|
||||||
}
|
}
|
||||||
|
|
||||||
let newBrightness = parseInt(initialBrightness + deltaBrightness * tElapsed / transition);
|
let newBrightness = Math.round(
|
||||||
|
initialBrightness + (deltaBrightness * tElapsed) / transition,
|
||||||
|
);
|
||||||
|
|
||||||
if (tElapsed === transition) {
|
if (tElapsed === transition) {
|
||||||
nSteps++;
|
nSteps++;
|
||||||
this._clearDeviceTransitionTimer(deviceId);
|
this._clearDeviceTransitionTimer(deviceId);
|
||||||
newBrightness = targetBrightness;
|
newBrightness = targetBrightness;
|
||||||
dbgLogger(`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${tElapsedMs / (nSteps || 1)} ms.`);
|
logger.debug(
|
||||||
|
`Queueing finalize ${deviceName} (${deviceId}) transition from ${initialBrightness} to ${targetBrightness} in ${tElapsedMs}ms. Done steps ${nSteps}. Average interval ${
|
||||||
|
tElapsedMs / (nSteps || 1)
|
||||||
|
} ms.`,
|
||||||
|
);
|
||||||
this._setBrightness(deviceId, newBrightness, true, deviceName);
|
this._setBrightness(deviceId, newBrightness, true, deviceName);
|
||||||
} else {
|
} else {
|
||||||
nSteps++;
|
nSteps++;
|
||||||
vrbLogger(`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`);
|
logger.verbose(
|
||||||
|
`Queueing dim transition for ${deviceName} (${deviceId}) to ${newBrightness}. Total queue length ${this.writeQueue.length}`,
|
||||||
|
);
|
||||||
this._setBrightness(deviceId, newBrightness, false, deviceName);
|
this._setBrightness(deviceId, newBrightness, false, deviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, transitionInterval);
|
}, transitionInterval);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
if (transition && isDimmable) {
|
if (transition && isDimmable) {
|
||||||
dbgLogger(`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`)
|
logger.debug(
|
||||||
|
`Could not transition light change. Either initial value is unknown or change is too small. Requested from ${initialBrightness} to ${targetBrightness}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this._setBrightness(deviceId, targetBrightness, true, deviceName);
|
this._setBrightness(deviceId, targetBrightness, true, deviceName);
|
||||||
}
|
}
|
||||||
|
|
@ -325,49 +352,61 @@ class PlejdService extends EventEmitter {
|
||||||
let log = '';
|
let log = '';
|
||||||
|
|
||||||
if (!brightness && brightness !== 0) {
|
if (!brightness && brightness !== 0) {
|
||||||
dbgLogger(`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`);
|
logger.debug(
|
||||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009701', 'hex');
|
`Queueing turn on ${deviceName} (${deviceId}). No brightness specified, setting DIM to previous.`,
|
||||||
|
);
|
||||||
|
payload = Buffer.from(`${deviceId.toString(16).padStart(2, '0')}0110009701`, 'hex');
|
||||||
log = 'ON';
|
log = 'ON';
|
||||||
}
|
} else if (brightness <= 0) {
|
||||||
else {
|
logger.debug(`Queueing turn off ${deviceId}`);
|
||||||
if (brightness <= 0) {
|
payload = Buffer.from(`${deviceId.toString(16).padStart(2, '0')}0110009700`, 'hex');
|
||||||
dbgLogger(`Queueing turn off ${deviceId}`);
|
log = 'OFF';
|
||||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009700', 'hex');
|
} else {
|
||||||
log = 'OFF';
|
if (brightness > 255) {
|
||||||
}
|
// eslint-disable-next-line no-param-reassign
|
||||||
else {
|
brightness = 255;
|
||||||
if (brightness > 255) {
|
|
||||||
brightness = 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbgLogger(`Queueing ${deviceId} set brightness to ${brightness}`);
|
|
||||||
const brightnessVal = (brightness << 8) | brightness;
|
|
||||||
payload = Buffer.from((deviceId).toString(16).padStart(2, '0') + '0110009801' + (brightnessVal).toString(16).padStart(4, '0'), 'hex');
|
|
||||||
log = `DIM ${brightness}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.debug(`Queueing ${deviceId} set brightness to ${brightness}`);
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
const brightnessVal = (brightness << 8) | brightness;
|
||||||
|
payload = Buffer.from(
|
||||||
|
`${deviceId.toString(16).padStart(2, '0')}0110009801${brightnessVal
|
||||||
|
.toString(16)
|
||||||
|
.padStart(4, '0')}`,
|
||||||
|
'hex',
|
||||||
|
);
|
||||||
|
log = `DIM ${brightness}`;
|
||||||
}
|
}
|
||||||
this.writeQueue.unshift({deviceId, log, shouldRetry, payload});
|
this.writeQueue.unshift({
|
||||||
|
deviceId,
|
||||||
|
log,
|
||||||
|
shouldRetry,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerScene(sceneIndex) {
|
triggerScene(sceneIndex) {
|
||||||
const sceneName = this._getDeviceName(sceneIndex);
|
const sceneName = this._getDeviceName(sceneIndex);
|
||||||
infLogger(`Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`);
|
logger.info(
|
||||||
|
`Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`,
|
||||||
|
);
|
||||||
this.sceneManager.executeScene(sceneIndex, this);
|
this.sceneManager.executeScene(sceneIndex, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authenticate() {
|
async authenticate() {
|
||||||
infLogger('authenticate()');
|
logger.info('authenticate()');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dbgLogger('Sending challenge to device');
|
logger.debug('Sending challenge to device');
|
||||||
await this.characteristics.auth.WriteValue([0], {});
|
await this.characteristics.auth.WriteValue([0], {});
|
||||||
dbgLogger('Reading response from device');
|
logger.debug('Reading response from device');
|
||||||
const challenge = await this.characteristics.auth.ReadValue({});
|
const challenge = await this.characteristics.auth.ReadValue({});
|
||||||
const response = this._createChallengeResponse(this.cryptoKey, Buffer.from(challenge));
|
const response = this._createChallengeResponse(this.cryptoKey, Buffer.from(challenge));
|
||||||
dbgLogger('Responding to authenticate');
|
logger.debug('Responding to authenticate');
|
||||||
await this.characteristics.auth.WriteValue([...response], {});
|
await this.characteristics.auth.WriteValue([...response], {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger('Failed to authenticate: ', err);
|
logger.error('Failed to authenticate: ', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// auth done, start ping
|
// auth done, start ping
|
||||||
|
|
@ -376,39 +415,47 @@ class PlejdService extends EventEmitter {
|
||||||
|
|
||||||
// After we've authenticated, we need to hook up the event listener
|
// After we've authenticated, we need to hook up the event listener
|
||||||
// for changes to lastData.
|
// for changes to lastData.
|
||||||
this.characteristics.lastDataProperties.on('PropertiesChanged', this.onLastDataUpdated.bind(this));
|
this.characteristics.lastDataProperties.on(
|
||||||
|
'PropertiesChanged',
|
||||||
|
this.onLastDataUpdated.bind(this),
|
||||||
|
);
|
||||||
this.characteristics.lastData.StartNotify();
|
this.characteristics.lastData.StartNotify();
|
||||||
}
|
}
|
||||||
|
|
||||||
async throttledInit(delay) {
|
async throttledInit(delay) {
|
||||||
if(this.initInProgress){
|
if (this.initInProgress) {
|
||||||
dbgLogger('ThrottledInit already in progress. Skipping this call and returning existing promise.')
|
logger.debug(
|
||||||
|
'ThrottledInit already in progress. Skipping this call and returning existing promise.',
|
||||||
|
);
|
||||||
return this.initInProgress;
|
return this.initInProgress;
|
||||||
}
|
}
|
||||||
this.initInProgress = new Promise((resolve) => setTimeout(async () => {
|
this.initInProgress = new Promise((resolve) => setTimeout(async () => {
|
||||||
const result = await this.init().catch((err) => { errLogger('TrottledInit exception calling init(). Will re-throw.', err); throw err; });
|
const result = await this.init().catch((err) => {
|
||||||
this.initInProgress = null;
|
logger.error('TrottledInit exception calling init(). Will re-throw.', err);
|
||||||
resolve(result)
|
throw err;
|
||||||
}, delay))
|
});
|
||||||
return this.initInProgress;
|
this.initInProgress = null;
|
||||||
|
resolve(result);
|
||||||
|
}, delay));
|
||||||
|
return this.initInProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
async write(data) {
|
async write(data) {
|
||||||
if (!data || !this.plejdService || !this.characteristics.data) {
|
if (!data || !this.plejdService || !this.characteristics.data) {
|
||||||
dbgLogger('data, plejdService or characteristics not available. Cannot write()');
|
logger.debug('data, plejdService or characteristics not available. Cannot write()');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
vrbLogger(`Sending ${data.length} byte(s) of data to Plejd`, data);
|
logger.verbose(`Sending ${data.length} byte(s) of data to Plejd. ${data.toString('hex')}`);
|
||||||
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
|
const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data);
|
||||||
await this.characteristics.data.WriteValue([...encryptedData], {});
|
await this.characteristics.data.WriteValue([...encryptedData], {});
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message === 'In Progress') {
|
if (err.message === 'In Progress') {
|
||||||
dbgLogger('Write failed due to \'In progress\' ', err);
|
logger.debug("Write failed due to 'In progress' ", err);
|
||||||
} else {
|
} else {
|
||||||
dbgLogger('Write failed ', err);
|
logger.debug('Write failed ', err);
|
||||||
}
|
}
|
||||||
await this.throttledInit(this.connectionTimeout * 1000);
|
await this.throttledInit(this.connectionTimeout * 1000);
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -416,45 +463,49 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
startPing() {
|
startPing() {
|
||||||
infLogger('startPing()');
|
logger.info('startPing()');
|
||||||
clearInterval(this.pingRef);
|
clearInterval(this.pingRef);
|
||||||
|
|
||||||
this.pingRef = setInterval(async () => {
|
this.pingRef = setInterval(async () => {
|
||||||
vrbLogger('ping');
|
logger.silly('ping');
|
||||||
await this.ping();
|
await this.ping();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
onPingSuccess(nr) {
|
onPingSuccess(nr) {
|
||||||
vrbLogger('pong: ' + nr);
|
logger.silly(`pong: ${nr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async onPingFailed(error) {
|
async onPingFailed(error) {
|
||||||
dbgLogger('onPingFailed(' + error + ')');
|
logger.debug(`onPingFailed(${error})`);
|
||||||
infLogger('ping failed, reconnecting.');
|
logger.info('ping failed, reconnecting.');
|
||||||
|
|
||||||
clearInterval(this.pingRef);
|
clearInterval(this.pingRef);
|
||||||
await this.init();
|
return this.init().catch((err) => {
|
||||||
|
logger.error('onPingFailed exception calling init(). Will swallow error.', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async ping() {
|
async ping() {
|
||||||
vrbLogger('ping()');
|
logger.silly('ping()');
|
||||||
|
|
||||||
var ping = crypto.randomBytes(1);
|
const ping = crypto.randomBytes(1);
|
||||||
let pong = null;
|
let pong = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.characteristics.ping.WriteValue([...ping], {});
|
await this.characteristics.ping.WriteValue([...ping], {});
|
||||||
pong = await this.characteristics.ping.ReadValue({});
|
pong = await this.characteristics.ping.ReadValue({});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errLogger('writing to plejd: ', err);
|
logger.error('Error writing to plejd: ', err);
|
||||||
this.emit('pingFailed', 'write error');
|
this.emit('pingFailed', 'write error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
if (((ping[0] + 1) & 0xff) !== pong[0]) {
|
if (((ping[0] + 1) & 0xff) !== pong[0]) {
|
||||||
errLogger('plejd ping failed');
|
logger.error('Plejd ping failed');
|
||||||
this.emit('pingFailed', 'plejd ping failed ' + ping[0] + ' - ' + pong[0]);
|
this.emit('pingFailed', `plejd ping failed ${ping[0]} - ${pong[0]}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -462,7 +513,7 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
startWriteQueue() {
|
startWriteQueue() {
|
||||||
infLogger('startWriteQueue()');
|
logger.info('startWriteQueue()');
|
||||||
clearTimeout(this.writeQueueRef);
|
clearTimeout(this.writeQueueRef);
|
||||||
|
|
||||||
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
|
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
|
||||||
|
|
@ -473,31 +524,39 @@ class PlejdService extends EventEmitter {
|
||||||
while (this.writeQueue.length > 0) {
|
while (this.writeQueue.length > 0) {
|
||||||
const queueItem = this.writeQueue.pop();
|
const queueItem = this.writeQueue.pop();
|
||||||
const deviceName = this._getDeviceName(queueItem.deviceId);
|
const deviceName = this._getDeviceName(queueItem.deviceId);
|
||||||
dbgLogger(`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`);
|
logger.debug(
|
||||||
|
`Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
|
if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) {
|
||||||
vrbLogger(`Skipping ${deviceName} (${queueItem.deviceId}) ${queueItem.log} due to more recent command in queue.`);
|
logger.verbose(
|
||||||
continue; // Skip commands if new ones exist for the same deviceId, but still process all messages in order
|
`Skipping ${deviceName} (${queueItem.deviceId}) `
|
||||||
}
|
+ `${queueItem.log} due to more recent command in queue.`,
|
||||||
|
);
|
||||||
const success = await this.write(queueItem.payload);
|
// Skip commands if new ones exist for the same deviceId
|
||||||
if (!success && queueItem.shouldRetry) {
|
// still process all messages in order
|
||||||
queueItem.retryCount = (queueItem.retryCount || 0) + 1;
|
} else {
|
||||||
dbgLogger('Will retry command, count failed so far', queueItem.retryCount);
|
// eslint-disable-next-line no-await-in-loop
|
||||||
if (queueItem.retryCount <= MAX_RETRY_COUNT) {
|
const success = await this.write(queueItem.payload);
|
||||||
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
|
if (!success && queueItem.shouldRetry) {
|
||||||
}
|
queueItem.retryCount = (queueItem.retryCount || 0) + 1;
|
||||||
else {
|
logger.debug(`Will retry command, count failed so far ${queueItem.retryCount}`);
|
||||||
errLogger(`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log} failed.`);
|
if (queueItem.retryCount <= MAX_RETRY_COUNT) {
|
||||||
break;
|
this.writeQueue.push(queueItem); // Add back to top of queue to be processed next;
|
||||||
}
|
} else {
|
||||||
if (queueItem.retryCount > 1) {
|
logger.error(
|
||||||
break; // First retry directly, consecutive after writeQueueWaitTime ms
|
`Write queue: Exceeed max retry count (${MAX_RETRY_COUNT}) for ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log} failed.`,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (queueItem.retryCount > 1) {
|
||||||
|
break; // First retry directly, consecutive after writeQueueWaitTime ms
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errLogger('Error in writeQueue loop, values probably not written to Plejd', e);
|
logger.error('Error in writeQueue loop, values probably not written to Plejd', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
|
this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime);
|
||||||
|
|
@ -505,12 +564,11 @@ class PlejdService extends EventEmitter {
|
||||||
|
|
||||||
async _processPlejdService(path, characteristics) {
|
async _processPlejdService(path, characteristics) {
|
||||||
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
|
||||||
const service = await proxyObject.getInterface(GATT_SERVICE_ID);
|
|
||||||
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||||
|
|
||||||
const uuid = (await properties.Get(GATT_SERVICE_ID, 'UUID')).value;
|
const uuid = (await properties.Get(GATT_SERVICE_ID, 'UUID')).value;
|
||||||
if (uuid !== PLEJD_SERVICE) {
|
if (uuid !== PLEJD_SERVICE) {
|
||||||
errLogger('not a Plejd device.');
|
logger.error('not a Plejd device.');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -519,14 +577,14 @@ class PlejdService extends EventEmitter {
|
||||||
const dirtyAddr = regex.exec(dev);
|
const dirtyAddr = regex.exec(dev);
|
||||||
const addr = this._reverseBuffer(
|
const addr = this._reverseBuffer(
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
String(dirtyAddr[1])
|
String(dirtyAddr[1]).replace(/-/g, '').replace(/_/g, '').replace(/:/g, ''),
|
||||||
.replace(/\-/g, '')
|
'hex',
|
||||||
.replace(/\_/g, '')
|
),
|
||||||
.replace(/\:/g, ''), 'hex'
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const chPath of characteristics) {
|
for (const chPath of characteristics) {
|
||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
const chProxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, chPath);
|
const chProxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, chPath);
|
||||||
const ch = await chProxyObject.getInterface(GATT_CHRC_ID);
|
const ch = await chProxyObject.getInterface(GATT_CHRC_ID);
|
||||||
const prop = await chProxyObject.getInterface(DBUS_PROP_INTERFACE);
|
const prop = await chProxyObject.getInterface(DBUS_PROP_INTERFACE);
|
||||||
|
|
@ -534,37 +592,39 @@ class PlejdService extends EventEmitter {
|
||||||
const chUuid = (await prop.Get(GATT_CHRC_ID, 'UUID')).value;
|
const chUuid = (await prop.Get(GATT_CHRC_ID, 'UUID')).value;
|
||||||
|
|
||||||
if (chUuid === DATA_UUID) {
|
if (chUuid === DATA_UUID) {
|
||||||
dbgLogger('found DATA characteristic.');
|
logger.debug('found DATA characteristic.');
|
||||||
this.characteristics.data = ch;
|
this.characteristics.data = ch;
|
||||||
} else if (chUuid === LAST_DATA_UUID) {
|
} else if (chUuid === LAST_DATA_UUID) {
|
||||||
dbgLogger('found LAST_DATA characteristic.');
|
logger.debug('found LAST_DATA characteristic.');
|
||||||
this.characteristics.lastData = ch;
|
this.characteristics.lastData = ch;
|
||||||
this.characteristics.lastDataProperties = prop;
|
this.characteristics.lastDataProperties = prop;
|
||||||
} else if (chUuid === AUTH_UUID) {
|
} else if (chUuid === AUTH_UUID) {
|
||||||
dbgLogger('found AUTH characteristic.');
|
logger.debug('found AUTH characteristic.');
|
||||||
this.characteristics.auth = ch;
|
this.characteristics.auth = ch;
|
||||||
} else if (chUuid === PING_UUID) {
|
} else if (chUuid === PING_UUID) {
|
||||||
dbgLogger('found PING characteristic.');
|
logger.debug('found PING characteristic.');
|
||||||
this.characteristics.ping = ch;
|
this.characteristics.ping = ch;
|
||||||
}
|
}
|
||||||
|
/* eslint-eslint no-await-in-loop */
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addr: addr
|
addr,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async onDeviceConnected(device) {
|
async onDeviceConnected(device) {
|
||||||
infLogger('onDeviceConnected()');
|
logger.info('onDeviceConnected()');
|
||||||
dbgLogger('Device: ', device);
|
logger.debug(`Device: ${device}`);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
errLogger('Device is null. Should we break/return when this happens?');
|
logger.error('Device is null. Should we break/return when this happens?');
|
||||||
}
|
}
|
||||||
|
|
||||||
const objects = await this.objectManager.GetManagedObjects();
|
const objects = await this.objectManager.GetManagedObjects();
|
||||||
const paths = Object.keys(objects);
|
const paths = Object.keys(objects);
|
||||||
let characteristics = [];
|
const characteristics = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
const interfaces = Object.keys(objects[path]);
|
const interfaces = Object.keys(objects[path]);
|
||||||
if (interfaces.indexOf(GATT_CHRC_ID) > -1) {
|
if (interfaces.indexOf(GATT_CHRC_ID) > -1) {
|
||||||
|
|
@ -572,17 +632,19 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
const interfaces = Object.keys(objects[path]);
|
const interfaces = Object.keys(objects[path]);
|
||||||
if (interfaces.indexOf(GATT_SERVICE_ID) > -1) {
|
if (interfaces.indexOf(GATT_SERVICE_ID) > -1) {
|
||||||
let chPaths = [];
|
const chPaths = [];
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const c of characteristics) {
|
for (const c of characteristics) {
|
||||||
if (c.startsWith(path + '/')) {
|
if (c.startsWith(`${path}/`)) {
|
||||||
chPaths.push(c);
|
chPaths.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infLogger('trying ' + chPaths.length + ' characteristics');
|
logger.info(`trying ${chPaths.length} characteristics`);
|
||||||
|
|
||||||
this.plejdService = await this._processPlejdService(path, chPaths);
|
this.plejdService = await this._processPlejdService(path, chPaths);
|
||||||
if (this.plejdService) {
|
if (this.plejdService) {
|
||||||
|
|
@ -592,21 +654,22 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.plejdService) {
|
if (!this.plejdService) {
|
||||||
infLogger('warning: wasn\'t able to connect to Plejd, will retry.');
|
logger.info("warning: wasn't able to connect to Plejd, will retry.");
|
||||||
this.emit('connectFailed');
|
this.emit('connectFailed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.characteristics.auth) {
|
if (!this.characteristics.auth) {
|
||||||
errLogger('unable to enumerate characteristics.');
|
logger.error('unable to enumerate characteristics.');
|
||||||
this.emit('connectFailed');
|
this.emit('connectFailed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connectedDevice = device['device'];
|
this.connectedDevice = device.device;
|
||||||
await this.authenticate();
|
await this.authenticate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
async onLastDataUpdated(iface, properties, invalidated) {
|
async onLastDataUpdated(iface, properties, invalidated) {
|
||||||
if (iface !== GATT_CHRC_ID) {
|
if (iface !== GATT_CHRC_ID) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -617,7 +680,7 @@ class PlejdService extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = await properties['Value'];
|
const value = await properties.Value;
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -629,67 +692,71 @@ class PlejdService extends EventEmitter {
|
||||||
// What is bytes 2-3?
|
// What is bytes 2-3?
|
||||||
const cmd = decoded.toString('hex', 3, 5);
|
const cmd = decoded.toString('hex', 3, 5);
|
||||||
const state = parseInt(decoded.toString('hex', 5, 6), 10); // Overflows for command 0x001b, scene command
|
const state = parseInt(decoded.toString('hex', 5, 6), 10); // Overflows for command 0x001b, scene command
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
const data2 = parseInt(decoded.toString('hex', 6, 8), 16) >> 8;
|
const data2 = parseInt(decoded.toString('hex', 6, 8), 16) >> 8;
|
||||||
|
|
||||||
if (decoded.length < 5) {
|
if (decoded.length < 5) {
|
||||||
dbgLogger('Too short raw event ignored: ', decoded.toString('hex'));
|
logger.debug(`Too short raw event ignored: ${decoded.toString('hex')}`);
|
||||||
// ignore the notification since too small
|
// ignore the notification since too small
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceName = (logVerbose || logDebug) ? this._getDeviceName(deviceId) : '';
|
const deviceName = this._getDeviceName(deviceId);
|
||||||
vrbLogger('Raw event received: ', decoded.toString('hex'));
|
logger.verbose(`Raw event received: ${decoded.toString('hex')}`);
|
||||||
vrbLogger(`Device ${deviceId}, cmd ${cmd.toString('hex')}, state ${state}, dim/data2 ${data2}`);
|
logger.verbose(
|
||||||
|
`Device ${deviceId}, cmd ${cmd.toString('hex')}, state ${state}, dim/data2 ${data2}`,
|
||||||
|
);
|
||||||
|
|
||||||
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) {
|
||||||
const dim = data2;
|
const dim = data2;
|
||||||
|
|
||||||
dbgLogger(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`);
|
logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`);
|
||||||
|
|
||||||
this.emit('stateChanged', deviceId, {
|
this.emit('stateChanged', deviceId, {
|
||||||
state: state,
|
state,
|
||||||
brightness: dim
|
brightness: dim,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.plejdDevices[deviceId] = {
|
this.plejdDevices[deviceId] = {
|
||||||
state: state,
|
state,
|
||||||
dim: dim
|
dim,
|
||||||
};
|
};
|
||||||
vrbLogger('All states: ', this.plejdDevices);
|
logger.verbose(`All states: ${JSON.stringify(this.plejdDevices)}`);
|
||||||
} else if (cmd === BLE_CMD_STATE_CHANGE) {
|
} else if (cmd === BLE_CMD_STATE_CHANGE) {
|
||||||
dbgLogger(`${deviceName} (${deviceId}) got state update. S: ${state}`);
|
logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`);
|
||||||
this.emit('stateChanged', deviceId, {
|
this.emit('stateChanged', deviceId, {
|
||||||
state: state
|
state,
|
||||||
});
|
});
|
||||||
this.plejdDevices[deviceId] = {
|
this.plejdDevices[deviceId] = {
|
||||||
state: state,
|
state,
|
||||||
dim: 0
|
dim: 0,
|
||||||
};
|
};
|
||||||
vrbLogger('All states: ', this.plejdDevices);
|
logger.verbose(`All states: ${this.plejdDevices}`);
|
||||||
} else if (cmd === BLE_CMD_SCENE_TRIG) {
|
} else if (cmd === BLE_CMD_SCENE_TRIG) {
|
||||||
const sceneId = parseInt(decoded.toString('hex', 5, 6), 16);
|
const sceneId = parseInt(decoded.toString('hex', 5, 6), 16);
|
||||||
const sceneName = this._getDeviceName(sceneId);
|
const sceneName = this._getDeviceName(sceneId);
|
||||||
|
|
||||||
dbgLogger(`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`);
|
logger.debug(
|
||||||
|
`${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`,
|
||||||
|
);
|
||||||
|
|
||||||
this.emit('sceneTriggered', deviceId, sceneId);
|
this.emit('sceneTriggered', deviceId, sceneId);
|
||||||
}
|
} else if (cmd === '001b') {
|
||||||
else if (cmd === '001b') {
|
logger.silly('Command 001b seems to be some kind of often repeating ping/mesh data');
|
||||||
// vrbLogger('Command 001b seems to be some kind of often repeating ping/mesh data');
|
} else {
|
||||||
}
|
logger.verbose(`Command ${cmd.toString('hex')} unknown. Device ${deviceName} (${deviceId})`);
|
||||||
else {
|
|
||||||
vrbLogger(`Command ${cmd.toString('hex')} unknown. Device ${deviceName} (${deviceId})`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wireEvents() {
|
wireEvents() {
|
||||||
infLogger('wireEvents()');
|
logger.info('wireEvents()');
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
this.on('pingFailed', this.onPingFailed.bind(self));
|
this.on('pingFailed', this.onPingFailed.bind(self));
|
||||||
this.on('pingSuccess', this.onPingSuccess.bind(self));
|
this.on('pingSuccess', this.onPingSuccess.bind(self));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_createChallengeResponse(key, challenge) {
|
_createChallengeResponse(key, challenge) {
|
||||||
const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest();
|
const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest();
|
||||||
const part1 = intermediate.subarray(0, 16);
|
const part1 = intermediate.subarray(0, 16);
|
||||||
|
|
@ -700,18 +767,20 @@ class PlejdService extends EventEmitter {
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_encryptDecrypt(key, addr, data) {
|
_encryptDecrypt(key, addr, data) {
|
||||||
var buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
const buf = Buffer.concat([addr, addr, addr.subarray(0, 4)]);
|
||||||
|
|
||||||
var cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
const cipher = crypto.createCipheriv('aes-128-ecb', key, '');
|
||||||
cipher.setAutoPadding(false);
|
cipher.setAutoPadding(false);
|
||||||
|
|
||||||
var ct = cipher.update(buf).toString('hex');
|
let ct = cipher.update(buf).toString('hex');
|
||||||
ct += cipher.final().toString('hex');
|
ct += cipher.final().toString('hex');
|
||||||
ct = Buffer.from(ct, 'hex');
|
ct = Buffer.from(ct, 'hex');
|
||||||
|
|
||||||
var output = '';
|
let output = '';
|
||||||
for (var i = 0, length = data.length; i < length; i++) {
|
for (let i = 0, { length } = data; i < length; i++) {
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
output += String.fromCharCode(data[i] ^ ct[i % 16]);
|
output += String.fromCharCode(data[i] ^ ct[i % 16]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -719,18 +788,19 @@ class PlejdService extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
_getDeviceName(deviceId) {
|
_getDeviceName(deviceId) {
|
||||||
return (this.devices.find(d => d.id === deviceId) || {}).name;
|
return (this.devices.find((d) => d.id === deviceId) || {}).name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
_reverseBuffer(src) {
|
_reverseBuffer(src) {
|
||||||
var buffer = Buffer.allocUnsafe(src.length)
|
const buffer = Buffer.allocUnsafe(src.length);
|
||||||
|
|
||||||
for (var i = 0, j = src.length - 1; i <= j; ++i, --j) {
|
for (let i = 0, j = src.length - 1; i <= j; ++i, --j) {
|
||||||
buffer[i] = src[j]
|
buffer[i] = src[j];
|
||||||
buffer[j] = src[i]
|
buffer[j] = src[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
167
plejd/README.md
167
plejd/README.md
|
|
@ -1,4 +1,5 @@
|
||||||
# Hass.io Plejd add-on
|
# Hass.io Plejd add-on
|
||||||
|
|
||||||
Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant.
|
Hass.io add-on for Plejd home automation devices. Gives you the ability to control the Plejd home automation devices through Home Assistant.
|
||||||
It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range.
|
It uses MQTT to communicate with Home Assistant and supports auto discovery of the devices in range.
|
||||||
|
|
||||||
|
|
@ -15,54 +16,68 @@ I am in no way affiliated with Plejd and am solely doing this as a hobby project
|
||||||
[](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://gitter.im/hassio-plejd/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To get started, make sure that the following requirements are met:
|
To get started, make sure that the following requirements are met:
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
* A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
|
||||||
* An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
- A Bluetooth device (BLE), for eg. the built-in device in Raspberry Pi 4.
|
||||||
|
- An MQTT broker (the Mosquitto Hass.io add-on works perfectly well).
|
||||||
|
|
||||||
### Tested on
|
### Tested on
|
||||||
|
|
||||||
The add-on has been tested on the following platforms:
|
The add-on has been tested on the following platforms:
|
||||||
* Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
|
||||||
* Raspberry Pi 4 with Hass.io
|
- Mac OS Catalina 10.15.1 with Node v. 13.2.0
|
||||||
|
- Raspberry Pi 4 with Hass.io
|
||||||
|
|
||||||
#### Tested Plejd devices
|
#### Tested Plejd devices
|
||||||
* DIM-01
|
|
||||||
* DIM-02
|
- DIM-01
|
||||||
* LED-10
|
- DIM-02
|
||||||
* CTR-01
|
- LED-10
|
||||||
* REL-01
|
- CTR-01
|
||||||
* REL-02
|
- REL-01
|
||||||
|
- REL-02
|
||||||
|
|
||||||
### Easy Installation
|
### Easy Installation
|
||||||
|
|
||||||
Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left.
|
Browse to your Home Assistant installation in a web browser and click on `Hass.io` in the navigation bar to the left.
|
||||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
|
||||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||||
* Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||||
* Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
- Paste the URL to this repo https://github.com/icanos/hassio-plejd.git in the `Add new repository by URL` field and hit `Add`.
|
||||||
* Enjoy!
|
- Scroll down and you should find a Plejd add-on that can be installed. Open that and install.
|
||||||
|
- Enjoy!
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc.
|
Browse your Hass.io installation using a tool that allows you to manage files, for eg. SMB or an SFTP client etc.
|
||||||
* Open the `/addon` directory
|
|
||||||
* Create a new folder named `hassio-plejd`
|
- Open the `/addon` directory
|
||||||
* Copy all files from this repository into that newly created one.
|
- Create a new folder named `hassio-plejd`
|
||||||
* Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
- Copy all files from this repository into that newly created one.
|
||||||
* Click on `Add-on Store` in the top navigation bar of that page.
|
- Open the Home Assistant web console and click `Hass.io` in the menu on the left side.
|
||||||
* Click on the refresh button in the upper right corner.
|
- Click on `Add-on Store` in the top navigation bar of that page.
|
||||||
* A new Local Add-on should appear named Plejd. Open that and install.
|
- Click on the refresh button in the upper right corner.
|
||||||
* Enjoy!
|
- A new Local Add-on should appear named Plejd. Open that and install.
|
||||||
|
- Enjoy!
|
||||||
|
|
||||||
### NOTE
|
### NOTE
|
||||||
|
|
||||||
When starting the add-on, the log displays this message:
|
When starting the add-on, the log displays this message:
|
||||||
|
|
||||||
```
|
```
|
||||||
parse error: Expected string key before ':' at line 1, column 4
|
parse error: Expected string key before ':' at line 1, column 4
|
||||||
[08:56:24] ERROR: Unknown HTTP error occured
|
[08:56:24] ERROR: Unknown HTTP error occured
|
||||||
```
|
```
|
||||||
|
|
||||||
However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though.
|
However, the add-on still works as expected and this is something I'm looking into, but not with that much effort yet though.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
You need to add the following to your `configuration.yaml` file:
|
You need to add the following to your `configuration.yaml` file:
|
||||||
|
|
||||||
```
|
```
|
||||||
mqtt:
|
mqtt:
|
||||||
broker: [point to your broker IP eg. 'mqtt://localhost']
|
broker: [point to your broker IP eg. 'mqtt://localhost']
|
||||||
|
|
@ -77,77 +92,95 @@ mqtt:
|
||||||
topic: 'hass/status'
|
topic: 'hass/status'
|
||||||
payload: 'offline'
|
payload: 'offline'
|
||||||
```
|
```
|
||||||
|
|
||||||
The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing all devices).
|
The above is used to notify the add-on when Home Assistant has started successfully and let the add-on send the discovery response (containing all devices).
|
||||||
|
|
||||||
The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it.
|
The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it.
|
||||||
|
|
||||||
Parameter | Value
|
| Parameter | Value |
|
||||||
--- | ---
|
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
site | Name of your Plejd site, the name is displayed in the Plejd app (top bar).
|
| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). |
|
||||||
username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||||
password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API.
|
| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. |
|
||||||
mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost
|
| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost |
|
||||||
mqttUsername | Username of the MQTT broker
|
| mqttUsername | Username of the MQTT broker |
|
||||||
mqttPassword | Password of the MQTT broker
|
| mqttPassword | Password of the MQTT broker |
|
||||||
includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. *Added in v. 5*.
|
| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. _Added in v. 5_. |
|
||||||
connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds.
|
| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. |
|
||||||
|
|
||||||
## I want voice control!
|
## I want voice control!
|
||||||
|
|
||||||
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
With the Google Home integration in Home Assistant, you can get voice control for your Plejd lights right away, check this out for more information:
|
||||||
https://www.home-assistant.io/integrations/google_assistant/
|
https://www.home-assistant.io/integrations/google_assistant/
|
||||||
|
|
||||||
### I don't want voice, I want HomeKit!
|
### I don't want voice, I want HomeKit!
|
||||||
|
|
||||||
Check this out for more information on how you can get your Plejd lights controlled using HomeKit:
|
Check this out for more information on how you can get your Plejd lights controlled using HomeKit:
|
||||||
https://www.home-assistant.io/integrations/homekit/
|
https://www.home-assistant.io/integrations/homekit/
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
*v 0.3.4*:
|
|
||||||
* NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
|
||||||
* FIX: Reworked some logging to get better understanding of what happens.
|
|
||||||
|
|
||||||
*v 0.3.0*:
|
_v 0.3.4_:
|
||||||
* NEW: New BLE manager, DBus instead of noble
|
|
||||||
* FIX: Adding entities as devices now as well
|
|
||||||
* FIX: Bug fixes
|
|
||||||
|
|
||||||
*v 0.2.8*:
|
- NEW: `connectionTimeout` configuration parameter to enable tweaking of wait time on connection, usable for RPi 3B+.
|
||||||
* FIX: Reset characteristic state on disconnect
|
- FIX: Reworked some logging to get better understanding of what happens.
|
||||||
|
|
||||||
*v 0.2.7*:
|
_v 0.3.0_:
|
||||||
* FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
|
||||||
|
|
||||||
*v 0.2.6*:
|
- NEW: New BLE manager, DBus instead of noble
|
||||||
* FIX: Added null check to remove listeners for characteristics
|
- FIX: Adding entities as devices now as well
|
||||||
|
- FIX: Bug fixes
|
||||||
|
|
||||||
*v 0.2.5*:
|
_v 0.2.8_:
|
||||||
* FIX: Invalid scene id in events/scene message
|
|
||||||
|
|
||||||
*v 0.2.4*:
|
- FIX: Reset characteristic state on disconnect
|
||||||
* Stability improvements
|
|
||||||
|
|
||||||
*v 0.2.3*:
|
_v 0.2.7_:
|
||||||
* FIX: Container build error fix
|
|
||||||
|
|
||||||
*v 0.2.2*:
|
- FIX: Added exception handling to unsubscribing lastData characteristic if already disconnected
|
||||||
* Stability improvements
|
|
||||||
|
|
||||||
*v 0.2.1*:
|
_v 0.2.6_:
|
||||||
* Stability improvements
|
|
||||||
|
|
||||||
*v 0.2.0*:
|
- FIX: Added null check to remove listeners for characteristics
|
||||||
* Stability improvements
|
|
||||||
* Bugfixes
|
|
||||||
|
|
||||||
*v 0.1.1*:
|
_v 0.2.5_:
|
||||||
* FIX: Fixed missing reference on startup, preventing add-on from starting
|
|
||||||
|
|
||||||
*v 0.1.0*:
|
- FIX: Invalid scene id in events/scene message
|
||||||
* NEW: Rewrote the BLE integration for more stability
|
|
||||||
* FIX: discovery wasn't always sent
|
|
||||||
|
|
||||||
*previous*:
|
_v 0.2.4_:
|
||||||
* FIX: bug preventing add-on from building
|
|
||||||
* NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.3_:
|
||||||
|
|
||||||
|
- FIX: Container build error fix
|
||||||
|
|
||||||
|
_v 0.2.2_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.1_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
|
||||||
|
_v 0.2.0_:
|
||||||
|
|
||||||
|
- Stability improvements
|
||||||
|
- Bugfixes
|
||||||
|
|
||||||
|
_v 0.1.1_:
|
||||||
|
|
||||||
|
- FIX: Fixed missing reference on startup, preventing add-on from starting
|
||||||
|
|
||||||
|
_v 0.1.0_:
|
||||||
|
|
||||||
|
- NEW: Rewrote the BLE integration for more stability
|
||||||
|
- FIX: discovery wasn't always sent
|
||||||
|
|
||||||
|
_previous_:
|
||||||
|
|
||||||
|
- FIX: bug preventing add-on from building
|
||||||
|
- NEW: Added support for Plejd devices with multiple outputs (such as DIM-02)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
19
plejd/Scene.js
Normal file
19
plejd/Scene.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
const SceneStep = require('./SceneStep');
|
||||||
|
|
||||||
|
class Scene {
|
||||||
|
constructor(idx, scene, steps) {
|
||||||
|
this.id = idx;
|
||||||
|
this.title = scene.title;
|
||||||
|
this.sceneId = scene.sceneId;
|
||||||
|
|
||||||
|
const sceneSteps = steps.filter((x) => x.sceneId === scene.sceneId);
|
||||||
|
this.steps = [];
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const step of sceneSteps) {
|
||||||
|
this.steps.push(new SceneStep(step));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Scene;
|
||||||
48
plejd/SceneManager.js
Normal file
48
plejd/SceneManager.js
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
const EventEmitter = require('events');
|
||||||
|
const Scene = require('./Scene');
|
||||||
|
|
||||||
|
class SceneManager extends EventEmitter {
|
||||||
|
constructor(site, devices) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.site = site;
|
||||||
|
this.scenes = [];
|
||||||
|
this.devices = devices;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false);
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const scene of scenes) {
|
||||||
|
const idx = this.site.sceneIndex[scene.sceneId];
|
||||||
|
this.scenes.push(new Scene(idx, scene, this.site.sceneSteps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
executeScene(sceneIndex, ble) {
|
||||||
|
const scene = this.scenes.find((x) => x.id === sceneIndex);
|
||||||
|
if (!scene) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const step of scene.steps) {
|
||||||
|
const device = this.devices.find((x) => x.serialNumber === step.deviceId);
|
||||||
|
if (device) {
|
||||||
|
if (device.dimmable && step.state) {
|
||||||
|
ble.turnOn(device.id, { brightness: step.brightness });
|
||||||
|
} else if (!device.dimmable && step.state) {
|
||||||
|
ble.turnOn(device.id, {});
|
||||||
|
} else if (!step.state) {
|
||||||
|
ble.turnOff(device.id, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SceneManager;
|
||||||
|
/* eslint-disable */
|
||||||
10
plejd/SceneStep.js
Normal file
10
plejd/SceneStep.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
class SceneStep {
|
||||||
|
constructor(step) {
|
||||||
|
this.sceneId = step.sceneId;
|
||||||
|
this.deviceId = step.deviceId;
|
||||||
|
this.state = step.state === 'On' ? 1 : 0;
|
||||||
|
this.brightness = step.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SceneStep;
|
||||||
341
plejd/api.js
341
plejd/api.js
|
|
@ -1,341 +0,0 @@
|
||||||
const axios = require('axios');
|
|
||||||
const EventEmitter = require('events');
|
|
||||||
|
|
||||||
API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak';
|
|
||||||
API_BASE_URL = 'https://cloud.plejd.com/parse/';
|
|
||||||
API_LOGIN_URL = 'login';
|
|
||||||
API_SITE_LIST_URL = 'functions/getSiteList';
|
|
||||||
API_SITE_DETAILS_URL = 'functions/getSiteById';
|
|
||||||
|
|
||||||
const logInfo = true; // Normal operations
|
|
||||||
const logDebug = false; // Chatty
|
|
||||||
const logVerbose = false; // Very chatty
|
|
||||||
|
|
||||||
const consoleLogger = (level) => (...msg) =>
|
|
||||||
console.log(new Date().toISOString().replace("T", " ").substring(0, 19) + "Z", level, "plejd-api", ...msg);
|
|
||||||
|
|
||||||
const getLogger = (level, shouldLog) => (shouldLog ? consoleLogger(level) : () => {});
|
|
||||||
|
|
||||||
const errLogger = getLogger("ERR", true);
|
|
||||||
const infLogger = getLogger("INF", logInfo);
|
|
||||||
const dbgLogger = getLogger("DBG", logDebug);
|
|
||||||
const vrbLogger = getLogger("vrb", logVerbose);
|
|
||||||
|
|
||||||
|
|
||||||
class PlejdApi extends EventEmitter {
|
|
||||||
constructor(siteName, username, password, includeRoomsAsLights) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.includeRoomsAsLights = includeRoomsAsLights;
|
|
||||||
this.siteName = siteName;
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
|
|
||||||
this.sessionToken = '';
|
|
||||||
this.site = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettings(settings) {
|
|
||||||
logVerbose("Got new settings: ", settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
login() {
|
|
||||||
infLogger('login()');
|
|
||||||
infLogger('logging into ' + this.siteName);
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const instance = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'X-Parse-Application-Id': API_APP_ID,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
dbgLogger('sending POST to ' + API_BASE_URL + API_LOGIN_URL);
|
|
||||||
|
|
||||||
instance.post(
|
|
||||||
API_LOGIN_URL,
|
|
||||||
{
|
|
||||||
'username': this.username,
|
|
||||||
'password': this.password
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
infLogger('got session token response');
|
|
||||||
self.sessionToken = response.data.sessionToken;
|
|
||||||
|
|
||||||
if (!self.sessionToken) {
|
|
||||||
errLogger('No session token received');
|
|
||||||
reject('no session token received.');
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
if (error.response.status === 400) {
|
|
||||||
errLogger('Server returned status 400. probably invalid credentials, please verify.');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
errLogger('Unable to retrieve session token response: ' + error);
|
|
||||||
}
|
|
||||||
|
|
||||||
reject('unable to retrieve session token response: ' + error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getSites() {
|
|
||||||
infLogger('getSites()');
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const instance = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'X-Parse-Application-Id': API_APP_ID,
|
|
||||||
'X-Parse-Session-Token': this.sessionToken,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
dbgLogger('sending POST to ' + API_BASE_URL + API_SITE_LIST_URL);
|
|
||||||
|
|
||||||
instance.post(API_SITE_LIST_URL)
|
|
||||||
.then((response) => {
|
|
||||||
infLogger('got site list response');
|
|
||||||
const site = response.data.result.find(x => x.site.title == self.siteName);
|
|
||||||
|
|
||||||
if (!site) {
|
|
||||||
errLogger('error: failed to find a site named ' + self.siteName);
|
|
||||||
reject('failed to find a site named ' + self.siteName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(site);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errLogger('error: unable to retrieve list of sites. error: ' + error);
|
|
||||||
return reject('plejd-api: unable to retrieve list of sites. error: ' + error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getSite(siteId) {
|
|
||||||
infLogger('getSite(...)');
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const instance = axios.create({
|
|
||||||
baseURL: API_BASE_URL,
|
|
||||||
headers: {
|
|
||||||
'X-Parse-Application-Id': API_APP_ID,
|
|
||||||
'X-Parse-Session-Token': this.sessionToken,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
dbgLogger('sending POST to ' + API_BASE_URL + API_SITE_DETAILS_URL);
|
|
||||||
|
|
||||||
instance.post(API_SITE_DETAILS_URL, { siteId: siteId })
|
|
||||||
.then((response) => {
|
|
||||||
infLogger('got site details response');
|
|
||||||
if (response.data.result.length === 0) {
|
|
||||||
const msg = 'no site with ID ' + siteId + ' was found.';
|
|
||||||
errLogger('error: ' + msg);
|
|
||||||
reject(msg);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.site = response.data.result[0];
|
|
||||||
self.cryptoKey = self.site.plejdMesh.cryptoKey;
|
|
||||||
|
|
||||||
resolve(self.cryptoKey);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
errLogger('error: unable to retrieve the crypto key. error: ' + error);
|
|
||||||
return reject('plejd-api: unable to retrieve the crypto key. error: ' + error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDevices() {
|
|
||||||
let devices = [];
|
|
||||||
|
|
||||||
vrbLogger(JSON.stringify(this.site));
|
|
||||||
|
|
||||||
const roomDevices = {};
|
|
||||||
|
|
||||||
for (let i = 0; i < this.site.devices.length; i++) {
|
|
||||||
const device = this.site.devices[i];
|
|
||||||
const deviceId = device.deviceId;
|
|
||||||
|
|
||||||
const settings = this.site.outputSettings.find(x => x.deviceParseId == device.objectId);
|
|
||||||
let deviceNum = this.site.deviceAddress[deviceId];
|
|
||||||
|
|
||||||
if (settings) {
|
|
||||||
const outputs = this.site.outputAddress[deviceId];
|
|
||||||
deviceNum = outputs[settings.output];
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if device is dimmable
|
|
||||||
const plejdDevice = this.site.plejdDevices.find(x => x.deviceId == deviceId);
|
|
||||||
let { name, type, dimmable } = this._getDeviceType(plejdDevice.hardwareId);
|
|
||||||
|
|
||||||
if (settings) {
|
|
||||||
dimmable = settings.dimCurve != 'NonDimmable';
|
|
||||||
}
|
|
||||||
|
|
||||||
const newDevice = {
|
|
||||||
id: deviceNum,
|
|
||||||
name: device.title,
|
|
||||||
type: type,
|
|
||||||
typeName: name,
|
|
||||||
dimmable: dimmable,
|
|
||||||
version: plejdDevice.firmware.version,
|
|
||||||
serialNumber: plejdDevice.deviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (newDevice.typeName === 'WPH-01') {
|
|
||||||
// WPH-01 is special, it has two buttons which needs to be
|
|
||||||
// registered separately.
|
|
||||||
const inputs = this.site.inputAddress[deviceId];
|
|
||||||
const first = inputs[0];
|
|
||||||
const second = inputs[1];
|
|
||||||
|
|
||||||
let switchDevice = {
|
|
||||||
id: first,
|
|
||||||
name: device.title + ' knapp vä',
|
|
||||||
type: type,
|
|
||||||
typeName: name,
|
|
||||||
dimmable: dimmable,
|
|
||||||
version: plejdDevice.firmware.version,
|
|
||||||
serialNumber: plejdDevice.deviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (roomDevices[device.roomId]) {
|
|
||||||
roomDevices[device.roomId].push(switchDevice);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomDevices[device.roomId] = [switchDevice];
|
|
||||||
}
|
|
||||||
devices.push(switchDevice);
|
|
||||||
|
|
||||||
switchDevice = {
|
|
||||||
id: second,
|
|
||||||
name: device.title + ' knapp hö',
|
|
||||||
type: type,
|
|
||||||
typeName: name,
|
|
||||||
dimmable: dimmable,
|
|
||||||
version: plejdDevice.firmware.version,
|
|
||||||
serialNumber: plejdDevice.deviceId
|
|
||||||
};
|
|
||||||
|
|
||||||
if (roomDevices[device.roomId]) {
|
|
||||||
roomDevices[device.roomId].push(switchDevice);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomDevices[device.roomId] = [switchDevice];
|
|
||||||
}
|
|
||||||
devices.push(switchDevice);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (roomDevices[device.roomId]) {
|
|
||||||
roomDevices[device.roomId].push(newDevice);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomDevices[device.roomId] = [newDevice];
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.push(newDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.includeRoomsAsLights) {
|
|
||||||
dbgLogger('includeRoomsAsLights is set to true, adding rooms too.');
|
|
||||||
for (let i = 0; i < this.site.rooms.length; i++) {
|
|
||||||
const room = this.site.rooms[i];
|
|
||||||
const roomId = room.roomId;
|
|
||||||
const roomAddress = this.site.roomAddress[roomId];
|
|
||||||
|
|
||||||
const newDevice = {
|
|
||||||
id: roomAddress,
|
|
||||||
name: room.title,
|
|
||||||
type: 'light',
|
|
||||||
typeName: 'Room',
|
|
||||||
dimmable: roomDevices[roomId].filter(x => x.dimmable).length > 0
|
|
||||||
};
|
|
||||||
|
|
||||||
devices.push(newDevice);
|
|
||||||
}
|
|
||||||
dbgLogger('includeRoomsAsLights done.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// add scenes as switches
|
|
||||||
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
|
|
||||||
|
|
||||||
for (const scene of scenes) {
|
|
||||||
const sceneNum = this.site.sceneIndex[scene.sceneId];
|
|
||||||
const newScene = {
|
|
||||||
id: sceneNum,
|
|
||||||
name: scene.title,
|
|
||||||
type: 'switch',
|
|
||||||
typeName: 'Scene',
|
|
||||||
dimmable: false,
|
|
||||||
version: '1.0',
|
|
||||||
serialNumber: scene.objectId
|
|
||||||
};
|
|
||||||
|
|
||||||
devices.push(newScene);
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
_getDeviceType(hardwareId) {
|
|
||||||
switch (parseInt(hardwareId)) {
|
|
||||||
case 1:
|
|
||||||
case 11:
|
|
||||||
return { name: "DIM-01", type: 'light', dimmable: true };
|
|
||||||
case 2:
|
|
||||||
return { name: "DIM-02", type: 'light', dimmable: true };
|
|
||||||
case 3:
|
|
||||||
return { name: "CTR-01", type: 'light', dimmable: false };
|
|
||||||
case 4:
|
|
||||||
return { name: "GWY-01", type: 'sensor', dimmable: false };
|
|
||||||
case 5:
|
|
||||||
return { name: "LED-10", type: 'light', dimmable: true };
|
|
||||||
case 6:
|
|
||||||
return { name: "WPH-01", type: 'switch', dimmable: false };
|
|
||||||
case 7:
|
|
||||||
return { name: "REL-01", type: 'switch', dimmable: false };
|
|
||||||
case 8:
|
|
||||||
case 9:
|
|
||||||
// Unknown
|
|
||||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
|
||||||
case 10:
|
|
||||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
|
||||||
case 12:
|
|
||||||
// Unknown
|
|
||||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
|
||||||
case 13:
|
|
||||||
return { name: "Generic", type: 'light', dimmable: false };
|
|
||||||
case 14:
|
|
||||||
case 15:
|
|
||||||
case 16:
|
|
||||||
// Unknown
|
|
||||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
|
||||||
case 17:
|
|
||||||
return { name: "REL-01", type: 'switch', dimmable: false };
|
|
||||||
case 18:
|
|
||||||
return { name: "REL-02", type: 'switch', dimmable: false };
|
|
||||||
case 19:
|
|
||||||
// Unknown
|
|
||||||
return { name: "-unknown-", type: 'light', dimmable: false };
|
|
||||||
case 20:
|
|
||||||
return { name: "SPR-01", type: 'switch', dimmable: false };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { PlejdApi };
|
|
||||||
|
|
@ -4,13 +4,7 @@
|
||||||
"slug": "plejd",
|
"slug": "plejd",
|
||||||
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
"description": "Adds support for the Swedish home automation devices from Plejd.",
|
||||||
"url": "https://github.com/icanos/hassio-plejd/",
|
"url": "https://github.com/icanos/hassio-plejd/",
|
||||||
"arch": [
|
"arch": ["armhf", "armv7", "aarch64", "amd64", "i386"],
|
||||||
"armhf",
|
|
||||||
"armv7",
|
|
||||||
"aarch64",
|
|
||||||
"amd64",
|
|
||||||
"i386"
|
|
||||||
],
|
|
||||||
"startup": "application",
|
"startup": "application",
|
||||||
"boot": "auto",
|
"boot": "auto",
|
||||||
"host_network": true,
|
"host_network": true,
|
||||||
|
|
@ -24,6 +18,7 @@
|
||||||
"mqttUsername": "",
|
"mqttUsername": "",
|
||||||
"mqttPassword": "",
|
"mqttPassword": "",
|
||||||
"includeRoomsAsLights": false,
|
"includeRoomsAsLights": false,
|
||||||
|
"logLevel": "info",
|
||||||
"connectionTimeout": 2,
|
"connectionTimeout": 2,
|
||||||
"writeQueueWaitTime": 400
|
"writeQueueWaitTime": 400
|
||||||
},
|
},
|
||||||
|
|
@ -35,6 +30,7 @@
|
||||||
"mqttUsername": "str",
|
"mqttUsername": "str",
|
||||||
"mqttPassword": "str",
|
"mqttPassword": "str",
|
||||||
"includeRoomsAsLights": "bool",
|
"includeRoomsAsLights": "bool",
|
||||||
|
"logLevel": "list(error|warn|info|debug|verbose|silly)",
|
||||||
"connectionTimeout": "int",
|
"connectionTimeout": "int",
|
||||||
"writeQueueWaitTime": "int"
|
"writeQueueWaitTime": "int"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
139
plejd/main.js
139
plejd/main.js
|
|
@ -1,23 +1,31 @@
|
||||||
const api = require('./api');
|
const PlejdApi = require('./PlejdApi');
|
||||||
const mqtt = require('./mqtt');
|
const MqttClient = require('./MqttClient');
|
||||||
const fs = require('fs');
|
|
||||||
const PlejdService = require('./ble.bluez');
|
|
||||||
const SceneManager = require('./scene.manager');
|
|
||||||
|
|
||||||
const version = "0.4.8";
|
const Logger = require('./Logger');
|
||||||
|
const PlejdService = require('./PlejdService');
|
||||||
|
const SceneManager = require('./SceneManager');
|
||||||
|
const Configuration = require('./Configuration');
|
||||||
|
|
||||||
|
const logger = Logger.getLogger('plejd-main');
|
||||||
|
|
||||||
|
const version = '0.4.8';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('starting Plejd add-on v. ' + version);
|
logger.info(`Starting Plejd add-on v. ${version}`);
|
||||||
|
|
||||||
const rawData = fs.readFileSync('/data/plejd.json');
|
const config = Configuration.getConfiguration();
|
||||||
const config = JSON.parse(rawData);
|
|
||||||
|
|
||||||
if (!config.connectionTimeout) {
|
if (!config.connectionTimeout) {
|
||||||
config.connectionTimeout = 2;
|
config.connectionTimeout = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plejdApi = new api.PlejdApi(config.site, config.username, config.password, config.includeRoomsAsLights);
|
const plejdApi = new PlejdApi(
|
||||||
const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword);
|
config.site,
|
||||||
|
config.username,
|
||||||
|
config.password,
|
||||||
|
config.includeRoomsAsLights,
|
||||||
|
);
|
||||||
|
const client = new MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword);
|
||||||
|
|
||||||
plejdApi.login().then(() => {
|
plejdApi.login().then(() => {
|
||||||
// load all sites and find the one that we want (from config)
|
// load all sites and find the one that we want (from config)
|
||||||
|
|
@ -28,83 +36,98 @@ async function main() {
|
||||||
const devices = plejdApi.getDevices();
|
const devices = plejdApi.getDevices();
|
||||||
|
|
||||||
client.on('connected', () => {
|
client.on('connected', () => {
|
||||||
console.log('plejd-mqtt: connected to mqtt.');
|
try {
|
||||||
client.discover(devices);
|
logger.verbose('connected to mqtt.');
|
||||||
|
client.discover(devices);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error in MqttClient.connected callback in main.js', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
client.init();
|
client.init();
|
||||||
|
|
||||||
// init the BLE interface
|
// init the BLE interface
|
||||||
const sceneManager = new SceneManager(plejdApi.site, devices);
|
const sceneManager = new SceneManager(plejdApi.site, devices);
|
||||||
const plejd = new PlejdService(cryptoKey, devices, sceneManager, config.connectionTimeout, config.writeQueueWaitTime, true);
|
const plejd = new PlejdService(
|
||||||
|
cryptoKey,
|
||||||
|
devices,
|
||||||
|
sceneManager,
|
||||||
|
config.connectionTimeout,
|
||||||
|
config.writeQueueWaitTime,
|
||||||
|
);
|
||||||
plejd.on('connectFailed', () => {
|
plejd.on('connectFailed', () => {
|
||||||
console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.');
|
logger.verbose('Were unable to connect, will retry connection in 10 seconds.');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
plejd.init();
|
plejd
|
||||||
|
.init()
|
||||||
|
.catch((e) => logger.error('Error in init() from connectFailed in main.js', e));
|
||||||
}, 10000);
|
}, 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
plejd.init();
|
plejd.init();
|
||||||
|
|
||||||
plejd.on('authenticated', () => {
|
plejd.on('authenticated', () => {
|
||||||
console.log('plejd: connected via bluetooth.');
|
logger.verbose('plejd: connected via bluetooth.');
|
||||||
});
|
});
|
||||||
|
|
||||||
// subscribe to changes from Plejd
|
// subscribe to changes from Plejd
|
||||||
plejd.on('stateChanged', (deviceId, command) => {
|
plejd.on('stateChanged', (deviceId, command) => {
|
||||||
client.updateState(deviceId, command);
|
try {
|
||||||
|
client.updateState(deviceId, command);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error in PlejdService.stateChanged callback in main.js', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
plejd.on('sceneTriggered', (deviceId, scene) => {
|
plejd.on('sceneTriggered', (deviceId, scene) => {
|
||||||
client.sceneTriggered(scene);
|
try {
|
||||||
|
client.sceneTriggered(scene);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error in PlejdService.sceneTriggered callback in main.js', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// subscribe to changes from HA
|
// subscribe to changes from HA
|
||||||
client.on('stateChanged', (device, command) => {
|
client.on('stateChanged', (device, command) => {
|
||||||
const deviceId = device.id;
|
try {
|
||||||
|
const deviceId = device.id;
|
||||||
|
|
||||||
if (device.typeName === 'Scene') {
|
if (device.typeName === 'Scene') {
|
||||||
// we're triggering a scene, lets do that and jump out.
|
// we're triggering a scene, lets do that and jump out.
|
||||||
// since scenes aren't "real" devices.
|
// since scenes aren't "real" devices.
|
||||||
plejd.triggerScene(device.id);
|
plejd.triggerScene(device.id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = 'OFF';
|
let state = 'OFF';
|
||||||
let commandObj = {};
|
let commandObj = {};
|
||||||
|
|
||||||
if (typeof command === 'string') {
|
if (typeof command === 'string') {
|
||||||
// switch command
|
// switch command
|
||||||
state = command;
|
state = command;
|
||||||
commandObj = {
|
commandObj = {
|
||||||
state: state
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
// since the switch doesn't get any updates on whether it's on or not,
|
// since the switch doesn't get any updates on whether it's on or not,
|
||||||
// we fake this by directly send the updateState back to HA in order for
|
// we fake this by directly send the updateState back to HA in order for
|
||||||
// it to change state.
|
// it to change state.
|
||||||
client.updateState(deviceId, {
|
client.updateState(deviceId, {
|
||||||
state: state === 'ON' ? 1 : 0
|
state: state === 'ON' ? 1 : 0,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
state = command.state;
|
// eslint-disable-next-line prefer-destructuring
|
||||||
commandObj = command;
|
state = command.state;
|
||||||
}
|
commandObj = command;
|
||||||
|
}
|
||||||
|
|
||||||
if (state === 'ON') {
|
if (state === 'ON') {
|
||||||
plejd.turnOn(deviceId, commandObj);
|
plejd.turnOn(deviceId, commandObj);
|
||||||
} else {
|
} else {
|
||||||
plejd.turnOff(deviceId, commandObj);
|
plejd.turnOff(deviceId, commandObj);
|
||||||
}
|
}
|
||||||
});
|
} catch (err) {
|
||||||
|
logger.error('Error in MqttClient.stateChanged callback in main.js', err);
|
||||||
client.on('settingsChanged', (settings) => {
|
|
||||||
if (settings.module === 'mqtt') {
|
|
||||||
client.updateSettings(settings);
|
|
||||||
} else if (settings.module === 'ble') {
|
|
||||||
plejd.updateSettings(settings);
|
|
||||||
} else if (settings.module === 'api') {
|
|
||||||
plejdApi.updateSettings(settings);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
214
plejd/mqtt.js
214
plejd/mqtt.js
|
|
@ -1,214 +0,0 @@
|
||||||
const EventEmitter = require('events');
|
|
||||||
const mqtt = require('mqtt');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const startTopic = 'hass/status';
|
|
||||||
|
|
||||||
// #region logging
|
|
||||||
let debug = '';
|
|
||||||
|
|
||||||
const getLogger = () => {
|
|
||||||
const consoleLogger = msg => console.log('plejd-mqtt', msg);
|
|
||||||
if (debug === 'console') {
|
|
||||||
return consoleLogger;
|
|
||||||
}
|
|
||||||
return _.noop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const logger = getLogger();
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// #region discovery
|
|
||||||
|
|
||||||
const discoveryPrefix = 'homeassistant';
|
|
||||||
const nodeId = 'plejd';
|
|
||||||
|
|
||||||
const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
|
|
||||||
const getPath = ({ id, type }) =>
|
|
||||||
`${discoveryPrefix}/${type}/${nodeId}/${id}`;
|
|
||||||
const getConfigPath = plug => `${getPath(plug)}/config`;
|
|
||||||
const getStateTopic = plug => `${getPath(plug)}/state`;
|
|
||||||
const getCommandTopic = plug => `${getPath(plug)}/set`;
|
|
||||||
const getSceneEventTopic = () => `plejd/event/scene`;
|
|
||||||
const getSettingsTopic = () => `plejd/settings`;
|
|
||||||
|
|
||||||
const getDiscoveryPayload = device => ({
|
|
||||||
schema: 'json',
|
|
||||||
name: device.name,
|
|
||||||
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
|
|
||||||
state_topic: getStateTopic(device),
|
|
||||||
command_topic: getCommandTopic(device),
|
|
||||||
optimistic: false,
|
|
||||||
brightness: `${device.dimmable}`,
|
|
||||||
device: {
|
|
||||||
identifiers: device.serialNumber + '_' + device.id,
|
|
||||||
manufacturer: 'Plejd',
|
|
||||||
model: device.typeName,
|
|
||||||
name: device.name,
|
|
||||||
sw_version: device.version
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const getSwitchPayload = device => ({
|
|
||||||
name: device.name,
|
|
||||||
state_topic: getStateTopic(device),
|
|
||||||
command_topic: getCommandTopic(device),
|
|
||||||
optimistic: false,
|
|
||||||
device: {
|
|
||||||
identifiers: device.serialNumber + '_' + device.id,
|
|
||||||
manufacturer: 'Plejd',
|
|
||||||
model: device.typeName,
|
|
||||||
name: device.name,
|
|
||||||
sw_version: device.version
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
class MqttClient extends EventEmitter {
|
|
||||||
constructor(mqttBroker, username, password) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.mqttBroker = mqttBroker;
|
|
||||||
this.username = username;
|
|
||||||
this.password = password;
|
|
||||||
this.deviceMap = {};
|
|
||||||
this.devices = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
this.client = mqtt.connect(this.mqttBroker, {
|
|
||||||
username: this.username,
|
|
||||||
password: this.password
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('connect', () => {
|
|
||||||
logger('connected to MQTT.');
|
|
||||||
|
|
||||||
this.client.subscribe(startTopic, (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger('error: unable to subscribe to ' + startTopic);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit('connected');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.subscribe(getSubscribePath(), (err) => {
|
|
||||||
if (err) {
|
|
||||||
logger('error: unable to subscribe to control topics');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.subscribe(getSettingsTopic(), (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.log('error: could not subscribe to settings topic');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('close', () => {
|
|
||||||
self.reconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.client.on('message', (topic, message) => {
|
|
||||||
//const command = message.toString();
|
|
||||||
const command = message.toString().substring(0, 1) === '{'
|
|
||||||
? JSON.parse(message.toString())
|
|
||||||
: message.toString();
|
|
||||||
|
|
||||||
if (topic === startTopic) {
|
|
||||||
logger('home assistant has started. lets do discovery.');
|
|
||||||
self.emit('connected');
|
|
||||||
}
|
|
||||||
else if (topic === getSettingsTopic()) {
|
|
||||||
self.emit('settingsChanged', command);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_.includes(topic, 'set')) {
|
|
||||||
const device = self.devices.find(x => getCommandTopic(x) === topic);
|
|
||||||
self.emit('stateChanged', device, command);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSettings(settings) {
|
|
||||||
if (settings.debug) {
|
|
||||||
debug = 'console';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
debug = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reconnect() {
|
|
||||||
this.client.reconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
discover(devices) {
|
|
||||||
this.devices = devices;
|
|
||||||
|
|
||||||
const self = this;
|
|
||||||
logger('sending discovery of ' + devices.length + ' device(s).');
|
|
||||||
|
|
||||||
devices.forEach((device) => {
|
|
||||||
logger(`sending discovery for ${device.name}`);
|
|
||||||
|
|
||||||
let payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
|
||||||
console.log(`plejd-mqtt: discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`);
|
|
||||||
|
|
||||||
self.deviceMap[device.id] = payload.unique_id;
|
|
||||||
|
|
||||||
self.client.publish(
|
|
||||||
getConfigPath(device),
|
|
||||||
JSON.stringify(payload)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState(deviceId, data) {
|
|
||||||
const device = this.devices.find(x => x.id === deviceId);
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
logger('error: ' + deviceId + ' is not handled by us.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger('updating state for ' + device.name + ': ' + data.state);
|
|
||||||
let payload = null;
|
|
||||||
|
|
||||||
if (device.type === 'switch') {
|
|
||||||
payload = data.state === 1 ? 'ON' : 'OFF';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (device.dimmable) {
|
|
||||||
payload = {
|
|
||||||
state: data.state === 1 ? 'ON' : 'OFF',
|
|
||||||
brightness: data.brightness
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
payload = {
|
|
||||||
state: data.state === 1 ? 'ON' : 'OFF'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload = JSON.stringify(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.client.publish(
|
|
||||||
getStateTopic(device),
|
|
||||||
payload
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
sceneTriggered(scene) {
|
|
||||||
this.client.publish(
|
|
||||||
getSceneEventTopic(),
|
|
||||||
JSON.stringify({ scene: scene })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = { MqttClient };
|
|
||||||
1359
plejd/package-lock.json
generated
1359
plejd/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +1,31 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abandonware/bluetooth-hci-socket": "0.5.3-3",
|
"@abandonware/bluetooth-hci-socket": "0.5.3-3",
|
||||||
"axios": "^0.21.1",
|
"axios": "~0.21.1",
|
||||||
"buffer-xor": "^2.0.2",
|
"buffer-xor": "~2.0.2",
|
||||||
"dbus-next": "^0.8.1",
|
"dbus-next": "~0.9.1",
|
||||||
"fs": "0.0.1-security",
|
"fs": "0.0.1-security",
|
||||||
"jspack": "0.0.4",
|
"jspack": "~0.0.4",
|
||||||
"lodash": "^4.17.19",
|
"mqtt": "~3.0.0",
|
||||||
"mqtt": "^3.0.0",
|
"sleep": "~6.1.0",
|
||||||
"sleep": "^6.1.0"
|
"winston": "~3.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-eslint": "~10.1.0",
|
||||||
|
"eslint": "~7.18.0",
|
||||||
|
"eslint-config-airbnb": "~18.2.1",
|
||||||
|
"eslint-config-prettier": "~7.2.0",
|
||||||
|
"eslint-plugin-import": "~2.22.1",
|
||||||
|
"eslint-plugin-prettier": "~3.3.1",
|
||||||
|
"prettier": "~2.2.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"lint": "prettier \"../*.{js*,md}\" --check & eslint **/*.js",
|
||||||
|
"lint:fix": "prettier .. --check --write & eslint **/*.js --fix",
|
||||||
|
"lint:prettier:fix": "npm run lint:prettier --write",
|
||||||
|
"lint:errors": "npm run lint:prettier & npm run lint:styles --quiet & npm run lint:types & npm run lint:scripts --quiet",
|
||||||
|
"lint:errors:fix": "npm run lint:prettier --write & npm run lint:scripts --quiet --fix",
|
||||||
|
"lint:prettier": "prettier --check \"**/*.js\"",
|
||||||
|
"lint:scripts": "eslint --config ./.eslintrc.js \"**/*.js\""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,4 @@
|
||||||
#!/usr/bin/with-contenv bashio
|
#!/usr/bin/with-contenv bashio
|
||||||
|
|
||||||
CONFIG_PATH=/data/options.json
|
|
||||||
|
|
||||||
SITE=$(jq --raw-output ".site" $CONFIG_PATH)
|
|
||||||
USERNAME=$(jq --raw-output ".username" $CONFIG_PATH)
|
|
||||||
PASSWORD=$(jq --raw-output ".password" $CONFIG_PATH)
|
|
||||||
MQTTBROKER=$(jq --raw-output ".mqttBroker" $CONFIG_PATH)
|
|
||||||
MQTTUSERNAME=$(jq --raw-output ".mqttUsername" $CONFIG_PATH)
|
|
||||||
MQTTPASSWORD=$(jq --raw-output ".mqttPassword" $CONFIG_PATH)
|
|
||||||
INCLUDEROOMSASLIGHTS=$(jq --raw-output ".includeRoomsAsLights" $CONFIG_PATH)
|
|
||||||
CONNECTIONTIMEOUT=$(jq --raw-output ".connectionTimeout" $CONFIG_PATH)
|
|
||||||
WRITEQUEUEWAITTIME=$(jq --raw-output ".writeQueueWaitTime" $CONFIG_PATH)
|
|
||||||
|
|
||||||
PLEJD_PATH=/data/plejd.json
|
|
||||||
PLEJD_CONFIG="{
|
|
||||||
\"site\": \"$SITE\",
|
|
||||||
\"username\": \"$USERNAME\",
|
|
||||||
\"password\": \"$PASSWORD\",
|
|
||||||
\"mqttBroker\": \"$MQTTBROKER\",
|
|
||||||
\"mqttUsername\": \"$MQTTUSERNAME\",
|
|
||||||
\"mqttPassword\": \"$MQTTPASSWORD\",
|
|
||||||
\"includeRoomsAsLights\": \"$INCLUDEROOMSASLIGHTS\",
|
|
||||||
\"connectionTimeout\": \"$CONNECTIONTIMEOUT\",
|
|
||||||
\"writeQueueWaitTime\": \"$WRITEQUEUEWAITTIME\"
|
|
||||||
}
|
|
||||||
"
|
|
||||||
|
|
||||||
bashio::log.info 'Wrote plejd.json'
|
|
||||||
echo "$PLEJD_CONFIG" > $PLEJD_PATH
|
|
||||||
|
|
||||||
bashio::log.info 'Running add-on'
|
bashio::log.info 'Running add-on'
|
||||||
exec node /plejd/main.js
|
exec node /plejd/main.js
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
const EventEmitter = require('events');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
class SceneManager extends EventEmitter {
|
|
||||||
constructor(site, devices) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.site = site;
|
|
||||||
this.scenes = [];
|
|
||||||
this.devices = devices;
|
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
|
|
||||||
for (const scene of scenes) {
|
|
||||||
const idx = this.site.sceneIndex[scene.sceneId];
|
|
||||||
this.scenes.push(new Scene(idx, scene, this.site.sceneSteps));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeScene(sceneIndex, ble) {
|
|
||||||
const scene = this.scenes.find(x => x.id === sceneIndex);
|
|
||||||
if (!scene) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const step of scene.steps) {
|
|
||||||
const device = this.devices.find(x => x.serialNumber === step.deviceId);
|
|
||||||
if (!device) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device.dimmable && step.state) {
|
|
||||||
ble.turnOn(device.id, { brightness: step.brightness });
|
|
||||||
}
|
|
||||||
else if (!device.dimmable && step.state) {
|
|
||||||
ble.turnOn(device.id, {});
|
|
||||||
}
|
|
||||||
else if (!step.state) {
|
|
||||||
ble.turnOff(device.id, {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Scene {
|
|
||||||
constructor(idx, scene, steps) {
|
|
||||||
this.id = idx;
|
|
||||||
this.title = scene.title;
|
|
||||||
this.sceneId = scene.sceneId;
|
|
||||||
|
|
||||||
const sceneSteps = steps.filter(x => x.sceneId === scene.sceneId);
|
|
||||||
this.steps = [];
|
|
||||||
|
|
||||||
for (const step of sceneSteps) {
|
|
||||||
this.steps.push(new SceneStep(step));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SceneStep {
|
|
||||||
constructor(step) {
|
|
||||||
this.sceneId = step.sceneId;
|
|
||||||
this.deviceId = step.deviceId;
|
|
||||||
this.state = step.state === 'On' ? 1 : 0;
|
|
||||||
this.brightness = step.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SceneManager;
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const PlejdService = require('../ble.bluez');
|
const PlejdService = require('../PlejdService');
|
||||||
|
|
||||||
const cryptoKey = '';
|
const cryptoKey = '';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
const PlejdService = require('../ble');
|
|
||||||
|
|
||||||
const plejd = new PlejdService('todo-insert-crypto-key', true);
|
|
||||||
plejd.on('authenticated', () => {
|
|
||||||
plejd.disconnect();
|
|
||||||
console.log('ok, done! disconnected.');
|
|
||||||
});
|
|
||||||
plejd.scan();
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "Unofficial Plejd Add-on",
|
"name": "Unofficial Plejd Add-on",
|
||||||
"url": "https://github.com/icanos/hassio-plejd",
|
"url": "https://github.com/icanos/hassio-plejd",
|
||||||
"maintainer": "Marcus Westin <marcus@sekurbit.se>"
|
"maintainer": "Marcus Westin <marcus@sekurbit.se>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue