mirror of
https://github.com/mtan93/homebridge.git
synced 2026-03-08 05:31:55 +00:00
Compare commits
15 Commits
v0.4.21
...
osx-launch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febed4cae6 | ||
|
|
17f13a51a4 | ||
|
|
2b11db4560 | ||
|
|
cf885b6d29 | ||
|
|
267470c871 | ||
|
|
ae2d53262f | ||
|
|
241b22db87 | ||
|
|
8d94366e3c | ||
|
|
950e6a8211 | ||
|
|
f3cab9a529 | ||
|
|
25981deac4 | ||
|
|
6540c25baf | ||
|
|
6f71faf355 | ||
|
|
8131f6936e | ||
|
|
ac0c967ec0 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -7,8 +7,14 @@ node_modules/
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
.node-version
|
.node-version
|
||||||
|
|
||||||
# Ignore any extra plugins in the example directory that aren't in Git already
|
# Intellij
|
||||||
# (this is a sandbox for the user)
|
.idea/
|
||||||
example-plugins
|
*.iml
|
||||||
|
|
||||||
.idea
|
# HomeBridge
|
||||||
|
config.json
|
||||||
|
config.test.json
|
||||||
|
persist/
|
||||||
|
log/
|
||||||
|
|
||||||
|
.AppleDouble
|
||||||
|
|||||||
0
.gitmodules
vendored
Normal file
0
.gitmodules
vendored
Normal file
198
README.md
198
README.md
@@ -1,164 +1,130 @@
|
|||||||
|
|
||||||
[](https://homebridge-slackin.herokuapp.com)
|
|
||||||
|
|
||||||
# Homebridge
|
# Homebridge
|
||||||
|
|
||||||

|
Homebridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It includes a set of "shims" (found in the [accessories](accessories/) and [platforms](platforms/) folders) that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
|
||||||
|
|
||||||
Homebridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It supports Plugins, which are community-contributed modules that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
|
Since Siri supports devices added through HomeKit, this means that with Homebridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using the included shims, you can say things like:
|
||||||
|
|
||||||
Since Siri supports devices added through HomeKit, this means that with Homebridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using just some of the available plugins, you can say:
|
* _Siri, unlock the front door._ ([Lockitron](https://lockitron.com))
|
||||||
|
* _Siri, open the garage door._ ([LiftMaster MyQ](https://www.myliftmaster.com))
|
||||||
|
* _Siri, turn on the Leaf._ ([Carwings](http://www.nissanusa.com/innovations/carwings.article.html))
|
||||||
|
* _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com))
|
||||||
|
* _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/))
|
||||||
|
* _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html))
|
||||||
|
* _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [Home Assistant](http://home-assistant.io) [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/))
|
||||||
|
* _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/))
|
||||||
|
|
||||||
* _Siri, unlock the back door._ [pictured above]
|
If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list.
|
||||||
* _Siri, open the garage door._
|
|
||||||
* _Siri, turn on the coffee maker._
|
|
||||||
* _Siri, turn on the living room lights._
|
|
||||||
* _Siri, good morning!_
|
|
||||||
|
|
||||||
You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin).
|
# Shim types
|
||||||
|
There are 2 types of shims supported in Homebridge.
|
||||||
|
|
||||||
# Community
|
* Accessory - Individual device
|
||||||
|
* Platform - A full bridge to another system
|
||||||
|
|
||||||
If you're having an issue with a particular plugin, open an issue in that plugin's Github repository. If you're having an issue with Homebridge itself, feel free to open issues and PRs here.
|
## Accessories
|
||||||
|
|
||||||
You can also chat with us in our nascent [Slack instance](http://homebridge-slackin.herokuapp.com).
|
Accessories are individual devices you would like to bridge to HomeKit. You set them up by declaring them individually in your `config.json` file. Generally, you specify them by `name` or `id` and which system they use.
|
||||||
|
|
||||||
# Installation
|
## Platforms
|
||||||
|
|
||||||
**Note:** If you're running on Linux, you'll need to make sure you have the `libavahi-compat-libdnssd-dev` package installed. If you're running on a Raspberry Pi, you should have a look at the [Wiki](https://github.com/nfarina/homebridge/wiki/Running-HomeBridge-on-a-Raspberry-Pi).
|
Platforms bridge entire systems to HomeKit. Platforms can be things like Wink or SmartThings or Vera. By adding a platform to your `config.json`, Homebridge will automatically detect all of your devices for you.
|
||||||
|
|
||||||
Homebridge is published through [NPM](https://www.npmjs.com/package/homebridge) and should be installed "globally" by typing:
|
All you have to do is add the right config options so Homebridge can authenticate and communicate with your other system, and voila, your devices will be available to HomeKit via Homebridge.
|
||||||
|
|
||||||
sudo npm install -g --unsafe-perm homebridge
|
# Why?
|
||||||
|
|
||||||
You may need to use the `--unsafe-perm` flag if you receive an error similar to this:
|
Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, these shims will be obsolete, and I hope that happens soon. In the meantime, this server is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market.
|
||||||
|
|
||||||
gyp WARN EACCES user "root" does not have permission to access the dev dir "/root/.node-gyp/5.5.0"
|
# Credit
|
||||||
|
|
||||||
Now you should be able to run Homebridge:
|
Homebridge itself is basically just a set of shims and a README. The actual HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project. Additionally, many of the shims benefit from amazing NodeJS projects out there like `sonos` and `wemo` that implement all the interesting functionality.
|
||||||
|
|
||||||
$ homebridge
|
# Before you Begin
|
||||||
No plugins found. See the README for information on installing plugins.
|
|
||||||
|
|
||||||
Homebridge will complain if you don't have any Plugins installed, since it will essentially be useless, although you can still "pair" with it. See the next section "Installing Plugins" for more info.
|
I would call this project a "novelty" in its current form, and is for **intrepid hackers only**. To make any of this work, you'll need:
|
||||||
|
|
||||||
Once you've installed a Plugin or two, you can run Homebridge again:
|
* An app on your iOS device that can manage your HomeKit database.
|
||||||
|
* An always-running server (like a Raspberry Pi) on which you can install NodeJS.
|
||||||
|
* Knowledge of Git submodules and npm.
|
||||||
|
|
||||||
$ homebridge
|
You'll also need some patience, as Siri can be very strict about sentence structure, and occasionally she will forget about HomeKit altogether. But it's not surprising that HomeKit isn't rock solid, since almost no one can actually use it today besides developers who are creating hardware accessories for it. There are, to my knowledge, exactly zero licensed HomeKit devices on the market right now, so Apple can easily get away with this all being a work in progress.
|
||||||
Couldn't find a config.json file [snip]
|
|
||||||
|
|
||||||
However, Homebridge won't do anything until you've created a `config.json` file containing your accessories and/or platforms. You can start by copying and modifying the included `config-sample.json` file which includes declarations for some example accessories and platforms. Each Plugin will have its own expected configuration; the documentation for Plugins should give you some real-world examples for that plugin.
|
# Getting Started
|
||||||
|
|
||||||
**NOTE**: Your `config.json` file MUST be inside of `.homebridge`, which is inside of your home folder. On macOS and Linux, the full path for your `config.json` would be `~/.homebridge/config.json`. Any error messages will contain the exact path where your config is expected to be found.
|
OK, if you're still excited enough about ordering Siri to make your coffee (which, who wouldn't be!) then here's how to set things up.
|
||||||
|
|
||||||
**REALLY IMPORTANT**: You must use a "plain text" editor to create or modify `config.json`. Do NOT use apps like TextEdit on Mac or Wordpad on Windows. Apps like these will corrupt the formatting of the file in hard-to-debug ways, making improper `"` signs is an example. I suggest using the free [Atom text editor](http://atom.io).
|
**Note:** If you're running on Linux, you'll need to make sure you have the `libavahi-compat-libdnssd-dev` package installed.
|
||||||
|
|
||||||
|
First, clone this repo:
|
||||||
|
|
||||||
Once you've added your config file, you should be able to run Homebridge again:
|
$ git clone https://github.com/nfarina/homebridge.git
|
||||||
|
$ cd homebridge
|
||||||
|
$ script/bootstrap
|
||||||
|
|
||||||
$ homebridge
|
**Node**: You'll need to have NodeJS version 0.12.x or better installed for required submodule `HAP-NodeJS` to load as well as the `forever` node package..
|
||||||
Loaded plugin: homebridge-lockitron
|
|
||||||
Registering accessory 'Lockitron'
|
|
||||||
---
|
|
||||||
Loaded config.json with 1 accessories and 0 platforms.
|
|
||||||
---
|
|
||||||
Loading 0 platforms...
|
|
||||||
Loading 1 accessories...
|
|
||||||
[Back Door] Initializing Lockitron accessory...
|
|
||||||
|
|
||||||
Homebridge is now ready to receive commands from iOS.
|
The server won't do anything until you've edited your `config.json` file containing your home devices (or _accessories_ in HomeKit parlance) or platforms you wish to make available to iOS. The sample configuration has been copied for you into `config.json`. It includes declarations for all supported accessories and platforms. Remove everything except for the accessories and platforms you'll be using.
|
||||||
|
|
||||||
|
Now you should be able to run the homebridge server:
|
||||||
|
|
||||||
# Installing Plugins
|
$ cd homebridge
|
||||||
|
$ script/server
|
||||||
|
|
||||||
Plugins are NodeJS modules published through NPM and tagged with the keyword `homebridge-plugin`. They must have a name with the prefix `homebridge-`, like **homebridge-mysmartlock**.
|
Starting Homebridge server...
|
||||||
|
Loading 6 accessories...
|
||||||
|
[Speakers] Initializing 'Sonos' accessory...
|
||||||
|
[Coffee Maker] Initializing 'WeMo' accessory...
|
||||||
|
[Speakers] Initializing 'Sonos' accessory...
|
||||||
|
[Coffee Maker] Initializing 'WeMo' accessory...
|
||||||
|
[Wink] Initializing Wink platform...
|
||||||
|
[Wink] Fetching Wink devices.
|
||||||
|
[Wink] Initializing device with name Living Room Lamp...
|
||||||
|
|
||||||
Plugins can publish Accessories and/or Platforms. Accessories are individual devices, like a smart switch or a garage door. Platforms act like a single device but can expose a set of devices, like a house full of smart lightbulbs.
|
Your server is now ready to receive commands from iOS.
|
||||||
|
|
||||||
You install Plugins the same way you installed Homebridge - as a global NPM module. For example:
|
# Installing Homebridge to Run at Boot and in the Background
|
||||||
|
Homebridge can be run at boot and in the background on OS X and any Linux variation that uses SysVinit (/etc/init.d scripts) to launch services. To install homebridge as
|
||||||
|
a service:
|
||||||
|
|
||||||
sudo npm install -g homebridge-lockitron
|
$ script/install
|
||||||
|
|
||||||
You can explore all available plugins at the NPM website by [searching for the keyword `homebridge-plugin`](https://www.npmjs.com/search?q=homebridge-plugin).
|
It should load for you in the background. You can find logs in `log/logs.log`. To uninstall it you can run `script/uninstall`. To restart it you can run `script/restart`.
|
||||||
|
|
||||||
**IMPORTANT**: Many of the plugins that Homebridge used to include with its default installation have been moved to the single plugin [homebridge-legacy-plugins](https://www.npmjs.com/package/homebridge-legacy-plugins).
|
# Upgrading
|
||||||
|
|
||||||
# Adding Homebridge to iOS
|
If you want to upgrade homebridge, simply run:
|
||||||
|
|
||||||
HomeKit itself is actually not an app; it's a "database" similar to HealthKit and PassKit. Where HealthKit has the companion _Health_ app and PassKit has _Passbook_, HomeKit has the _Home_ app, introduced with iOS 10.
|
$ script/upgrade
|
||||||
|
|
||||||
If you are a member of the iOS developer program, you might also find Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app to be useful, as it provides straightforward and comprehensive management of all HomeKit database "objects".
|
It will pull the newest version from the repo on GitHub and restart itself.
|
||||||
|
|
||||||
Using the Home app (or most other HomeKit apps), you should be able to add the single accessory "Homebridge", assuming that you're still running Homebridge and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
|
# Adding your devices to iOS
|
||||||
|
|
||||||
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`).
|
HomeKit is actually not an app; it's a "database" similar to HealthKit and PassKit. But where HealthKit has the companion _Health_ app and PassKit has _Passbook_, Apple has supplied no app for managing your HomeKit database (at least [not yet](http://9to5mac.com/2015/05/20/apples-planned-ios-9-home-app-uses-virtual-rooms-to-manage-homekit-accessories/)). However, the HomeKit API is open for developers to write their own apps for adding devices to HomeKit.
|
||||||
|
|
||||||
|
Fortunately, there are now a few apps in the App Store that can manage your HomeKit devices. The most comprehensive one I've used is [MyTouchHome](https://itunes.apple.com/us/app/mytouchhome/id965142360?mt=8&at=11lvmd&ct=mhweb) which costs $2.
|
||||||
|
|
||||||
|
There are also some free apps that work OK. Try [Insteon+](https://itunes.apple.com/US/app/id919270334?mt=8) or [Lutron](https://itunes.apple.com/us/app/lutron-app-for-caseta-wireless/id886753021?mt=8) or a number of others.
|
||||||
|
|
||||||
|
If you are a member of the iOS developer program, I highly recommend Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app, as it is reliable and comprehensive and free (and open source).
|
||||||
|
|
||||||
|
## Adding HomeKit Accessories
|
||||||
|
|
||||||
|
Once you've gotten a HomeKit app running on your iOS device, you can use it to add your Homebridge devices. The app should "discover" the single accessory "Homebridge", assuming that you're still running the Homebridge server and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
|
||||||
|
|
||||||
|
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`). This process will create some files in the `persist` directory of the Homebridge server, which stores the pairing relationship.
|
||||||
|
|
||||||
# Interacting with your Devices
|
# Interacting with your Devices
|
||||||
|
|
||||||
Once your device has been added to HomeKit, you should be able to tell Siri to control your devices. However, realize that Siri is a cloud service, and iOS may need some time to synchronize your device information with iCloud.
|
Once your device has been added to HomeKit, you should be able to tell Siri to control your devices. However, realize that Siri is a cloud service, and iOS may need some time to synchronize your device information with iCloud.
|
||||||
|
|
||||||
One final thing to remember is that Siri will almost always prefer its default phrase handling over HomeKit devices. For instance, if you name your Sonos device "Radio" and try saying "Siri, turn on the Radio" then Siri will probably start playing an iTunes Radio station on your phone. Even if you name it "Sonos" and say "Siri, turn on the Sonos", Siri will probably just launch the Sonos app instead. This is why, for instance, the suggested `name` for the Sonos accessory is "Speakers".
|
One final thing to remember is that Siri will almost always prefer its default phrase handling over HomeKit devices. For instance, if you name your Sonos device "Radio" and try saying "Siri, turn on the Radio" then Siri will probably start playing an iTunes Radio station on your phone. Even if you name it "Sonos" and say "Siri, turn on the Sonos", Siri will probably just launch the Sonos app instead. This is why, for instance, the suggested `name` for the Sonos shim in `config-samples.json` is "Speakers".
|
||||||
|
|
||||||
# Writing Plugins
|
# Final Notes
|
||||||
|
|
||||||
We don't have a lot of documentation right now for creating plugins, but there are many existing plugins you can study.
|
HomeKit is definitely amazing when it works. Speaking to Siri is often much quicker and easier than launching whatever app your device manufacturer provides.
|
||||||
|
|
||||||
The best place to start is the included [Example Plugins](https://github.com/nfarina/homebridge/tree/master/example-plugins). Right now this contains a single plugin that registers a platform that offers fake light accessories. This will show you how to use the Homebridge Plugin API.
|
I welcome any suggestions or pull requests, but keep in mind that it's likely not possible to support all the things you might want to do with a device through HomeKit. For instance, you might want to hack the Sonos shim to play the specific kind of music you want and that's great, but it might not be appropriate to merge those specific changes into this repository. The shims here should be mostly simple "canonical examples" and easily hackable by others.
|
||||||
|
|
||||||
For more example on how to construct HomeKit Services and Characteristics, see the many Accessories in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/accessories) repository.
|
Good luck!
|
||||||
|
|
||||||
You can also view the [full list of supported HomeKit Services and Characteristics in the HAP-NodeJS protocol repository](https://github.com/KhaosT/HAP-NodeJS/blob/master/lib/gen/HomeKitTypes.js).
|
|
||||||
|
|
||||||
And you can find an example plugin that publishes an individual accessory at [here](https://github.com/nfarina/homebridge/tree/6500912f54a70ff479e63e2b72760ab589fa558a/example-plugins/homebridge-lockitron).
|
|
||||||
|
|
||||||
See more examples on how to create Platform classes in the [Legacy Plugins](https://github.com/nfarina/homebridge-legacy-plugins/tree/master/platforms) repository.
|
|
||||||
|
|
||||||
# Plugin Development
|
|
||||||
|
|
||||||
When writing your plugin, you'll want Homebridge to load it from your development directory instead of publishing it to `npm` each time. You can tell Homebridge to look for your plugin at a specific location using the command-line parameter `-P`. For example, if you are in the Homebridge directory (as checked out from Github), you might type:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
DEBUG=* ./bin/homebridge -D -P ../my-great-plugin/
|
|
||||||
```
|
|
||||||
|
|
||||||
This will start up Homebridge and load your in-development plugin from a nearby directory. Note that you can also direct Homebridge to load your configuration from somewhere besides the default `~/.homebridge`, for example:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
DEBUG=* ./bin/homebridge -D -U ~/.homebridge-dev -P ../my-great-plugin/
|
|
||||||
```
|
|
||||||
|
|
||||||
This is very useful when you are already using your development machine to host a "real" Homebridge instance (with all your accessories) that you don't want to disturb.
|
|
||||||
|
|
||||||
# Common Issues
|
|
||||||
|
|
||||||
### My iOS App Can't Find Homebridge
|
|
||||||
|
|
||||||
Two reasons why Homebridge may not be discoverable:
|
|
||||||
|
|
||||||
1. Homebridge server thinks it's been paired with, but iOS thinks otherwise. Fix: deleted `persist/` directory which is next to your `config.json`.
|
|
||||||
|
|
||||||
2. iOS device has gotten your Homebridge `username` (looks like a MAC address) "stuck" somehow, where it's in the database but inactive. Fix: change your `username` in the "bridge" section of `config.json` to be some new value.
|
|
||||||
|
|
||||||
### Errors on startup
|
|
||||||
|
|
||||||
The following errors are experienced when starting Homebridge and can be safely ignored. The cost of removing the issue at the core of the errors isn't worth the effort.
|
|
||||||
|
|
||||||
```
|
|
||||||
*** WARNING *** The program 'nodejs' uses the Apple Bonjour compatibility layer of Avahi
|
|
||||||
*** WARNING *** Please fix your application to use the native API of Avahi!
|
|
||||||
*** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs
|
|
||||||
*** WARNING *** The program 'nodejs' called 'DNSServiceRegister()' which is not supported (or only supported partially) in the Apple Bonjour compatibility layer of Avahi
|
|
||||||
*** WARNING *** Please fix your application to use the native API of Avahi!
|
|
||||||
*** WARNING *** For more information see http://0pointerde/avahi-compat?s=libdns_sd&e=nodejs&f=DNSServiceRegister
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limitations
|
|
||||||
|
|
||||||
* One installation of Homebridge can only expose 100 accessories due to a HomeKit limit. You can however run multiple Homebridge instances by pointing them to different config and persistence paths (see issue #827).
|
|
||||||
* Once an accessory has been added to the Home app, changing its name via Homebridge won't be automatically reflected in iOS. You must change it via the Home app as well.
|
|
||||||
|
|
||||||
# Why Homebridge?
|
|
||||||
|
|
||||||
Technically, the device manufacturers should be the ones implementing the HomeKit API. And I'm sure they will - eventually. When they do, this project will be obsolete, and I hope that happens soon. In the meantime, Homebridge is a fun way to get a taste of the future, for those who just can't bear to wait until "real" HomeKit devices are on the market.
|
|
||||||
|
|
||||||
# Credit
|
|
||||||
|
|
||||||
The original HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project.
|
|
||||||
|
|||||||
245
accessories/AD2USB.js
Normal file
245
accessories/AD2USB.js
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var AD2USB = require('ad2usb');
|
||||||
|
var CUSTOM_PANEL_LCD_TEXT_CTYPE = "A3E7B8F9-216E-42C1-A21C-97D4E3BE52C8";
|
||||||
|
|
||||||
|
function AD2USBAccessory(log, config) {
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.host = config["host"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.pin = config["pin"];
|
||||||
|
var that = this;
|
||||||
|
this.currentArmState = 2;
|
||||||
|
this.currentStateCharacteristic = undefined;
|
||||||
|
this.targetStateCharacteristic = undefined;
|
||||||
|
this.lcdCharacteristic = undefined;
|
||||||
|
|
||||||
|
var alarm = AD2USB.connect(this.host, this.port, function() {
|
||||||
|
|
||||||
|
// Send an initial empty character to get status
|
||||||
|
alarm.send('');
|
||||||
|
|
||||||
|
// Armed Away
|
||||||
|
alarm.on('armedAway', function() {
|
||||||
|
|
||||||
|
that.log("Armed to AWAY");
|
||||||
|
if (that.currentStateCharacteristic) {
|
||||||
|
that.currentStateCharacteristic.updateValue(0, null);
|
||||||
|
}
|
||||||
|
if (that.targetStateCharacteristic) {
|
||||||
|
that.targetStateCharacteristic.updateValue(1, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Armed Stay
|
||||||
|
alarm.on('armedStay', function() {
|
||||||
|
|
||||||
|
that.log("Armed to STAY");
|
||||||
|
if (that.currentStateCharacteristic) {
|
||||||
|
that.currentStateCharacteristic.updateValue(0, null);
|
||||||
|
}
|
||||||
|
if (that.targetStateCharacteristic) {
|
||||||
|
that.targetStateCharacteristic.updateValue(0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Armed Night
|
||||||
|
alarm.on('armedNight', function() {
|
||||||
|
|
||||||
|
that.log("Armed to NIGHT");
|
||||||
|
if (that.currentStateCharacteristic) {
|
||||||
|
that.currentStateCharacteristic.updateValue(0, null);
|
||||||
|
}
|
||||||
|
if (that.targetStateCharacteristic) {
|
||||||
|
that.targetStateCharacteristic.updateValue(2, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disarmed
|
||||||
|
alarm.on('disarmed', function() {
|
||||||
|
|
||||||
|
that.log("Disarmed");
|
||||||
|
if (that.currentStateCharacteristic) {
|
||||||
|
that.currentStateCharacteristic.updateValue(1, null);
|
||||||
|
}
|
||||||
|
if (that.targetStateCharacteristic) {
|
||||||
|
that.targetStateCharacteristic.updateValue(3, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Text Change
|
||||||
|
alarm.on('lcdtext', function(newText) {
|
||||||
|
|
||||||
|
that.log("LCD: " + newText);
|
||||||
|
if (that.lcdCharacteristic) {
|
||||||
|
that.lcdCharacteristic.updateValue(newText, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
this.alarm = alarm;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
AD2USBAccessory.prototype = {
|
||||||
|
|
||||||
|
setArmState: function(targetArmState) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
that.log("Desired target arm state: " + targetArmState);
|
||||||
|
|
||||||
|
// TARGET
|
||||||
|
// 0 - Stay
|
||||||
|
// 1 - Away
|
||||||
|
// 2 - Night
|
||||||
|
// 3 - Disarm
|
||||||
|
if (targetArmState == 0) {
|
||||||
|
that.alarm.armStay(that.pin);
|
||||||
|
}
|
||||||
|
else if (targetArmState == 1) {
|
||||||
|
that.alarm.armAway(that.pin);
|
||||||
|
}
|
||||||
|
else if (targetArmState == 2) {
|
||||||
|
that.alarm.armNight(that.pin);
|
||||||
|
}
|
||||||
|
else if (targetArmState == 3) {
|
||||||
|
that.alarm.disarm(that.pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CURRENT
|
||||||
|
// 0 - Armed
|
||||||
|
// 1 - Disarmed
|
||||||
|
// 2 - Hold
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Nutech",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "AD2USB",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "AD2USBIF",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.ALARM_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.ALARM_CURRENT_STATE_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRegister: function(characteristic) {
|
||||||
|
|
||||||
|
that.currentStateCharacteristic = characteristic;
|
||||||
|
characteristic.eventEnabled = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 2,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Alarm current arm state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.ALARM_TARGET_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setArmState(value); },
|
||||||
|
onRegister: function(characteristic) {
|
||||||
|
|
||||||
|
that.targetStateCharacteristic = characteristic;
|
||||||
|
characteristic.eventEnabled = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 1,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Alarm target arm state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cType: CUSTOM_PANEL_LCD_TEXT_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRegister: function(characteristic) {
|
||||||
|
|
||||||
|
that.lcdCharacteristic = characteristic;
|
||||||
|
characteristic.eventEnabled = true;
|
||||||
|
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Unknown",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Keypad Text",
|
||||||
|
designedMaxLength: 64
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = AD2USBAccessory;
|
||||||
126
accessories/Carwings.js
Normal file
126
accessories/Carwings.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var carwings = require("carwingsjs");
|
||||||
|
|
||||||
|
function CarwingsAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
}
|
||||||
|
|
||||||
|
CarwingsAccessory.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
carwings.login(this.username, this.password, function(err, result) {
|
||||||
|
if (!err) {
|
||||||
|
that.vin = result.vin;
|
||||||
|
that.log("Got VIN: " + that.vin);
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
carwings.startClimateControl(that.vin, null, function(err, result) {
|
||||||
|
if (!err)
|
||||||
|
that.log("Started climate control.");
|
||||||
|
else
|
||||||
|
that.log("Error starting climate control: " + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
carwings.stopClimateControl(that.vin, function(err, result) {
|
||||||
|
if (!err)
|
||||||
|
that.log("Stopped climate control.");
|
||||||
|
else
|
||||||
|
that.log("Error stopping climate control: " + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error logging in: " + err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Nissan",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.SWITCH_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state of the car",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = CarwingsAccessory;
|
||||||
126
accessories/ELKM1.js
Normal file
126
accessories/ELKM1.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var elkington = require("elkington");
|
||||||
|
|
||||||
|
function ElkM1Accessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.zone = config["zone"];
|
||||||
|
this.host = config["host"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.pin = config["pin"];
|
||||||
|
this.arm = config["arm"];
|
||||||
|
}
|
||||||
|
|
||||||
|
ElkM1Accessory.prototype = {
|
||||||
|
setPowerState: function(alarmOn) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (!alarmOn)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var elk = elkington.createConnection({
|
||||||
|
port: that.port,
|
||||||
|
host: that.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (that.arm)
|
||||||
|
{
|
||||||
|
case 'Away':
|
||||||
|
elk.armAway({area: that.zone, code: that.pin});
|
||||||
|
break;
|
||||||
|
case 'Stay':
|
||||||
|
elk.armStay({area: that.zone, code: that.pin});
|
||||||
|
break;
|
||||||
|
case 'Night':
|
||||||
|
elk.armNightInstant({area: that.zone, code: that.pin});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Elk",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "M1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.SWITCH_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Alarm the Zone",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = ElkM1Accessory;
|
||||||
76
accessories/FileSensor.js
Normal file
76
accessories/FileSensor.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var chokidar = require("chokidar");
|
||||||
|
var debug = require("debug")("FileSensorAccessory");
|
||||||
|
var crypto = require("crypto");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: FileSensorAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileSensorAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
// url info
|
||||||
|
this.name = config["name"];
|
||||||
|
this.path = config["path"];
|
||||||
|
this.window_seconds = config["window_seconds"] || 5;
|
||||||
|
this.sensor_type = config["sensor_type"] || "m";
|
||||||
|
this.inverse = config["inverse"] || false;
|
||||||
|
|
||||||
|
if(config["sn"]){
|
||||||
|
this.sn = config["sn"];
|
||||||
|
} else {
|
||||||
|
var shasum = crypto.createHash('sha1');
|
||||||
|
shasum.update(this.path);
|
||||||
|
this.sn = shasum.digest('base64');
|
||||||
|
debug('Computed SN ' + this.sn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileSensorAccessory.prototype = {
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
// you can OPTIONALLY create an information service if you wish to override
|
||||||
|
// the default values for things like serial number, model, etc.
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Name, this.name)
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Homebridge")
|
||||||
|
.setCharacteristic(Characteristic.Model, "File Sensor")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.sn);
|
||||||
|
|
||||||
|
var service, changeAction;
|
||||||
|
if(this.sensor_type === "c"){
|
||||||
|
service = new Service.ContactSensor();
|
||||||
|
changeAction = function(newState){
|
||||||
|
service.getCharacteristic(Characteristic.ContactSensorState)
|
||||||
|
.setValue(newState ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
service = new Service.MotionSensor();
|
||||||
|
changeAction = function(newState){
|
||||||
|
service.getCharacteristic(Characteristic.MotionDetected)
|
||||||
|
.setValue(newState);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var changeHandler = function(path, stats){
|
||||||
|
var d = new Date();
|
||||||
|
if(d.getTime() - stats.mtime.getTime() <= (this.window_seconds * 1000)){
|
||||||
|
var newState = this.inverse ? false : true;
|
||||||
|
changeAction(newState);
|
||||||
|
if(this.timer !== undefined) clearTimeout(this.timer);
|
||||||
|
this.timer = setTimeout(function(){changeAction(!newState);}, this.window_seconds * 1000);
|
||||||
|
}
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
var watcher = chokidar.watch(this.path, {alwaysStat: true});
|
||||||
|
watcher.on('add', changeHandler);
|
||||||
|
watcher.on('change', changeHandler);
|
||||||
|
|
||||||
|
return [informationService, service];
|
||||||
|
}
|
||||||
|
};
|
||||||
58
accessories/GenericRS232Device.js
Normal file
58
accessories/GenericRS232Device.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var SerialPort = require("serialport").SerialPort;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: GenericRS232DeviceAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function GenericRS232DeviceAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.id = config["id"];
|
||||||
|
this.name = config["name"];
|
||||||
|
this.model_name = config["model_name"];
|
||||||
|
this.manufacturer = config["manufacturer"];
|
||||||
|
this.on_command = config["on_command"];
|
||||||
|
this.off_command = config["off_command"];
|
||||||
|
this.device = config["device"];
|
||||||
|
this.baudrate = config["baudrate"];
|
||||||
|
}
|
||||||
|
|
||||||
|
GenericRS232DeviceAccessory.prototype = {
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var that = this;
|
||||||
|
var command = powerOn ? that.on_command : that.off_command;
|
||||||
|
var serialPort = new SerialPort(that.device, { baudrate: that.baudrate }, false);
|
||||||
|
serialPort.open(function (error) {
|
||||||
|
if (error) {
|
||||||
|
callback(new Error('Can not communicate with ' + that.name + " (" + error + ")"))
|
||||||
|
} else {
|
||||||
|
serialPort.write(command, function(err, results) {
|
||||||
|
if (error) {
|
||||||
|
callback(new Error('Can not send power command to ' + that.name + " (" + err + ")"))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var switchService = new Service.Switch(this.name);
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, this.manufacturer)
|
||||||
|
.setCharacteristic(Characteristic.Model, this.model_name)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.id);
|
||||||
|
|
||||||
|
switchService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, switchService];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.accessory = GenericRS232DeviceAccessory;
|
||||||
141
accessories/HomeMatic.js
Normal file
141
accessories/HomeMatic.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function HomeMatic(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.ccuID = config["ccu_id"];
|
||||||
|
this.ccuIP = config["ccu_ip"];
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeMatic.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
|
||||||
|
var binaryState = powerOn ? 1 : 0;
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting power state of CCU to " + powerOn);
|
||||||
|
this.log(this.ccuID+ powerOn);
|
||||||
|
|
||||||
|
request.put({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+this.ccuID+"&new_value="+ powerOn,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
that.log("State change complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting lock state: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getPowerState: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Getting Power State of CCU");
|
||||||
|
request.get({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
//that.log("Response:"+response.body);
|
||||||
|
var responseString = response.body.substring(83,87);
|
||||||
|
//that.log(responseString);
|
||||||
|
switch(responseString){
|
||||||
|
case "true": {modvalue = "1";break;}
|
||||||
|
case "fals": {modvalue = "0";break;}
|
||||||
|
}
|
||||||
|
callback(parseInt(modvalue));
|
||||||
|
that.log("Getting Power State complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting Power State: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "WeMo",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.SWITCH_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
onRead: function(callback) { that.getPowerState(callback); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state of a Variable",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = HomeMatic;
|
||||||
264
accessories/HomeMaticThermo.js
Normal file
264
accessories/HomeMaticThermo.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function HomeMaticThermo(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.ccuIDTargetTemp = config["ccu_id_TargetTemp"];
|
||||||
|
this.ccuIDCurrentTemp = config["ccu_id_CurrentTemp"];
|
||||||
|
this.ccuIDControlMode = config["ccu_id_ControlMode"];
|
||||||
|
this.ccuIDManuMode = config["ccu_id_ManuMode"];
|
||||||
|
this.ccuIDAutoMode = config["ccu_id_AutoMode"];
|
||||||
|
this.ccuIP = config["ccu_ip"];
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeMaticThermo.prototype = {
|
||||||
|
|
||||||
|
setTargetTemperature: function(value) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting target Temperature of CCU to " + value);
|
||||||
|
this.log(this.ccuIDTargetTemp + " " + value);
|
||||||
|
|
||||||
|
request.put({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+this.ccuIDTargetTemp+"&new_value="+ value,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
that.log("State change complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting Temperature: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getCurrentTemperature: function(callback) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Getting current Temperature of CCU");
|
||||||
|
request.get({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDCurrentTemp,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
//that.log("Response:"+response.body);
|
||||||
|
var responseString = response.body.substring(83,87);
|
||||||
|
//that.log(responseString);
|
||||||
|
callback(parseFloat(responseString));
|
||||||
|
//that.log("Getting current temperature complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting Temperature: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getTargetTemperature: function(callback) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Getting target Temperature of CCU");
|
||||||
|
request.get({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDTargetTemp,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
//that.log("Response:"+response.body);
|
||||||
|
var responseString = response.body.substring(83,87);
|
||||||
|
//that.log(responseString);
|
||||||
|
callback(parseFloat(responseString));
|
||||||
|
//that.log("Getting target temperature complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting Temperature: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getMode: function(callback) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
//this.log("Getting target Mode of CCU");
|
||||||
|
//this.log(this.ccuID+ value);
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDControlMode,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
//that.log("Response:"+response.body);
|
||||||
|
var responseInt = response.body.substring(83,84);
|
||||||
|
//that.log(responseString);
|
||||||
|
if (responseInt == 1)
|
||||||
|
{ callback(parseInt("0")); }
|
||||||
|
if (responseInt == 0)
|
||||||
|
{ callback(parseInt("1")); }
|
||||||
|
//that.log("Getting mode complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting Mode: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setMode: function(value) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
//this.log("Seting target Mode of CCU:" + value);
|
||||||
|
var modvalue;
|
||||||
|
var dpID;
|
||||||
|
switch(value) {
|
||||||
|
case 3: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //auto
|
||||||
|
case 1: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //heating => auto
|
||||||
|
default: {modvalue = "1";dpID=this.ccuIDManuMode;} //default => off (manual)
|
||||||
|
}
|
||||||
|
|
||||||
|
request.put({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+dpID+"&new_value="+ modvalue,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
//that.log("Setting Mode complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting Mode: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "test",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "test",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NREF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.THERMOSTAT_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||||
|
onRead: function(callback) { that.getMode(callback); },
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Mode",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 2,
|
||||||
|
designedMinStep: 1,
|
||||||
|
},{
|
||||||
|
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||||
|
onRead: function(callback) { that.getMode(callback); },
|
||||||
|
onUpdate: function(value) { that.setMode(value);},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Mode",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 3,
|
||||||
|
designedMinStep: 1,
|
||||||
|
},{
|
||||||
|
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||||
|
onRead: function(callback) { that.getCurrentTemperature(callback); },
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "float",
|
||||||
|
initialValue: 13.0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Temperature",
|
||||||
|
unit: "celsius"
|
||||||
|
},{
|
||||||
|
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setTargetTemperature(value); },
|
||||||
|
onRead: function(callback) { that.getTargetTemperature(callback); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "float",
|
||||||
|
initialValue: 19.0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Temperature",
|
||||||
|
designedMinValue: 4,
|
||||||
|
designedMaxValue: 25,
|
||||||
|
designedMinStep: 0.1,
|
||||||
|
unit: "celsius"
|
||||||
|
},{
|
||||||
|
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Unit"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = HomeMaticThermo;
|
||||||
123
accessories/HomeMaticWindow.js
Normal file
123
accessories/HomeMaticWindow.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function HomeMaticWindow(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.ccuID = config["ccu_id"];
|
||||||
|
this.ccuIP = config["ccu_ip"];
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeMaticWindow.prototype = {
|
||||||
|
|
||||||
|
|
||||||
|
getPowerState: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Getting Window State of CCU");
|
||||||
|
request.get({
|
||||||
|
url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
//that.log("Response:"+response.body);
|
||||||
|
var responseString = response.body.substring(83,84);
|
||||||
|
//that.log(responseString);
|
||||||
|
switch(responseString){
|
||||||
|
case "0": {callback(Characteristic.ContactSensorState.CONTACT_DETECTED);break;}
|
||||||
|
case "1": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;}
|
||||||
|
case "2": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;}
|
||||||
|
}
|
||||||
|
that.log("Getting Window State complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting Window State: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Homematic",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "HM-Sec-RHS",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.CONTACT_SENSOR_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.CONTACT_SENSOR_STATE_CTYPE,
|
||||||
|
onRead: function(callback) { that.getPowerState(callback); },
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Get Window state of a Variable",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = HomeMaticWindow;
|
||||||
100
accessories/Http.js
Normal file
100
accessories/Http.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: HttpAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function HttpAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
// url info
|
||||||
|
this.on_url = config["on_url"];
|
||||||
|
this.off_url = config["off_url"];
|
||||||
|
this.brightness_url = config["brightness_url"];
|
||||||
|
this.http_method = config["http_method"];
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpAccessory.prototype = {
|
||||||
|
|
||||||
|
httpRequest: function(url, method, callback) {
|
||||||
|
request({
|
||||||
|
url: url,
|
||||||
|
method: method
|
||||||
|
},
|
||||||
|
function (error, response, body) {
|
||||||
|
callback(error, response, body)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var url;
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
url = this.on_url;
|
||||||
|
this.log("Setting power state to on");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
url = this.off_url;
|
||||||
|
this.log("Setting power state to off");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.httpRequest(url, this.http_method, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
this.log('HTTP power function failed: %s', error.message);
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log('HTTP power function succeeded!');
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level, callback) {
|
||||||
|
var url = this.brightness_url.replace("%b", level)
|
||||||
|
|
||||||
|
this.log("Setting brightness to %s", level);
|
||||||
|
|
||||||
|
this.httpRequest(url, this.http_method, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
this.log('HTTP brightness function failed: %s', error);
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log('HTTP brightness function succeeded!');
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
// you can OPTIONALLY create an information service if you wish to override
|
||||||
|
// the default values for things like serial number, model, etc.
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||||
|
.setCharacteristic(Characteristic.Model, "HTTP Model")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||||
|
|
||||||
|
var lightbulbService = new Service.Lightbulb();
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(new Characteristic.Brightness())
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
|
||||||
|
return [informationService, lightbulbService];
|
||||||
|
}
|
||||||
|
};
|
||||||
71
accessories/HttpHygrometer.js
Normal file
71
accessories/HttpHygrometer.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: HygrometerAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function HygrometerAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
// url info
|
||||||
|
this.url = config["url"];
|
||||||
|
this.http_method = config["http_method"];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HygrometerAccessory.prototype = {
|
||||||
|
|
||||||
|
httpRequest: function(url, method, callback) {
|
||||||
|
request({
|
||||||
|
url: url,
|
||||||
|
method: method
|
||||||
|
},
|
||||||
|
function (error, response, body) {
|
||||||
|
callback(error, response, body)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentRelativeHumidity: function (callback) {
|
||||||
|
var that = this;
|
||||||
|
that.log ("getting CurrentCurrentRelativeHumidity");
|
||||||
|
|
||||||
|
this.httpRequest(this.url, this.http_method, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
this.log('HTTP function failed: %s', error);
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log('HTTP function succeeded - %s', body);
|
||||||
|
callback(null, Number(body));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
// you can OPTIONALLY create an information service if you wish to override
|
||||||
|
// the default values for things like serial number, model, etc.
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||||
|
.setCharacteristic(Characteristic.Model, "HTTP Hygrometer")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||||
|
|
||||||
|
var humidityService = new Service.HumiditySensor();
|
||||||
|
|
||||||
|
humidityService
|
||||||
|
.getCharacteristic(Characteristic.CurrentRelativeHumidity)
|
||||||
|
.on('get', this.getCurrentRelativeHumidity.bind(this));
|
||||||
|
|
||||||
|
return [informationService, humidityService];
|
||||||
|
}
|
||||||
|
};
|
||||||
79
accessories/HttpThermometer.js
Normal file
79
accessories/HttpThermometer.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: ThermometerAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThermometerAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
// url info
|
||||||
|
this.url = config["url"];
|
||||||
|
this.http_method = config["http_method"];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThermometerAccessory.prototype = {
|
||||||
|
|
||||||
|
httpRequest: function(url, method, callback) {
|
||||||
|
request({
|
||||||
|
url: url,
|
||||||
|
method: method
|
||||||
|
},
|
||||||
|
function (error, response, body) {
|
||||||
|
callback(error, response, body)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentTemperature: function (callback) {
|
||||||
|
var that = this;
|
||||||
|
that.log ("getting CurrentTemperature");
|
||||||
|
|
||||||
|
|
||||||
|
this.httpRequest(this.url, this.http_method, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
this.log('HTTP function failed: %s', error);
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log('HTTP function succeeded - %s', body);
|
||||||
|
callback(null, Number(body));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
getTemperatureUnits: function (callback) {
|
||||||
|
var that = this;
|
||||||
|
that.log ("getTemperature Units");
|
||||||
|
// 1 = F and 0 = C
|
||||||
|
callback (null, 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
// you can OPTIONALLY create an information service if you wish to override
|
||||||
|
// the default values for things like serial number, model, etc.
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
|
||||||
|
.setCharacteristic(Characteristic.Model, "HTTP Thermometer")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
|
||||||
|
|
||||||
|
var temperatureService = new Service.TemperatureSensor();
|
||||||
|
|
||||||
|
temperatureService
|
||||||
|
.getCharacteristic(Characteristic.CurrentTemperature)
|
||||||
|
.on('get', this.getCurrentTemperature.bind(this));
|
||||||
|
|
||||||
|
return [informationService, temperatureService];
|
||||||
|
}
|
||||||
|
};
|
||||||
221
accessories/Hyperion.js
Normal file
221
accessories/Hyperion.js
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var net = require('net');
|
||||||
|
var Color = require('color');
|
||||||
|
|
||||||
|
function HyperionAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.host = config["host"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.name = config["name"];
|
||||||
|
this.color = Color().hsv([0, 0, 0]);
|
||||||
|
this.prevColor = Color().hsv([0,0,100]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
HyperionAccessory.prototype = {
|
||||||
|
|
||||||
|
sendHyperionCommand: function(command, cmdParams, priority) {
|
||||||
|
var that = this;
|
||||||
|
var client = new net.Socket();
|
||||||
|
var data = {};
|
||||||
|
|
||||||
|
if (typeof priority === 'undefined') { priority = 100; }
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case 'color':
|
||||||
|
data = {"command":"color", "priority":priority,"color":cmdParams};
|
||||||
|
break;
|
||||||
|
case 'blacklevel':
|
||||||
|
data = {"command":"transform","transform":{"blacklevel":cmdParams}}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
that.log("Hyperion command not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//that.log(JSON.stringify(data));
|
||||||
|
|
||||||
|
client.connect(that.port, that.host, function() {
|
||||||
|
client.write(JSON.stringify(data) + "\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('data', function(data){
|
||||||
|
that.log("Response: " + data.toString().trim());
|
||||||
|
that.log("***** Color HSV:" + that.color.hsvArray() + "*****");
|
||||||
|
that.log("***** Color RGB:" + that.color.rgbArray() + "*****");
|
||||||
|
client.end();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
that.log("Setting power state on the '"+that.name+"' to on");
|
||||||
|
that.color.rgb(that.prevColor.rgb());
|
||||||
|
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||||
|
} else {
|
||||||
|
that.log("Setting power state on the '"+that.name+"' to off");
|
||||||
|
that.prevColor.rgb(that.color.rgb());
|
||||||
|
that.color.value(0);
|
||||||
|
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||||
|
that.sendHyperionCommand('blacklevel', [0,0,0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.color.value(level);
|
||||||
|
that.log("Setting brightness on the '"+that.name+"' to '" + level + "'");
|
||||||
|
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setHue: function(level) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.color.hue(level);
|
||||||
|
that.prevColor.hue(level);
|
||||||
|
that.log("Setting hue on the '"+that.name+"' to '" + level + "'");
|
||||||
|
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setSaturation: function(level) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.color.saturationv(level);
|
||||||
|
that.prevColor.saturationv(level);
|
||||||
|
that.log("Setting saturation on the '"+that.name+"' to '" + level + "'");
|
||||||
|
that.sendHyperionCommand('color', that.color.rgbArray());
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Hyperion",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "DEADBEEF",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
onRead: ((that.color.value() > 0) ? true : false),
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Turn on the light",
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setBrightness(value); },
|
||||||
|
onRead: that.color.value(),
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.color.value(),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
},{
|
||||||
|
cType: types.HUE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setHue(value) },
|
||||||
|
onRead: that.color.hue(),
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.color.hue(),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Hue",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 360,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "arcdegrees"
|
||||||
|
},{
|
||||||
|
cType: types.SATURATION_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setSaturation(value) },
|
||||||
|
onRead: that.color.saturationv(),
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.color.saturationv(),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Saturation",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = HyperionAccessory;
|
||||||
306
accessories/LiftMaster.js
Normal file
306
accessories/LiftMaster.js
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
// This seems to be the "id" of the official LiftMaster iOS app
|
||||||
|
var APP_ID = "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu"
|
||||||
|
|
||||||
|
function LiftMasterAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
this.requiredDeviceId = config["requiredDeviceId"];
|
||||||
|
}
|
||||||
|
|
||||||
|
LiftMasterAccessory.prototype = {
|
||||||
|
|
||||||
|
setState: function(state) {
|
||||||
|
this.targetState = state;
|
||||||
|
this.login();
|
||||||
|
},
|
||||||
|
|
||||||
|
login: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// reset our logged-in state hint until we're logged in
|
||||||
|
this.deviceId = null;
|
||||||
|
|
||||||
|
// querystring params
|
||||||
|
var query = {
|
||||||
|
appId: APP_ID,
|
||||||
|
username: this.username,
|
||||||
|
password: this.password,
|
||||||
|
culture: "en"
|
||||||
|
};
|
||||||
|
|
||||||
|
// login to liftmaster
|
||||||
|
request.get({
|
||||||
|
url: "https://myqexternal.myqdevice.com/api/user/validatewithculture",
|
||||||
|
qs: query
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
// parse and interpret the response
|
||||||
|
var json = JSON.parse(body);
|
||||||
|
that.userId = json["UserId"];
|
||||||
|
that.securityToken = json["SecurityToken"];
|
||||||
|
that.log("Logged in with user ID " + that.userId);
|
||||||
|
that.getDevice();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' logging in: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// find your garage door ID
|
||||||
|
getDevice: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// querystring params
|
||||||
|
var query = {
|
||||||
|
appId: APP_ID,
|
||||||
|
SecurityToken: this.securityToken,
|
||||||
|
filterOn: "true"
|
||||||
|
};
|
||||||
|
|
||||||
|
// some necessary duplicated info in the headers
|
||||||
|
var headers = {
|
||||||
|
MyQApplicationId: APP_ID,
|
||||||
|
SecurityToken: this.securityToken
|
||||||
|
};
|
||||||
|
|
||||||
|
// request details of all your devices
|
||||||
|
request.get({
|
||||||
|
url: "https://myqexternal.myqdevice.com/api/v4/userdevicedetails/get",
|
||||||
|
qs: query,
|
||||||
|
headers: headers
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
// parse and interpret the response
|
||||||
|
var json = JSON.parse(body);
|
||||||
|
var devices = json["Devices"];
|
||||||
|
var foundDoors = [];
|
||||||
|
|
||||||
|
// look through the array of devices for an opener
|
||||||
|
for (var i=0; i<devices.length; i++) {
|
||||||
|
var device = devices[i];
|
||||||
|
|
||||||
|
if (device["MyQDeviceTypeName"] == "GarageDoorOpener" || device["MyQDeviceTypeName"] == "VGDO") {
|
||||||
|
|
||||||
|
// If we haven't explicity specified a door ID, we'll loop to make sure we don't have multiple openers, which is confusing
|
||||||
|
if (!that.requiredDeviceId) {
|
||||||
|
var thisDeviceId = device.MyQDeviceId;
|
||||||
|
var thisDoorName = "Unknown";
|
||||||
|
for (var j = 0; j < device.Attributes.length; j ++) {
|
||||||
|
var thisAttributeSet = device.Attributes[j];
|
||||||
|
if (thisAttributeSet.AttributeDisplayName == "desc") {
|
||||||
|
thisDoorName = thisAttributeSet.Value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundDoors.push(thisDeviceId + " - " + thisDoorName);
|
||||||
|
that.deviceId = thisDeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We specified a door ID, sanity check to make sure it's the one we expected
|
||||||
|
else if (that.requiredDeviceId == device.MyQDeviceId) {
|
||||||
|
that.deviceId = device.MyQDeviceId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have multiple found doors, refuse to proceed
|
||||||
|
if (foundDoors.length > 1) {
|
||||||
|
that.log("WARNING: You have multiple doors on your MyQ account.");
|
||||||
|
that.log("WARNING: Specify the ID of the door you want to control using the 'requiredDeviceId' property in your config.json file.");
|
||||||
|
that.log("WARNING: You can have multiple liftmaster accessories to cover your multiple doors");
|
||||||
|
|
||||||
|
for (var j = 0; j < foundDoors.length; j++) {
|
||||||
|
that.log("Found Door: " + foundDoors[j]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw "FATAL: Please specify which specific door this Liftmaster accessory should control - you have multiples on your account";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did we get a device ID?
|
||||||
|
if (that.deviceId) {
|
||||||
|
that.log("Found an opener with ID " + that.deviceId +". Ready to send command...");
|
||||||
|
that.setTargetState();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
that.log("Error: Couldn't find a door device, or the ID you specified isn't associated with your account");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' getting devices: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetState: function() {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var liftmasterState = (this.targetState + "") == "1" ? "0" : "1";
|
||||||
|
|
||||||
|
// querystring params
|
||||||
|
var query = {
|
||||||
|
appId: APP_ID,
|
||||||
|
SecurityToken: this.securityToken,
|
||||||
|
filterOn: "true"
|
||||||
|
};
|
||||||
|
|
||||||
|
// some necessary duplicated info in the headers
|
||||||
|
var headers = {
|
||||||
|
MyQApplicationId: APP_ID,
|
||||||
|
SecurityToken: this.securityToken
|
||||||
|
};
|
||||||
|
|
||||||
|
// PUT request body
|
||||||
|
var body = {
|
||||||
|
AttributeName: "desireddoorstate",
|
||||||
|
AttributeValue: liftmasterState,
|
||||||
|
ApplicationId: APP_ID,
|
||||||
|
SecurityToken: this.securityToken,
|
||||||
|
MyQDeviceId: this.deviceId
|
||||||
|
};
|
||||||
|
|
||||||
|
// send the state request to liftmaster
|
||||||
|
request.put({
|
||||||
|
url: "https://myqexternal.myqdevice.com/api/v4/DeviceAttribute/PutDeviceAttribute",
|
||||||
|
qs: query,
|
||||||
|
headers: headers,
|
||||||
|
body: body,
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
|
||||||
|
if (json["ReturnCode"] == "0")
|
||||||
|
that.log("State was successfully set.");
|
||||||
|
else
|
||||||
|
that.log("Bad return code: " + json["ReturnCode"]);
|
||||||
|
that.log("Raw response " + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting door state: " + JSON.stringify(json));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "LiftMaster",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.GARAGE_DOOR_OPENER_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Garage Door Opener Control",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.CURRENT_DOOR_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.log("Update current state to " + value); },
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "BlaBla",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 4,
|
||||||
|
designedMinStep: 1,
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.TARGET_DOORSTATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setState(value); },
|
||||||
|
perms: ["pr","pw","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "BlaBla",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 1,
|
||||||
|
designedMinStep: 1,
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.OBSTRUCTION_DETECTED_CTYPE,
|
||||||
|
onUpdate: function(value) { that.log("Obstruction detected: " + value); },
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "BlaBla"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = LiftMasterAccessory;
|
||||||
80
accessories/Lockitron.js
Normal file
80
accessories/Lockitron.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: LockitronAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function LockitronAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.accessToken = config["api_token"];
|
||||||
|
this.lockID = config["lock_id"];
|
||||||
|
|
||||||
|
this.service = new Service.LockMechanism(this.name);
|
||||||
|
|
||||||
|
this.service
|
||||||
|
.getCharacteristic(Characteristic.LockCurrentState)
|
||||||
|
.on('get', this.getState.bind(this));
|
||||||
|
|
||||||
|
this.service
|
||||||
|
.getCharacteristic(Characteristic.LockTargetState)
|
||||||
|
.on('get', this.getState.bind(this))
|
||||||
|
.on('set', this.setState.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
LockitronAccessory.prototype.getState = function(callback) {
|
||||||
|
this.log("Getting current state...");
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||||
|
qs: { access_token: this.accessToken }
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
var json = JSON.parse(body);
|
||||||
|
var state = json.state; // "lock" or "unlock"
|
||||||
|
this.log("Lock state is %s", state);
|
||||||
|
var locked = state == "lock"
|
||||||
|
callback(null, locked); // success
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error getting state (status code %s): %s", response.statusCode, err);
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
LockitronAccessory.prototype.setState = function(state, callback) {
|
||||||
|
var lockitronState = (state == Characteristic.LockTargetState.SECURED) ? "lock" : "unlock";
|
||||||
|
|
||||||
|
this.log("Set state to %s", lockitronState);
|
||||||
|
|
||||||
|
request.put({
|
||||||
|
url: "https://api.lockitron.com/v2/locks/"+this.lockID,
|
||||||
|
qs: { access_token: this.accessToken, state: lockitronState }
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
this.log("State change complete.");
|
||||||
|
|
||||||
|
// we succeeded, so update the "current" state as well
|
||||||
|
var currentState = (state == Characteristic.LockTargetState.SECURED) ?
|
||||||
|
Characteristic.LockCurrentState.SECURED : Characteristic.LockCurrentState.UNSECURED;
|
||||||
|
|
||||||
|
this.service
|
||||||
|
.setCharacteristic(Characteristic.LockCurrentState, currentState);
|
||||||
|
|
||||||
|
callback(null); // success
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error '%s' setting lock state. Response: %s", err, body);
|
||||||
|
callback(err || new Error("Error setting lock state."));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
LockitronAccessory.prototype.getServices = function() {
|
||||||
|
return [this.service];
|
||||||
|
}
|
||||||
119
accessories/Tesla.js
Normal file
119
accessories/Tesla.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var tesla = require("teslams");
|
||||||
|
|
||||||
|
function TeslaAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
}
|
||||||
|
|
||||||
|
TeslaAccessory.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
tesla.get_vid({email: this.username, password: this.password}, function(vehicle) {
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
tesla.auto_conditioning({id:vehicle, climate: 'start'}, function(response) {
|
||||||
|
if (response.result)
|
||||||
|
that.log("Started climate control.");
|
||||||
|
else
|
||||||
|
that.log("Error starting climate control: " + response.reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
tesla.auto_conditioning({id:vehicle, climate: 'stop'}, function(response) {
|
||||||
|
if (response.result)
|
||||||
|
that.log("Stopped climate control.");
|
||||||
|
else
|
||||||
|
that.log("Error stopping climate control: " + response.reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Tesla",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.SWITCH_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state of the car",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = TeslaAccessory;
|
||||||
169
accessories/WeMo.js
Normal file
169
accessories/WeMo.js
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var wemo = require('wemo');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: WeMoAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
function WeMoAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.service = config["service"] || "Switch";
|
||||||
|
this.wemoName = config["wemo_name"] || this.name; // fallback to "name" if you didn't specify an exact "wemo_name"
|
||||||
|
this.device = null; // instance of WeMo, for controlling the discovered device
|
||||||
|
this.log("Searching for WeMo device with exact name '" + this.wemoName + "'...");
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.search = function() {
|
||||||
|
wemo.Search(this.wemoName, function(err, device) {
|
||||||
|
if (!err && device) {
|
||||||
|
this.log("Found '"+this.wemoName+"' device at " + device.ip);
|
||||||
|
this.device = new wemo(device.ip, device.port);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error finding device '" + this.wemoName + "': " + err);
|
||||||
|
this.log("Continuing search for WeMo device with exact name '" + this.wemoName + "'...");
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.getMotion = function(callback) {
|
||||||
|
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Getting motion state on the '%s'...", this.wemoName);
|
||||||
|
|
||||||
|
this.device.getBinaryState(function(err, result) {
|
||||||
|
if (!err) {
|
||||||
|
var binaryState = parseInt(result);
|
||||||
|
var powerOn = binaryState > 0;
|
||||||
|
this.log("Motion state for the '%s' is %s", this.wemoName, binaryState);
|
||||||
|
callback(null, powerOn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error getting motion state on the '%s': %s", this.wemoName, err.message);
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.getPowerOn = function(callback) {
|
||||||
|
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Getting power state on the '%s'...", this.wemoName);
|
||||||
|
|
||||||
|
this.device.getBinaryState(function(err, result) {
|
||||||
|
if (!err) {
|
||||||
|
var binaryState = parseInt(result);
|
||||||
|
var powerOn = binaryState > 0;
|
||||||
|
this.log("Power state for the '%s' is %s", this.wemoName, binaryState);
|
||||||
|
callback(null, powerOn);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error getting power state on the '%s': %s", this.wemoName, err.message);
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.setPowerOn = function(powerOn, callback) {
|
||||||
|
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||||
|
callback(new Error("Device not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var binaryState = powerOn ? 1 : 0; // wemo langauge
|
||||||
|
this.log("Setting power state on the '%s' to %s", this.wemoName, binaryState);
|
||||||
|
|
||||||
|
this.device.setBinaryState(binaryState, function(err, result) {
|
||||||
|
if (!err) {
|
||||||
|
this.log("Successfully set power state on the '%s' to %s", this.wemoName, binaryState);
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error setting power state to %s on the '%s'", binaryState, this.wemoName);
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.setTargetDoorState = function(targetDoorState, callback) {
|
||||||
|
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '%s' device found (yet?)", this.wemoName);
|
||||||
|
callback(new Error("Device not found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Activating WeMo switch '%s'", this.wemoName);
|
||||||
|
|
||||||
|
this.device.setBinaryState(1, function(err, result) {
|
||||||
|
if (!err) {
|
||||||
|
this.log("Successfully activated WeMo switch '%s'", this.wemoName);
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.log("Error activating WeMo switch '%s'", this.wemoName);
|
||||||
|
callback(err);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
WeMoAccessory.prototype.getServices = function() {
|
||||||
|
|
||||||
|
if (this.service == "Switch") {
|
||||||
|
var switchService = new Service.Switch(this.name);
|
||||||
|
|
||||||
|
switchService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerOn.bind(this))
|
||||||
|
.on('set', this.setPowerOn.bind(this));
|
||||||
|
|
||||||
|
return [switchService];
|
||||||
|
}
|
||||||
|
else if (this.service == "GarageDoor") {
|
||||||
|
var garageDoorService = new Service.GarageDoorOpener("Garage Door Opener");
|
||||||
|
|
||||||
|
garageDoorService
|
||||||
|
.getCharacteristic(Characteristic.TargetDoorState)
|
||||||
|
.on('set', this.setTargetDoorState.bind(this));
|
||||||
|
|
||||||
|
return [garageDoorService];
|
||||||
|
}
|
||||||
|
else if (this.service == "Light") {
|
||||||
|
var lightbulbService = new Service.Lightbulb(this.name);
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerOn.bind(this))
|
||||||
|
.on('set', this.setPowerOn.bind(this));
|
||||||
|
|
||||||
|
return [lightbulbService];
|
||||||
|
}
|
||||||
|
else if (this.service == "MotionSensor") {
|
||||||
|
var motionSensorService = new Service.MotionSensor(this.name);
|
||||||
|
|
||||||
|
motionSensorService
|
||||||
|
.getCharacteristic(Characteristic.MotionDetected)
|
||||||
|
.on('get', this.getMotion.bind(this));
|
||||||
|
|
||||||
|
return [motionSensorService];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Error("Unknown service type '%s'", this.service);
|
||||||
|
}
|
||||||
|
}
|
||||||
151
accessories/X10.js
Normal file
151
accessories/X10.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function X10(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.ip_address = config["ip_address"];
|
||||||
|
this.name = config["name"];
|
||||||
|
this.deviceID = config["device_id"];
|
||||||
|
this.protocol = config["protocol"];
|
||||||
|
this.canDim = config["can_dim"];
|
||||||
|
}
|
||||||
|
|
||||||
|
X10.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
|
||||||
|
var binaryState = powerOn ? "on" : "off";
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting power state of " + this.deviceID + " to " + powerOn);
|
||||||
|
request.put({
|
||||||
|
url: "http://"+this.ip_address+"/x10/"+this.deviceID+"/power/"+binaryState+"?protocol="+this.protocol,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
that.log("State change complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting power state: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightnessLevel: function(value) {
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting brightness level of " + this.deviceID + " to " + value);
|
||||||
|
request.put({
|
||||||
|
url: "http://"+this.ip_address+"/x10/"+this.deviceID+"/brightness/?protocol="+this.protocol+"&value="+value,
|
||||||
|
}, function(err, response, body) {
|
||||||
|
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
that.log("State change complete.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Error '"+err+"' setting brightness level: " + body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "X10",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPowerState(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state of a Variable",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
if (that.canDim) {
|
||||||
|
services[1].characteristics.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setBrightnessLevel(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = X10;
|
||||||
130
accessories/iControl.js
Normal file
130
accessories/iControl.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
var iControl = require('node-icontrol').iControl;
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: iControlAccessory
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a Security System accessory for an iControl-based security system like Xfinity Home.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function iControlAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
this.iControl = new iControl({
|
||||||
|
system: iControl.Systems[config.system],
|
||||||
|
email: config.email,
|
||||||
|
password: config.password,
|
||||||
|
pinCode: config.pin
|
||||||
|
});
|
||||||
|
|
||||||
|
this.iControl.on('change', this._handleChange.bind(this));
|
||||||
|
this.iControl.on('error', this._handleError.bind(this));
|
||||||
|
|
||||||
|
this.log("Logging into iControl...");
|
||||||
|
this.iControl.login();
|
||||||
|
|
||||||
|
this._securitySystem = new Service.SecuritySystem("Security System");
|
||||||
|
|
||||||
|
this._securitySystem
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||||
|
.on('get', this._getTargetState.bind(this))
|
||||||
|
.on('set', this._setTargetState.bind(this));
|
||||||
|
|
||||||
|
this._securitySystem
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||||
|
.on('get', this._getCurrentState.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._getTargetState = function(callback) {
|
||||||
|
this.iControl.getArmState(function(err, armState) {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
var currentState = this._getHomeKitStateFromArmState(armState);
|
||||||
|
callback(null, currentState);
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._getCurrentState = function(callback) {
|
||||||
|
this.iControl.getArmState(function(err, armState) {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
var currentState = this._getHomeKitStateFromArmState(armState);
|
||||||
|
callback(null, currentState);
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._setTargetState = function(targetState, callback, context) {
|
||||||
|
if (context == "internal") return callback(null); // we set this state ourself, no need to react to it
|
||||||
|
|
||||||
|
var armState = this._getArmStateFromHomeKitState(targetState);
|
||||||
|
this.log("Setting target state to %s", armState);
|
||||||
|
|
||||||
|
this.iControl.setArmState(armState, function(err) {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
this.log("Successfully set target state to %s", armState);
|
||||||
|
|
||||||
|
// also update current state
|
||||||
|
this._securitySystem
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||||
|
.setValue(targetState);
|
||||||
|
|
||||||
|
callback(null); // success!
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._handleChange = function(armState) {
|
||||||
|
this.log("Arm state changed to %s", armState);
|
||||||
|
|
||||||
|
var homeKitState = this._getHomeKitStateFromArmState(armState);
|
||||||
|
|
||||||
|
this._securitySystem
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||||
|
.setValue(homeKitState);
|
||||||
|
|
||||||
|
this._securitySystem
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||||
|
.setValue(homeKitState, null, "internal"); // these characteristics happen to share underlying values
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._handleError = function(err) {
|
||||||
|
this.log(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype.getServices = function() {
|
||||||
|
return [this._securitySystem];
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._getHomeKitStateFromArmState = function(armState) {
|
||||||
|
switch (armState) {
|
||||||
|
case "disarmed": return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||||
|
case "away": return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
||||||
|
case "night": return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
||||||
|
case "stay": return Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
iControlAccessory.prototype._getArmStateFromHomeKitState = function(homeKitState) {
|
||||||
|
switch (homeKitState) {
|
||||||
|
case Characteristic.SecuritySystemCurrentState.DISARMED: return "disarmed";
|
||||||
|
case Characteristic.SecuritySystemCurrentState.AWAY_ARM: return "away";
|
||||||
|
case Characteristic.SecuritySystemCurrentState.NIGHT_ARM: return "night";
|
||||||
|
case Characteristic.SecuritySystemCurrentState.STAY_ARM: return "stay";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TESTING
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
var config = JSON.parse(require('fs').readFileSync("config.json")).accessories[0];
|
||||||
|
var accessory = new iControlAccessory(console.log, config);
|
||||||
|
}
|
||||||
1032
accessories/knxdevice.js
Normal file
1032
accessories/knxdevice.js
Normal file
File diff suppressed because it is too large
Load Diff
89
accessories/mpdclient.js
Normal file
89
accessories/mpdclient.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
var komponist = require('komponist')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: MpdClient
|
||||||
|
}
|
||||||
|
|
||||||
|
function MpdClient(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.host = config["host"] || 'localhost';
|
||||||
|
this.port = config["port"] || 6600;
|
||||||
|
}
|
||||||
|
|
||||||
|
MpdClient.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
|
||||||
|
var log = this.log;
|
||||||
|
|
||||||
|
komponist.createConnection(this.port, this.host, function(error, client) {
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
client.play(function(error) {
|
||||||
|
log("start playing");
|
||||||
|
client.destroy();
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
client.stop(function(error) {
|
||||||
|
log("stop playing");
|
||||||
|
client.destroy();
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getPowerState: function(callback) {
|
||||||
|
|
||||||
|
komponist.createConnection(this.port, this.host, function(error, client) {
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.status(function(error, status) {
|
||||||
|
|
||||||
|
client.destroy();
|
||||||
|
|
||||||
|
if (status['state'] == 'play') {
|
||||||
|
callback(error, 1);
|
||||||
|
} else {
|
||||||
|
callback(error, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "MPD")
|
||||||
|
.setCharacteristic(Characteristic.Model, "MPD Client")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "81536334");
|
||||||
|
|
||||||
|
var switchService = new Service.Switch();
|
||||||
|
|
||||||
|
switchService.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerState.bind(this))
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, switchService];
|
||||||
|
}
|
||||||
|
};
|
||||||
223
app.js
Normal file
223
app.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
var storage = require('node-persist');
|
||||||
|
var hap = require("hap-nodejs");
|
||||||
|
var uuid = require("hap-nodejs").uuid;
|
||||||
|
var Bridge = require("hap-nodejs").Bridge;
|
||||||
|
var Accessory = require("hap-nodejs").Accessory;
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var accessoryLoader = require("hap-nodejs").AccessoryLoader;
|
||||||
|
var once = require("hap-nodejs/lib/util/once").once;
|
||||||
|
|
||||||
|
console.log("Starting HomeBridge server...");
|
||||||
|
|
||||||
|
console.log("_____________________________________________________________________");
|
||||||
|
console.log("IMPORTANT: Homebridge is in the middle of some big changes.");
|
||||||
|
console.log(" Read more about it here:");
|
||||||
|
console.log(" https://github.com/nfarina/homebridge/wiki/Migration-Guide");
|
||||||
|
console.log("_____________________________________________________________________");
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
// Look for the configuration file
|
||||||
|
var configPath = path.join(__dirname, "config.json");
|
||||||
|
|
||||||
|
// Complain and exit if it doesn't exist yet
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
console.log("Couldn't find a config.json file in the same directory as app.js. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize HAP-NodeJS
|
||||||
|
hap.init();
|
||||||
|
|
||||||
|
// Load up the configuration file
|
||||||
|
var config;
|
||||||
|
try {
|
||||||
|
config = JSON.parse(fs.readFileSync(configPath));
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.log("There was a problem reading your config.json file.");
|
||||||
|
console.log("Please try pasting your config.json file here to validate it: http://jsonlint.com");
|
||||||
|
console.log("");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pull out our custom Bridge settings from config.json, if any
|
||||||
|
var bridgeConfig = config.bridge || {};
|
||||||
|
|
||||||
|
// Start by creating our Bridge which will host all loaded Accessories
|
||||||
|
var bridge = new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge"));
|
||||||
|
|
||||||
|
// keep track of async calls we're waiting for callbacks on before we can start up
|
||||||
|
// this is hacky but this is all going away once we build proper plugin support
|
||||||
|
var asyncCalls = 0;
|
||||||
|
var asyncWait = false;
|
||||||
|
|
||||||
|
function startup() {
|
||||||
|
asyncWait = true;
|
||||||
|
if (config.platforms) loadPlatforms();
|
||||||
|
if (config.accessories) loadAccessories();
|
||||||
|
asyncWait = false;
|
||||||
|
|
||||||
|
// publish now unless we're waiting on anyone
|
||||||
|
if (asyncCalls == 0)
|
||||||
|
publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAccessories() {
|
||||||
|
|
||||||
|
// Instantiate all accessories in the config
|
||||||
|
console.log("Loading " + config.accessories.length + " accessories...");
|
||||||
|
|
||||||
|
for (var i=0; i<config.accessories.length; i++) {
|
||||||
|
|
||||||
|
var accessoryConfig = config.accessories[i];
|
||||||
|
|
||||||
|
// Load up the class for this accessory
|
||||||
|
var accessoryType = accessoryConfig["accessory"]; // like "WeMo"
|
||||||
|
var accessoryModule = require('./accessories/' + accessoryType + ".js"); // like "./accessories/WeMo.js"
|
||||||
|
var accessoryConstructor = accessoryModule.accessory; // like "WeMoAccessory", a JavaScript constructor
|
||||||
|
|
||||||
|
// Create a custom logging function that prepends the device display name for debugging
|
||||||
|
var accessoryName = accessoryConfig["name"];
|
||||||
|
var log = createLog(accessoryName);
|
||||||
|
|
||||||
|
log("Initializing %s accessory...", accessoryType);
|
||||||
|
|
||||||
|
var accessoryInstance = new accessoryConstructor(log, accessoryConfig);
|
||||||
|
var accessory = createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
|
||||||
|
|
||||||
|
// add it to the bridge
|
||||||
|
bridge.addBridgedAccessory(accessory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlatforms() {
|
||||||
|
|
||||||
|
console.log("Loading " + config.platforms.length + " platforms...");
|
||||||
|
|
||||||
|
for (var i=0; i<config.platforms.length; i++) {
|
||||||
|
|
||||||
|
var platformConfig = config.platforms[i];
|
||||||
|
|
||||||
|
// Load up the class for this accessory
|
||||||
|
var platformType = platformConfig["platform"]; // like "Wink"
|
||||||
|
var platformName = platformConfig["name"];
|
||||||
|
var platformModule = require('./platforms/' + platformType + ".js"); // like "./platforms/Wink.js"
|
||||||
|
var platformConstructor = platformModule.platform; // like "WinkPlatform", a JavaScript constructor
|
||||||
|
|
||||||
|
// Create a custom logging function that prepends the platform name for debugging
|
||||||
|
var log = createLog(platformName);
|
||||||
|
|
||||||
|
log("Initializing %s platform...", platformType);
|
||||||
|
|
||||||
|
var platformInstance = new platformConstructor(log, platformConfig);
|
||||||
|
loadPlatformAccessories(platformInstance, log, platformType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlatformAccessories(platformInstance, log, platformType) {
|
||||||
|
asyncCalls++;
|
||||||
|
platformInstance.accessories(once(function(foundAccessories){
|
||||||
|
asyncCalls--;
|
||||||
|
|
||||||
|
// loop through accessories adding them to the list and registering them
|
||||||
|
for (var i = 0; i < foundAccessories.length; i++) {
|
||||||
|
var accessoryInstance = foundAccessories[i];
|
||||||
|
var accessoryName = accessoryInstance.name; // assume this property was set
|
||||||
|
|
||||||
|
log("Initializing platform accessory '%s'...", accessoryName);
|
||||||
|
|
||||||
|
var accessory = createAccessory(accessoryInstance, accessoryName, platformType, accessoryInstance.uuid_base);
|
||||||
|
|
||||||
|
// add it to the bridge
|
||||||
|
bridge.addBridgedAccessory(accessory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// were we the last callback?
|
||||||
|
if (asyncCalls === 0 && !asyncWait)
|
||||||
|
publish();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAccessory(accessoryInstance, displayName, accessoryType, uuid_base) {
|
||||||
|
|
||||||
|
var services = accessoryInstance.getServices();
|
||||||
|
|
||||||
|
if (!(services[0] instanceof Service)) {
|
||||||
|
// The returned "services" for this accessory is assumed to be the old style: a big array
|
||||||
|
// of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader.
|
||||||
|
|
||||||
|
// Create the actual HAP-NodeJS "Accessory" instance
|
||||||
|
return accessoryLoader.parseAccessoryJSON({
|
||||||
|
displayName: displayName,
|
||||||
|
services: services
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The returned "services" for this accessory are simply an array of new-API-style
|
||||||
|
// Service instances which we can add to a created HAP-NodeJS Accessory directly.
|
||||||
|
|
||||||
|
var accessoryUUID = uuid.generate(accessoryType + ":" + (uuid_base || displayName));
|
||||||
|
|
||||||
|
var accessory = new Accessory(displayName, accessoryUUID);
|
||||||
|
|
||||||
|
// listen for the identify event if the accessory instance has defined an identify() method
|
||||||
|
if (accessoryInstance.identify)
|
||||||
|
accessory.on('identify', function(paired, callback) { accessoryInstance.identify(callback); });
|
||||||
|
|
||||||
|
services.forEach(function(service) {
|
||||||
|
|
||||||
|
// if you returned an AccessoryInformation service, merge its values with ours
|
||||||
|
if (service instanceof Service.AccessoryInformation) {
|
||||||
|
var existingService = accessory.getService(Service.AccessoryInformation);
|
||||||
|
|
||||||
|
// pull out any values you may have defined
|
||||||
|
var manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value;
|
||||||
|
var model = service.getCharacteristic(Characteristic.Model).value;
|
||||||
|
var serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value;
|
||||||
|
|
||||||
|
if (manufacturer) existingService.setCharacteristic(Characteristic.Manufacturer, manufacturer);
|
||||||
|
if (model) existingService.setCharacteristic(Characteristic.Model, model);
|
||||||
|
if (serialNumber) existingService.setCharacteristic(Characteristic.SerialNumber, serialNumber);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
accessory.addService(service);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return accessory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the setup code in a scannable format.
|
||||||
|
function printPin(pin) {
|
||||||
|
console.log("Scan this code with your HomeKit App on your iOS device:");
|
||||||
|
console.log("\x1b[30;47m%s\x1b[0m", " ");
|
||||||
|
console.log("\x1b[30;47m%s\x1b[0m", " ┌────────────┐ ");
|
||||||
|
console.log("\x1b[30;47m%s\x1b[0m", " │ " + pin + " │ ");
|
||||||
|
console.log("\x1b[30;47m%s\x1b[0m", " └────────────┘ ");
|
||||||
|
console.log("\x1b[30;47m%s\x1b[0m", " ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a logging function that prepends messages with the given name in [brackets].
|
||||||
|
function createLog(name) {
|
||||||
|
return function(message) {
|
||||||
|
var rest = Array.prototype.slice.call(arguments, 1 ); // any arguments after message
|
||||||
|
var args = ["[%s] " + message, name].concat(rest);
|
||||||
|
console.log.apply(console, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function publish() {
|
||||||
|
printPin(bridgeConfig.pin);
|
||||||
|
bridge.publish({
|
||||||
|
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
|
||||||
|
port: bridgeConfig.port || 51826,
|
||||||
|
pincode: bridgeConfig.pin || "031-45-154",
|
||||||
|
category: Accessory.Categories.OTHER
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
startup();
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
//
|
|
||||||
// This executable sets up the environment and runs the HomeBridge CLI.
|
|
||||||
//
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
process.title = 'homebridge';
|
|
||||||
|
|
||||||
// Find the HomeBridge lib
|
|
||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
|
||||||
var lib = path.join(path.dirname(fs.realpathSync(__filename)), '../lib');
|
|
||||||
|
|
||||||
// Run HomeBridge
|
|
||||||
require(lib + '/cli')();
|
|
||||||
@@ -6,19 +6,257 @@
|
|||||||
"pin": "031-45-154"
|
"pin": "031-45-154"
|
||||||
},
|
},
|
||||||
|
|
||||||
"description": "This is an example configuration file with one fake accessory and one fake platform. You can use this as a template for creating your own configuration file containing devices you actually own.",
|
"description": "This is an example configuration file with all supported devices. You can use this as a template for creating your own configuration file containing devices you actually own.",
|
||||||
|
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"platform" : "Nest",
|
||||||
|
"name" : "Nest",
|
||||||
|
"username" : "username",
|
||||||
|
"password" : "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform" : "TelldusLive",
|
||||||
|
"name" : "Telldus Live!",
|
||||||
|
"public_key" : "telldus public key",
|
||||||
|
"private_key" : "telldus private key",
|
||||||
|
"token" : "telldus token",
|
||||||
|
"token_secret" : "telldus token secret"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform" : "Telldus",
|
||||||
|
"name" : "Telldus"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "Wink",
|
||||||
|
"name": "Wink",
|
||||||
|
"client_id": "YOUR_WINK_API_CLIENT_ID",
|
||||||
|
"client_secret": "YOUR_WINK_API_CLIENT_SECRET",
|
||||||
|
"username": "your@email.com",
|
||||||
|
"password": "WINK_PASSWORD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "SmartThings",
|
||||||
|
"name": "SmartThings",
|
||||||
|
"app_id": "JSON SmartApp Id",
|
||||||
|
"access_token": "JSON SmartApp AccessToken"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "Domoticz",
|
||||||
|
"name": "Domoticz",
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"port": "8080",
|
||||||
|
"roomid": 0,
|
||||||
|
"loadscenes": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "PhilipsHue",
|
||||||
|
"name": "Phillips Hue",
|
||||||
|
"username": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "ISY",
|
||||||
|
"name": "ISY",
|
||||||
|
"host": "192.168.1.20",
|
||||||
|
"port": "8000",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "LogitechHarmony",
|
||||||
|
"name": "Living Room Harmony Hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "Sonos",
|
||||||
|
"name": "Sonos",
|
||||||
|
"play_volume": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "YamahaAVR",
|
||||||
|
"play_volume": -35,
|
||||||
|
"setMainInputTo": "AirPlay"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "ZWayServer",
|
||||||
|
"url": "http://192.168.1.10:8083/",
|
||||||
|
"login": "zwayusername",
|
||||||
|
"password": "zwayuserpassword",
|
||||||
|
"poll_interval": 2,
|
||||||
|
"split_services": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "MiLight",
|
||||||
|
"name": "MiLight",
|
||||||
|
"ip_address": "255.255.255.255",
|
||||||
|
"port": 8899,
|
||||||
|
"type": "rgbw",
|
||||||
|
"delay": 30,
|
||||||
|
"repeat": 3,
|
||||||
|
"zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "HomeAssistant",
|
||||||
|
"name": "HomeAssistant",
|
||||||
|
"host": "http://192.168.1.10:8123",
|
||||||
|
"password": "XXXXX",
|
||||||
|
"supported_types": ["light", "switch", "media_player", "scene"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"platform": "LIFx",
|
||||||
|
"name": "LIFx",
|
||||||
|
"access_token": "XXXXXXXX generate at https://cloud.lifx.com/settings"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
"accessories": [
|
"accessories": [
|
||||||
{
|
{
|
||||||
"accessory": "WeMo",
|
"accessory": "WeMo",
|
||||||
"name": "Coffee Maker"
|
"name": "Coffee Maker",
|
||||||
}
|
"description": "This shim supports Belkin WeMo devices on the same network as this server. You can create duplicate entries for this device and change the 'name' attribute to reflect what device is plugged into the WeMo, for instance 'Air Conditioner' or 'Coffee Maker'. This name will be used by Siri. Make sure to update the 'wemo_name' attribute with the EXACT name of the device in the WeMo app itself. This can be the same value as 'name' but it doesn't have to be.",
|
||||||
],
|
"wemo_name": "CoffeeMaker"
|
||||||
|
},
|
||||||
"platforms": [
|
|
||||||
{
|
{
|
||||||
"platform" : "PhilipsHue",
|
"accessory": "LiftMaster",
|
||||||
"name" : "Hue"
|
"name": "Garage Door",
|
||||||
|
"description": "This shim supports LiftMaster garage door openers that are already internet-connected to the 'MyQ' service.",
|
||||||
|
// "requiredDeviceId", "<ID of door if you have multiple doors, prompted by shim during startup if needed>",
|
||||||
|
"username": "your-liftmaster-username",
|
||||||
|
"password" : "your-liftmaster-password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "Lockitron",
|
||||||
|
"name": "Front Door",
|
||||||
|
"description": "This shim supports Lockitron locks. It uses the Lockitron cloud API, so the Lockitron must be 'awake' for locking and unlocking to actually happen. You can wake up Lockitron after issuing an lock/unlock command by knocking on the door.",
|
||||||
|
"lock_id": "your-lock-id",
|
||||||
|
"api_token" : "your-lockitron-api-access-token"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "Carwings",
|
||||||
|
"name": "Leaf",
|
||||||
|
"description": "This shim supports controlling climate control on Nissan cars with Carwings. Note that Carwings is super slow and it may take up to 5 minutes for your command to be processed by the Carwings system.",
|
||||||
|
"username": "your-carwings-username",
|
||||||
|
"password" : "your-carwings-password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "iControl",
|
||||||
|
"name": "Xfinity Home",
|
||||||
|
"description": "This shim supports iControl-based security systems like Xfinity Home.",
|
||||||
|
"system": "XFINITY_HOME",
|
||||||
|
"email": "your-comcast-email",
|
||||||
|
"password": "your-comcast-password",
|
||||||
|
"pin": "your-security-system-pin-code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "HomeMatic",
|
||||||
|
"name": "Light",
|
||||||
|
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||||
|
"ccu_id": "The XMP-API id of your HomeMatic device",
|
||||||
|
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "HomeMaticWindow",
|
||||||
|
"name": "Contact",
|
||||||
|
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||||
|
"ccu_id": "The XMP-API id of your HomeMatic device (type HM-Sec-RHS)",
|
||||||
|
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "HomeMaticThermo",
|
||||||
|
"name": "Contact",
|
||||||
|
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
|
||||||
|
"ccu_id_TargetTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||||
|
"ccu_id_CurrentTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||||
|
"ccu_id_ControlMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||||
|
"ccu_id_ManuMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||||
|
"ccu_id_AutoMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )",
|
||||||
|
"ccu_ip": "The IP-Adress of your HomeMatic CCU device"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "X10",
|
||||||
|
"name": "Lamp",
|
||||||
|
"ip_address": "localhost:3000",
|
||||||
|
"device_id": "E1",
|
||||||
|
"protocol": "pl",
|
||||||
|
"can_dim": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "Http",
|
||||||
|
"name": "Kitchen Lamp",
|
||||||
|
"on_url": "https://192.168.1.22:3030/devices/23222/on",
|
||||||
|
"off_url": "https://192.168.1.22:3030/devices/23222/off",
|
||||||
|
"brightness_url": "https://192.168.1.22:3030/devices/23222/brightness/%b",
|
||||||
|
"http_method": "POST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "HttpHygrometer",
|
||||||
|
"name": "Kitchen",
|
||||||
|
"url": "http://host/URL",
|
||||||
|
"http_method": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "HttpThermometer",
|
||||||
|
"name": "Garage",
|
||||||
|
"url": "http://home/URL",
|
||||||
|
"http_method": "GET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "ELKM1",
|
||||||
|
"name": "Security System",
|
||||||
|
"description": "Allows basic control of Elk M1 security system. You can use 1 of 3 arm modes: Away, Stay, Night. If you need to access all 3, create 3 accessories with different names.",
|
||||||
|
"zone": "1",
|
||||||
|
"host": "192.168.1.10",
|
||||||
|
"port": "2101",
|
||||||
|
"pin": "1234",
|
||||||
|
"arm": "Away"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "AD2USB",
|
||||||
|
"name": "Alarm",
|
||||||
|
"description": "Arm, disarm, and status monitoring of the default partition for Honeywell/Ademco alarm systems. Requires network configured AD2USB interface",
|
||||||
|
"host": "192.168.1.200", // IP address of the SER2SOCK service
|
||||||
|
"port" : 4999, // Port the SER2SOCK process is running on
|
||||||
|
"pin": "1234" // PIN used for arming / disarming
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "Tesla",
|
||||||
|
"name": "Tesla",
|
||||||
|
"description": "This shim supports controlling climate control on the Tesla Model S.",
|
||||||
|
"username": "tesla_email",
|
||||||
|
"password" : "tesla_password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "Hyperion",
|
||||||
|
"name": "TV Backlight",
|
||||||
|
"description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion",
|
||||||
|
"host": "localhost",
|
||||||
|
"port": "19444"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "mpdclient",
|
||||||
|
"name" : "mpd",
|
||||||
|
"host" : "localhost",
|
||||||
|
"port" : 6600,
|
||||||
|
"description": "Allows some control of an MPD server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "FileSensor",
|
||||||
|
"name": "File Time Motion Sensor",
|
||||||
|
"path": "/tmp/CameraDump/",
|
||||||
|
"window_seconds": 5,
|
||||||
|
"sensor_type": "m",
|
||||||
|
"inverse": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory": "GenericRS232Device",
|
||||||
|
"name": "Projector",
|
||||||
|
"description": "Make sure you set a 'Siri-Name' for your iOS-Device (example: 'Home Cinema') otherwise it might not work.",
|
||||||
|
"id": "TYDYMU044UVNP",
|
||||||
|
"baudrate": 9600,
|
||||||
|
"device": "/dev/tty.usbserial",
|
||||||
|
"manufacturer": "Acer",
|
||||||
|
"model_name": "H6510BD",
|
||||||
|
"on_command": "* 0 IR 001\r",
|
||||||
|
"off_command": "* 0 IR 002\r"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
var http = require('http');
|
|
||||||
var Accessory, Service, Characteristic, UUIDGen;
|
|
||||||
|
|
||||||
module.exports = function(homebridge) {
|
|
||||||
console.log("homebridge API version: " + homebridge.version);
|
|
||||||
|
|
||||||
// Accessory must be created from PlatformAccessory Constructor
|
|
||||||
Accessory = homebridge.platformAccessory;
|
|
||||||
|
|
||||||
// Service and Characteristic are from hap-nodejs
|
|
||||||
Service = homebridge.hap.Service;
|
|
||||||
Characteristic = homebridge.hap.Characteristic;
|
|
||||||
UUIDGen = homebridge.hap.uuid;
|
|
||||||
|
|
||||||
// For platform plugin to be considered as dynamic platform plugin,
|
|
||||||
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
|
|
||||||
homebridge.registerPlatform("homebridge-samplePlatform", "SamplePlatform", SamplePlatform, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform constructor
|
|
||||||
// config may be null
|
|
||||||
// api may be null if launched from old homebridge version
|
|
||||||
function SamplePlatform(log, config, api) {
|
|
||||||
log("SamplePlatform Init");
|
|
||||||
var platform = this;
|
|
||||||
this.log = log;
|
|
||||||
this.config = config;
|
|
||||||
this.accessories = [];
|
|
||||||
|
|
||||||
this.requestServer = http.createServer(function(request, response) {
|
|
||||||
if (request.url === "/add") {
|
|
||||||
this.addAccessory(new Date().toISOString());
|
|
||||||
response.writeHead(204);
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.url == "/reachability") {
|
|
||||||
this.updateAccessoriesReachability();
|
|
||||||
response.writeHead(204);
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.url == "/remove") {
|
|
||||||
this.removeAccessory();
|
|
||||||
response.writeHead(204);
|
|
||||||
response.end();
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this.requestServer.listen(18081, function() {
|
|
||||||
platform.log("Server Listening...");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (api) {
|
|
||||||
// Save the API object as plugin needs to register new accessory via this object.
|
|
||||||
this.api = api;
|
|
||||||
|
|
||||||
// Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories
|
|
||||||
// Platform Plugin should only register new accessory that doesn't exist in homebridge after this event.
|
|
||||||
// Or start discover new accessories
|
|
||||||
this.api.on('didFinishLaunching', function() {
|
|
||||||
platform.log("DidFinishLaunching");
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function invoked when homebridge tries to restore cached accessory
|
|
||||||
// Developer can configure accessory at here (like setup event handler)
|
|
||||||
// Update current value
|
|
||||||
SamplePlatform.prototype.configureAccessory = function(accessory) {
|
|
||||||
this.log(accessory.displayName, "Configure Accessory");
|
|
||||||
var platform = this;
|
|
||||||
|
|
||||||
// set the accessory to reachable if plugin can currently process the accessory
|
|
||||||
// otherwise set to false and update the reachability later by invoking
|
|
||||||
// accessory.updateReachability()
|
|
||||||
accessory.reachable = true;
|
|
||||||
|
|
||||||
accessory.on('identify', function(paired, callback) {
|
|
||||||
platform.log(accessory.displayName, "Identify!!!");
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (accessory.getService(Service.Lightbulb)) {
|
|
||||||
accessory.getService(Service.Lightbulb)
|
|
||||||
.getCharacteristic(Characteristic.On)
|
|
||||||
.on('set', function(value, callback) {
|
|
||||||
platform.log(accessory.displayName, "Light -> " + value);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.accessories.push(accessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
//Handler will be invoked when user try to config your plugin
|
|
||||||
//Callback can be cached and invoke when nessary
|
|
||||||
SamplePlatform.prototype.configurationRequestHandler = function(context, request, callback) {
|
|
||||||
this.log("Context: ", JSON.stringify(context));
|
|
||||||
this.log("Request: ", JSON.stringify(request));
|
|
||||||
|
|
||||||
// Check the request response
|
|
||||||
if (request && request.response && request.response.inputs && request.response.inputs.name) {
|
|
||||||
this.addAccessory(request.response.inputs.name);
|
|
||||||
|
|
||||||
// Invoke callback with config will let homebridge save the new config into config.json
|
|
||||||
// Callback = function(response, type, replace, config)
|
|
||||||
// set "type" to platform if the plugin is trying to modify platforms section
|
|
||||||
// set "replace" to true will let homebridge replace existing config in config.json
|
|
||||||
// "config" is the data platform trying to save
|
|
||||||
callback(null, "platform", true, {"platform":"SamplePlatform", "otherConfig":"SomeData"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// - UI Type: Input
|
|
||||||
// Can be used to request input from user
|
|
||||||
// User response can be retrieved from request.response.inputs next time
|
|
||||||
// when configurationRequestHandler being invoked
|
|
||||||
|
|
||||||
var respDict = {
|
|
||||||
"type": "Interface",
|
|
||||||
"interface": "input",
|
|
||||||
"title": "Add Accessory",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": "name",
|
|
||||||
"title": "Name",
|
|
||||||
"placeholder": "Fancy Light"
|
|
||||||
}//,
|
|
||||||
// {
|
|
||||||
// "id": "pw",
|
|
||||||
// "title": "Password",
|
|
||||||
// "secure": true
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// - UI Type: List
|
|
||||||
// Can be used to ask user to select something from the list
|
|
||||||
// User response can be retrieved from request.response.selections next time
|
|
||||||
// when configurationRequestHandler being invoked
|
|
||||||
|
|
||||||
// var respDict = {
|
|
||||||
// "type": "Interface",
|
|
||||||
// "interface": "list",
|
|
||||||
// "title": "Select Something",
|
|
||||||
// "allowMultipleSelection": true,
|
|
||||||
// "items": [
|
|
||||||
// "A","B","C"
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// - UI Type: Instruction
|
|
||||||
// Can be used to ask user to do something (other than text input)
|
|
||||||
// Hero image is base64 encoded image data. Not really sure the maximum length HomeKit allows.
|
|
||||||
|
|
||||||
// var respDict = {
|
|
||||||
// "type": "Interface",
|
|
||||||
// "interface": "instruction",
|
|
||||||
// "title": "Almost There",
|
|
||||||
// "detail": "Please press the button on the bridge to finish the setup.",
|
|
||||||
// "heroImage": "base64 image data",
|
|
||||||
// "showActivityIndicator": true,
|
|
||||||
// "showNextButton": true,
|
|
||||||
// "buttonText": "Login in browser",
|
|
||||||
// "actionURL": "https://google.com"
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Plugin can set context to allow it track setup process
|
|
||||||
context.ts = "Hello";
|
|
||||||
|
|
||||||
//invoke callback to update setup UI
|
|
||||||
callback(respDict);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample function to show how developer can add accessory dynamically from outside event
|
|
||||||
SamplePlatform.prototype.addAccessory = function(accessoryName) {
|
|
||||||
this.log("Add Accessory");
|
|
||||||
var platform = this;
|
|
||||||
var uuid;
|
|
||||||
|
|
||||||
uuid = UUIDGen.generate(accessoryName);
|
|
||||||
|
|
||||||
var newAccessory = new Accessory(accessoryName, uuid);
|
|
||||||
newAccessory.on('identify', function(paired, callback) {
|
|
||||||
platform.log(accessory.displayName, "Identify!!!");
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
// Plugin can save context on accessory
|
|
||||||
// To help restore accessory in configureAccessory()
|
|
||||||
// newAccessory.context.something = "Something"
|
|
||||||
|
|
||||||
// Make sure you provided a name for service otherwise it may not visible in some HomeKit apps.
|
|
||||||
newAccessory.addService(Service.Lightbulb, "Test Light")
|
|
||||||
.getCharacteristic(Characteristic.On)
|
|
||||||
.on('set', function(value, callback) {
|
|
||||||
platform.log(accessory.displayName, "Light -> " + value);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.accessories.push(newAccessory);
|
|
||||||
this.api.registerPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", [newAccessory]);
|
|
||||||
}
|
|
||||||
|
|
||||||
SamplePlatform.prototype.updateAccessoriesReachability = function() {
|
|
||||||
this.log("Update Reachability");
|
|
||||||
for (var index in this.accessories) {
|
|
||||||
var accessory = this.accessories[index];
|
|
||||||
accessory.updateReachability(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sample function to show how developer can remove accessory dynamically from outside event
|
|
||||||
SamplePlatform.prototype.removeAccessory = function() {
|
|
||||||
this.log("Remove Accessory");
|
|
||||||
this.api.unregisterPlatformAccessories("homebridge-samplePlatform", "SamplePlatform", this.accessories);
|
|
||||||
|
|
||||||
this.accessories = [];
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "homebridge-samplePlatform",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "Sample Platform plugin for homebridge: https://github.com/nfarina/homebridge",
|
|
||||||
"license": "ISC",
|
|
||||||
"keywords": [
|
|
||||||
"homebridge-plugin"
|
|
||||||
],
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git://github.com/example/homebridge.git"
|
|
||||||
},
|
|
||||||
"bugs": {
|
|
||||||
"url": "http://github.com/example/homebridge/issues"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12.0",
|
|
||||||
"homebridge": ">=0.2.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
174
lib/api.js
174
lib/api.js
@@ -1,174 +0,0 @@
|
|||||||
var inherits = require('util').inherits;
|
|
||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
var hap = require("hap-nodejs");
|
|
||||||
var hapLegacyTypes = require("hap-nodejs/accessories/types.js");
|
|
||||||
var log = require("./logger")._system;
|
|
||||||
var User = require("./user").User;
|
|
||||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
|
||||||
var serverVersion = require("./version");
|
|
||||||
|
|
||||||
// The official homebridge API is the object we feed the plugin's exported initializer function.
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
API: API
|
|
||||||
}
|
|
||||||
|
|
||||||
function API() {
|
|
||||||
this._accessories = {}; // this._accessories[pluginName.accessoryName] = accessory constructor
|
|
||||||
this._platforms = {}; // this._platforms[pluginName.platformName] = platform constructor
|
|
||||||
|
|
||||||
this._configurableAccessories = {};
|
|
||||||
this._dynamicPlatforms = {}; // this._dynamicPlatforms[pluginName.platformName] = platform constructor
|
|
||||||
|
|
||||||
// expose the homebridge API version
|
|
||||||
this.version = 2.2;
|
|
||||||
|
|
||||||
// expose the homebridge server version
|
|
||||||
this.serverVersion = serverVersion;
|
|
||||||
|
|
||||||
// expose the User class methods to plugins to get paths. Example: homebridge.user.storagePath()
|
|
||||||
this.user = User;
|
|
||||||
|
|
||||||
// expose HAP-NodeJS in its entirely for plugins to use instead of making Plugins
|
|
||||||
// require() it as a dependency - it's a heavy dependency so we don't want it in
|
|
||||||
// every single plugin.
|
|
||||||
this.hap = hap;
|
|
||||||
|
|
||||||
// we also need to "bolt on" the legacy "types" constants for older accessories/platforms
|
|
||||||
// still using the "object literal" style JSON.
|
|
||||||
this.hapLegacyTypes = hapLegacyTypes;
|
|
||||||
|
|
||||||
this.platformAccessory = PlatformAccessory;
|
|
||||||
}
|
|
||||||
|
|
||||||
inherits(API, EventEmitter);
|
|
||||||
|
|
||||||
API.prototype.accessory = function(name) {
|
|
||||||
|
|
||||||
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
|
|
||||||
// see if it matches exactly one accessory.
|
|
||||||
if (name.indexOf('.') == -1) {
|
|
||||||
var found = [];
|
|
||||||
for (var fullName in this._accessories) {
|
|
||||||
if (fullName.split(".")[1] == name)
|
|
||||||
found.push(fullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found.length == 1) {
|
|
||||||
return this._accessories[found[0]];
|
|
||||||
}
|
|
||||||
else if (found.length > 1) {
|
|
||||||
throw new Error("The requested accessory '" + name + "' has been registered multiple times. Please be more specific by writing one of: " + found.join(", "));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error("The requested accessory '" + name + "' was not registered by any plugin.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
if (!this._accessories[name])
|
|
||||||
throw new Error("The requested accessory '" + name + "' was not registered by any plugin.");
|
|
||||||
|
|
||||||
return this._accessories[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.registerAccessory = function(pluginName, accessoryName, constructor, configurationRequestHandler) {
|
|
||||||
var fullName = pluginName + "." + accessoryName;
|
|
||||||
|
|
||||||
if (this._accessories[fullName])
|
|
||||||
throw new Error("Attempting to register an accessory '" + fullName + "' which has already been registered!");
|
|
||||||
|
|
||||||
log.info("Registering accessory '%s'", fullName);
|
|
||||||
|
|
||||||
this._accessories[fullName] = constructor;
|
|
||||||
|
|
||||||
// The plugin supports configuration
|
|
||||||
if (configurationRequestHandler) {
|
|
||||||
this._configurableAccessories[fullName] = configurationRequestHandler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.publishCameraAccessories = function(pluginName, accessories) {
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
if (!(accessory instanceof PlatformAccessory)) {
|
|
||||||
throw new Error(pluginName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
|
||||||
}
|
|
||||||
accessory._associatedPlugin = pluginName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('publishCameraAccessories', accessories);
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.platform = function(name) {
|
|
||||||
|
|
||||||
// if you passed the "short form" name like "Lockitron" instead of "homebridge-lockitron.Lockitron",
|
|
||||||
// see if it matches exactly one platform.
|
|
||||||
if (name.indexOf('.') == -1) {
|
|
||||||
var found = [];
|
|
||||||
for (var fullName in this._platforms) {
|
|
||||||
if (fullName.split(".")[1] == name)
|
|
||||||
found.push(fullName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found.length == 1) {
|
|
||||||
return this._platforms[found[0]];
|
|
||||||
}
|
|
||||||
else if (found.length > 1) {
|
|
||||||
throw new Error("The requested platform '" + name + "' has been registered multiple times. Please be more specific by writing one of: " + found.join(", "));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error("The requested platform '" + name + "' was not registered by any plugin.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
if (!this._platforms[name])
|
|
||||||
throw new Error("The requested platform '" + name + "' was not registered by any plugin.");
|
|
||||||
|
|
||||||
return this._platforms[name];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.registerPlatform = function(pluginName, platformName, constructor, dynamic) {
|
|
||||||
var fullName = pluginName + "." + platformName;
|
|
||||||
|
|
||||||
if (this._platforms[fullName])
|
|
||||||
throw new Error("Attempting to register a platform '" + fullName + "' which has already been registered!");
|
|
||||||
|
|
||||||
log.info("Registering platform '%s'", fullName);
|
|
||||||
|
|
||||||
this._platforms[fullName] = constructor;
|
|
||||||
|
|
||||||
if (dynamic) {
|
|
||||||
this._dynamicPlatforms[fullName] = constructor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.registerPlatformAccessories = function(pluginName, platformName, accessories) {
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
if (!(accessory instanceof PlatformAccessory)) {
|
|
||||||
throw new Error(pluginName + " - " + platformName + " attempt to register an accessory that isn\'t PlatformAccessory!");
|
|
||||||
}
|
|
||||||
accessory._associatedPlugin = pluginName;
|
|
||||||
accessory._associatedPlatform = platformName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('registerPlatformAccessories', accessories);
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.updatePlatformAccessories = function(accessories) {
|
|
||||||
this.emit('updatePlatformAccessories', accessories);
|
|
||||||
}
|
|
||||||
|
|
||||||
API.prototype.unregisterPlatformAccessories = function(pluginName, platformName, accessories) {
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
if (!(accessory instanceof PlatformAccessory)) {
|
|
||||||
throw new Error(pluginName + " - " + platformName + " attempt to unregister an accessory that isn\'t PlatformAccessory!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.emit('unregisterPlatformAccessories', accessories);
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
var inherits = require('util').inherits;
|
|
||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
var Service = require("hap-nodejs").Service;
|
|
||||||
var Characteristic = require("hap-nodejs").Characteristic;
|
|
||||||
var SetupSession = require("./bridgeSetupSession").SetupSession;
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
BridgeSetupManager: BridgeSetupManager
|
|
||||||
}
|
|
||||||
|
|
||||||
function BridgeSetupManager() {
|
|
||||||
this.session;
|
|
||||||
|
|
||||||
this.service = new Service(null, "49FB9D4D-0FEA-4BF1-8FA6-E7B18AB86DCE");
|
|
||||||
|
|
||||||
this.stateCharacteristic = new Characteristic("State", "77474A2F-FA98-485E-97BE-4762458774D8", {
|
|
||||||
format: Characteristic.Formats.UINT8,
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 1,
|
|
||||||
minStep: 1,
|
|
||||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
|
||||||
});
|
|
||||||
this.stateCharacteristic.value = 0;
|
|
||||||
this.service.addCharacteristic(this.stateCharacteristic);
|
|
||||||
|
|
||||||
this.versionCharacteristic = new Characteristic("Version", "FD9FE4CC-D06F-4FFE-96C6-595D464E1026", {
|
|
||||||
format: Characteristic.Formats.STRING,
|
|
||||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY]
|
|
||||||
});
|
|
||||||
this.versionCharacteristic.value = "1.0";
|
|
||||||
this.service.addCharacteristic(this.versionCharacteristic);
|
|
||||||
|
|
||||||
this.controlPointCharacteristic = new Characteristic("Control Point", "5819A4C2-E1B0-4C9D-B761-3EB1AFF43073", {
|
|
||||||
format: Characteristic.Formats.DATA,
|
|
||||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
|
||||||
})
|
|
||||||
this.controlPointCharacteristic.on('get', function(callback, context) {
|
|
||||||
this.handleReadRequest(callback, context);
|
|
||||||
}.bind(this));
|
|
||||||
this.controlPointCharacteristic.on('set', function(newValue, callback, context) {
|
|
||||||
this.handleWriteRequest(newValue, callback, context);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this.controlPointCharacteristic.value = null;
|
|
||||||
this.service.addCharacteristic(this.controlPointCharacteristic);
|
|
||||||
}
|
|
||||||
|
|
||||||
inherits(BridgeSetupManager, EventEmitter);
|
|
||||||
|
|
||||||
BridgeSetupManager.prototype.handleReadRequest = function(callback, context) {
|
|
||||||
if (!context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.session) {
|
|
||||||
callback(null, null);
|
|
||||||
} else {
|
|
||||||
this.session.handleReadRequest(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BridgeSetupManager.prototype.handleWriteRequest = function(value, callback, context) {
|
|
||||||
if (!context) {
|
|
||||||
callback();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = new Buffer(value, 'base64');
|
|
||||||
var request = JSON.parse(data.toString());
|
|
||||||
callback();
|
|
||||||
|
|
||||||
if (!this.session || this.session.sessionUUID !== request.sid) {
|
|
||||||
if (this.session) {
|
|
||||||
this.session.removeAllListeners();
|
|
||||||
this.session.validSession = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.session = new SetupSession(this.stateCharacteristic, this.controlPointCharacteristic);
|
|
||||||
this.session.configurablePlatformPlugins = this.configurablePlatformPlugins;
|
|
||||||
this.session.on('newConfig', function(type, name, replace, config) {
|
|
||||||
this.emit('newConfig', type, name, replace, config);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this.session.on('requestCurrentConfig', function(callback) {
|
|
||||||
this.emit('requestCurrentConfig', callback);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this.session.on('end', function() {
|
|
||||||
this.session = null;
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.session.handleWriteRequest(request);
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
41
lib/cli.js
41
lib/cli.js
@@ -1,41 +0,0 @@
|
|||||||
var program = require('commander');
|
|
||||||
var hap = require("hap-nodejs");
|
|
||||||
var version = require('./version');
|
|
||||||
var Server = require('./server').Server;
|
|
||||||
var Plugin = require('./plugin').Plugin;
|
|
||||||
var User = require('./user').User;
|
|
||||||
var log = require("./logger")._system;
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = function() {
|
|
||||||
|
|
||||||
var insecureAccess = false;
|
|
||||||
|
|
||||||
program
|
|
||||||
.version(version)
|
|
||||||
.option('-P, --plugin-path [path]', 'look for plugins installed at [path] as well as the default locations ([path] can also point to a single plugin)', function(p) { Plugin.addPluginPath(p); })
|
|
||||||
.option('-U, --user-storage-path [path]', 'look for homebridge user files at [path] instead of the default location (~/.homebridge)', function(p) { User.setStoragePath(p); })
|
|
||||||
.option('-D, --debug', 'turn on debug level logging', function() { require('./logger').setDebugEnabled(true) })
|
|
||||||
.option('-I, --insecure', 'allow unauthenticated requests (for easier hacking)', function() { insecureAccess = true })
|
|
||||||
.parse(process.argv);
|
|
||||||
|
|
||||||
// Initialize HAP-NodeJS with a custom persist directory
|
|
||||||
hap.init(User.persistPath());
|
|
||||||
|
|
||||||
var server = new Server(insecureAccess);
|
|
||||||
|
|
||||||
var signals = { 'SIGINT': 2, 'SIGTERM': 15 };
|
|
||||||
Object.keys(signals).forEach(function (signal) {
|
|
||||||
process.on(signal, function () {
|
|
||||||
log.info("Got %s, shutting down Homebridge...", signal);
|
|
||||||
|
|
||||||
// Save cached accessories to persist storage.
|
|
||||||
server._updateCachedAccessories();
|
|
||||||
|
|
||||||
process.exit(128 + signals[signal]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
server.run();
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
var chalk = require('chalk');
|
|
||||||
var util = require('util');
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Logger: Logger,
|
|
||||||
setDebugEnabled: setDebugEnabled,
|
|
||||||
_system: new Logger() // system logger, for internal use only
|
|
||||||
}
|
|
||||||
|
|
||||||
var DEBUG_ENABLED = false;
|
|
||||||
|
|
||||||
// Turns on debug level logging
|
|
||||||
function setDebugEnabled(enabled) {
|
|
||||||
DEBUG_ENABLED = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
// global cache of logger instances by plugin name
|
|
||||||
var loggerCache = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logger class
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Logger(prefix) {
|
|
||||||
this.prefix = prefix;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.debug = function(msg) {
|
|
||||||
if (DEBUG_ENABLED)
|
|
||||||
this.log.apply(this, ['debug'].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.info = function(msg) {
|
|
||||||
this.log.apply(this, ['info'].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.warn = function(msg) {
|
|
||||||
this.log.apply(this, ['warn'].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.error = function(msg) {
|
|
||||||
this.log.apply(this, ['error'].concat(Array.prototype.slice.call(arguments)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.prototype.log = function(level, msg) {
|
|
||||||
|
|
||||||
msg = util.format.apply(util, Array.prototype.slice.call(arguments, 1));
|
|
||||||
func = console.log;
|
|
||||||
|
|
||||||
if (level == 'debug') {
|
|
||||||
msg = chalk.gray(msg);
|
|
||||||
}
|
|
||||||
else if (level == 'warn') {
|
|
||||||
msg = chalk.yellow(msg);
|
|
||||||
func = console.error;
|
|
||||||
}
|
|
||||||
else if (level == 'error') {
|
|
||||||
msg = chalk.bold.red(msg);
|
|
||||||
func = console.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepend prefix if applicable
|
|
||||||
if (this.prefix)
|
|
||||||
msg = chalk.cyan("[" + this.prefix + "]") + " " + msg;
|
|
||||||
|
|
||||||
// prepend timestamp
|
|
||||||
var date = new Date();
|
|
||||||
msg = chalk.white("[" + date.toLocaleString() + "]") + " " + msg;
|
|
||||||
|
|
||||||
func(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.withPrefix = function(prefix) {
|
|
||||||
|
|
||||||
if (!loggerCache[prefix]) {
|
|
||||||
// create a class-like logger thing that acts as a function as well
|
|
||||||
// as an instance of Logger.
|
|
||||||
var logger = new Logger(prefix);
|
|
||||||
var log = logger.info.bind(logger);
|
|
||||||
log.debug = logger.debug;
|
|
||||||
log.info = logger.info;
|
|
||||||
log.warn = logger.warn;
|
|
||||||
log.error = logger.error;
|
|
||||||
log.log = logger.log;
|
|
||||||
log.prefix = logger.prefix;
|
|
||||||
loggerCache[prefix] = log;
|
|
||||||
}
|
|
||||||
|
|
||||||
return loggerCache[prefix];
|
|
||||||
}
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
var uuid = require("hap-nodejs").uuid;
|
|
||||||
var Accessory = require("hap-nodejs").Accessory;
|
|
||||||
var Service = require("hap-nodejs").Service;
|
|
||||||
var Characteristic = require("hap-nodejs").Characteristic;
|
|
||||||
var inherits = require('util').inherits;
|
|
||||||
var EventEmitter = require('events').EventEmitter;
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
PlatformAccessory: PlatformAccessory
|
|
||||||
}
|
|
||||||
|
|
||||||
function PlatformAccessory(displayName, UUID, category) {
|
|
||||||
if (!displayName) throw new Error("Accessories must be created with a non-empty displayName.");
|
|
||||||
if (!UUID) throw new Error("Accessories must be created with a valid UUID.");
|
|
||||||
if (!uuid.isValid(UUID)) throw new Error("UUID '" + UUID + "' is not a valid UUID. Try using the provided 'generateUUID' function to create a valid UUID from any arbitrary string, like a serial number.");
|
|
||||||
|
|
||||||
this.displayName = displayName;
|
|
||||||
this.UUID = UUID;
|
|
||||||
this.category = category || Accessory.Categories.OTHER;
|
|
||||||
this.services = [];
|
|
||||||
this.reachable = false;
|
|
||||||
this.context = {};
|
|
||||||
|
|
||||||
this._associatedPlugin;
|
|
||||||
this._associatedPlatform;
|
|
||||||
this._associatedHAPAccessory;
|
|
||||||
|
|
||||||
this
|
|
||||||
.addService(Service.AccessoryInformation)
|
|
||||||
.setCharacteristic(Characteristic.Name, displayName)
|
|
||||||
.setCharacteristic(Characteristic.Manufacturer, "Default-Manufacturer")
|
|
||||||
.setCharacteristic(Characteristic.Model, "Default-Model")
|
|
||||||
.setCharacteristic(Characteristic.SerialNumber, "Default-SerialNumber");
|
|
||||||
}
|
|
||||||
|
|
||||||
inherits(PlatformAccessory, EventEmitter);
|
|
||||||
|
|
||||||
PlatformAccessory.prototype.addService = function(service) {
|
|
||||||
// service might be a constructor like `Service.AccessoryInformation` instead of an instance
|
|
||||||
// of Service. Coerce if necessary.
|
|
||||||
if (typeof service === 'function')
|
|
||||||
service = new (Function.prototype.bind.apply(service, arguments));
|
|
||||||
|
|
||||||
// check for UUID+subtype conflict
|
|
||||||
for (var index in this.services) {
|
|
||||||
var existing = this.services[index];
|
|
||||||
if (existing.UUID === service.UUID) {
|
|
||||||
// OK we have two Services with the same UUID. Check that each defines a `subtype` property and that each is unique.
|
|
||||||
if (!service.subtype)
|
|
||||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' as another Service in this Accessory without also defining a unique 'subtype' property.");
|
|
||||||
|
|
||||||
if (service.subtype.toString() === existing.subtype.toString())
|
|
||||||
throw new Error("Cannot add a Service with the same UUID '" + existing.UUID + "' and subtype '" + existing.subtype + "' as another Service in this Accessory.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.services.push(service);
|
|
||||||
|
|
||||||
if (this._associatedHAPAccessory) {
|
|
||||||
this._associatedHAPAccessory.addService(service);
|
|
||||||
}
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlatformAccessory.prototype.removeService = function(service) {
|
|
||||||
var targetServiceIndex;
|
|
||||||
|
|
||||||
for (var index in this.services) {
|
|
||||||
var existingService = this.services[index];
|
|
||||||
|
|
||||||
if (existingService === service) {
|
|
||||||
targetServiceIndex = index;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetServiceIndex) {
|
|
||||||
this.services.splice(targetServiceIndex, 1);
|
|
||||||
service.removeAllListeners();
|
|
||||||
|
|
||||||
if (this._associatedHAPAccessory) {
|
|
||||||
this._associatedHAPAccessory.removeService(service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
|
||||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
|
||||||
* @param {ServiceConstructor|string} name
|
|
||||||
* @returns Service
|
|
||||||
*/
|
|
||||||
PlatformAccessory.prototype.getService = function(name) {
|
|
||||||
for (var index in this.services) {
|
|
||||||
var service = this.services[index];
|
|
||||||
|
|
||||||
if (typeof name === 'string' && (service.displayName === name || service.name === name))
|
|
||||||
return service;
|
|
||||||
else if (typeof name === 'function' && ((service instanceof name) || (name.UUID === service.UUID)))
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* searchs for a Service in the services collection and returns the first Service object that matches.
|
|
||||||
* If multiple services of the same type are present in one accessory, use getServiceByUUIDAndSubType instead.
|
|
||||||
* @param {string} UUID Can be an UUID, a service.displayName, or a constructor of a Service
|
|
||||||
* @param {string} subtype A subtype string to match
|
|
||||||
* @returns Service
|
|
||||||
*/
|
|
||||||
PlatformAccessory.prototype.getServiceByUUIDAndSubType = function(UUID, subtype) {
|
|
||||||
for (var index in this.services) {
|
|
||||||
var service = this.services[index];
|
|
||||||
|
|
||||||
if (typeof UUID === 'string' && (service.displayName === UUID || service.name === UUID) && service.subtype === subtype )
|
|
||||||
return service;
|
|
||||||
else if (typeof UUID === 'function' && ((service instanceof UUID) || (UUID.UUID === service.UUID)) && service.subtype === subtype)
|
|
||||||
return service;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
PlatformAccessory.prototype.updateReachability = function(reachable) {
|
|
||||||
this.reachable = reachable;
|
|
||||||
|
|
||||||
if (this._associatedHAPAccessory) {
|
|
||||||
this._associatedHAPAccessory.updateReachability(reachable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlatformAccessory.prototype.configureCameraSource = function(cameraSource) {
|
|
||||||
this.cameraSource = cameraSource;
|
|
||||||
for (var index in cameraSource.services) {
|
|
||||||
var service = cameraSource.services[index];
|
|
||||||
this.addService(service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PlatformAccessory.prototype._prepareAssociatedHAPAccessory = function () {
|
|
||||||
this._associatedHAPAccessory = new Accessory(this.displayName, this.UUID);
|
|
||||||
|
|
||||||
if (this.cameraSource) {
|
|
||||||
this._associatedHAPAccessory.configureCameraSource(this.cameraSource);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._associatedHAPAccessory._sideloadServices(this.services);
|
|
||||||
this._associatedHAPAccessory.category = this.category;
|
|
||||||
this._associatedHAPAccessory.reachable = this.reachable;
|
|
||||||
this._associatedHAPAccessory.on('identify', function(paired, callback) {
|
|
||||||
if (this.listeners('identify').length > 0) {
|
|
||||||
// allow implementors to identify this Accessory in whatever way is appropriate, and pass along
|
|
||||||
// the standard callback for completion.
|
|
||||||
this.emit('identify', paired, callback);
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
PlatformAccessory.prototype._dictionaryPresentation = function() {
|
|
||||||
var accessory = {};
|
|
||||||
|
|
||||||
accessory.plugin = this._associatedPlugin;
|
|
||||||
accessory.platform = this._associatedPlatform;
|
|
||||||
accessory.displayName = this.displayName;
|
|
||||||
accessory.UUID = this.UUID;
|
|
||||||
accessory.category = this.category;
|
|
||||||
accessory.context = this.context;
|
|
||||||
|
|
||||||
var services = [];
|
|
||||||
for (var index in this.services) {
|
|
||||||
var service = this.services[index];
|
|
||||||
var servicePresentation = {};
|
|
||||||
servicePresentation.displayName = service.displayName;
|
|
||||||
servicePresentation.UUID = service.UUID;
|
|
||||||
servicePresentation.subtype = service.subtype;
|
|
||||||
|
|
||||||
var characteristics = [];
|
|
||||||
for (var cIndex in service.characteristics) {
|
|
||||||
var characteristic = service.characteristics[cIndex];
|
|
||||||
var characteristicPresentation = {};
|
|
||||||
characteristicPresentation.displayName = characteristic.displayName;
|
|
||||||
characteristicPresentation.UUID = characteristic.UUID;
|
|
||||||
characteristicPresentation.props = characteristic.props;
|
|
||||||
characteristicPresentation.value = characteristic.value;
|
|
||||||
characteristicPresentation.eventOnlyCharacteristic = characteristic.eventOnlyCharacteristic;
|
|
||||||
characteristics.push(characteristicPresentation);
|
|
||||||
}
|
|
||||||
|
|
||||||
servicePresentation.characteristics = characteristics;
|
|
||||||
services.push(servicePresentation);
|
|
||||||
}
|
|
||||||
|
|
||||||
accessory.services = services;
|
|
||||||
return accessory;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlatformAccessory.prototype._configFromData = function(data) {
|
|
||||||
this._associatedPlugin = data.plugin;
|
|
||||||
this._associatedPlatform = data.platform;
|
|
||||||
this.displayName = data.displayName;
|
|
||||||
this.UUID = data.UUID;
|
|
||||||
this.category = data.category;
|
|
||||||
this.context = data.context;
|
|
||||||
this.reachable = false;
|
|
||||||
|
|
||||||
var services = [];
|
|
||||||
for (var index in data.services) {
|
|
||||||
var service = data.services[index];
|
|
||||||
var hapService = new Service(service.displayName, service.UUID, service.subtype);
|
|
||||||
|
|
||||||
var characteristics = [];
|
|
||||||
for (var cIndex in service.characteristics) {
|
|
||||||
var characteristic = service.characteristics[cIndex];
|
|
||||||
var hapCharacteristic = new Characteristic(characteristic.displayName, characteristic.UUID, characteristic.props);
|
|
||||||
hapCharacteristic.eventOnlyCharacteristic = characteristic.eventOnlyCharacteristic;
|
|
||||||
hapCharacteristic.value = characteristic.value;
|
|
||||||
characteristics.push(hapCharacteristic);
|
|
||||||
}
|
|
||||||
|
|
||||||
hapService._sideloadCharacteristics(characteristics);
|
|
||||||
services.push(hapService);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.services = services;
|
|
||||||
}
|
|
||||||
206
lib/plugin.js
206
lib/plugin.js
@@ -1,206 +0,0 @@
|
|||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
|
||||||
var semver = require('semver');
|
|
||||||
var User = require('./user').User;
|
|
||||||
var version = require('./version');
|
|
||||||
var log = require("./logger")._system;
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Plugin: Plugin
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Homebridge Plugin.
|
|
||||||
*
|
|
||||||
* Allows for discovering and loading installed Homebridge plugins.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Plugin(pluginPath) {
|
|
||||||
this.pluginPath = pluginPath; // like "/usr/local/lib/node_modules/homebridge-lockitron"
|
|
||||||
this.initializer; // exported function from the plugin that initializes it
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.prototype.name = function() {
|
|
||||||
return path.basename(this.pluginPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.prototype.load = function(options) {
|
|
||||||
options = options || {};
|
|
||||||
|
|
||||||
// does this plugin exist at all?
|
|
||||||
if (!fs.existsSync(this.pluginPath)) {
|
|
||||||
throw new Error("Plugin " + this.pluginPath + " was not found. Make sure the module '" + this.pluginPath + "' is installed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt to load package.json
|
|
||||||
var pjson = Plugin.loadPackageJSON(this.pluginPath);
|
|
||||||
|
|
||||||
// very temporary fix for first wave of plugins
|
|
||||||
if (pjson.peerDepdendencies && (!pjson.engines || !pjson.engines.homebridge)) {
|
|
||||||
var engines = pjson.engines || {}
|
|
||||||
engines.homebridge = pjson.peerDepdendencies.homebridge;
|
|
||||||
pjson.engines = engines;
|
|
||||||
}
|
|
||||||
|
|
||||||
// pluck out the HomeBridge version requirement
|
|
||||||
if (!pjson.engines || !pjson.engines.homebridge) {
|
|
||||||
throw new Error("Plugin " + this.pluginPath + " does not contain the 'homebridge' package in 'engines'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionRequired = pjson.engines.homebridge;
|
|
||||||
|
|
||||||
// make sure the version is satisfied by the currently running version of HomeBridge
|
|
||||||
if (!semver.satisfies(version, versionRequired)) {
|
|
||||||
throw new Error("Plugin " + this.pluginPath + " requires a HomeBridge version of " + versionRequired + " which does not satisfy the current HomeBridge version of " + version + ". You may need to upgrade your installation of HomeBridge.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// figure out the main module - index.js unless otherwise specified
|
|
||||||
var main = pjson.main || "./index.js";
|
|
||||||
|
|
||||||
var mainPath = path.join(this.pluginPath, main);
|
|
||||||
|
|
||||||
// try to require() it and grab the exported initialization hook
|
|
||||||
this.initializer = require(mainPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.loadPackageJSON = function(pluginPath) {
|
|
||||||
// check for a package.json
|
|
||||||
var pjsonPath = path.join(pluginPath, "package.json");
|
|
||||||
var pjson = null;
|
|
||||||
|
|
||||||
if (!fs.existsSync(pjsonPath)) {
|
|
||||||
throw new Error("Plugin " + pluginPath + " does not contain a package.json.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// attempt to parse package.json
|
|
||||||
pjson = JSON.parse(fs.readFileSync(pjsonPath));
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
throw new Error("Plugin " + pluginPath + " contains an invalid package.json. Error: " + err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the name is prefixed with 'homebridge-'
|
|
||||||
if (!pjson.name || pjson.name.indexOf('homebridge-') != 0) {
|
|
||||||
throw new Error("Plugin " + pluginPath + " does not have a package name that begins with 'homebridge-'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify that it's tagged with the correct keyword
|
|
||||||
if (!pjson.keywords || pjson.keywords.indexOf("homebridge-plugin") == -1) {
|
|
||||||
throw new Error("Plugin " + pluginPath + " package.json does not contain the keyword 'homebridge-plugin'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return pjson;
|
|
||||||
}
|
|
||||||
|
|
||||||
Plugin.getDefaultPaths = function() {
|
|
||||||
var win32 = process.platform === 'win32';
|
|
||||||
var paths = [];
|
|
||||||
|
|
||||||
// add the paths used by require()
|
|
||||||
paths = paths.concat(require.main.paths);
|
|
||||||
|
|
||||||
// THIS SECTION FROM: https://github.com/yeoman/environment/blob/master/lib/resolver.js
|
|
||||||
|
|
||||||
// Adding global npm directories
|
|
||||||
// We tried using npm to get the global modules path, but it haven't work out
|
|
||||||
// because of bugs in the parseable implementation of `ls` command and mostly
|
|
||||||
// performance issues. So, we go with our best bet for now.
|
|
||||||
if (process.env.NODE_PATH) {
|
|
||||||
paths = process.env.NODE_PATH.split(path.delimiter)
|
|
||||||
.filter(function(p) { return !!p; }) // trim out empty values
|
|
||||||
.concat(paths);
|
|
||||||
} else {
|
|
||||||
// Default paths for each system
|
|
||||||
if (win32) {
|
|
||||||
paths.push(path.join(process.env.APPDATA, 'npm/node_modules'));
|
|
||||||
} else {
|
|
||||||
paths.push('/usr/local/lib/node_modules');
|
|
||||||
paths.push('/usr/lib/node_modules');
|
|
||||||
const exec = require('child_process').execSync;
|
|
||||||
paths.push(exec('/bin/echo -n "$(npm -g prefix)/lib/node_modules"').toString('utf8'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All search paths we will use to discover installed plugins
|
|
||||||
Plugin.paths = Plugin.getDefaultPaths();
|
|
||||||
|
|
||||||
Plugin.addPluginPath = function(pluginPath) {
|
|
||||||
Plugin.paths.unshift(path.resolve(process.cwd(), pluginPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gets all plugins installed on the local system
|
|
||||||
Plugin.installed = function() {
|
|
||||||
|
|
||||||
var plugins = [];
|
|
||||||
var pluginsByName = {}; // don't add duplicate plugins
|
|
||||||
var searchedPaths = {}; // don't search the same paths twice
|
|
||||||
|
|
||||||
// search for plugins among all known paths, in order
|
|
||||||
for (var index in Plugin.paths) {
|
|
||||||
var requirePath = Plugin.paths[index];
|
|
||||||
|
|
||||||
// did we already search this path?
|
|
||||||
if (searchedPaths[requirePath])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
searchedPaths[requirePath] = true;
|
|
||||||
|
|
||||||
// just because this path is in require.main.paths doesn't mean it necessarily exists!
|
|
||||||
if (!fs.existsSync(requirePath))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var names = fs.readdirSync(requirePath);
|
|
||||||
|
|
||||||
// does this path point inside a single plugin and not a directory containing plugins?
|
|
||||||
if (fs.existsSync(path.join(requirePath, "package.json")))
|
|
||||||
names = [""];
|
|
||||||
|
|
||||||
// read through each directory in this node_modules folder
|
|
||||||
for (var index2 in names) {
|
|
||||||
var name = names[index2];
|
|
||||||
|
|
||||||
// reconstruct full path
|
|
||||||
var pluginPath = path.join(requirePath, name);
|
|
||||||
try {
|
|
||||||
// we only care about directories
|
|
||||||
if (!fs.statSync(pluginPath).isDirectory()) continue;
|
|
||||||
} catch (e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// does this module contain a package.json?
|
|
||||||
var pjson;
|
|
||||||
try {
|
|
||||||
// throws an Error if this isn't a homebridge plugin
|
|
||||||
pjson = Plugin.loadPackageJSON(pluginPath);
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
// is this "trying" to be a homebridge plugin? if so let you know what went wrong.
|
|
||||||
if (!name || name.indexOf('homebridge-') == 0) {
|
|
||||||
log.warn(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip this module
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get actual name if this path points inside a single plugin
|
|
||||||
if (!name) name = pjson.name;
|
|
||||||
|
|
||||||
// add it to the return list
|
|
||||||
if (!pluginsByName[name]) {
|
|
||||||
pluginsByName[name] = pluginPath;
|
|
||||||
plugins.push(new Plugin(pluginPath));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
log.warn("Warning: skipping plugin found at '" + pluginPath + "' since we already loaded the same plugin from '" + pluginsByName[name] + "'.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
589
lib/server.js
589
lib/server.js
@@ -1,589 +0,0 @@
|
|||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
|
||||||
var uuid = require("hap-nodejs").uuid;
|
|
||||||
var accessoryStorage = require('node-persist').create();
|
|
||||||
var Bridge = require("hap-nodejs").Bridge;
|
|
||||||
var Accessory = require("hap-nodejs").Accessory;
|
|
||||||
var Service = require("hap-nodejs").Service;
|
|
||||||
var Characteristic = require("hap-nodejs").Characteristic;
|
|
||||||
var AccessoryLoader = require("hap-nodejs").AccessoryLoader;
|
|
||||||
var once = require("hap-nodejs/lib/util/once").once;
|
|
||||||
var Plugin = require('./plugin').Plugin;
|
|
||||||
var User = require('./user').User;
|
|
||||||
var API = require('./api').API;
|
|
||||||
var PlatformAccessory = require("./platformAccessory").PlatformAccessory;
|
|
||||||
var BridgeSetupManager = require("./bridgeSetupManager").BridgeSetupManager;
|
|
||||||
var log = require("./logger")._system;
|
|
||||||
var Logger = require('./logger').Logger;
|
|
||||||
var mac = require("./util/mac");
|
|
||||||
var chalk = require('chalk');
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
Server: Server
|
|
||||||
}
|
|
||||||
|
|
||||||
function Server(insecureAccess, opts) {
|
|
||||||
opts = opts || {};
|
|
||||||
|
|
||||||
// Setup Accessory Cache Storage
|
|
||||||
accessoryStorage.initSync({ dir: User.cachedAccessoryPath() });
|
|
||||||
|
|
||||||
this._api = new API(); // object we feed to Plugins
|
|
||||||
|
|
||||||
this._api.on('registerPlatformAccessories', function(accessories) {
|
|
||||||
this._handleRegisterPlatformAccessories(accessories);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this._api.on('updatePlatformAccessories', function(accessories) {
|
|
||||||
this._handleUpdatePlatformAccessories(accessories);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this._api.on('unregisterPlatformAccessories', function(accessories) {
|
|
||||||
this._handleUnregisterPlatformAccessories(accessories);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this._api.on('publishCameraAccessories', function(accessories) {
|
|
||||||
this._handlePublishCameraAccessories(accessories);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this._plugins = this._loadPlugins(); // plugins[name] = Plugin instance
|
|
||||||
this._config = opts.config || this._loadConfig();
|
|
||||||
this._cachedPlatformAccessories = this._loadCachedPlatformAccessories();
|
|
||||||
this._bridge = this._createBridge();
|
|
||||||
|
|
||||||
this._activeDynamicPlugins = {};
|
|
||||||
this._configurablePlatformPlugins = {};
|
|
||||||
this._publishedCameras = {};
|
|
||||||
this._setupManager = new BridgeSetupManager();
|
|
||||||
this._setupManager.on('newConfig', this._handleNewConfig.bind(this));
|
|
||||||
|
|
||||||
this._setupManager.on('requestCurrentConfig', function(callback) {
|
|
||||||
callback(this._config);
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
// Server is "secure by default", meaning it creates a top-level Bridge accessory that
|
|
||||||
// will not allow unauthenticated requests. This matches the behavior of actual HomeKit
|
|
||||||
// accessories. However you can set this to true to allow all requests without authentication,
|
|
||||||
// which can be useful for easy hacking. Note that this will expose all functions of your
|
|
||||||
// bridged accessories, like changing charactersitics (i.e. flipping your lights on and off).
|
|
||||||
this._allowInsecureAccess = insecureAccess || false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype.run = function() {
|
|
||||||
|
|
||||||
// keep track of async calls we're waiting for callbacks on before we can start up
|
|
||||||
this._asyncCalls = 0;
|
|
||||||
this._asyncWait = true;
|
|
||||||
|
|
||||||
if (this._config.platforms) this._loadPlatforms();
|
|
||||||
if (this._config.accessories) this._loadAccessories();
|
|
||||||
this._loadDynamicPlatforms();
|
|
||||||
this._configCachedPlatformAccessories();
|
|
||||||
this._setupManager.configurablePlatformPlugins = this._configurablePlatformPlugins;
|
|
||||||
this._bridge.addService(this._setupManager.service);
|
|
||||||
|
|
||||||
this._asyncWait = false;
|
|
||||||
|
|
||||||
// publish now unless we're waiting on anyone
|
|
||||||
if (this._asyncCalls == 0)
|
|
||||||
this._publish();
|
|
||||||
|
|
||||||
this._api.emit('didFinishLaunching');
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._publish = function() {
|
|
||||||
// pull out our custom Bridge settings from config.json, if any
|
|
||||||
var bridgeConfig = this._config.bridge || {};
|
|
||||||
|
|
||||||
var info = this._bridge.getService(Service.AccessoryInformation);
|
|
||||||
if (bridgeConfig.manufacturer)
|
|
||||||
info.setCharacteristic(Characteristic.Manufacturer, bridgeConfig.manufacturer);
|
|
||||||
if (bridgeConfig.model)
|
|
||||||
info.setCharacteristic(Characteristic.Model, bridgeConfig.model);
|
|
||||||
if (bridgeConfig.serialNumber)
|
|
||||||
info.setCharacteristic(Characteristic.SerialNumber, bridgeConfig.serialNumber);
|
|
||||||
|
|
||||||
this._printPin(bridgeConfig.pin);
|
|
||||||
|
|
||||||
this._bridge.on('listening', function(port) {
|
|
||||||
log.info("Homebridge is running on port %s.", port);
|
|
||||||
});
|
|
||||||
|
|
||||||
this._bridge.publish({
|
|
||||||
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
|
|
||||||
port: bridgeConfig.port || 0,
|
|
||||||
pincode: bridgeConfig.pin || "031-45-154",
|
|
||||||
category: Accessory.Categories.BRIDGE
|
|
||||||
}, this._allowInsecureAccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadPlugins = function(accessories, platforms) {
|
|
||||||
|
|
||||||
var plugins = {};
|
|
||||||
var foundOnePlugin = false;
|
|
||||||
|
|
||||||
// load and validate plugins - check for valid package.json, etc.
|
|
||||||
Plugin.installed().forEach(function(plugin) {
|
|
||||||
|
|
||||||
// attempt to load it
|
|
||||||
try {
|
|
||||||
plugin.load();
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
log.error("====================")
|
|
||||||
log.error("ERROR LOADING PLUGIN " + plugin.name() + ":")
|
|
||||||
log.error(err.stack);
|
|
||||||
log.error("====================")
|
|
||||||
plugin.loadError = err;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plugin.loadError) {
|
|
||||||
|
|
||||||
// add it to our dict for easy lookup later
|
|
||||||
plugins[plugin.name()] = plugin;
|
|
||||||
|
|
||||||
log.info("Loaded plugin: " + plugin.name());
|
|
||||||
|
|
||||||
// call the plugin's initializer and pass it our API instance
|
|
||||||
plugin.initializer(this._api);
|
|
||||||
|
|
||||||
log.info("---");
|
|
||||||
foundOnePlugin = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
// Complain if you don't have any plugins.
|
|
||||||
if (!foundOnePlugin) {
|
|
||||||
log.warn("No plugins found. See the README for information on installing plugins.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadConfig = function() {
|
|
||||||
|
|
||||||
// Look for the configuration file
|
|
||||||
var configPath = User.configPath();
|
|
||||||
|
|
||||||
// Complain and exit if it doesn't exist yet
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
log.warn("config.json (%s) not found.", configPath);
|
|
||||||
|
|
||||||
var config = {};
|
|
||||||
|
|
||||||
config.bridge = {
|
|
||||||
"name": "Homebridge",
|
|
||||||
"username": "CC:22:3D:E3:CE:30",
|
|
||||||
"pin": "031-45-154"
|
|
||||||
};
|
|
||||||
|
|
||||||
return config;
|
|
||||||
// log.error("Couldn't find a config.json file at '"+configPath+"'. Look at config-sample.json for examples of how to format your config.js and add your home accessories.");
|
|
||||||
// process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load up the configuration file
|
|
||||||
var config;
|
|
||||||
try {
|
|
||||||
config = JSON.parse(fs.readFileSync(configPath));
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
log.error("There was a problem reading your config.json file.");
|
|
||||||
log.error("Please try pasting your config.json file here to validate it: http://jsonlint.com");
|
|
||||||
log.error("");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessoryCount = (config.accessories && config.accessories.length) || 0;
|
|
||||||
|
|
||||||
var username = config.bridge.username;
|
|
||||||
var validMac = /^([0-9A-F]{2}:){5}([0-9A-F]{2})$/;
|
|
||||||
if (!validMac.test(username)){
|
|
||||||
throw new Error('Not a valid username: ' + username + '. Must be 6 pairs of colon-' +
|
|
||||||
'separated hexadecimal chars (A-F 0-9), like a MAC address.');
|
|
||||||
}
|
|
||||||
|
|
||||||
var accessoryCount = (config.accessories && config.accessories.length) || 0;
|
|
||||||
var platformCount = (config.platforms && config.platforms.length) || 0;
|
|
||||||
log.info("Loaded config.json with %s accessories and %s platforms.", accessoryCount, platformCount);
|
|
||||||
|
|
||||||
log.info("---");
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadCachedPlatformAccessories = function() {
|
|
||||||
var cachedAccessories = accessoryStorage.getItem("cachedAccessories");
|
|
||||||
var platformAccessories = [];
|
|
||||||
|
|
||||||
if (cachedAccessories) {
|
|
||||||
for (var index in cachedAccessories) {
|
|
||||||
var serializedAccessory = cachedAccessories[index];
|
|
||||||
var platformAccessory = new PlatformAccessory(serializedAccessory.displayName, serializedAccessory.UUID, serializedAccessory.category);
|
|
||||||
platformAccessory._configFromData(serializedAccessory);
|
|
||||||
|
|
||||||
platformAccessories.push(platformAccessory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return platformAccessories;
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._createBridge = function() {
|
|
||||||
// pull out our custom Bridge settings from config.json, if any
|
|
||||||
var bridgeConfig = this._config.bridge || {};
|
|
||||||
|
|
||||||
// Create our Bridge which will host all loaded Accessories
|
|
||||||
return new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadAccessories = function() {
|
|
||||||
|
|
||||||
// Instantiate all accessories in the config
|
|
||||||
log.info("Loading " + this._config.accessories.length + " accessories...");
|
|
||||||
|
|
||||||
for (var i=0; i<this._config.accessories.length; i++) {
|
|
||||||
|
|
||||||
var accessoryConfig = this._config.accessories[i];
|
|
||||||
|
|
||||||
// Load up the class for this accessory
|
|
||||||
var accessoryType = accessoryConfig["accessory"]; // like "Lockitron"
|
|
||||||
var accessoryConstructor = this._api.accessory(accessoryType); // like "LockitronAccessory", a JavaScript constructor
|
|
||||||
|
|
||||||
if (!accessoryConstructor)
|
|
||||||
throw new Error("Your config.json is requesting the accessory '" + accessoryType + "' which has not been published by any installed plugins.");
|
|
||||||
|
|
||||||
// Create a custom logging function that prepends the device display name for debugging
|
|
||||||
var accessoryName = accessoryConfig["name"];
|
|
||||||
var accessoryLogger = Logger.withPrefix(accessoryName);
|
|
||||||
|
|
||||||
accessoryLogger("Initializing %s accessory...", accessoryType);
|
|
||||||
|
|
||||||
var accessoryInstance = new accessoryConstructor(accessoryLogger, accessoryConfig);
|
|
||||||
var accessory = this._createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
|
|
||||||
|
|
||||||
// add it to the bridge
|
|
||||||
this._bridge.addBridgedAccessory(accessory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadPlatforms = function() {
|
|
||||||
|
|
||||||
log.info("Loading " + this._config.platforms.length + " platforms...");
|
|
||||||
|
|
||||||
for (var i=0; i<this._config.platforms.length; i++) {
|
|
||||||
|
|
||||||
var platformConfig = this._config.platforms[i];
|
|
||||||
|
|
||||||
// Load up the class for this accessory
|
|
||||||
var platformType = platformConfig["platform"]; // like "Wink"
|
|
||||||
var platformName = platformConfig["name"];
|
|
||||||
var platformConstructor = this._api.platform(platformType); // like "WinkPlatform", a JavaScript constructor
|
|
||||||
|
|
||||||
if (!platformConstructor)
|
|
||||||
throw new Error("Your config.json is requesting the platform '" + platformType + "' which has not been published by any installed plugins.");
|
|
||||||
|
|
||||||
// Create a custom logging function that prepends the platform name for debugging
|
|
||||||
var platformLogger = Logger.withPrefix(platformName);
|
|
||||||
|
|
||||||
platformLogger("Initializing %s platform...", platformType);
|
|
||||||
|
|
||||||
var platformInstance = new platformConstructor(platformLogger, platformConfig, this._api);
|
|
||||||
|
|
||||||
if (platformInstance.configureAccessory == undefined) {
|
|
||||||
// Plugin 1.0, load accessories
|
|
||||||
this._loadPlatformAccessories(platformInstance, platformLogger, platformType);
|
|
||||||
} else {
|
|
||||||
this._activeDynamicPlugins[platformType] = platformInstance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platformInstance.configurationRequestHandler != undefined) {
|
|
||||||
this._configurablePlatformPlugins[platformType] = platformInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadDynamicPlatforms = function() {
|
|
||||||
for (var dynamicPluginName in this._api._dynamicPlatforms) {
|
|
||||||
if (!this._activeDynamicPlugins[dynamicPluginName] && !this._activeDynamicPlugins[dynamicPluginName.split(".")[1]]) {
|
|
||||||
console.log("Load " + dynamicPluginName);
|
|
||||||
var platformConstructor = this._api._dynamicPlatforms[dynamicPluginName];
|
|
||||||
var platformLogger = Logger.withPrefix(dynamicPluginName);
|
|
||||||
var platformInstance = new platformConstructor(platformLogger, null, this._api);
|
|
||||||
this._activeDynamicPlugins[dynamicPluginName] = platformInstance;
|
|
||||||
|
|
||||||
if (platformInstance.configurationRequestHandler != undefined) {
|
|
||||||
this._configurablePlatformPlugins[dynamicPluginName] = platformInstance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._configCachedPlatformAccessories = function() {
|
|
||||||
for (var index in this._cachedPlatformAccessories) {
|
|
||||||
var accessory = this._cachedPlatformAccessories[index];
|
|
||||||
|
|
||||||
if (!(accessory instanceof PlatformAccessory)) {
|
|
||||||
console.log("Unexpected Accessory!");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fullName = accessory._associatedPlugin + "." + accessory._associatedPlatform;
|
|
||||||
var platformInstance = this._activeDynamicPlugins[fullName];
|
|
||||||
|
|
||||||
if (!platformInstance) {
|
|
||||||
platformInstance = this._activeDynamicPlugins[accessory._associatedPlatform];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platformInstance) {
|
|
||||||
platformInstance.configureAccessory(accessory);
|
|
||||||
} else {
|
|
||||||
console.log("Failed to find plugin to handle accessory " + accessory.displayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
accessory._prepareAssociatedHAPAccessory();
|
|
||||||
this._bridge.addBridgedAccessory(accessory._associatedHAPAccessory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._loadPlatformAccessories = function(platformInstance, log, platformType) {
|
|
||||||
this._asyncCalls++;
|
|
||||||
platformInstance.accessories(once(function(foundAccessories){
|
|
||||||
this._asyncCalls--;
|
|
||||||
|
|
||||||
// loop through accessories adding them to the list and registering them
|
|
||||||
for (var i = 0; i < foundAccessories.length; i++) {
|
|
||||||
var accessoryInstance = foundAccessories[i];
|
|
||||||
var accessoryName = accessoryInstance.name; // assume this property was set
|
|
||||||
|
|
||||||
log("Initializing platform accessory '%s'...", accessoryName);
|
|
||||||
|
|
||||||
var accessory = this._createAccessory(accessoryInstance, accessoryName, platformType, accessoryInstance.uuid_base);
|
|
||||||
|
|
||||||
// add it to the bridge
|
|
||||||
this._bridge.addBridgedAccessory(accessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// were we the last callback?
|
|
||||||
if (this._asyncCalls === 0 && !this._asyncWait)
|
|
||||||
this._publish();
|
|
||||||
}.bind(this)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._createAccessory = function(accessoryInstance, displayName, accessoryType, uuid_base) {
|
|
||||||
|
|
||||||
var services = accessoryInstance.getServices();
|
|
||||||
|
|
||||||
if (!(services[0] instanceof Service)) {
|
|
||||||
// The returned "services" for this accessory is assumed to be the old style: a big array
|
|
||||||
// of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader.
|
|
||||||
|
|
||||||
// Create the actual HAP-NodeJS "Accessory" instance
|
|
||||||
return AccessoryLoader.parseAccessoryJSON({
|
|
||||||
displayName: displayName,
|
|
||||||
services: services
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// The returned "services" for this accessory are simply an array of new-API-style
|
|
||||||
// Service instances which we can add to a created HAP-NodeJS Accessory directly.
|
|
||||||
|
|
||||||
var accessoryUUID = uuid.generate(accessoryType + ":" + (uuid_base || displayName));
|
|
||||||
|
|
||||||
var accessory = new Accessory(displayName, accessoryUUID);
|
|
||||||
|
|
||||||
// listen for the identify event if the accessory instance has defined an identify() method
|
|
||||||
if (accessoryInstance.identify)
|
|
||||||
accessory.on('identify', function(paired, callback) { accessoryInstance.identify(callback); });
|
|
||||||
|
|
||||||
services.forEach(function(service) {
|
|
||||||
|
|
||||||
// if you returned an AccessoryInformation service, merge its values with ours
|
|
||||||
if (service instanceof Service.AccessoryInformation) {
|
|
||||||
var existingService = accessory.getService(Service.AccessoryInformation);
|
|
||||||
|
|
||||||
// pull out any values you may have defined
|
|
||||||
var manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value;
|
|
||||||
var model = service.getCharacteristic(Characteristic.Model).value;
|
|
||||||
var serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value;
|
|
||||||
|
|
||||||
if (manufacturer) existingService.setCharacteristic(Characteristic.Manufacturer, manufacturer);
|
|
||||||
if (model) existingService.setCharacteristic(Characteristic.Model, model);
|
|
||||||
if (serialNumber) existingService.setCharacteristic(Characteristic.SerialNumber, serialNumber);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
accessory.addService(service);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return accessory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._handleRegisterPlatformAccessories = function(accessories) {
|
|
||||||
var hapAccessories = [];
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
|
|
||||||
accessory._prepareAssociatedHAPAccessory();
|
|
||||||
hapAccessories.push(accessory._associatedHAPAccessory);
|
|
||||||
|
|
||||||
this._cachedPlatformAccessories.push(accessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._bridge.addBridgedAccessories(hapAccessories);
|
|
||||||
this._updateCachedAccessories();
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._handleUpdatePlatformAccessories = function(accessories) {
|
|
||||||
// Update persisted accessories
|
|
||||||
this._updateCachedAccessories();
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._handleUnregisterPlatformAccessories = function(accessories) {
|
|
||||||
var hapAccessories = [];
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
|
|
||||||
if (accessory._associatedHAPAccessory) {
|
|
||||||
hapAccessories.push(accessory._associatedHAPAccessory);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var targetIndex in this._cachedPlatformAccessories) {
|
|
||||||
var existing = this._cachedPlatformAccessories[targetIndex];
|
|
||||||
if (existing.UUID === accessory.UUID) {
|
|
||||||
this._cachedPlatformAccessories.splice(targetIndex, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._bridge.removeBridgedAccessories(hapAccessories);
|
|
||||||
this._updateCachedAccessories();
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._handlePublishCameraAccessories = function(accessories) {
|
|
||||||
var accessoryPin = (this._config.bridge || {}).pin || "031-45-154";
|
|
||||||
|
|
||||||
for (var index in accessories) {
|
|
||||||
var accessory = accessories[index];
|
|
||||||
|
|
||||||
accessory._prepareAssociatedHAPAccessory();
|
|
||||||
var hapAccessory = accessory._associatedHAPAccessory;
|
|
||||||
var advertiseAddress = mac.generate(accessory.UUID);
|
|
||||||
|
|
||||||
if (this._publishedCameras[advertiseAddress]) {
|
|
||||||
throw new Error("Camera accessory " + accessory.displayName + " experienced an address collision.");
|
|
||||||
} else {
|
|
||||||
this._publishedCameras[advertiseAddress] = accessory;
|
|
||||||
}
|
|
||||||
|
|
||||||
hapAccessory.on('listening', function(port) {
|
|
||||||
log.info("%s is running on port %s.", accessory.displayName, port);
|
|
||||||
});
|
|
||||||
|
|
||||||
hapAccessory.publish({
|
|
||||||
username: advertiseAddress,
|
|
||||||
pincode: accessoryPin,
|
|
||||||
category: accessory.category
|
|
||||||
}, this._allowInsecureAccess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._updateCachedAccessories = function() {
|
|
||||||
var serializedAccessories = [];
|
|
||||||
|
|
||||||
for (var index in this._cachedPlatformAccessories) {
|
|
||||||
var accessory = this._cachedPlatformAccessories[index];
|
|
||||||
serializedAccessories.push(accessory._dictionaryPresentation());
|
|
||||||
}
|
|
||||||
|
|
||||||
accessoryStorage.setItemSync("cachedAccessories", serializedAccessories);
|
|
||||||
}
|
|
||||||
|
|
||||||
Server.prototype._handleNewConfig = function(type, name, replace, config) {
|
|
||||||
if (type === "accessory") {
|
|
||||||
// TODO: Load new accessory
|
|
||||||
if (!this._config.accessories) {
|
|
||||||
this._config.accessories = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!replace) {
|
|
||||||
this._config.accessories.push(config);
|
|
||||||
} else {
|
|
||||||
var targetName;
|
|
||||||
if (name.indexOf('.') !== -1) {
|
|
||||||
targetName = name.split(".")[1];
|
|
||||||
}
|
|
||||||
var found = false;
|
|
||||||
for (var index in this._config.accessories) {
|
|
||||||
var accessoryConfig = this._config.accessories[index];
|
|
||||||
if (accessoryConfig.accessory === name) {
|
|
||||||
this._config.accessories[index] = config;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetName && (accessoryConfig.accessory === targetName)) {
|
|
||||||
this._config.accessories[index] = config;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
this._config.accessories.push(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (type === "platform") {
|
|
||||||
if (!this._config.platforms) {
|
|
||||||
this._config.platforms = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!replace) {
|
|
||||||
this._config.platforms.push(config);
|
|
||||||
} else {
|
|
||||||
var targetName;
|
|
||||||
if (name.indexOf('.') !== -1) {
|
|
||||||
targetName = name.split(".")[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
var found = false;
|
|
||||||
for (var index in this._config.platforms) {
|
|
||||||
var platformConfig = this._config.platforms[index];
|
|
||||||
if (platformConfig.platform === name) {
|
|
||||||
this._config.platforms[index] = config;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetName && (platformConfig.platform === targetName)) {
|
|
||||||
this._config.platforms[index] = config;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
this._config.platforms.push(config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var serializedConfig = JSON.stringify(this._config, null, ' ');
|
|
||||||
var configPath = User.configPath();
|
|
||||||
fs.writeFileSync(configPath, serializedConfig, 'utf8');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the setup code in a scannable format.
|
|
||||||
Server.prototype._printPin = function(pin) {
|
|
||||||
console.log("Scan this code with your HomeKit App on your iOS device to pair with Homebridge:");
|
|
||||||
console.log(chalk.black.bgWhite(" "));
|
|
||||||
console.log(chalk.black.bgWhite(" ┌────────────┐ "));
|
|
||||||
console.log(chalk.black.bgWhite(" │ " + pin + " │ "));
|
|
||||||
console.log(chalk.black.bgWhite(" └────────────┘ "));
|
|
||||||
console.log(chalk.black.bgWhite(" "));
|
|
||||||
}
|
|
||||||
47
lib/user.js
47
lib/user.js
@@ -1,47 +0,0 @@
|
|||||||
var path = require('path');
|
|
||||||
var fs = require('fs');
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
User: User
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages user settings and storage locations.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// global cached config
|
|
||||||
var config;
|
|
||||||
|
|
||||||
// optional custom storage path
|
|
||||||
var customStoragePath;
|
|
||||||
|
|
||||||
function User() {
|
|
||||||
}
|
|
||||||
|
|
||||||
User.config = function() {
|
|
||||||
return config || (config = Config.load(User.configPath()));
|
|
||||||
}
|
|
||||||
|
|
||||||
User.storagePath = function() {
|
|
||||||
if (customStoragePath) return customStoragePath;
|
|
||||||
var home = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
|
||||||
return path.join(home, ".homebridge");
|
|
||||||
}
|
|
||||||
|
|
||||||
User.configPath = function() {
|
|
||||||
return path.join(User.storagePath(), "config.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
User.persistPath = function() {
|
|
||||||
return path.join(User.storagePath(), "persist");
|
|
||||||
}
|
|
||||||
|
|
||||||
User.cachedAccessoryPath = function() {
|
|
||||||
return path.join(User.storagePath(), "accessories");
|
|
||||||
}
|
|
||||||
|
|
||||||
User.setStoragePath = function(path) {
|
|
||||||
customStoragePath = path;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
var crypto = require('crypto');
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
generate: generate
|
|
||||||
}
|
|
||||||
|
|
||||||
function generate(data) {
|
|
||||||
var sha1sum = crypto.createHash('sha1');
|
|
||||||
sha1sum.update(data);
|
|
||||||
var s = sha1sum.digest('hex');
|
|
||||||
var i = -1;
|
|
||||||
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, function(c) {
|
|
||||||
i += 1;
|
|
||||||
return s[i];
|
|
||||||
}).toUpperCase();
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = getVersion();
|
|
||||||
|
|
||||||
function getVersion() {
|
|
||||||
var packageJSONPath = path.join(__dirname, '../package.json');
|
|
||||||
var packageJSON = JSON.parse(fs.readFileSync(packageJSONPath));
|
|
||||||
return packageJSON.version;
|
|
||||||
}
|
|
||||||
56
package.json
56
package.json
@@ -1,33 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "homebridge",
|
"name": "homebridge",
|
||||||
"description": "HomeKit support for the impatient",
|
"description": "HomeKit support for the impatient",
|
||||||
"version": "0.4.21",
|
"version": "0.1.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "DEBUG=* ./bin/homebridge -D -P example-plugins/ || true"
|
"start": "DEBUG=* node app.js || true"
|
||||||
},
|
|
||||||
"author": {
|
|
||||||
"name": "Nick Farina"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/nfarina/homebridge.git"
|
"url": "git://github.com/nfarina/homebridge.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
|
||||||
"url": "http://github.com/nfarina/homebridge/issues"
|
|
||||||
},
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
|
||||||
"homebridge": "bin/homebridge"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.3.2"
|
|
||||||
},
|
|
||||||
"preferGlobal": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^1.1.1",
|
"ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local",
|
||||||
"commander": "2.8.1",
|
"async": "^1.4.2",
|
||||||
"hap-nodejs": "0.4.26",
|
"carwingsjs": "0.0.x",
|
||||||
"semver": "5.0.3",
|
"chokidar": "^1.0.5",
|
||||||
"node-persist": "^0.0.8"
|
"color": "0.10.x",
|
||||||
|
"debug": "^2.2.0",
|
||||||
|
"eibd": "^0.3.1",
|
||||||
|
"elkington": "kevinohara80/elkington",
|
||||||
|
"hap-nodejs": "^0.0.2",
|
||||||
|
"harmonyhubjs-client": "^1.1.6",
|
||||||
|
"harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git",
|
||||||
|
"isy-js": "",
|
||||||
|
"komponist": "0.1.0",
|
||||||
|
"lifx": "git+https://github.com/magicmonkey/lifxjs.git",
|
||||||
|
"lifx-api": "^1.0.1",
|
||||||
|
"mdns": "^2.2.4",
|
||||||
|
"node-hue-api": "^1.0.5",
|
||||||
|
"node-icontrol": "^0.1.5",
|
||||||
|
"node-milight-promise": "0.0.x",
|
||||||
|
"node-persist": "0.0.x",
|
||||||
|
"node-xmpp-client": "1.0.0-alpha23",
|
||||||
|
"q": "1.4.x",
|
||||||
|
"queue": "^3.1.0",
|
||||||
|
"request": "2.49.x",
|
||||||
|
"sonos": "0.8.x",
|
||||||
|
"telldus-live": "^0.2.1",
|
||||||
|
"teslams": "1.0.1",
|
||||||
|
"tough-cookie": "^2.0.0",
|
||||||
|
"unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98",
|
||||||
|
"wemo": "0.2.x",
|
||||||
|
"wink-js": "0.0.5",
|
||||||
|
"xml2js": "0.4.x",
|
||||||
|
"xmldoc": "0.1.x",
|
||||||
|
"yamaha-nodejs": "0.4.x"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
378
platforms/Domoticz.js
Normal file
378
platforms/Domoticz.js
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
// Domoticz Platform Shim for HomeBridge
|
||||||
|
// Written by Joep Verhaeg (http://www.joepverhaeg.nl)
|
||||||
|
//
|
||||||
|
// Revisions:
|
||||||
|
//
|
||||||
|
// 12 June 2015 [GizMoCuz]
|
||||||
|
// - Added support for RGB lights
|
||||||
|
// - Added support for Scenes
|
||||||
|
// - Sorting device names
|
||||||
|
//
|
||||||
|
// 22 July 2015 [lukeredpath]
|
||||||
|
// - Added SSL and basic auth support
|
||||||
|
//
|
||||||
|
// 26 August 2015 [EddyK69]
|
||||||
|
// - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes
|
||||||
|
// - Fixed issue with dimmer-range; was 0-100, should be 0-16
|
||||||
|
//
|
||||||
|
// 27 August 2015 [EddyK69]
|
||||||
|
// - Fixed issue that 'on/off'-type lights showed as dimmers in HomeKit. Checking now on SwitchType instead of HaveDimmer
|
||||||
|
// - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches
|
||||||
|
// (Cannot determine if 'on/off'-type device is a Light or a Switch :( )
|
||||||
|
//
|
||||||
|
// 14 September 2015 [lukeredpath]
|
||||||
|
// - Fixed incorrect dimmer range for LightwaveRF lights (0-32 required, MaxDimLevel should be honored)
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Domoticz JSON API required
|
||||||
|
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "Domoticz",
|
||||||
|
// "name": "Domoticz",
|
||||||
|
// "server": "127.0.0.1",
|
||||||
|
// "port": "8080",
|
||||||
|
// "roomid": 123, (0=no roomplan)
|
||||||
|
// "loadscenes": 1 (0=disable scenes)
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// If your server uses HTTPS, you can specify "ssl": true in your config. If
|
||||||
|
// your server uses a self-signed certificate, you'll need to run the following
|
||||||
|
// before starting the server or you will get an error:
|
||||||
|
//
|
||||||
|
// export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
//
|
||||||
|
// For basic auth support, specify the "user" and "password" in your config.
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function DomoticzPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.user = config["user"];
|
||||||
|
this.password = config["password"];
|
||||||
|
this.server = config["server"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.protocol = config["ssl"] ? "https" : "http";
|
||||||
|
this.roomid = 0;
|
||||||
|
if (typeof config["roomid"] != 'undefined') {
|
||||||
|
this.roomid = config["roomid"];
|
||||||
|
}
|
||||||
|
this.loadscenes = 1;
|
||||||
|
if (typeof config["loadscenes"] != 'undefined') {
|
||||||
|
this.loadscenes = config["loadscenes"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByKey(array, key) {
|
||||||
|
return array.sort(function(a, b) {
|
||||||
|
var x = a[key]; var y = b[key];
|
||||||
|
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DomoticzPlatform.prototype = {
|
||||||
|
urlForQuery: function(query) {
|
||||||
|
var serverString = this.server;
|
||||||
|
if (this.user && this.password) {
|
||||||
|
serverString = this.user + ":" + this.password + "@" + serverString;
|
||||||
|
}
|
||||||
|
return this.protocol + "://" + serverString + ":" + this.port + "/json.htm?" + query;
|
||||||
|
},
|
||||||
|
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Domoticz lights and switches...");
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
// mechanism to ensure callback is only executed once all requests complete
|
||||||
|
var asyncCalls = 0;
|
||||||
|
function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); }
|
||||||
|
|
||||||
|
if (this.roomid == 0) {
|
||||||
|
//Get Lights
|
||||||
|
asyncCalls++;
|
||||||
|
request.get({
|
||||||
|
url: this.urlForQuery("type=devices&filter=light&used=true&order=Name"),
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json['result'] != undefined) {
|
||||||
|
var sArray=sortByKey(json['result'],"Name");
|
||||||
|
sArray.map(function(s) {
|
||||||
|
var havedimmer = (s.SwitchType == 'Dimmer')
|
||||||
|
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
callbackLater();
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem connecting to Domoticz. (" + err + ")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Get all devices specified in the room
|
||||||
|
asyncCalls++;
|
||||||
|
request.get({
|
||||||
|
url: this.urlForQuery("type=devices&plan=" + this.roomid),
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json['result'] != undefined) {
|
||||||
|
var sArray=sortByKey(json['result'],"Name");
|
||||||
|
sArray.map(function(s) {
|
||||||
|
//only accept switches for now
|
||||||
|
if (typeof s.SwitchType != 'undefined') {
|
||||||
|
var havedimmer = (s.SwitchType == 'Dimmer')
|
||||||
|
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
callbackLater();
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem connecting to Domoticz.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//Get Scenes
|
||||||
|
if (this.loadscenes == 1) {
|
||||||
|
asyncCalls++;
|
||||||
|
request.get({
|
||||||
|
url: this.urlForQuery("type=scenes"),
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json['result'] != undefined) {
|
||||||
|
var sArray=sortByKey(json['result'],"Name");
|
||||||
|
sArray.map(function(s) {
|
||||||
|
accessory = new DomoticzAccessory(that.log, that, true, s.idx, s.Name, false, 0, false);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
callbackLater();
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem connecting to Domoticz.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function DomoticzAccessory(log, platform, IsScene, idx, name, HaveDimmer, MaxDimLevel, HaveRGB) {
|
||||||
|
// device info
|
||||||
|
this.IsScene = IsScene;
|
||||||
|
this.idx = idx;
|
||||||
|
this.name = name;
|
||||||
|
this.HaveDimmer = HaveDimmer;
|
||||||
|
this.MaxDimLevel = MaxDimLevel;
|
||||||
|
this.HaveRGB = HaveRGB;
|
||||||
|
this.log = log;
|
||||||
|
this.platform = platform;
|
||||||
|
}
|
||||||
|
|
||||||
|
DomoticzAccessory.prototype = {
|
||||||
|
command: function(c,value) {
|
||||||
|
this.log(this.name + " sending command " + c + " with value " + value);
|
||||||
|
if (this.IsScene == false) {
|
||||||
|
//Lights
|
||||||
|
if (c == "On" || c == "Off") {
|
||||||
|
url = this.platform.urlForQuery("type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=" + c + "&level=0");
|
||||||
|
}
|
||||||
|
else if (c == "setHue") {
|
||||||
|
url = this.platform.urlForQuery("type=command¶m=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false");
|
||||||
|
}
|
||||||
|
else if (c == "setLevel") {
|
||||||
|
value = this.dimmerLevelForValue(value)
|
||||||
|
url = this.platform.urlForQuery("type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value);
|
||||||
|
}
|
||||||
|
else if (value != undefined) {
|
||||||
|
this.log(this.name + " Unhandled Light command! cmd=" + c + ", value=" + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//Scenes
|
||||||
|
if (c == "On" || c == "Off") {
|
||||||
|
url = this.platform.urlForQuery("type=command¶m=switchscene&idx=" + this.idx + "&switchcmd=" + c);
|
||||||
|
}
|
||||||
|
else if (value != undefined) {
|
||||||
|
this.log(this.name + " Unhandled Scene command! cmd=" + c + ", value=" + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
request.put({ url: url }, function(err, response) {
|
||||||
|
if (err) {
|
||||||
|
that.log("There was a problem sending command " + c + " to" + that.name);
|
||||||
|
that.log(url);
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " sent command " + c + " (value: " + value + ")");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
// translates the HomeKit dim level as a percentage to whatever scale the device requires
|
||||||
|
dimmerLevelForValue: function(value) {
|
||||||
|
if (this.MaxDimLevel == 100) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return Math.round((value / 100.0) * this.MaxDimLevel)
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Domoticz",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function(that) {
|
||||||
|
cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (this.idx != undefined) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("Off")
|
||||||
|
} else {
|
||||||
|
that.command("On")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.HaveDimmer == true) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) { that.command("setLevel", value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: this.MaxDimLevel,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.HaveRGB == true) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.HUE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.command("setHue", value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Hue of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 360,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "arcdegrees"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes
|
||||||
|
},
|
||||||
|
|
||||||
|
sType: function() {
|
||||||
|
//if (this.HaveDimmer == true) {
|
||||||
|
return types.LIGHTBULB_STYPE
|
||||||
|
//} else {
|
||||||
|
// return types.SWITCH_STYPE
|
||||||
|
//}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: this.informationCharacteristics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: this.sType(),
|
||||||
|
characteristics: this.controlCharacteristics(that)
|
||||||
|
}];
|
||||||
|
this.log("Loaded services for " + this.name)
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = DomoticzAccessory;
|
||||||
|
module.exports.platform = DomoticzPlatform;
|
||||||
2159
platforms/FHEM.js
Normal file
2159
platforms/FHEM.js
Normal file
File diff suppressed because it is too large
Load Diff
253
platforms/FibaroHC2.js
Normal file
253
platforms/FibaroHC2.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// Fibaro Home Center 2 Platform Shim for HomeBridge
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "FibaroHC2",
|
||||||
|
// "name": "FibaroHC2",
|
||||||
|
// "host": "PUT IP ADDRESS OF YOUR HC2 HERE",
|
||||||
|
// "username": "PUT USERNAME OF YOUR HC2 HERE",
|
||||||
|
// "password": "PUT PASSWORD OF YOUR HC2 HERE"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function FibaroHC2Platform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.host = config["host"];
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
this.auth = "Basic " + new Buffer(this.username + ":" + this.password).toString("base64");
|
||||||
|
this.url = "http://"+this.host+"/api/devices";
|
||||||
|
|
||||||
|
startPollingUpdate( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
FibaroHC2Platform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Fibaro Home Center devices...");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url: this.url,
|
||||||
|
headers : {
|
||||||
|
"Authorization" : this.auth
|
||||||
|
},
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json != undefined) {
|
||||||
|
json.map(function(s) {
|
||||||
|
that.log("Found: " + s.type);
|
||||||
|
if (s.visible == true) {
|
||||||
|
var accessory = null;
|
||||||
|
if (s.type == "com.fibaro.multilevelSwitch")
|
||||||
|
accessory = new FibaroAccessory(new Service.Lightbulb(s.name), [Characteristic.On, Characteristic.Brightness]);
|
||||||
|
else if (s.type == "com.fibaro.FGRM222" || s.type == "com.fibaro.FGR221")
|
||||||
|
accessory = new FibaroAccessory(new Service.WindowCovering(s.name), [Characteristic.CurrentPosition, Characteristic.TargetPosition, Characteristic.PositionState]);
|
||||||
|
else if (s.type == "com.fibaro.binarySwitch" || s.type == "com.fibaro.developer.bxs.virtualBinarySwitch")
|
||||||
|
accessory = new FibaroAccessory(new Service.Switch(s.name), [Characteristic.On]);
|
||||||
|
else if (s.type == "com.fibaro.FGMS001" || s.type == "com.fibaro.motionSensor")
|
||||||
|
accessory = new FibaroAccessory(new Service.MotionSensor(s.name), [Characteristic.MotionDetected]);
|
||||||
|
else if (s.type == "com.fibaro.temperatureSensor")
|
||||||
|
accessory = new FibaroAccessory(new Service.TemperatureSensor(s.name), [Characteristic.CurrentTemperature]);
|
||||||
|
else if (s.type == "com.fibaro.doorSensor")
|
||||||
|
accessory = new FibaroAccessory(new Service.ContactSensor(s.name), [Characteristic.ContactSensorState]);
|
||||||
|
else if (s.type == "com.fibaro.lightSensor")
|
||||||
|
accessory = new FibaroAccessory(new Service.LightSensor(s.name), [Characteristic.CurrentAmbientLightLevel]);
|
||||||
|
else if (s.type == "com.fibaro.FGWP101")
|
||||||
|
accessory = new FibaroAccessory(new Service.Outlet(s.name), [Characteristic.On, Characteristic.OutletInUse]);
|
||||||
|
if (accessory != null) {
|
||||||
|
accessory.getServices = function() {
|
||||||
|
return that.getServices(accessory);
|
||||||
|
};
|
||||||
|
accessory.platform = that;
|
||||||
|
accessory.remoteAccessory = s;
|
||||||
|
accessory.id = s.id;
|
||||||
|
accessory.name = s.name;
|
||||||
|
accessory.model = s.type;
|
||||||
|
accessory.manufacturer = "Fibaro";
|
||||||
|
accessory.serialNumber = "<unknown>";
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
callback(foundAccessories);
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem connecting with FibaroHC2.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
command: function(c,value, that) {
|
||||||
|
var url = "http://"+this.host+"/api/devices/"+that.id+"/action/"+c;
|
||||||
|
var body = value != undefined ? JSON.stringify({
|
||||||
|
"args": [ value ]
|
||||||
|
}) : null;
|
||||||
|
var method = "post";
|
||||||
|
request({
|
||||||
|
url: url,
|
||||||
|
body: body,
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
"Authorization" : this.auth
|
||||||
|
},
|
||||||
|
}, function(err, response) {
|
||||||
|
if (err) {
|
||||||
|
that.platform.log("There was a problem sending command " + c + " to" + that.name);
|
||||||
|
that.platform.log(url);
|
||||||
|
} else {
|
||||||
|
that.platform.log(that.name + " sent command " + c);
|
||||||
|
that.platform.log(url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getAccessoryValue: function(callback, returnBoolean, homebridgeAccessory, powerValue) {
|
||||||
|
var url = "http://"+homebridgeAccessory.platform.host+"/api/devices/"+homebridgeAccessory.id+"/properties/";
|
||||||
|
if (powerValue)
|
||||||
|
url = url + "power";
|
||||||
|
else
|
||||||
|
url = url + "value";
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
headers : {
|
||||||
|
"Authorization" : homebridgeAccessory.platform.auth
|
||||||
|
},
|
||||||
|
json: true,
|
||||||
|
url: url
|
||||||
|
}, function(err, response, json) {
|
||||||
|
homebridgeAccessory.platform.log(url);
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (powerValue) {
|
||||||
|
callback(undefined, parseFloat(json.value) > 1.0 ? true : false);
|
||||||
|
} else if (returnBoolean)
|
||||||
|
callback(undefined, json.value == 0 ? 0 : 1);
|
||||||
|
else
|
||||||
|
callback(undefined, json.value);
|
||||||
|
} else {
|
||||||
|
homebridgeAccessory.platform.log("There was a problem getting value from" + homebridgeAccessory.id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getInformationService: function(homebridgeAccessory) {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Name, homebridgeAccessory.name)
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, homebridgeAccessory.manufacturer)
|
||||||
|
.setCharacteristic(Characteristic.Model, homebridgeAccessory.model)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, homebridgeAccessory.serialNumber);
|
||||||
|
return informationService;
|
||||||
|
},
|
||||||
|
bindCharacteristicEvents: function(characteristic, homebridgeAccessory) {
|
||||||
|
var onOff = characteristic.props.format == "bool" ? true : false;
|
||||||
|
var readOnly = true;
|
||||||
|
for (var i = 0; i < characteristic.props.perms.length; i++)
|
||||||
|
if (characteristic.props.perms[i] == "pw")
|
||||||
|
readOnly = false;
|
||||||
|
var powerValue = (characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") ? true : false;
|
||||||
|
subscribeUpdate(characteristic, homebridgeAccessory, onOff);
|
||||||
|
if (!readOnly) {
|
||||||
|
characteristic
|
||||||
|
.on('set', function(value, callback, context) {
|
||||||
|
if( context !== 'fromFibaro' ) {
|
||||||
|
if (onOff)
|
||||||
|
homebridgeAccessory.platform.command(value == 0 ? "turnOff": "turnOn", null, homebridgeAccessory);
|
||||||
|
else
|
||||||
|
homebridgeAccessory.platform.command("setValue", value, homebridgeAccessory);
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}.bind(this) );
|
||||||
|
}
|
||||||
|
characteristic
|
||||||
|
.on('get', function(callback) {
|
||||||
|
homebridgeAccessory.platform.getAccessoryValue(callback, onOff, homebridgeAccessory, powerValue);
|
||||||
|
}.bind(this) );
|
||||||
|
},
|
||||||
|
getServices: function(homebridgeAccessory) {
|
||||||
|
var informationService = homebridgeAccessory.platform.getInformationService(homebridgeAccessory);
|
||||||
|
for (var i=0; i < homebridgeAccessory.characteristics.length; i++) {
|
||||||
|
var characteristic = homebridgeAccessory.controlService.getCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||||
|
if (characteristic == undefined)
|
||||||
|
characteristic = homebridgeAccessory.controlService.addCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||||
|
homebridgeAccessory.platform.bindCharacteristicEvents(characteristic, homebridgeAccessory);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [informationService, homebridgeAccessory.controlService];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function FibaroAccessory(controlService, characteristics) {
|
||||||
|
this.controlService = controlService;
|
||||||
|
this.characteristics = characteristics;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastPoll=0;
|
||||||
|
var pollingUpdateRunning = false;
|
||||||
|
|
||||||
|
function startPollingUpdate( platform )
|
||||||
|
{
|
||||||
|
if( pollingUpdateRunning )
|
||||||
|
return;
|
||||||
|
pollingUpdateRunning = true;
|
||||||
|
|
||||||
|
var updateUrl = "http://"+platform.host+"/api/refreshStates?last="+lastPoll;
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url: updateUrl,
|
||||||
|
headers : {
|
||||||
|
"Authorization" : platform.auth
|
||||||
|
},
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json != undefined) {
|
||||||
|
lastPoll = json.last;
|
||||||
|
if (json.changes != undefined) {
|
||||||
|
json.changes.map(function(s) {
|
||||||
|
if (s.value != undefined) {
|
||||||
|
|
||||||
|
var value=parseInt(s.value);
|
||||||
|
if (isNaN(value))
|
||||||
|
value=(s.value === "true");
|
||||||
|
for (i=0;i<updateSubscriptions.length; i++) {
|
||||||
|
var subscription = updateSubscriptions[i];
|
||||||
|
if (subscription.id == s.id) {
|
||||||
|
if (s.power != undefined && subscription.characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") {
|
||||||
|
subscription.characteristic.setValue(parseFloat(s.power) > 1.0 ? true : false, undefined, 'fromFibaro');
|
||||||
|
} else if ((subscription.onOff && typeof(value) == "boolean") || !subscription.onOff)
|
||||||
|
subscription.characteristic.setValue(value, undefined, 'fromFibaro');
|
||||||
|
else
|
||||||
|
subscription.characteristic.setValue(value == 0 ? false : true, undefined, 'fromFibaro');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
platform.log("There was a problem connecting with FibaroHC2.");
|
||||||
|
}
|
||||||
|
pollingUpdateRunning = false;
|
||||||
|
setTimeout( function(){startPollingUpdate(platform)}, 2000 );
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateSubscriptions = [];
|
||||||
|
function subscribeUpdate(characteristic, accessory, onOff)
|
||||||
|
{
|
||||||
|
// TODO: optimized management of updateSubscription data structure (no array with sequential access)
|
||||||
|
updateSubscriptions.push({ 'id': accessory.id, 'characteristic': characteristic, 'accessory': accessory, 'onOff': onOff });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.platform = FibaroHC2Platform;
|
||||||
542
platforms/HomeAssistant.js
Normal file
542
platforms/HomeAssistant.js
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
// Home Assistant
|
||||||
|
//
|
||||||
|
// Current Support: lights
|
||||||
|
//
|
||||||
|
// This is a shim to publish lights maintained by Home Assistant.
|
||||||
|
// Home Assistant is an open-source home automation platform.
|
||||||
|
// URL: http://home-assistant.io
|
||||||
|
// GitHub: https://github.com/balloob/home-assistant
|
||||||
|
//
|
||||||
|
// HA accessories supported: Lights, Switches, Media Players, Scenes.
|
||||||
|
//
|
||||||
|
// Optional Devices - Edit the supported_types key in the config to pick which
|
||||||
|
// of the 4 types you would like to expose to HomeKit from
|
||||||
|
// Home Assistant. light, switch, media_player, scene.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Scene Support
|
||||||
|
//
|
||||||
|
// You can optionally import your Home Assistant scenes. These will appear to
|
||||||
|
// HomeKit as switches. You can simply say "turn on party time". In some cases
|
||||||
|
// scenes names are already rerved in HomeKit...like "Good Morning" and
|
||||||
|
// "Good Night". You will be able to just say "Good Morning" or "Good Night" to
|
||||||
|
// have these triggered.
|
||||||
|
//
|
||||||
|
// You might want to play with the wording to figure out what ends up working well
|
||||||
|
// for your scene names. It's also important to not populate any actual HomeKit
|
||||||
|
// scenes with the same names, as Siri will pick these instead of your Home
|
||||||
|
// Assistant scenes.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Media Player Support
|
||||||
|
//
|
||||||
|
// Media players on your Home Assistant will be added to your HomeKit as a light.
|
||||||
|
// While this seems like a hack at first, it's actually quite useful. You can
|
||||||
|
// turn them on, off, and set their volume (as a function of brightness).
|
||||||
|
//
|
||||||
|
// There are some rules to know about how on/off treats your media player. If
|
||||||
|
// your media player supports play/pause, then turning them on and off via
|
||||||
|
// HomeKit will play and pause them. If they do not support play/pause but then
|
||||||
|
// support on/off they will be turned on and then off.
|
||||||
|
//
|
||||||
|
// HomeKit does not have a characteristic of Volume or a Speaker type. So we are
|
||||||
|
// using the light device type here. So to turn your speaker up and down, you
|
||||||
|
// will need to use the same language you use to set the brighness of a light.
|
||||||
|
// You can play around with language to see what fits best.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Examples
|
||||||
|
//
|
||||||
|
// Dim the Kitchen Speaker to 40% - sets volume to 40%
|
||||||
|
// Dim the the Kitchen Speaker 10% - lowers the volume by 10%
|
||||||
|
// Set the brightness of the Kitchen Speaker to 40%
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "HomeAssistant",
|
||||||
|
// "name": "HomeAssistant",
|
||||||
|
// "host": "http://192.168.1.50:8123",
|
||||||
|
// "password": "xxx",
|
||||||
|
// "supported_types": ["light", "switch", "media_player", "scene"]
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var url = require('url')
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
var communicationError = new Error('Can not communicate with Home Assistant.')
|
||||||
|
|
||||||
|
function HomeAssistantPlatform(log, config){
|
||||||
|
|
||||||
|
// auth info
|
||||||
|
this.host = config["host"];
|
||||||
|
this.password = config["password"];
|
||||||
|
this.supportedTypes = config["supported_types"];
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantPlatform.prototype = {
|
||||||
|
_request: function(method, path, options, callback) {
|
||||||
|
var self = this
|
||||||
|
var requestURL = this.host + '/api' + path
|
||||||
|
options = options || {}
|
||||||
|
options.query = options.query || {}
|
||||||
|
|
||||||
|
var reqOpts = {
|
||||||
|
url: url.parse(requestURL),
|
||||||
|
method: method || 'GET',
|
||||||
|
qs: options.query,
|
||||||
|
body: JSON.stringify(options.body),
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-ha-access': this.password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request(reqOpts, function onResponse(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
callback(error, response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode === 401) {
|
||||||
|
callback(new Error('You are not authenticated'), response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json = JSON.parse(body)
|
||||||
|
callback(error, response, json)
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
fetchState: function(entity_id, callback){
|
||||||
|
this._request('GET', '/states/' + entity_id, {}, function(error, response, data){
|
||||||
|
if (error) {
|
||||||
|
callback(null)
|
||||||
|
}else{
|
||||||
|
callback(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
callService: function(domain, service, service_data, callback){
|
||||||
|
var options = {}
|
||||||
|
options.body = service_data
|
||||||
|
|
||||||
|
this._request('POST', '/services/' + domain + '/' + service, options, function(error, response, data){
|
||||||
|
if (error) {
|
||||||
|
callback(null)
|
||||||
|
}else{
|
||||||
|
callback(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching HomeAssistant devices.");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
this._request('GET', '/states', {}, function(error, response, data){
|
||||||
|
|
||||||
|
for (var i = 0; i < data.length; i++) {
|
||||||
|
entity = data[i]
|
||||||
|
entity_type = entity.entity_id.split('.')[0]
|
||||||
|
|
||||||
|
// ignore devices that are not in the list of supported types
|
||||||
|
if (that.supportedTypes.indexOf(entity_type) == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore hidden devices
|
||||||
|
if (entity.attributes && entity.attributes.hidden) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessory = null
|
||||||
|
|
||||||
|
if (entity_type == 'light') {
|
||||||
|
accessory = new HomeAssistantLight(that.log, entity, that)
|
||||||
|
}else if (entity_type == 'switch'){
|
||||||
|
console.log(JSON.stringify(entity))
|
||||||
|
console.log("");
|
||||||
|
console.log("");
|
||||||
|
accessory = new HomeAssistantSwitch(that.log, entity, that)
|
||||||
|
}else if (entity_type == 'scene'){
|
||||||
|
accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene')
|
||||||
|
}else if (entity_type == 'media_player' && entity.attributes && entity.attributes.supported_media_commands){
|
||||||
|
accessory = new HomeAssistantMediaPlayer(that.log, entity, that)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accessory) {
|
||||||
|
foundAccessories.push(accessory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(foundAccessories)
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeAssistantLight(log, data, client) {
|
||||||
|
// device info
|
||||||
|
this.domain = "light"
|
||||||
|
this.data = data
|
||||||
|
this.entity_id = data.entity_id
|
||||||
|
if (data.attributes && data.attributes.friendly_name) {
|
||||||
|
this.name = data.attributes.friendly_name
|
||||||
|
}else{
|
||||||
|
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = client
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantLight.prototype = {
|
||||||
|
getPowerState: function(callback){
|
||||||
|
this.log("fetching power state for: " + this.name);
|
||||||
|
|
||||||
|
this.client.fetchState(this.entity_id, function(data){
|
||||||
|
if (data) {
|
||||||
|
powerState = data.state == 'on'
|
||||||
|
callback(null, powerState)
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
getBrightness: function(callback){
|
||||||
|
this.log("fetching brightness for: " + this.name);
|
||||||
|
|
||||||
|
this.client.fetchState(this.entity_id, function(data){
|
||||||
|
if (data && data.attributes) {
|
||||||
|
brightness = ((data.attributes.brightness || 0) / 255)*100
|
||||||
|
callback(null, brightness)
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var that = this;
|
||||||
|
var service_data = {}
|
||||||
|
service_data.entity_id = this.entity_id
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to on");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}else{
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to off");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'turn_off', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setBrightness: function(level, callback) {
|
||||||
|
var that = this;
|
||||||
|
var service_data = {}
|
||||||
|
service_data.entity_id = this.entity_id
|
||||||
|
|
||||||
|
service_data.brightness = 255*(level/100.0)
|
||||||
|
|
||||||
|
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var lightbulbService = new Service.Lightbulb();
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||||
|
.setCharacteristic(Characteristic.Model, "Light")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerState.bind(this))
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', this.getBrightness.bind(this))
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
|
||||||
|
return [informationService, lightbulbService];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function HomeAssistantMediaPlayer(log, data, client) {
|
||||||
|
var SUPPORT_PAUSE = 1
|
||||||
|
var SUPPORT_SEEK = 2
|
||||||
|
var SUPPORT_VOLUME_SET = 4
|
||||||
|
var SUPPORT_VOLUME_MUTE = 8
|
||||||
|
var SUPPORT_PREVIOUS_TRACK = 16
|
||||||
|
var SUPPORT_NEXT_TRACK = 32
|
||||||
|
var SUPPORT_YOUTUBE = 64
|
||||||
|
var SUPPORT_TURN_ON = 128
|
||||||
|
var SUPPORT_TURN_OFF = 256
|
||||||
|
|
||||||
|
// device info
|
||||||
|
this.domain = "media_player"
|
||||||
|
this.data = data
|
||||||
|
this.entity_id = data.entity_id
|
||||||
|
this.supportsVolume = false
|
||||||
|
this.supportedMediaCommands = data.attributes.supported_media_commands
|
||||||
|
|
||||||
|
if (data.attributes && data.attributes.friendly_name) {
|
||||||
|
this.name = data.attributes.friendly_name
|
||||||
|
}else{
|
||||||
|
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.supportedMediaCommands | SUPPORT_PAUSE) == this.supportedMediaCommands) {
|
||||||
|
this.onState = "playing"
|
||||||
|
this.offState = "paused"
|
||||||
|
this.onService = "media_play"
|
||||||
|
this.offService = "media_pause"
|
||||||
|
}else if ((this.supportedMediaCommands | SUPPORT_TURN_ON) == this.supportedMediaCommands && (this.supportedMediaCommands | SUPPORT_TURN_OFF) == this.supportedMediaCommands) {
|
||||||
|
this.onState = "on"
|
||||||
|
this.offState = "off"
|
||||||
|
this.onService = "turn_on"
|
||||||
|
this.offService = "turn_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((this.supportedMediaCommands | SUPPORT_VOLUME_SET) == this.supportedMediaCommands) {
|
||||||
|
this.supportsVolume = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = client
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantMediaPlayer.prototype = {
|
||||||
|
getPowerState: function(callback){
|
||||||
|
this.log("fetching power state for: " + this.name);
|
||||||
|
|
||||||
|
this.client.fetchState(this.entity_id, function(data){
|
||||||
|
if (data) {
|
||||||
|
powerState = data.state == this.onState
|
||||||
|
callback(null, powerState)
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
getVolume: function(callback){
|
||||||
|
this.log("fetching volume for: " + this.name);
|
||||||
|
that = this
|
||||||
|
this.client.fetchState(this.entity_id, function(data){
|
||||||
|
if (data && data.attributes) {
|
||||||
|
that.log(JSON.stringify(data.attributes))
|
||||||
|
level = data.attributes.volume_level ? data.attributes.volume_level*100 : 0
|
||||||
|
callback(null, level)
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var that = this;
|
||||||
|
var service_data = {}
|
||||||
|
service_data.entity_id = this.entity_id
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to on");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, this.onService, service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}else{
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to off");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, this.offService, service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setVolume: function(level, callback) {
|
||||||
|
var that = this;
|
||||||
|
var service_data = {}
|
||||||
|
service_data.entity_id = this.entity_id
|
||||||
|
|
||||||
|
service_data.volume_level = level/100.0
|
||||||
|
|
||||||
|
this.log("Setting volume on the '"+this.name+"' to " + level);
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'volume_set', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set volume on the '"+that.name+"' to " + level);
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var lightbulbService = new Service.Lightbulb();
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||||
|
.setCharacteristic(Characteristic.Model, "Media Player")
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerState.bind(this))
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
|
||||||
|
if (this.supportsVolume) {
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', this.getVolume.bind(this))
|
||||||
|
.on('set', this.setVolume.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [informationService, lightbulbService];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function HomeAssistantSwitch(log, data, client, type) {
|
||||||
|
// device info
|
||||||
|
this.domain = type || "switch"
|
||||||
|
this.data = data
|
||||||
|
this.entity_id = data.entity_id
|
||||||
|
if (data.attributes && data.attributes.friendly_name) {
|
||||||
|
this.name = data.attributes.friendly_name
|
||||||
|
}else{
|
||||||
|
this.name = data.entity_id.split('.').pop().replace(/_/g, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client = client
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeAssistantSwitch.prototype = {
|
||||||
|
getPowerState: function(callback){
|
||||||
|
this.log("fetching power state for: " + this.name);
|
||||||
|
|
||||||
|
this.client.fetchState(this.entity_id, function(data){
|
||||||
|
if (data) {
|
||||||
|
powerState = data.state == 'on'
|
||||||
|
callback(null, powerState)
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
},
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var that = this;
|
||||||
|
var service_data = {}
|
||||||
|
service_data.entity_id = this.entity_id
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to on");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'turn_on', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}else{
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to off");
|
||||||
|
|
||||||
|
this.client.callService(this.domain, 'turn_off', service_data, function(data){
|
||||||
|
if (data) {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||||
|
callback()
|
||||||
|
}else{
|
||||||
|
callback(communicationError)
|
||||||
|
}
|
||||||
|
}.bind(this))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var switchService = new Service.Switch();
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
var model;
|
||||||
|
|
||||||
|
switch (this.domain) {
|
||||||
|
case "scene":
|
||||||
|
model = "Scene"
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
model = "Switch"
|
||||||
|
}
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Home Assistant")
|
||||||
|
.setCharacteristic(Characteristic.Model, model)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "xxx");
|
||||||
|
|
||||||
|
if (this.domain == 'switch') {
|
||||||
|
switchService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerState.bind(this))
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
}else{
|
||||||
|
switchService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [informationService, switchService];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.accessory = HomeAssistantLight;
|
||||||
|
module.exports.accessory = HomeAssistantMediaPlayer;
|
||||||
|
module.exports.accessory = HomeAssistantSwitch;
|
||||||
|
module.exports.platform = HomeAssistantPlatform;
|
||||||
1066
platforms/HomeSeer.js
Normal file
1066
platforms/HomeSeer.js
Normal file
File diff suppressed because it is too large
Load Diff
385
platforms/ISY.js
Normal file
385
platforms/ISY.js
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var xml2js = require('xml2js');
|
||||||
|
var request = require('request');
|
||||||
|
var util = require('util');
|
||||||
|
|
||||||
|
var parser = new xml2js.Parser();
|
||||||
|
|
||||||
|
|
||||||
|
var power_state_ctype = {
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { return; },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
function ISYURL(user, pass, host, port, path) {
|
||||||
|
return util.format("http://%s:%s@%s:%d%s", user, pass, host, port, encodeURI(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
function ISYPlatform(log, config) {
|
||||||
|
this.host = config["host"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.user = config["username"];
|
||||||
|
this.pass = config["password"];
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
ISYPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching ISY Devices.");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var url = ISYURL(this.user, this.pass, this.host, this.port, "/rest/nodes");
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: url,
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
console.trace("Requesting ISY devices.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.parseString(body, function(err, result) {
|
||||||
|
result.nodes.node.forEach(function(obj) {
|
||||||
|
var enabled = obj.enabled[0] == 'true';
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
var device = new ISYAccessory(
|
||||||
|
that.log,
|
||||||
|
that.host,
|
||||||
|
that.port,
|
||||||
|
that.user,
|
||||||
|
that.pass,
|
||||||
|
obj.name[0],
|
||||||
|
obj.address[0],
|
||||||
|
obj.property[0].$.uom
|
||||||
|
);
|
||||||
|
|
||||||
|
foundAccessories.push(device);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(foundAccessories.sort(function (a,b) {
|
||||||
|
return (a.name > b.name) - (a.name < b.name);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ISYAccessory(log, host, port, user, pass, name, address, uom) {
|
||||||
|
this.log = log;
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.user = user;
|
||||||
|
this.pass = pass;
|
||||||
|
this.name = name;
|
||||||
|
this.address = address;
|
||||||
|
this.uom = uom;
|
||||||
|
}
|
||||||
|
|
||||||
|
ISYAccessory.prototype = {
|
||||||
|
query: function() {
|
||||||
|
var path = util.format("/rest/status/%s", encodeURI(this.address));
|
||||||
|
var url = ISYURL(this.user, this.pass, this.host, this.port, path);
|
||||||
|
|
||||||
|
var options = { url: url, method: 'GET' };
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
console.trace("Requesting Device Status.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.parseString(body, function(err, result) {
|
||||||
|
var value = result.properties.property[0].$.value;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
command: function(c, value) {
|
||||||
|
this.log(this.name + " sending command " + c + " with value " + value);
|
||||||
|
|
||||||
|
switch (c)
|
||||||
|
{
|
||||||
|
case 'On':
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DFON";
|
||||||
|
break;
|
||||||
|
case 'Off':
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DFOF";
|
||||||
|
break;
|
||||||
|
case 'Low':
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DON/85";
|
||||||
|
break;
|
||||||
|
case 'Medium':
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DON/128";
|
||||||
|
break;
|
||||||
|
case 'High':
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DON/255";
|
||||||
|
break;
|
||||||
|
case 'setLevel':
|
||||||
|
if (value > 0)
|
||||||
|
{
|
||||||
|
path = "/rest/nodes/" + this.address + "/cmd/DON/" + Math.floor(255 * (value / 100));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.log("Unimplemented command sent to " + this.name + " Command " + c);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path)
|
||||||
|
{
|
||||||
|
var url = ISYURL(this.user, this.pass, this.host, this.port, path);
|
||||||
|
var options = {
|
||||||
|
url: url,
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error)
|
||||||
|
{
|
||||||
|
console.trace("Sending Command.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
that.log("Sent command " + path + " to " + that.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "SmartHome",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.address,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function(that) {
|
||||||
|
cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (this.uom == "%/on/off") {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("Off")
|
||||||
|
} else {
|
||||||
|
that.command("On")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRead: function() {
|
||||||
|
return this.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%",
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.command("setLevel", value);
|
||||||
|
},
|
||||||
|
onRead: function() {
|
||||||
|
var val = this.query();
|
||||||
|
that.log("Query: " + val);
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (this.uom == "off/low/med/high")
|
||||||
|
{
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("Off")
|
||||||
|
} else {
|
||||||
|
that.command("On")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRead: function() {
|
||||||
|
return this.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.ROTATION_SPEED_CTYPE,
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the speed of the fan",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("Off");
|
||||||
|
} else if (value > 0 && value < 40) {
|
||||||
|
that.command("Low");
|
||||||
|
} else if (value > 40 && value < 75) {
|
||||||
|
that.command("Medium");
|
||||||
|
} else {
|
||||||
|
that.command("High");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRead: function() {
|
||||||
|
return this.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (this.uom == "on/off")
|
||||||
|
{
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("Off")
|
||||||
|
} else {
|
||||||
|
that.command("On")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRead: function() {
|
||||||
|
return this.query();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes;
|
||||||
|
},
|
||||||
|
|
||||||
|
sType: function() {
|
||||||
|
if (this.uom == "%/on/off") {
|
||||||
|
return types.LIGHTBULB_STYPE;
|
||||||
|
} else if (this.uom == "on/off") {
|
||||||
|
return types.SWITCH_STYPE;
|
||||||
|
} else if (this.uom == "off/low/med/high") {
|
||||||
|
return types.FAN_STYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.SWITCH_STYPE;
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: this.informationCharacteristics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: this.sType(),
|
||||||
|
characteristics: this.controlCharacteristics(that)
|
||||||
|
}];
|
||||||
|
|
||||||
|
//that.log("Loaded services for " + that.name);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = ISYAccessory;
|
||||||
|
module.exports.platform = ISYPlatform;
|
||||||
552
platforms/Indigo.js
Normal file
552
platforms/Indigo.js
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
// Indigo Platform Shim for HomeBridge
|
||||||
|
// Written by Mike Riccio (https://github.com/webdeck)
|
||||||
|
// Based on many of the other HomeBridge plartform modules
|
||||||
|
// See http://www.indigodomo.com/ for more info on Indigo
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "Indigo", // required
|
||||||
|
// "name": "Indigo", // required
|
||||||
|
// "host": "127.0.0.1", // required
|
||||||
|
// "port": "8176", // required
|
||||||
|
// "username": "username", // optional
|
||||||
|
// "password": "password" // optional
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var request = require('request');
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
|
||||||
|
function IndigoPlatform(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
this.baseURL = "http://" + config["host"] + ":" + config["port"];
|
||||||
|
|
||||||
|
if (config["username"] && config["password"]) {
|
||||||
|
this.auth = {
|
||||||
|
'user': config["username"],
|
||||||
|
'pass': config["password"],
|
||||||
|
'sendImmediately': false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndigoPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
this.log("Discovering Indigo Devices.");
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: this.baseURL + "/devices.json/",
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
this.foundAccessories = [];
|
||||||
|
this.callback = callback;
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
console.trace("Requesting Indigo devices.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheesy hack because response may have an extra comma at the start of the array, which is invalid
|
||||||
|
var firstComma = body.indexOf(",");
|
||||||
|
if (firstComma < 10) {
|
||||||
|
body = "[" + body.substr(firstComma + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JSON.parse(body);
|
||||||
|
async.eachSeries(json, function(item, asyncCallback) {
|
||||||
|
var deviceURL = that.baseURL + item.restURL;
|
||||||
|
var deviceOptions = {
|
||||||
|
url: deviceURL,
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
if (that.auth) {
|
||||||
|
deviceOptions['auth'] = that.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(deviceOptions, function(deviceError, deviceResponse, deviceBody) {
|
||||||
|
if (deviceError) {
|
||||||
|
console.trace("Requesting Indigo device info: " + deviceURL + "\nError: " + deviceError + "\nResponse: " + deviceBody);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try {
|
||||||
|
var deviceJson = JSON.parse(deviceBody);
|
||||||
|
that.log("Discovered " + deviceJson.type + ": " + deviceJson.name);
|
||||||
|
that.foundAccessories.push(
|
||||||
|
new IndigoAccessory(that.log, that.auth, deviceURL, deviceJson));
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
that.log("Error parsing Indigo device info: " + deviceURL + "\nException: " + e + "\nResponse: " + deviceBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asyncCallback();
|
||||||
|
});
|
||||||
|
}, function(asyncError) {
|
||||||
|
// This will be called after all the requests complete
|
||||||
|
if (asyncError) {
|
||||||
|
console.trace("Requesting Indigo device info.");
|
||||||
|
that.log(asyncError);
|
||||||
|
}
|
||||||
|
|
||||||
|
that.callback(that.foundAccessories.sort(function (a,b) {
|
||||||
|
return (a.name > b.name) - (a.name < b.name);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function IndigoAccessory(log, auth, deviceURL, json) {
|
||||||
|
this.log = log;
|
||||||
|
this.auth = auth;
|
||||||
|
this.deviceURL = deviceURL;
|
||||||
|
|
||||||
|
for (var prop in json) {
|
||||||
|
if (json.hasOwnProperty(prop)) {
|
||||||
|
this[prop] = json[prop];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndigoAccessory.prototype = {
|
||||||
|
getStatus: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: this.deviceURL,
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
console.trace("Requesting Device Status.");
|
||||||
|
that.log(error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("getStatus of " + that.name + ": " + body);
|
||||||
|
try {
|
||||||
|
var json = JSON.parse(body);
|
||||||
|
callback(json);
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.trace("Requesting Device Status.");
|
||||||
|
that.log("Exception: " + e + "\nResponse: " + body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: function(params) {
|
||||||
|
var that = this;
|
||||||
|
var options = {
|
||||||
|
url: this.deviceURL + "?" + params,
|
||||||
|
method: 'PUT'
|
||||||
|
};
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("updateStatus of " + that.name + ": " + params);
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
console.trace("Updating Device Status.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
query: function(prop, callback) {
|
||||||
|
this.getStatus(function(json) {
|
||||||
|
callback(json[prop]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
turnOn: function() {
|
||||||
|
if (this.typeSupportsOnOff) {
|
||||||
|
this.updateStatus("isOn=1");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
turnOff: function() {
|
||||||
|
if (this.typeSupportsOnOff) {
|
||||||
|
this.updateStatus("isOn=0");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(brightness) {
|
||||||
|
if (this.typeSupportsDim && brightness >= 0 && brightness <= 100) {
|
||||||
|
this.updateStatus("brightness=" + brightness);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setSpeedIndex: function(speedIndex) {
|
||||||
|
if (this.typeSupportsSpeedControl && speedIndex >= 0 && speedIndex <= 3) {
|
||||||
|
this.updateStatus("speedIndex=" + speedIndex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentHeatingCooling: function(callback) {
|
||||||
|
this.getStatus(function(json) {
|
||||||
|
var mode = 0;
|
||||||
|
if (json["hvacOperatonModeIsHeat"]) {
|
||||||
|
mode = 1;
|
||||||
|
}
|
||||||
|
else if (json["hvacOperationModeIsCool"]) {
|
||||||
|
mode = 2;
|
||||||
|
}
|
||||||
|
else if (json["hvacOperationModeIsAuto"]) {
|
||||||
|
mode = 3;
|
||||||
|
}
|
||||||
|
callback(mode);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetHeatingCooling: function(mode) {
|
||||||
|
if (mode == 0) {
|
||||||
|
param = "Off";
|
||||||
|
}
|
||||||
|
else if (mode == 1) {
|
||||||
|
param = "Heat";
|
||||||
|
}
|
||||||
|
else if (mode == 2) {
|
||||||
|
param = "Cool";
|
||||||
|
}
|
||||||
|
else if (mode == 3) {
|
||||||
|
param = "Auto";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (param) {
|
||||||
|
this.updateStatus("hvacOperationModeIs" + param + "=true");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Note: HomeKit wants all temperature values to be in celsius
|
||||||
|
getCurrentTemperature: function(callback) {
|
||||||
|
this.query("displayRawState", function(temperature) {
|
||||||
|
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getTargetTemperature: function(callback) {
|
||||||
|
this.getStatus(function(json) {
|
||||||
|
var temperature;
|
||||||
|
if (json["hvacOperatonModeIsHeat"]) {
|
||||||
|
temperature = json["setpointHeat"];
|
||||||
|
}
|
||||||
|
else if (json["hvacOperationModeIsCool"]) {
|
||||||
|
temperature = json["setpointCool"];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
temperature = (json["setpointHeat"] + json["setpointCool"]) / 2.0;
|
||||||
|
}
|
||||||
|
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetTemperature: function(temperature) {
|
||||||
|
var that = this;
|
||||||
|
var t = (temperature * 9.0 / 5.0) + 32.0;
|
||||||
|
this.getStatus(function(json) {
|
||||||
|
if (json["hvacOperatonModeIsHeat"]) {
|
||||||
|
that.updateStatus("setpointHeat=" + t);
|
||||||
|
}
|
||||||
|
else if (json["hvacOperationModeIsCool"]) {
|
||||||
|
that.updateStatus("setpointCool=" + t);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var cool = t + 5;
|
||||||
|
var heat = t - 5;
|
||||||
|
that.updateStatus("setpointCool=" + cool + "&setpointHeat=" + heat);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.READ],
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.READ],
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
initialValue: "Indigo",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.READ],
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
initialValue: this.type,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.READ],
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
initialValue: this.addressStr,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.WRITE],
|
||||||
|
format: Characteristic.Formats.BOOL,
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function(that) {
|
||||||
|
var hasAType = false;
|
||||||
|
|
||||||
|
var cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: [Characteristic.Perms.READ],
|
||||||
|
format: Characteristic.Formats.STRING,
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (that.typeSupportsDim) {
|
||||||
|
hasAType = true;
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
initialValue: that.brightness,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: Characteristic.Units.PERCENTAGE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setBrightness(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.query("brightness", callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (that.typeSupportsSpeedControl) {
|
||||||
|
hasAType = true;
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.ROTATION_SPEED_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the speed of the fan",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 3,
|
||||||
|
designedMinStep: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setSpeedIndex(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.query("speedIndex", callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (that.typeSupportsHVAC) {
|
||||||
|
hasAType = true;
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Mode",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 3,
|
||||||
|
designedMinStep: 1,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentHeatingCooling(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Mode",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 3,
|
||||||
|
designedMinStep: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setTargetHeatingCooling(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentHeatingCooling(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
designedMinValue: 16,
|
||||||
|
designedMaxValue: 38,
|
||||||
|
designedMinStep: 1,
|
||||||
|
initialValue: 20,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Temperature",
|
||||||
|
unit: Characteristic.Units.FAHRENHEIT,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentTemperature(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
designedMinValue: 16,
|
||||||
|
designedMaxValue: 38,
|
||||||
|
designedMinStep: 1,
|
||||||
|
initialValue: 20,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Temperature",
|
||||||
|
unit: Characteristic.Units.FAHRENHEIT,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setTargetTemperature(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getTargetTemperature(callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.INT,
|
||||||
|
initialValue: 1,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Unit",
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
callback(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (that.typeSupportsOnOff || !hasAType) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||||
|
format: Characteristic.Formats.BOOL,
|
||||||
|
initialValue: (that.isOn) ? 1 : 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.turnOff();
|
||||||
|
} else {
|
||||||
|
that.turnOn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.query("isOn", function(isOn) {
|
||||||
|
callback((isOn) ? 1 : 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes;
|
||||||
|
},
|
||||||
|
|
||||||
|
sType: function() {
|
||||||
|
if (this.typeSupportsHVAC) {
|
||||||
|
return types.THERMOSTAT_STYPE;
|
||||||
|
} else if (this.typeSupportsDim) {
|
||||||
|
return types.LIGHTBULB_STYPE;
|
||||||
|
} else if (this.typeSupportsSpeedControl) {
|
||||||
|
return types.FAN_STYPE;
|
||||||
|
} else if (this.typeSupportsOnOff) {
|
||||||
|
return types.SWITCH_STYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.SWITCH_STYPE;
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: that.informationCharacteristics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: that.sType(),
|
||||||
|
characteristics: that.controlCharacteristics(that)
|
||||||
|
}];
|
||||||
|
|
||||||
|
that.log("Loaded services for " + that.name);
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = IndigoAccessory;
|
||||||
|
module.exports.platform = IndigoPlatform;
|
||||||
156
platforms/KNX-sample-config.json
Normal file
156
platforms/KNX-sample-config.json
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
{
|
||||||
|
"bridge": {
|
||||||
|
"name": "Homebridge",
|
||||||
|
"username": "CC:22:3D:E3:CE:30",
|
||||||
|
"port": 51826,
|
||||||
|
"pin": "031-45-154"
|
||||||
|
},
|
||||||
|
"description": "This is an example configuration file for KNX platform shim",
|
||||||
|
"hint": "Always paste into jsonlint.com validation page before starting your homebridge, saves a lot of frustration",
|
||||||
|
"hint2": "Replace all group addresses by current addresses of your installation, these are arbitrary examples!",
|
||||||
|
"hint3": "For valid services and their characteristics have a look at the KNX.md file in folder platforms!",
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"platform": "KNX",
|
||||||
|
"name": "KNX",
|
||||||
|
"knxd_ip": "192.168.178.205",
|
||||||
|
"knxd_port": 6720,
|
||||||
|
"accessories": [
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"description": "Only generic type knxdevice is supported, all previous knx types have been merged into that.",
|
||||||
|
"name": "Living Room North Lamp",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "Lightbulb",
|
||||||
|
"description": "iOS8 Lightbulb type, supports On (Switch) and Brightness",
|
||||||
|
"name": "Living Room North Lamp",
|
||||||
|
"On": {
|
||||||
|
"Set": "1/1/6",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/63"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Brightness": {
|
||||||
|
"Set": "1/1/62",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services-description": "Services is an array, you CAN have multiple service types in one accessory, though it is not fully supported in many iOS HK apps, such as EVE and myTouchHome"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"name": "Office Temperature",
|
||||||
|
"description": "iOS8.4.1 TemperatureSensor type, supports CurrentTemperature",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "TemperatureSensor",
|
||||||
|
"name": "Raumtemperatur",
|
||||||
|
"CurrentTemperature": {
|
||||||
|
"Listen": "3/3/44"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"name": "Office Window Lock",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "LockMechanism",
|
||||||
|
"description": "iOS8 Lock mechanism, Supports LockCurrentState, LockTargetState, append R to the addresses if LOCKED is 1",
|
||||||
|
"name": "Office Window Lock",
|
||||||
|
"LockCurrentState": {
|
||||||
|
"Listen": "5/3/15R"
|
||||||
|
},
|
||||||
|
"LockTargetState": {
|
||||||
|
"Listen": "5/3/16R"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"description": "sample device with multiple services. Multiple services of different types are widely supported",
|
||||||
|
"name": "Office",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "Lightbulb",
|
||||||
|
"name": "Office Lamp",
|
||||||
|
"On": {
|
||||||
|
"Set": "1/3/5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Thermostat",
|
||||||
|
"description": "iOS8 Thermostat type, supports CurrentTemperature, TargetTemperature, CurrentHeatingCoolingState ",
|
||||||
|
"name": "Raumtemperatur",
|
||||||
|
"CurrentTemperature": {
|
||||||
|
"Listen": "3/3/44"
|
||||||
|
},
|
||||||
|
"TargetTemperature": {
|
||||||
|
"Set": "3/3/94"
|
||||||
|
},
|
||||||
|
"CurrentHeatingCoolingState": {
|
||||||
|
"Listen": "3/3/64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "WindowCovering",
|
||||||
|
"description": "iOS9 Window covering (blinds etc) type, still WIP",
|
||||||
|
"name": "Blinds",
|
||||||
|
"TargetPosition": {
|
||||||
|
"Set": "1/2/3",
|
||||||
|
"Listen": "1/2/4"
|
||||||
|
},
|
||||||
|
"CurrentPosition": {
|
||||||
|
"Set": "1/3/1",
|
||||||
|
"Listen": "1/3/2"
|
||||||
|
},
|
||||||
|
"PositionState": {
|
||||||
|
"Listen": "2/7/1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"description": "sample contact sensor device",
|
||||||
|
"name": "Office Contact",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "ContactSensor",
|
||||||
|
"name": "Office Door",
|
||||||
|
"ContactSensorState": {
|
||||||
|
"Listen": "5/3/5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"description": "sample garage door opener",
|
||||||
|
"name": "Office Garage",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "GarageDoorOpener",
|
||||||
|
"name": "Office Garage Opener",
|
||||||
|
"CurrentDoorState": {
|
||||||
|
"Listen": "5/4/5"
|
||||||
|
},
|
||||||
|
"TargetDoorState": {
|
||||||
|
"Listen": "5/4/6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"accessories": [
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
205
platforms/KNX.js
Normal file
205
platforms/KNX.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
/** Sample platform outline
|
||||||
|
* based on Sonos platform
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
|
||||||
|
var knxd = require('eibd');
|
||||||
|
|
||||||
|
function KNXPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// initiate connection to bus for listening ==> done with first shim
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
KNXPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching KNX devices.");
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
|
||||||
|
// iterate through all devices the platform my offer
|
||||||
|
// for each device, create an accessory
|
||||||
|
|
||||||
|
// read accessories from file !!!!!
|
||||||
|
var foundAccessories = this.config.accessories;
|
||||||
|
|
||||||
|
|
||||||
|
//create array of accessories
|
||||||
|
var myAccessories = [];
|
||||||
|
|
||||||
|
for (var int = 0; int < foundAccessories.length; int++) {
|
||||||
|
this.log("parsing acc " + int + " of " + foundAccessories.length);
|
||||||
|
// instantiate and push to array
|
||||||
|
switch (foundAccessories[int].accessory_type) {
|
||||||
|
case "knxdevice":
|
||||||
|
this.log("push new universal device "+foundAccessories[int].name);
|
||||||
|
// push knxd connection setting to each device from platform
|
||||||
|
foundAccessories[int].knxd_ip = this.config.knxd_ip;
|
||||||
|
foundAccessories[int].knxd_port = this.config.knxd_port;
|
||||||
|
var accConstructor = require('./../accessories/knxdevice.js');
|
||||||
|
var acc = new accConstructor.accessory(this.log,foundAccessories[int]);
|
||||||
|
this.log("created "+acc.name+" universal accessory");
|
||||||
|
myAccessories.push(acc);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do something else
|
||||||
|
this.log("unkown accessory type found");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// if done, return the array to callback function
|
||||||
|
this.log("returning "+myAccessories.length+" accessories");
|
||||||
|
callback(myAccessories);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buscallbacks module is to expose a simple function to listen on the bus and register callbacks for value changes
|
||||||
|
* of registered addresses.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* You can start the monitoring process at any time
|
||||||
|
startMonitor({host: name-ip, port: port-num });
|
||||||
|
|
||||||
|
* You can add addresses to the subscriptions using
|
||||||
|
|
||||||
|
registerGA(groupAddress, callback)
|
||||||
|
|
||||||
|
* groupAddress has to be an groupAddress in common knx notation string '1/2/3'
|
||||||
|
* the callback has to be a
|
||||||
|
* var f = function(value) { handle value update;}
|
||||||
|
* so you can do a
|
||||||
|
* registerGA('1/2/3', function(value){
|
||||||
|
* console.log('1/2/3 got a hit with '+value);
|
||||||
|
* });
|
||||||
|
* but of course it is meant to be used programmatically, not literally, otherwise it has no advantage
|
||||||
|
*
|
||||||
|
* You can also use arrays of addresses if your callback is supposed to listen to many addresses:
|
||||||
|
|
||||||
|
registerGA(groupAddresses[], callback)
|
||||||
|
|
||||||
|
* as in
|
||||||
|
* registerGA(['1/2/3','1/0/0'], function(value){
|
||||||
|
* console.log('1/2/3 or 1/0/0 got a hit with '+value);
|
||||||
|
* });
|
||||||
|
* if you are having central addresses like "all lights off" or additional response objects
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* callbacks can have a signature of
|
||||||
|
* function(value, src, dest, type) but do not have to support these parameters (order matters)
|
||||||
|
* src = physical address such as '1.1.20'
|
||||||
|
* dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3'
|
||||||
|
* type = Data point type, as 'DPT1'
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//array of registered addresses and their callbacks
|
||||||
|
var subscriptions = [];
|
||||||
|
//check variable to avoid running two listeners
|
||||||
|
var running;
|
||||||
|
|
||||||
|
function groupsocketlisten(opts, callback) {
|
||||||
|
var conn = knxd.Connection();
|
||||||
|
conn.socketRemote(opts, function() {
|
||||||
|
conn.openGroupSocket(0, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var registerSingleGA = function registerSingleGA (groupAddress, callback, reverse) {
|
||||||
|
subscriptions.push({address: groupAddress, callback: callback, reverse:reverse });
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* public busMonitor.startMonitor()
|
||||||
|
* starts listening for telegrams on KNX bus
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var startMonitor = function startMonitor(opts) { // using { host: name-ip, port: port-num } options object
|
||||||
|
if (!running) {
|
||||||
|
running = true;
|
||||||
|
} else {
|
||||||
|
console.log("<< knxd socket listener already running >>");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
console.log(">>> knxd groupsocketlisten starting <<<");
|
||||||
|
groupsocketlisten(opts, function(parser) {
|
||||||
|
//console.log("knxfunctions.read: in callback parser");
|
||||||
|
parser.on('write', function(src, dest, type, val){
|
||||||
|
// search the registered group addresses
|
||||||
|
//console.log('recv: Write from '+src+' to '+dest+': '+val+' ['+type+'], listeners:' + subscriptions.length);
|
||||||
|
for (var i = 0; i < subscriptions.length; i++) {
|
||||||
|
// iterate through all registered addresses
|
||||||
|
if (subscriptions[i].address === dest) {
|
||||||
|
// found one, notify
|
||||||
|
console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||||
|
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
parser.on('response', function(src, dest, type, val) {
|
||||||
|
// search the registered group addresses
|
||||||
|
// console.log('recv: resp from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||||
|
for (var i = 0; i < subscriptions.length; i++) {
|
||||||
|
// iterate through all registered addresses
|
||||||
|
if (subscriptions[i].address === dest) {
|
||||||
|
// found one, notify
|
||||||
|
// console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']');
|
||||||
|
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//dont care about reads here
|
||||||
|
// parser.on('read', function(src, dest) {
|
||||||
|
// console.log('Read from '+src+' to '+dest);
|
||||||
|
// });
|
||||||
|
//console.log("knxfunctions.read: in callback parser at end");
|
||||||
|
}); // groupsocketlisten parser
|
||||||
|
}; //startMonitor
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* public registerGA(groupAdresses[], callback(value))
|
||||||
|
* parameters
|
||||||
|
* callback: function(value, src, dest, type) called when a value is sent on the bus
|
||||||
|
* groupAddresses: (Array of) string(s) for group addresses
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
var registerGA = function (groupAddresses, callback) {
|
||||||
|
// check if the groupAddresses is an array
|
||||||
|
if (groupAddresses.constructor.toString().indexOf("Array") > -1) {
|
||||||
|
// handle multiple addresses
|
||||||
|
for (var i = 0; i < groupAddresses.length; i++) {
|
||||||
|
if (groupAddresses[i] && groupAddresses[i].match(/(\d*\/\d*\/\d*)/)) { // do not bind empty addresses or invalid addresses
|
||||||
|
// clean the addresses
|
||||||
|
registerSingleGA (groupAddresses[i].match(/(\d*\/\d*\/\d*)/)[0], callback,groupAddresses[i].match(/\d*\/\d*\/\d*(R)/) ? true:false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// it's only one
|
||||||
|
if (groupAddresses.match(/(\d*\/\d*\/\d*)/)) {
|
||||||
|
registerSingleGA (groupAddresses.match(/(\d*\/\d*\/\d*)/)[0], callback, groupAddresses.match(/\d*\/\d*\/\d*(R)/) ? true:false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// console.log("listeners now: " + subscriptions.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports.platform = KNXPlatform;
|
||||||
|
module.exports.registerGA = registerGA;
|
||||||
|
module.exports.startMonitor = startMonitor;
|
||||||
213
platforms/KNX.md
Normal file
213
platforms/KNX.md
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
# Syntax of the config.json
|
||||||
|
In the platforms section, you can insert a KNX type platform.
|
||||||
|
You need to configure all devices directly in the config.json.
|
||||||
|
````json
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"platform": "KNX",
|
||||||
|
"name": "KNX",
|
||||||
|
"knxd_ip": "192.168.178.205",
|
||||||
|
"knxd_port": 6720,
|
||||||
|
"accessories": [
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"name": "Living Room North Lamp",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"type": "Lightbulb",
|
||||||
|
"description": "iOS8 Lightbulb type, supports On (Switch) and Brightness",
|
||||||
|
"name": "Living Room North Lamp",
|
||||||
|
"On": {
|
||||||
|
"Set": "1/1/6",
|
||||||
|
"Listen": ["1/1/63"]
|
||||||
|
},
|
||||||
|
"Brightness": {
|
||||||
|
"Set": "1/1/62",
|
||||||
|
"Listen": ["1/1/64"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
````
|
||||||
|
In the accessories section (the array within the brackets [ ]) you can insert as many objects as you like in the following form
|
||||||
|
````json
|
||||||
|
{
|
||||||
|
"accessory_type": "knxdevice",
|
||||||
|
"name": "Here goes your display name, this will be shown in HomeKit apps",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
````
|
||||||
|
You have to add services in the following syntax:
|
||||||
|
````json
|
||||||
|
{
|
||||||
|
"type": "SERVICENAME",
|
||||||
|
"description": "This is just for you to remember things",
|
||||||
|
"name": "beer tap thermostat",
|
||||||
|
"CHARACTERISTIC1": {
|
||||||
|
"Set": "1/1/6",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/63"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"CHARACTERISTIC2": {
|
||||||
|
"Set": "1/1/62",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/64"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
````
|
||||||
|
`CHARACTERISTICx` are properties that are dependent on the service type, so they are listed below.
|
||||||
|
|
||||||
|
Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group address, to which changes are sent if the service supports changing values. Changes on the bus are listened to, too.
|
||||||
|
`"Listen":["1/2/3","1/2/4","1/2/5"]` is an array of addresses that are listened to additionally. To these addresses never values get written, but the on startup the service will issue *KNX read requests* to ALL addresses listed in `Set:` and in `Listen:`
|
||||||
|
|
||||||
|
|
||||||
|
For two characteristics there are additional minValue and maxValue attributes. These are CurrentTemperature and TargetTemperature, and are used in TemperatureSensor and Thermostat.
|
||||||
|
|
||||||
|
So the charcteristic section may look like:
|
||||||
|
|
||||||
|
````json
|
||||||
|
{
|
||||||
|
"type": "Thermostat",
|
||||||
|
"description": "Sample thermostat",
|
||||||
|
"name": "We need a name for each service, though it usually shows only if multiple services are present in one accessory",
|
||||||
|
"CurrentTemperature": {
|
||||||
|
"Set": "1/1/6",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/63"
|
||||||
|
],
|
||||||
|
"minValue": -18,
|
||||||
|
"maxValue": 30
|
||||||
|
},
|
||||||
|
"TargetTemperature": {
|
||||||
|
"Set": "1/1/62",
|
||||||
|
"Listen": [
|
||||||
|
"1/1/64"
|
||||||
|
],
|
||||||
|
"minValue": -4,
|
||||||
|
"maxValue": 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
````
|
||||||
|
|
||||||
|
|
||||||
|
## reversal of values for characteristics
|
||||||
|
In general, all DPT1 types can be reversed. If you need a 1 for "contact" of a contact senser, you can append an "R" to the group address.
|
||||||
|
Likewise, all percentages of DPT5 can be reversed, if you need a 100% (=255) for window closed, append an "R" to the group address. Do not forget the listening addresses!
|
||||||
|
````json
|
||||||
|
{
|
||||||
|
"type": "ContactSensor",
|
||||||
|
"description": "Sample ContactSensor with 1 as contact (0 is Apple's default)",
|
||||||
|
"name": "WindowContact1",
|
||||||
|
"ContactSensorState": {
|
||||||
|
"Listen": [
|
||||||
|
"1/1/100R"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
````
|
||||||
|
# Supported Services and their characteristics
|
||||||
|
## ContactSensor
|
||||||
|
- ContactSensorState: DPT 1.002, 0 as contact
|
||||||
|
- ~~ContactSensorStateContact1: DPT 1.002, 1 as contact~~
|
||||||
|
|
||||||
|
- StatusActive: DPT 1.011, 1 as true
|
||||||
|
- StatusFault: DPT 1.011, 1 as true
|
||||||
|
- StatusTampered: DPT 1.011, 1 as true
|
||||||
|
- StatusLowBattery: DPT 1.011, 1 as true
|
||||||
|
|
||||||
|
## GarageDoorOpener
|
||||||
|
- CurrentDoorState: DPT5 integer value in range 0..4
|
||||||
|
// Characteristic.CurrentDoorState.OPEN = 0;
|
||||||
|
// Characteristic.CurrentDoorState.CLOSED = 1;
|
||||||
|
// Characteristic.CurrentDoorState.OPENING = 2;
|
||||||
|
// Characteristic.CurrentDoorState.CLOSING = 3;
|
||||||
|
// Characteristic.CurrentDoorState.STOPPED = 4;
|
||||||
|
|
||||||
|
- TargetDoorState: DPT5 integer value in range 0..1
|
||||||
|
// Characteristic.TargetDoorState.OPEN = 0;
|
||||||
|
// Characteristic.TargetDoorState.CLOSED = 1;
|
||||||
|
|
||||||
|
- ObstructionDetected: DPT1, 1 as true
|
||||||
|
|
||||||
|
- LockCurrentState: DPT5 integer value in range 0..3
|
||||||
|
// Characteristic.LockCurrentState.UNSECURED = 0;
|
||||||
|
// Characteristic.LockCurrentState.SECURED = 1;
|
||||||
|
// Characteristic.LockCurrentState.JAMMED = 2;
|
||||||
|
// Characteristic.LockCurrentState.UNKNOWN = 3;
|
||||||
|
|
||||||
|
- LockTargetState: DPT5 integer value in range 0..1
|
||||||
|
// Characteristic.LockTargetState.UNSECURED = 0;
|
||||||
|
// Characteristic.LockTargetState.SECURED = 1;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Lightbulb
|
||||||
|
- On: DPT 1.001, 1 as on, 0 as off
|
||||||
|
- Brightness: DPT5.001 percentage, 100% (=255) the brightest
|
||||||
|
|
||||||
|
## LightSensor
|
||||||
|
- CurrentAmbientLightLevel: DPT 9.004, 0 to 100000 Lux
|
||||||
|
|
||||||
|
## LockMechanism (This is poorly mapped!)
|
||||||
|
- LockCurrentState: DPT 1, 1 as secured
|
||||||
|
- ~~LockCurrentStateSecured0: DPT 1, 0 as secured~~
|
||||||
|
- LockTargetState: DPT 1, 1 as secured
|
||||||
|
- ~~LockTargetStateSecured0: DPT 1, 0 as secured~~
|
||||||
|
|
||||||
|
*ToDo here: correction of mappings, HomeKit reqires lock states UNSECURED=0, SECURED=1, JAMMED = 2, UNKNOWN=3*
|
||||||
|
|
||||||
|
## MotionSensor
|
||||||
|
- MotionDetected: DPT 1.002, 1 as motion detected
|
||||||
|
|
||||||
|
- StatusActive: DPT 1.011, 1 as true
|
||||||
|
- StatusFault: DPT 1.011, 1 as true
|
||||||
|
- StatusTampered: DPT 1.011, 1 as true
|
||||||
|
- StatusLowBattery: DPT 1.011, 1 as true
|
||||||
|
|
||||||
|
## Outlet
|
||||||
|
- On: DPT 1.001, 1 as on, 0 as off
|
||||||
|
- OutletInUse: DPT 1.011, 1 as on, 0 as off
|
||||||
|
|
||||||
|
## Switch
|
||||||
|
- On: DPT 1.001, 1 as on, 0 as off
|
||||||
|
|
||||||
|
## TemperatureSensor
|
||||||
|
- CurrentTemperature: DPT9.001 in °C [listen only]
|
||||||
|
|
||||||
|
## Thermostat
|
||||||
|
- CurrentTemperature: DPT9.001 in °C [listen only], -40 to 80°C if not overriden as shown above
|
||||||
|
- TargetTemperature: DPT9.001, values 0..40°C only, all others are ignored
|
||||||
|
- CurrentHeatingCoolingState: DPT20.102 HVAC, because of the incompatible mapping only off and heating (=auto) are shown, [listen only]
|
||||||
|
- TargetHeatingCoolingState: DPT20.102 HVAC, as above
|
||||||
|
|
||||||
|
## Window
|
||||||
|
- CurrentPosition: DPT5.001 percentage
|
||||||
|
- TargetPosition: DPT5.001 percentage
|
||||||
|
- PositionState: DPT5.005 value [listen only: 0 Increasing, 1 Decreasing, 2 Stopped]
|
||||||
|
|
||||||
|
## WindowCovering
|
||||||
|
- CurrentPosition: DPT5 percentage
|
||||||
|
- TargetPosition: DPT5 percentage
|
||||||
|
- PositionState: DPT5 value [listen only: 0 Closing, 1 Opening, 2 Stopped]
|
||||||
|
|
||||||
|
### not yet supported
|
||||||
|
- HoldPosition
|
||||||
|
- TargetHorizontalTiltAngle
|
||||||
|
- TargetVerticalTiltAngle
|
||||||
|
- CurrentHorizontalTiltAngle
|
||||||
|
- CurrentVerticalTiltAngle
|
||||||
|
- ObstructionDetected
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# DISCLAIMER
|
||||||
|
**This is work in progress!**
|
||||||
|
|
||||||
302
platforms/LIFx.js
Normal file
302
platforms/LIFx.js
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// LiFX Platform Shim for HomeBridge
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "LIFx", // required
|
||||||
|
// "name": "LIFx", // required
|
||||||
|
// "access_token": "access token", // required
|
||||||
|
// "use_lan": "true" // optional set to "true" (gets and sets over the lan) or "get" (gets only over the lan)
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var lifxRemoteObj = require('lifx-api');
|
||||||
|
var lifx_remote;
|
||||||
|
|
||||||
|
var lifxLanObj;
|
||||||
|
var lifx_lan;
|
||||||
|
var use_lan;
|
||||||
|
|
||||||
|
function LIFxPlatform(log, config){
|
||||||
|
// auth info
|
||||||
|
this.access_token = config["access_token"];
|
||||||
|
|
||||||
|
lifx_remote = new lifxRemoteObj(this.access_token);
|
||||||
|
|
||||||
|
// use remote or lan api ?
|
||||||
|
use_lan = config["use_lan"] || false;
|
||||||
|
|
||||||
|
if (use_lan != false) {
|
||||||
|
lifxLanObj = require('lifx');
|
||||||
|
lifx_lan = lifxLanObj.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
LIFxPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching LIFx devices.");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
lifx_remote.listLights("all", function(body) {
|
||||||
|
var bulbs = JSON.parse(body);
|
||||||
|
|
||||||
|
for(var i = 0; i < bulbs.length; i ++) {
|
||||||
|
var accessory = new LIFxBulbAccessory(that.log, bulbs[i]);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
callback(foundAccessories)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function LIFxBulbAccessory(log, bulb) {
|
||||||
|
// device info
|
||||||
|
this.name = bulb.label;
|
||||||
|
this.model = bulb.product_name;
|
||||||
|
this.deviceId = bulb.id;
|
||||||
|
this.serial = bulb.uuid;
|
||||||
|
this.capabilities = bulb.capabilities;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
LIFxBulbAccessory.prototype = {
|
||||||
|
getLan: function(type, callback){
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (!lifx_lan.bulbs[this.deviceId]) {
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lifx_lan.requestStatus();
|
||||||
|
lifx_lan.on('bulbstate', function(bulb) {
|
||||||
|
if (callback == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bulb.addr.toString('hex') == that.deviceId) {
|
||||||
|
switch(type) {
|
||||||
|
case "power":
|
||||||
|
callback(null, bulb.state.power > 0);
|
||||||
|
break;
|
||||||
|
case "brightness":
|
||||||
|
callback(null, Math.round(bulb.state.brightness * 100 / 65535));
|
||||||
|
break;
|
||||||
|
case "hue":
|
||||||
|
callback(null, Math.round(bulb.state.hue * 360 / 65535));
|
||||||
|
break;
|
||||||
|
case "saturation":
|
||||||
|
callback(null, Math.round(bulb.state.saturation * 100 / 65535));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback = null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getRemote: function(type, callback){
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
lifx_remote.listLights("id:"+ that.deviceId, function(body) {
|
||||||
|
var bulb = JSON.parse(body);
|
||||||
|
|
||||||
|
if (bulb.connected != true) {
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case "power":
|
||||||
|
callback(null, bulb.power == "on" ? 1 : 0);
|
||||||
|
break;
|
||||||
|
case "brightness":
|
||||||
|
callback(null, Math.round(bulb.brightness * 100));
|
||||||
|
break;
|
||||||
|
case "hue":
|
||||||
|
callback(null, bulb.color.hue);
|
||||||
|
break;
|
||||||
|
case "saturation":
|
||||||
|
callback(null, Math.round(bulb.color.saturation * 100));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
identify: function(callback) {
|
||||||
|
lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setLanColor: function(type, value, callback){
|
||||||
|
var bulb = lifx_lan.bulbs[this.deviceId];
|
||||||
|
|
||||||
|
if (!bulb) {
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = {
|
||||||
|
hue: bulb.state.hue,
|
||||||
|
saturation: bulb.state.saturation,
|
||||||
|
brightness: bulb.state.brightness,
|
||||||
|
kelvin: bulb.state.kelvin
|
||||||
|
};
|
||||||
|
|
||||||
|
var scale = type == "hue" ? 360 : 100;
|
||||||
|
|
||||||
|
state[type] = Math.round(value * 65535 / scale) & 0xffff;
|
||||||
|
lifx_lan.lightsColour(state.hue, state.saturation, state.brightness, state.kelvin, 0, bulb);
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
},
|
||||||
|
setLanPower: function(state, callback){
|
||||||
|
var bulb = lifx_lan.bulbs[this.deviceId];
|
||||||
|
|
||||||
|
if (!bulb) {
|
||||||
|
callback(new Error("Device not found"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
lifx_lan.lightsOn(bulb);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
lifx_lan.lightsOff(bulb);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
},
|
||||||
|
setRemoteColor: function(type, value, callback){
|
||||||
|
var color;
|
||||||
|
|
||||||
|
switch(type) {
|
||||||
|
case "brightness":
|
||||||
|
color = "brightness:" + (value / 100);
|
||||||
|
break;
|
||||||
|
case "hue":
|
||||||
|
color = "hue:" + value;
|
||||||
|
break;
|
||||||
|
case "saturation":
|
||||||
|
color = "saturation:" + (value / 100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setRemotePower: function(state, callback){
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
lifx_remote.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = []
|
||||||
|
var service = new Service.Lightbulb(this.name);
|
||||||
|
|
||||||
|
switch(use_lan) {
|
||||||
|
case true:
|
||||||
|
case "true":
|
||||||
|
// gets and sets over the lan api
|
||||||
|
service
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', function(callback) { that.getLan("power", callback);})
|
||||||
|
.on('set', function(value, callback) {that.setLanPower(value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', function(callback) { that.getLan("brightness", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setLanColor("brightness", value, callback);});
|
||||||
|
|
||||||
|
if (this.capabilities.has_color == true) {
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Hue)
|
||||||
|
.on('get', function(callback) { that.getLan("hue", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setLanColor("hue", value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Saturation)
|
||||||
|
.on('get', function(callback) { that.getLan("saturation", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setLanColor("saturation", value, callback);});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "get":
|
||||||
|
// gets over the lan api, sets over the remote api
|
||||||
|
service
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', function(callback) { that.getLan("power", callback);})
|
||||||
|
.on('set', function(value, callback) {that.setRemotePower(value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', function(callback) { that.getLan("brightness", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);});
|
||||||
|
|
||||||
|
if (this.capabilities.has_color == true) {
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Hue)
|
||||||
|
.on('get', function(callback) { that.getLan("hue", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Saturation)
|
||||||
|
.on('get', function(callback) { that.getLan("saturation", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// gets and sets over the remote api
|
||||||
|
service
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', function(callback) { that.getRemote("power", callback);})
|
||||||
|
.on('set', function(value, callback) {that.setRemotePower(value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', function(callback) { that.getRemote("brightness", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);});
|
||||||
|
|
||||||
|
if (this.capabilities.has_color == true) {
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Hue)
|
||||||
|
.on('get', function(callback) { that.getRemote("hue", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);});
|
||||||
|
|
||||||
|
service
|
||||||
|
.addCharacteristic(Characteristic.Saturation)
|
||||||
|
.on('get', function(callback) { that.getRemote("saturation", callback);})
|
||||||
|
.on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
services.push(service);
|
||||||
|
|
||||||
|
service = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
service
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "LIFX")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.model)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.serial);
|
||||||
|
|
||||||
|
services.push(service);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.accessory = LIFxBulbAccessory;
|
||||||
|
module.exports.platform = LIFxPlatform;
|
||||||
267
platforms/LogitechHarmony.js
Normal file
267
platforms/LogitechHarmony.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Logitech Harmony Remote Platform Shim for HomeBridge
|
||||||
|
// Based on the Domoticz Platform Shim for HomeBridge by Joep Verhaeg (http://www.joepverhaeg.nl)
|
||||||
|
// Wriiten by John Wells (https://github.com/madmod)
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "LogitechHarmony",
|
||||||
|
// "name": "Logitech Harmony"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
var types = require('hap-nodejs/accessories/types.js');
|
||||||
|
|
||||||
|
var harmonyDiscover = require('harmonyhubjs-discover');
|
||||||
|
var harmony = require('harmonyhubjs-client');
|
||||||
|
|
||||||
|
var _harmonyHubPort = 61991;
|
||||||
|
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var Accessory = require("hap-nodejs").Accessory;
|
||||||
|
var uuid = require("hap-nodejs").uuid;
|
||||||
|
var inherits = require('util').inherits;
|
||||||
|
var queue = require('queue');
|
||||||
|
|
||||||
|
|
||||||
|
function sortByKey (array, key) {
|
||||||
|
return array.sort(function(a, b) {
|
||||||
|
var x = a[key]; var y = b[key];
|
||||||
|
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function LogitechHarmonyPlatform (log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.ip_address = config['ip_address'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
LogitechHarmonyPlatform.prototype = {
|
||||||
|
|
||||||
|
accessories: function (callback) {
|
||||||
|
var plat = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
var activityAccessories = [];
|
||||||
|
var hub = null;
|
||||||
|
var hubIP = null;
|
||||||
|
var hubQueue = queue();
|
||||||
|
hubQueue.concurrency = 1;
|
||||||
|
|
||||||
|
// Get the first hub
|
||||||
|
locateHub(function (err, client, clientIP) {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
plat.log("Fetching Logitech Harmony devices and activites...");
|
||||||
|
|
||||||
|
hub = client;
|
||||||
|
hubIP = clientIP;
|
||||||
|
//getDevices(hub);
|
||||||
|
getActivities();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find one Harmony remote hub (only support one for now)
|
||||||
|
function locateHub(callback) {
|
||||||
|
// Use the ip address in configuration if available
|
||||||
|
if (plat.ip_address) {
|
||||||
|
console.log("Using Logitech Harmony hub ip address from configuration");
|
||||||
|
|
||||||
|
return createClient(plat.ip_address, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
plat.log("Searching for Logitech Harmony remote hubs...");
|
||||||
|
|
||||||
|
// Discover the harmony hub with bonjour
|
||||||
|
var discover = new harmonyDiscover(_harmonyHubPort);
|
||||||
|
|
||||||
|
// TODO: Support update event with some way to add accessories
|
||||||
|
// TODO: Have some kind of timeout with an error message. Right now this searches forever until it finds one hub.
|
||||||
|
discover.on('online', function (hubInfo) {
|
||||||
|
plat.log("Found Logitech Harmony remote hub: " + hubInfo.ip);
|
||||||
|
|
||||||
|
// Stop looking for hubs once we find the first one
|
||||||
|
// TODO: Support multiple hubs
|
||||||
|
discover.stop();
|
||||||
|
|
||||||
|
createClient(hubInfo.ip, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start looking for hubs
|
||||||
|
discover.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to a Harmony hub
|
||||||
|
function createClient(ipAddress, callback) {
|
||||||
|
plat.log("Connecting to Logitech Harmony remote hub...");
|
||||||
|
harmony(ipAddress)
|
||||||
|
.then(function (client) {
|
||||||
|
plat.log("Connected to Logitech Harmony remote hub");
|
||||||
|
callback(null, client, ipAddress);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Harmony Activities
|
||||||
|
function getActivities() {
|
||||||
|
plat.log("Fetching Logitech Harmony activities...");
|
||||||
|
|
||||||
|
hub.getActivities()
|
||||||
|
.then(function (activities) {
|
||||||
|
plat.log("Found activities: \n" + activities.map(function (a) { return "\t" + a.label; }).join("\n"));
|
||||||
|
|
||||||
|
hub.getCurrentActivity().then(function (currentActivity) {
|
||||||
|
var actAccessories = [];
|
||||||
|
var sArray = sortByKey(activities, "label");
|
||||||
|
sArray.map(function(s) {
|
||||||
|
var accessory = createActivityAccessory(s);
|
||||||
|
if (accessory.id > 0) {
|
||||||
|
accessory.updateActivityState(currentActivity);
|
||||||
|
actAccessories.push(accessory);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
activityAccessories = actAccessories;
|
||||||
|
keepAliveRefreshLoop();
|
||||||
|
callback(foundAccessories);
|
||||||
|
}).catch(function (err) {
|
||||||
|
plat.log('Unable to get current activity with error', err);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createActivityAccessory(activity) {
|
||||||
|
var accessory = new LogitechHarmonyActivityAccessory(plat.log, activity, changeCurrentActivity.bind(plat), -1);
|
||||||
|
return accessory;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isChangingActivity = false;
|
||||||
|
function changeCurrentActivity(nextActivity, callback) {
|
||||||
|
if (!nextActivity) {
|
||||||
|
nextActivity = -1;
|
||||||
|
}
|
||||||
|
plat.log('Queue activity to ' + nextActivity);
|
||||||
|
executeOnHub(function(h, cb) {
|
||||||
|
plat.log('Set activity to ' + nextActivity);
|
||||||
|
h.startActivity(nextActivity)
|
||||||
|
.then(function () {
|
||||||
|
cb();
|
||||||
|
isChangingActivity = false;
|
||||||
|
plat.log('Finished setting activity to ' + nextActivity);
|
||||||
|
updateCurrentActivity(nextActivity);
|
||||||
|
if (callback) callback(null, nextActivity);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
cb();
|
||||||
|
isChangingActivity = false;
|
||||||
|
plat.log('Failed setting activity to ' + nextActivity + ' with error ' + err);
|
||||||
|
if (callback) callback(err);
|
||||||
|
});
|
||||||
|
}, function(){
|
||||||
|
callback(Error("Set activity failed too many times"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentActivity(currentActivity) {
|
||||||
|
var actAccessories = activityAccessories;
|
||||||
|
if (actAccessories instanceof Array) {
|
||||||
|
actAccessories.map(function(a) { a.updateActivityState(currentActivity); });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent connection from closing
|
||||||
|
function keepAliveRefreshLoop() {
|
||||||
|
setTimeout(function() {
|
||||||
|
setInterval(function() {
|
||||||
|
executeOnHub(function(h, cb) {
|
||||||
|
plat.log("Refresh Status");
|
||||||
|
h.getCurrentActivity()
|
||||||
|
.then(function(currentActivity){
|
||||||
|
cb();
|
||||||
|
updateCurrentActivity(currentActivity);
|
||||||
|
})
|
||||||
|
.catch(cb);
|
||||||
|
});
|
||||||
|
}, 20000);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeOnHub(func, funcMaxTimeout)
|
||||||
|
{
|
||||||
|
if (!func) return;
|
||||||
|
hubQueue.push(function(cb) {
|
||||||
|
var tout = setTimeout(function(){
|
||||||
|
plat.log("Reconnecting to Hub " + hubIP);
|
||||||
|
createClient(hubIP, function(err, newHub){
|
||||||
|
if (err) throw err;
|
||||||
|
hub = newHub;
|
||||||
|
if (funcMaxTimeout) {
|
||||||
|
funcMaxTimeout();
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}, 30000);
|
||||||
|
func(hub, function(){
|
||||||
|
clearTimeout(tout);
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!hubQueue.running){
|
||||||
|
hubQueue.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function LogitechHarmonyActivityAccessory (log, details, changeCurrentActivity) {
|
||||||
|
this.log = log;
|
||||||
|
this.id = details.id;
|
||||||
|
this.name = details.label;
|
||||||
|
this.isOn = false;
|
||||||
|
this.changeCurrentActivity = changeCurrentActivity;
|
||||||
|
Accessory.call(this, this.name, uuid.generate(this.id));
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.getService(Service.AccessoryInformation)
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Logitech")
|
||||||
|
.setCharacteristic(Characteristic.Model, "Harmony")
|
||||||
|
// TODO: Add hub unique id to this for people with multiple hubs so that it is really a guid.
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.id);
|
||||||
|
|
||||||
|
this.addService(Service.Switch)
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', function(callback) {
|
||||||
|
// Refreshed automatically by platform
|
||||||
|
callback(null, self.isOn);
|
||||||
|
})
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
}
|
||||||
|
inherits(LogitechHarmonyActivityAccessory, Accessory);
|
||||||
|
LogitechHarmonyActivityAccessory.prototype.parent = Accessory.prototype;
|
||||||
|
LogitechHarmonyActivityAccessory.prototype.getServices = function() {
|
||||||
|
return this.services;
|
||||||
|
};
|
||||||
|
|
||||||
|
LogitechHarmonyActivityAccessory.prototype.updateActivityState = function (currentActivity) {
|
||||||
|
this.isOn = (currentActivity === this.id);
|
||||||
|
// Force get to trigger 'change' if needed
|
||||||
|
this.getService(Service.Switch)
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.getValue();
|
||||||
|
};
|
||||||
|
|
||||||
|
LogitechHarmonyActivityAccessory.prototype.setPowerState = function (state, callback) {
|
||||||
|
this.changeCurrentActivity(state ? this.id : null, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.platform = LogitechHarmonyPlatform;
|
||||||
|
|
||||||
242
platforms/MiLight.js
Normal file
242
platforms/MiLight.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
MiLight platform shim for Homebridge
|
||||||
|
Written by Sam Edwards (https://samedwards.ca/)
|
||||||
|
|
||||||
|
Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from
|
||||||
|
applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details from (http://www.limitlessled.com/dev/)
|
||||||
|
|
||||||
|
Configure in config.json as follows:
|
||||||
|
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"platform":"MiLight",
|
||||||
|
"name":"MiLight",
|
||||||
|
"ip_address": "255.255.255.255",
|
||||||
|
"port": 8899,
|
||||||
|
"type": "rgbw",
|
||||||
|
"delay": 30,
|
||||||
|
"repeat": 3,
|
||||||
|
"zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Where the parameters are:
|
||||||
|
*platform (required): This must be "MiLight", and refers to the name of the accessory as exported from this file
|
||||||
|
*name (optional): The display name used for logging output by Homebridge. Best to set to "MiLight"
|
||||||
|
*ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified
|
||||||
|
*port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified
|
||||||
|
*type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw.
|
||||||
|
*delay (optional): Delay between commands sent over UDP. Default 30ms. May cause delays when sending a lot of commands. Try decreasing to improve.
|
||||||
|
*repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3
|
||||||
|
*zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone.
|
||||||
|
|
||||||
|
Tips and Tricks:
|
||||||
|
*Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting
|
||||||
|
*White and rgb bulbs don't support absolute brightness setting, so we just send a brightness up/brightness down command depending
|
||||||
|
if we got a percentage above/below 50% respectively
|
||||||
|
*The only exception to the above is that white bulbs support a "maximum brightness" command, so we send that when we get 100%
|
||||||
|
*Implemented warmer/cooler for white lamps in a similar way to brightnes, except this time above/below 180 degrees on the colour wheel
|
||||||
|
*I welcome feedback on a better way to work the brightness/hue for white and rgb bulbs
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
*Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var Milight = require('node-milight-promise').MilightController;
|
||||||
|
var commands = require('node-milight-promise').commands;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: MiLightAccessory,
|
||||||
|
platform: MiLightPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiLightPlatform(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
MiLightPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
var zones = [];
|
||||||
|
|
||||||
|
// Various error checking
|
||||||
|
if (this.config.zones) {
|
||||||
|
var zoneLength = this.config.zones.length;
|
||||||
|
} else {
|
||||||
|
this.log("ERROR: Could not read zones from configuration.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config["type"]) {
|
||||||
|
this.log("INFO: Type not specified, defaulting to rgbw");
|
||||||
|
this.config["type"] = "rgbw";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoneLength == 0) {
|
||||||
|
this.log("ERROR: No zones found in configuration.");
|
||||||
|
return;
|
||||||
|
} else if (this.config["type"] == "rgb" && zoneLength > 1) {
|
||||||
|
this.log("WARNING: RGB lamps only have a single zone. Only the first defined zone will be used.");
|
||||||
|
zoneLength = 1;
|
||||||
|
} else if (zoneLength > 4) {
|
||||||
|
this.log("WARNING: Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones.");
|
||||||
|
zoneLength = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create lamp accessories for all of the defined zones
|
||||||
|
for (var i=0; i < zoneLength; i++) {
|
||||||
|
if (!!this.config.zones[i]) {
|
||||||
|
this.config["name"] = this.config.zones[i];
|
||||||
|
this.config["zone"] = i+1;
|
||||||
|
lamp = new MiLightAccessory(this.log, this.config);
|
||||||
|
zones.push(lamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (zones.length > 0) {
|
||||||
|
callback(zones);
|
||||||
|
} else {
|
||||||
|
this.log("ERROR: Unable to find any valid zones");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiLightAccessory(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
// config info
|
||||||
|
this.ip_address = config["ip_address"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.name = config["name"];
|
||||||
|
this.zone = config["zone"];
|
||||||
|
this.type = config["type"];
|
||||||
|
this.delay = config["delay"];
|
||||||
|
this.repeat = config["repeat"];
|
||||||
|
|
||||||
|
this.light = new Milight({
|
||||||
|
ip: this.ip_address,
|
||||||
|
port: this.port,
|
||||||
|
delayBetweenCommands: this.delay,
|
||||||
|
commandRepeat: this.repeat
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
MiLightAccessory.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("["+this.name+"] Setting power state to on");
|
||||||
|
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||||
|
} else {
|
||||||
|
this.log("["+this.name+"] Setting power state to off");
|
||||||
|
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level, callback) {
|
||||||
|
if (level == 0) {
|
||||||
|
// If brightness is set to 0, turn off the lamp
|
||||||
|
this.log("["+this.name+"] Setting brightness to 0 (off)");
|
||||||
|
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||||
|
} else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) {
|
||||||
|
// If setting brightness to 2 or lower, instead set night mode for lamps that support it
|
||||||
|
this.log("["+this.name+"] Setting night mode", level);
|
||||||
|
|
||||||
|
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||||
|
// Ensure we're pausing for 100ms between these commands as per the spec
|
||||||
|
this.light.pause(100);
|
||||||
|
this.light.sendCommands(commands[this.type].nightMode(this.zone));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.log("["+this.name+"] Setting brightness to %s", level);
|
||||||
|
|
||||||
|
// Send on command to ensure we're addressing the right bulb
|
||||||
|
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||||
|
|
||||||
|
// If this is an rgbw lamp, set the absolute brightness specified
|
||||||
|
if (this.type == "rgbw") {
|
||||||
|
this.light.sendCommands(commands.rgbw.brightness(level));
|
||||||
|
} else {
|
||||||
|
// If this is an rgb or a white lamp, they only support brightness up and down.
|
||||||
|
// Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world.
|
||||||
|
if (level >= 50) {
|
||||||
|
if (this.type == "white" && level == 100) {
|
||||||
|
// But the white lamps do have a "maximum brightness" command
|
||||||
|
this.light.sendCommands(commands.white.maxBright(this.zone));
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands[this.type].brightUp());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands[this.type].brightDown());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
setHue: function(value, callback) {
|
||||||
|
this.log("["+this.name+"] Setting hue to %s", value);
|
||||||
|
|
||||||
|
var hue = Array(value, 0, 0);
|
||||||
|
|
||||||
|
// Send on command to ensure we're addressing the right bulb
|
||||||
|
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||||
|
|
||||||
|
if (this.type == "rgbw") {
|
||||||
|
if (value == 0) {
|
||||||
|
this.light.sendCommands(commands.rgbw.whiteMode(this.zone));
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||||
|
}
|
||||||
|
} else if (this.type == "rgb") {
|
||||||
|
this.light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||||
|
} else if (this.type == "white") {
|
||||||
|
// Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour
|
||||||
|
if (value >= 180) {
|
||||||
|
this.light.sendCommands(commands.white.cooler());
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands.white.warmer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("["+this.name+"] Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "MiLight")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.type)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345");
|
||||||
|
|
||||||
|
var lightbulbService = new Service.Lightbulb();
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(new Characteristic.Brightness())
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(new Characteristic.Hue())
|
||||||
|
.on('set', this.setHue.bind(this));
|
||||||
|
|
||||||
|
return [informationService, lightbulbService];
|
||||||
|
}
|
||||||
|
};
|
||||||
397
platforms/Nest.js
Normal file
397
platforms/Nest.js
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var nest = require('unofficial-nest-api');
|
||||||
|
|
||||||
|
function NestPlatform(log, config){
|
||||||
|
|
||||||
|
// auth info
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
NestPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Nest devices.");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
nest.login(this.username, this.password, function (err, data) {
|
||||||
|
if (err) {
|
||||||
|
that.log("There was a problem authenticating with Nest.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
for (var deviceId in data.device) {
|
||||||
|
if (data.device.hasOwnProperty(deviceId)) {
|
||||||
|
var device = data.device[deviceId];
|
||||||
|
// it's a thermostat, adjust this to detect other accessories
|
||||||
|
if (data.shared[deviceId].hasOwnProperty('current_temperature'))
|
||||||
|
{
|
||||||
|
var name = data.shared[deviceId].name
|
||||||
|
var accessory = new NestThermostatAccessory(that.log, name, device, deviceId);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback(foundAccessories)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function NestThermostatAccessory(log, name, device, deviceId) {
|
||||||
|
// device info
|
||||||
|
if (name) {
|
||||||
|
this.name = name;
|
||||||
|
} else {
|
||||||
|
this.name = "Nest" + device.serial_number;
|
||||||
|
}
|
||||||
|
this.model = device.model_version;
|
||||||
|
this.serial = device.serial_number;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
NestThermostatAccessory.prototype = {
|
||||||
|
getCurrentHeatingCooling: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Checking current heating cooling for: " + this.name);
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.device[that.deviceId];
|
||||||
|
|
||||||
|
var currentHeatingCooling = 0;
|
||||||
|
switch(device.current_schedule_mode) {
|
||||||
|
case "OFF":
|
||||||
|
targetHeatingCooling = 0;
|
||||||
|
break;
|
||||||
|
case "HEAT":
|
||||||
|
currentHeatingCooling = 1;
|
||||||
|
break;
|
||||||
|
case "COOL":
|
||||||
|
currentHeatingCooling = 2;
|
||||||
|
break;
|
||||||
|
case "RANGE":
|
||||||
|
currentHeatingCooling = 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
currentHeatingCooling = 0;
|
||||||
|
}
|
||||||
|
that.log("Current heating for " + this.name + "is: " + currentHeatingCooling);
|
||||||
|
callback(currentHeatingCooling);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getTargetHeatingCoooling: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Checking target heating cooling for: " + this.name);
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.device[that.deviceId];
|
||||||
|
|
||||||
|
var targetHeatingCooling = 0;
|
||||||
|
switch(device.target_temperature_type) {
|
||||||
|
case "off":
|
||||||
|
targetHeatingCooling = 0;
|
||||||
|
break;
|
||||||
|
case "heat":
|
||||||
|
targetHeatingCooling = 1;
|
||||||
|
break;
|
||||||
|
case "cool":
|
||||||
|
targetHeatingCooling = 2;
|
||||||
|
break;
|
||||||
|
case "range":
|
||||||
|
targetHeatingCooling = 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
targetHeatingCooling = 0;
|
||||||
|
}
|
||||||
|
that.log("Current target heating for " + this.name + " is: " + targetHeatingCooling);
|
||||||
|
callback(targetHeatingCooling);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentTemperature: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.shared[that.deviceId];
|
||||||
|
that.log("Current temperature for " + this.name + " is: " + device.current_temperature);
|
||||||
|
callback(device.current_temperature);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getTargetTemperature: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.shared[that.deviceId];
|
||||||
|
that.log("Target temperature for " + this.name + " is: " + device.target_temperature);
|
||||||
|
callback(device.target_temperature);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getTemperatureUnits: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.device[that.deviceId];
|
||||||
|
var temperatureUnits = 0;
|
||||||
|
switch(device.temperature_scale) {
|
||||||
|
case "F":
|
||||||
|
that.log("Tempature unit for " + this.name + " is: " + "Fahrenheit");
|
||||||
|
temperatureUnits = 1;
|
||||||
|
break;
|
||||||
|
case "C":
|
||||||
|
that.log("Tempature unit for " + this.name + " is: " + "Celsius");
|
||||||
|
temperatureUnits = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
temperatureUnits = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(temperatureUnits);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentRelativeHumidity: function(callback){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
nest.fetchStatus(function (data) {
|
||||||
|
var device = data.device[that.deviceId];
|
||||||
|
that.log("Humidity for " + this.name + " is: " + device.current_humidity);
|
||||||
|
callback(device.current_humidity);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetHeatingCooling: function(targetHeatingCooling){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var targetTemperatureType = 'off';
|
||||||
|
switch(targetHeatingCooling) {
|
||||||
|
case 0:
|
||||||
|
targetTemperatureType = 'off';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
targetTemperatureType = 'heat';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
targetTemperatureType = 'cool';
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
targetTemperatureType = 'range';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
targetTemperatureType = 'off';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Setting target heating cooling for " + this.name + " to: " + targetTemperatureType);
|
||||||
|
nest.setTargetTemperatureType(this.deviceId, targetTemperatureType);
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetTemperature: function(targetTemperature){
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting target temperature for " + this.name + " to: " + targetTemperature);
|
||||||
|
nest.setTemperature(this.deviceId, targetTemperature);
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Nest",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.model,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.serial,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.THERMOSTAT_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of thermostat",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentHeatingCooling(function(currentHeatingCooling){
|
||||||
|
callback(currentHeatingCooling);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Mode",
|
||||||
|
designedMaxLength: 1,
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 2,
|
||||||
|
designedMinStep: 1,
|
||||||
|
},{
|
||||||
|
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setTargetHeatingCooling(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getTargetHeatingCoooling(function(targetHeatingCooling){
|
||||||
|
callback(targetHeatingCooling);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Mode",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 3,
|
||||||
|
designedMinStep: 1,
|
||||||
|
},{
|
||||||
|
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentTemperature(function(currentTemperature){
|
||||||
|
callback(currentTemperature);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 20,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Current Temperature",
|
||||||
|
unit: "celsius"
|
||||||
|
},{
|
||||||
|
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setTargetTemperature(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getTargetTemperature(function(targetTemperature){
|
||||||
|
callback(targetTemperature);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 20,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Target Temperature",
|
||||||
|
designedMinValue: 16,
|
||||||
|
designedMaxValue: 38,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "celsius"
|
||||||
|
},{
|
||||||
|
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getTemperatureUnits(function(temperatureUnits){
|
||||||
|
callback(temperatureUnits);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Unit",
|
||||||
|
},{
|
||||||
|
cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getCurrentRelativeHumidity(function(currentRelativeHumidity){
|
||||||
|
callback(currentRelativeHumidity);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Humidity",
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.accessory = NestThermostatAccessory;
|
||||||
|
module.exports.platform = NestPlatform;
|
||||||
347
platforms/Openhab.js
Normal file
347
platforms/Openhab.js
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
// OpenHAB Platform Shim for HomeBridge
|
||||||
|
// Written by Tommaso Marchionni
|
||||||
|
// Based on many of the other HomeBridge platform modules
|
||||||
|
//
|
||||||
|
// Revisions:
|
||||||
|
//
|
||||||
|
// 17 October 2015 [tommasomarchionni]
|
||||||
|
// - Initial release
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "Openhab",
|
||||||
|
// "name": "Openhab",
|
||||||
|
// "server": "127.0.0.1",
|
||||||
|
// "port": "8080",
|
||||||
|
// "sitemap": "demo"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// Example of sitemap in OpenHAB:
|
||||||
|
// sitemap homekit label="HomeKit" {
|
||||||
|
// Switch item=Light_1 label="Light 1"
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
var Service = require("hap-nodejs/lib/Service.js").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
|
||||||
|
function OpenhabPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.user = config["user"];
|
||||||
|
this.password = config["password"];
|
||||||
|
this.server = config["server"];
|
||||||
|
this.port = config["port"];
|
||||||
|
|
||||||
|
this.protocol = "http";
|
||||||
|
|
||||||
|
this.sitemap = "demo";
|
||||||
|
if (typeof config["sitemap"] != 'undefined') {
|
||||||
|
this.sitemap = config["sitemap"];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenhabPlatform.prototype = {
|
||||||
|
|
||||||
|
sitemapUrl: function() {
|
||||||
|
var serverString = this.server;
|
||||||
|
//TODO da verificare
|
||||||
|
if (this.user && this.password) {
|
||||||
|
serverString = this.user + ":" + this.password + "@" + serverString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.protocol + "://" + serverString + ":" + this.port + "/rest/sitemaps/" + this.sitemap + "?type=json";
|
||||||
|
},
|
||||||
|
|
||||||
|
parseSitemap: function(sitemap) {
|
||||||
|
var widgets = [].concat(sitemap.homepage.widget);
|
||||||
|
var result = [];
|
||||||
|
for (var i = 0; i < widgets.length; i++) {
|
||||||
|
var widget = widgets[i];
|
||||||
|
if (!widget.item) {
|
||||||
|
//TODO to handle frame
|
||||||
|
this.log("WARN: The widget '" + widget.label + "' does not reference an item.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.item.type=="SwitchItem" || widget.item.type=="DimmerItem" || widget.item.type == "RollershutterItem"){
|
||||||
|
accessory = new OpenhabAccessory(this.log,this,widget.widgetId,widget.label,widget.item)
|
||||||
|
this.log("Accessory Found: " + widget.label);
|
||||||
|
result.push(accessory);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching OpenHAB devices.");
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
url = that.sitemapUrl();
|
||||||
|
this.log("Connecting to " + url);
|
||||||
|
request.get({
|
||||||
|
url: url,
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
callback(that.parseSitemap(json));
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem connecting to OpenHAB.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function OpenhabAccessory(log, platform, widgetId, label, detail) {
|
||||||
|
this.log = log;
|
||||||
|
this.platform = platform;
|
||||||
|
this.idx = widgetId;
|
||||||
|
this.name = label;
|
||||||
|
this.label = label;
|
||||||
|
this.type = detail.type;
|
||||||
|
this.deviceURL = detail.link;
|
||||||
|
this.addressStr = "n/a";
|
||||||
|
this.state = detail.state;
|
||||||
|
|
||||||
|
if (this.type == "DimmerItem") {
|
||||||
|
this.typeSupportsOnOff = true;
|
||||||
|
this.typeSupportsDim = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type == "SwitchItem") {
|
||||||
|
this.typeSupportsOnOff = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type == "RollershutterItem") {
|
||||||
|
this.typeSupportsWindowCovering = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenhabAccessory.prototype = {
|
||||||
|
|
||||||
|
updateStatus: function(command) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: this.deviceURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: "" + command
|
||||||
|
};
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
that.log("eseguo post");
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
console.trace("Updating Device Status.");
|
||||||
|
that.log(error);
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
that.log("updateStatus of " + that.name + ": " + command);
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServiceType: function() {
|
||||||
|
if (this.typeSupportsWindowCovering){
|
||||||
|
return new Service.WindowCovering;
|
||||||
|
} else if (this.typeSupportsDim) {
|
||||||
|
return new Service.Lightbulb;
|
||||||
|
} else if (this.typeSupportsOnOff) {
|
||||||
|
return new Service.Switch;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus: function(command, callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: this.deviceURL,
|
||||||
|
method: 'POST',
|
||||||
|
body: "" + command
|
||||||
|
};
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
//console.trace("Updating Device Status.");
|
||||||
|
//that.log(error);
|
||||||
|
//return error;
|
||||||
|
callback(new Error(error));
|
||||||
|
} else {
|
||||||
|
that.log("updateStatus of " + that.name + ": " + command);
|
||||||
|
callback(true);
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (this.typeSupportsOnOff) {
|
||||||
|
if (powerOn) {
|
||||||
|
var command = "ON";
|
||||||
|
} else {
|
||||||
|
var command = "OFF";
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to " + command);
|
||||||
|
this.updateStatus(command, function(noError){
|
||||||
|
if (noError) {
|
||||||
|
that.log("Successfully set '"+that.name+"' to " + command);
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
callback(new Error('Can not communicate with OpenHAB.'));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
}else{
|
||||||
|
callback(new Error(this.name + " not supports ONOFF"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getStatus: function(callback){
|
||||||
|
var that = this;
|
||||||
|
this.log("Fetching status brightness for: " + this.name);
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
url: this.deviceURL + '/state?type=json',
|
||||||
|
method: 'GET'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.auth) {
|
||||||
|
options['auth'] = this.auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(options, function(error, response, body) {
|
||||||
|
if (error) {
|
||||||
|
//console.trace("Requesting Device Status.");
|
||||||
|
//that.log(error);
|
||||||
|
//return error;
|
||||||
|
callback(new Error('Can not communicate with Home Assistant.'));
|
||||||
|
} else {
|
||||||
|
that.log("getStatus of " + that.name + ": " + body);
|
||||||
|
callback(null,body);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentPosition: function(callback){
|
||||||
|
callback(100);
|
||||||
|
},
|
||||||
|
|
||||||
|
getPositionState: function(callback){
|
||||||
|
this.log("Fetching position state for: " + this.name);
|
||||||
|
callback(Characteristic.PositionState.STOPPED);
|
||||||
|
},
|
||||||
|
|
||||||
|
setTargetPosition: function(level, callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting target position on the '"+this.name+"' to " + level);
|
||||||
|
|
||||||
|
this.updateStatus(level, function(noError){
|
||||||
|
if (noError) {
|
||||||
|
that.log("Successfully set position on the '"+that.name+"' to " + level);
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
callback(new Error('Can not communicate with OpenHAB.'));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level, callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (this.typeSupportsDim && level >= 0 && level <= 100) {
|
||||||
|
|
||||||
|
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||||
|
|
||||||
|
this.updateStatus(level, function(noError){
|
||||||
|
if (noError) {
|
||||||
|
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
callback(new Error('Can not communicate with OpenHAB.'));
|
||||||
|
}
|
||||||
|
}.bind(this));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "OpenHAB")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.type)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "1234567890")
|
||||||
|
.setCharacteristic(Characteristic.Name, this.label);
|
||||||
|
|
||||||
|
var otherService = this.getServiceType();
|
||||||
|
|
||||||
|
if (this.typeSupportsOnOff) {
|
||||||
|
otherService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getStatus.bind(this))
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.typeSupportsDim) {
|
||||||
|
otherService
|
||||||
|
.addCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('get', this.getStatus.bind(this))
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.typeSupportsWindowCovering) {
|
||||||
|
var currentPosition = 100;
|
||||||
|
|
||||||
|
otherService
|
||||||
|
.getCharacteristic(Characteristic.CurrentPosition)
|
||||||
|
.on('get', this.getCurrentPosition.bind(this))
|
||||||
|
.setValue(currentPosition);
|
||||||
|
|
||||||
|
otherService
|
||||||
|
.getCharacteristic(Characteristic.PositionState)
|
||||||
|
.on('get', this.getPositionState.bind(this))
|
||||||
|
.setValue(Characteristic.PositionState.STOPPED);
|
||||||
|
|
||||||
|
otherService
|
||||||
|
.getCharacteristic(Characteristic.TargetPosition)
|
||||||
|
.on('get', this.getCurrentPosition.bind(this))
|
||||||
|
.on('set', this.setTargetPosition.bind(this));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(informationService);
|
||||||
|
|
||||||
|
return [informationService, otherService];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.accessory = OpenhabAccessory;
|
||||||
|
module.exports.platform = OpenhabPlatform;
|
||||||
375
platforms/PhilipsHue.js
Normal file
375
platforms/PhilipsHue.js
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
// Philips Hue Platform Shim for HomeBridge
|
||||||
|
//
|
||||||
|
// Remember to add platform to config.json. Example:
|
||||||
|
// "platforms": [
|
||||||
|
// {
|
||||||
|
// "platform": "PhilipsHue",
|
||||||
|
// "name": "Philips Hue",
|
||||||
|
// "ip_address": "127.0.0.1",
|
||||||
|
// "username": "252deadbeef0bf3f34c7ecb810e832f"
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// If you do not know the IP address of your Hue Bridge, simply leave it blank and your Bridge
|
||||||
|
// will be discovered automatically.
|
||||||
|
//
|
||||||
|
// If you do not have a "username" for your Hue API already, simply leave the field blank and
|
||||||
|
// you will be prompted to press the link button on your Hue Bridge before running HomeBridge.
|
||||||
|
// A username will be created for you and printed out, then the server will exit so you may
|
||||||
|
// enter it in your config.json.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// When you attempt to add a device, it will ask for a "PIN code".
|
||||||
|
// The default code for all HomeBridge accessories is 031-45-154.
|
||||||
|
//
|
||||||
|
|
||||||
|
/* jslint node: true */
|
||||||
|
/* globals require: false */
|
||||||
|
/* globals config: false */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var hue = require("node-hue-api"),
|
||||||
|
HueApi = hue.HueApi,
|
||||||
|
lightState = hue.lightState;
|
||||||
|
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
|
||||||
|
function PhilipsHuePlatform(log, config) {
|
||||||
|
this.log = log;
|
||||||
|
this.ip_address = config["ip_address"];
|
||||||
|
this.username = config["username"];
|
||||||
|
}
|
||||||
|
|
||||||
|
function PhilipsHueAccessory(log, device, api) {
|
||||||
|
this.id = device.id;
|
||||||
|
this.name = device.name;
|
||||||
|
this.model = device.modelid;
|
||||||
|
this.device = device;
|
||||||
|
this.api = api;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the ip address of the first available bridge with meethue.com or a network scan.
|
||||||
|
var locateBridge = function (callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// Report the results of the scan to the user
|
||||||
|
var getIp = function (err, bridges) {
|
||||||
|
if (!bridges || bridges.length === 0) {
|
||||||
|
that.log("No Philips Hue bridges found.");
|
||||||
|
callback(err || new Error("No bridges found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bridges.length > 1) {
|
||||||
|
that.log("Warning: Multiple Philips Hue bridges detected. The first bridge will be used automatically. To use a different bridge, set the `ip_address` manually in the configuration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
that.log(
|
||||||
|
"Philips Hue bridges found:\n" +
|
||||||
|
(bridges.map(function (bridge) {
|
||||||
|
// Bridge name is only returned from meethue.com so use id instead if it isn't there
|
||||||
|
return "\t" + bridge.ipaddress + ' - ' + (bridge.name || bridge.id);
|
||||||
|
})).join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
callback(null, bridges[0].ipaddress);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to discover the bridge ip using meethue.com
|
||||||
|
that.log("Attempting to discover Philips Hue bridge with meethue.com...");
|
||||||
|
hue.nupnpSearch(function (locateError, bridges) {
|
||||||
|
if (locateError) {
|
||||||
|
that.log("Philips Hue bridge discovery with meethue.com failed. Register your bridge with the meethue.com for more reliable discovery.");
|
||||||
|
|
||||||
|
that.log("Attempting to discover Philips Hue bridge with network scan...");
|
||||||
|
|
||||||
|
// Timeout after one minute
|
||||||
|
hue.upnpSearch(60000)
|
||||||
|
.then(function (bridges) {
|
||||||
|
that.log("Scan complete");
|
||||||
|
getIp(null, bridges);
|
||||||
|
})
|
||||||
|
.fail(function (scanError) {
|
||||||
|
that.log("Philips Hue bridge discovery with network scan failed. Check your network connection or set ip_address manually in configuration.");
|
||||||
|
getIp(new Error("Scan failed: " + scanError.message));
|
||||||
|
}).done();
|
||||||
|
} else {
|
||||||
|
getIp(null, bridges);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
PhilipsHuePlatform.prototype = {
|
||||||
|
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Philips Hue lights...");
|
||||||
|
var that = this;
|
||||||
|
var getLights = function () {
|
||||||
|
var api = new HueApi(that.ip_address, that.username);
|
||||||
|
|
||||||
|
// Connect to the API
|
||||||
|
// Get a dump of all lights, so as not to hit rate limiting for installations with larger amounts of bulbs
|
||||||
|
|
||||||
|
api.fullState(function(err, response) {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
var foundAccessories = [];
|
||||||
|
for (var deviceId in response.lights) {
|
||||||
|
var device = response.lights[deviceId];
|
||||||
|
device.id = deviceId;
|
||||||
|
var accessory = new PhilipsHueAccessory(that.log, device, api);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
callback(foundAccessories);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new user if needed
|
||||||
|
function checkUsername() {
|
||||||
|
if (!that.username) {
|
||||||
|
var api = new HueApi(that.ip_address);
|
||||||
|
api.createUser(that.ip_address, null, null, function(err, user) {
|
||||||
|
|
||||||
|
// try and help explain this particular error
|
||||||
|
if (err && err.message == "link button not pressed")
|
||||||
|
throw "Please press the link button on your Philips Hue bridge, then start the HomeBridge server within 30 seconds.";
|
||||||
|
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
throw "Created a new username " + JSON.stringify(user) + " for your Philips Hue. Please add it to your config.json then start the HomeBridge server again: ";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
getLights();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discover the bridge if needed
|
||||||
|
if (!this.ip_address) {
|
||||||
|
locateBridge.call(this, function (err, ip_address) {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
// TODO: Find a way to persist this
|
||||||
|
that.ip_address = ip_address;
|
||||||
|
that.log("Save the Philips Hue bridge ip address "+ ip_address +" to your config to skip discovery.");
|
||||||
|
checkUsername();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
checkUsername();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PhilipsHueAccessory.prototype = {
|
||||||
|
// Convert 0-65535 to 0-360
|
||||||
|
hueToArcDegrees: function(value) {
|
||||||
|
value = value/65535;
|
||||||
|
value = value*360;
|
||||||
|
value = Math.round(value);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
// Convert 0-360 to 0-65535
|
||||||
|
arcDegreesToHue: function(value) {
|
||||||
|
value = value/360;
|
||||||
|
value = value*65535;
|
||||||
|
value = Math.round(value);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
// Convert 0-255 to 0-100
|
||||||
|
bitsToPercentage: function(value) {
|
||||||
|
value = value/255;
|
||||||
|
value = value*100;
|
||||||
|
value = Math.round(value);
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
// Create and set a light state
|
||||||
|
executeChange: function(api, device, characteristic, value) {
|
||||||
|
var that = this;
|
||||||
|
var state = lightState.create();
|
||||||
|
switch(characteristic.toLowerCase()) {
|
||||||
|
case 'identify':
|
||||||
|
state.alert('select');
|
||||||
|
break;
|
||||||
|
case 'power':
|
||||||
|
if (value) {
|
||||||
|
state.on();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
state.off();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'hue':
|
||||||
|
state.hue(this.arcDegreesToHue(value));
|
||||||
|
break;
|
||||||
|
case 'brightness':
|
||||||
|
state.brightness(value);
|
||||||
|
break;
|
||||||
|
case 'saturation':
|
||||||
|
state.saturation(value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
api.setLightState(device.id, state, function(err, lights) {
|
||||||
|
if (!err) {
|
||||||
|
that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + ".");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (err.code == "ECONNRESET") {
|
||||||
|
setTimeout(function() {
|
||||||
|
that.executeChange(api, device, characteristic, value);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
that.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Get Services
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var bulb_characteristics = [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.executeChange(that.api, that.device, "power", value);
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: that.device.state.on,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Turn On the Light",
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.executeChange(that.api, that.device, "brightness", value);
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.bitsToPercentage(that.device.state.bri),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// Handle the Hue/Hue Lux divergence
|
||||||
|
if (that.device.state.hasOwnProperty('hue') && that.device.state.hasOwnProperty('sat')) {
|
||||||
|
bulb_characteristics.push({
|
||||||
|
cType: types.HUE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.executeChange(that.api, that.device, "hue", value);
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.hueToArcDegrees(that.device.state.hue),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Hue of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 360,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "arcdegrees"
|
||||||
|
});
|
||||||
|
bulb_characteristics.push({
|
||||||
|
cType: types.SATURATION_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.executeChange(that.api, that.device, "saturation", value);
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.bitsToPercentage(that.device.state.sat),
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Saturation of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var accessory_data = [
|
||||||
|
{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Philips",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.model,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.device.uniqueid,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.executeChange(that.api, that.device, "identify", value);
|
||||||
|
},
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
// `bulb_characteristics` defined based on bulb type
|
||||||
|
characteristics: bulb_characteristics
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return accessory_data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.platform = PhilipsHuePlatform;
|
||||||
242
platforms/SmartThings.js
Normal file
242
platforms/SmartThings.js
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// SmartThings JSON API SmartApp required
|
||||||
|
// https://github.com/jnewland/SmartThings/blob/master/JSON.groovy
|
||||||
|
//
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var request = require("request");
|
||||||
|
|
||||||
|
function SmartThingsPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.app_id = config["app_id"];
|
||||||
|
this.access_token = config["access_token"];
|
||||||
|
}
|
||||||
|
|
||||||
|
SmartThingsPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching SmartThings devices...");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
request.get({
|
||||||
|
url: "https://graph.api.smartthings.com/api/smartapps/installations/"+this.app_id+"/devices?access_token="+this.access_token,
|
||||||
|
json: true
|
||||||
|
}, function(err, response, json) {
|
||||||
|
if (!err && response.statusCode == 200) {
|
||||||
|
if (json['switches'] != undefined) {
|
||||||
|
json['switches'].map(function(s) {
|
||||||
|
accessory = new SmartThingsAccessory(that.log, s.name, s.commands);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (json['hues'] != undefined) {
|
||||||
|
json['hues'].map(function(s) {
|
||||||
|
accessory = new SmartThingsAccessory(that.log, s.name, s.commands);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
callback(foundAccessories);
|
||||||
|
} else {
|
||||||
|
that.log("There was a problem authenticating with SmartThings.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SmartThingsAccessory(log, name, commands) {
|
||||||
|
// device info
|
||||||
|
this.name = name;
|
||||||
|
this.commands = commands;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
SmartThingsAccessory.prototype = {
|
||||||
|
|
||||||
|
command: function(c,value) {
|
||||||
|
this.log(this.name + " sending command " + c);
|
||||||
|
var url = this.commands[c];
|
||||||
|
if (value != undefined) {
|
||||||
|
url = this.commands[c] + "&value="+value
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
request.put({
|
||||||
|
url: url
|
||||||
|
}, function(err, response) {
|
||||||
|
if (err) {
|
||||||
|
that.log("There was a problem sending command " + c + " to" + that.name);
|
||||||
|
that.log(url);
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " sent command " + c);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "SmartThings",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function(that) {
|
||||||
|
cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}]
|
||||||
|
|
||||||
|
if (this.commands['on'] != undefined) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 0) {
|
||||||
|
that.command("off")
|
||||||
|
} else {
|
||||||
|
that.command("on")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.commands['on'] != undefined) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) { that.command("setLevel", value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.commands['setHue'] != undefined) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.HUE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.command("setHue", value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Hue of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 360,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "arcdegrees"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.commands['setSaturation'] != undefined) {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.SATURATION_CTYPE,
|
||||||
|
onUpdate: function(value) { that.command("setSaturation", value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes
|
||||||
|
},
|
||||||
|
|
||||||
|
sType: function() {
|
||||||
|
if (this.commands['setLevel'] != undefined) {
|
||||||
|
return types.LIGHTBULB_STYPE
|
||||||
|
} else {
|
||||||
|
return types.SWITCH_STYPE
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var services = [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: this.informationCharacteristics(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: this.sType(),
|
||||||
|
characteristics: this.controlCharacteristics(that)
|
||||||
|
}];
|
||||||
|
this.log("Loaded services for " + this.name)
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = SmartThingsAccessory;
|
||||||
|
module.exports.platform = SmartThingsPlatform;
|
||||||
188
platforms/Sonos.js
Normal file
188
platforms/Sonos.js
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var sonos = require('sonos');
|
||||||
|
|
||||||
|
function SonosPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.name = config["name"];
|
||||||
|
this.playVolume = config["play_volume"];
|
||||||
|
// timeout for device discovery
|
||||||
|
this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default
|
||||||
|
}
|
||||||
|
|
||||||
|
SonosPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Sonos devices.");
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
// track found devices so we don't add duplicates
|
||||||
|
var roomNamesFound = {};
|
||||||
|
|
||||||
|
// collector array for the devices from callbacks
|
||||||
|
var devicesFound = [];
|
||||||
|
// tell the sonos callbacks if timeout already occured
|
||||||
|
var timeout = false;
|
||||||
|
|
||||||
|
// the timeout event will push the accessories back
|
||||||
|
setTimeout(function(){
|
||||||
|
timeout=true;
|
||||||
|
callback(devicesFound);
|
||||||
|
}, this.discoveryTimeout);
|
||||||
|
|
||||||
|
|
||||||
|
sonos.search(function (device) {
|
||||||
|
that.log("Found device at " + device.host);
|
||||||
|
|
||||||
|
device.deviceDescription(function (err, description) {
|
||||||
|
if (description["zoneType"] != '11' && description["zoneType"] != '8' && description["zoneType"] != '4') { // 8 is the Sonos SUB, 4 is the Sonos Bridge
|
||||||
|
var roomName = description["roomName"];
|
||||||
|
|
||||||
|
if (!roomNamesFound[roomName]) {
|
||||||
|
roomNamesFound[roomName] = true;
|
||||||
|
that.log("Found playable device - " + roomName);
|
||||||
|
if (timeout) {
|
||||||
|
that.log("Ignored: Discovered after timeout (Set deviceDiscoveryTimeout parameter in Sonos section of config.json)");
|
||||||
|
}
|
||||||
|
// device is an instance of sonos.Sonos
|
||||||
|
var accessory = new SonosAccessory(that.log, that.config, device, description);
|
||||||
|
// add it to the collector array
|
||||||
|
devicesFound.push(accessory);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Ignoring playable device with duplicate room name - " + roomName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function SonosAccessory(log, config, device, description) {
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.device = device;
|
||||||
|
this.description = description;
|
||||||
|
|
||||||
|
this.name = this.description["roomName"] + " " + this.config["name"];
|
||||||
|
this.serviceName = this.description["roomName"] + " Speakers";
|
||||||
|
this.playVolume = this.config["play_volume"];
|
||||||
|
}
|
||||||
|
|
||||||
|
SonosAccessory.prototype = {
|
||||||
|
|
||||||
|
setPlaying: function(playing) {
|
||||||
|
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No device found (yet?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (playing) {
|
||||||
|
this.device.play(function(err, success) {
|
||||||
|
that.log("Playback attempt with success: " + success);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.playVolume) {
|
||||||
|
this.device.setVolume(this.playVolume, function(err, success) {
|
||||||
|
if (!err) {
|
||||||
|
that.log("Set volume to " + that.playVolume);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
that.log("Problem setting volume: " + err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.device.stop(function(err, success) {
|
||||||
|
that.log("Stop attempt with success: " + success);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Sonos",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.SWITCH_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.serviceName,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) { that.setPlaying(value); },
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the playback state of the sonos",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = SonosAccessory;
|
||||||
|
module.exports.platform = SonosPlatform;
|
||||||
265
platforms/Telldus.js
Normal file
265
platforms/Telldus.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var telldus = require('telldus');
|
||||||
|
|
||||||
|
function TelldusPlatform(log, config) {
|
||||||
|
var that = this;
|
||||||
|
that.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
TelldusPlatform.prototype = {
|
||||||
|
|
||||||
|
accessories: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.log("Fetching devices...");
|
||||||
|
|
||||||
|
var devices = telldus.getDevicesSync();
|
||||||
|
|
||||||
|
that.log("Found " + devices.length + " devices...");
|
||||||
|
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
// Clean non device
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
if (devices[i].type != 'DEVICE') {
|
||||||
|
devices.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
if (devices[i].type === 'DEVICE') {
|
||||||
|
TelldusAccessory.create(that.log, devices[i], function(err, accessory) {
|
||||||
|
if (!!err) that.log("Couldn't load device info");
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
if (foundAccessories.length >= devices.length) {
|
||||||
|
callback(foundAccessories);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var TelldusAccessory = function TelldusAccessory(log, device) {
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
|
||||||
|
var m = device.model.split(':');
|
||||||
|
|
||||||
|
this.dimTimeout = false;
|
||||||
|
|
||||||
|
// Set accessory info
|
||||||
|
this.device = device;
|
||||||
|
this.id = device.id;
|
||||||
|
this.name = device.name;
|
||||||
|
this.manufacturer = "Telldus"; // NOTE: Change this later
|
||||||
|
this.model = device.model;
|
||||||
|
this.status = device.status;
|
||||||
|
switch (device.status.name) {
|
||||||
|
case 'OFF':
|
||||||
|
this.state = 0;
|
||||||
|
this.stateValue = 0;
|
||||||
|
break;
|
||||||
|
case 'ON':
|
||||||
|
this.state = 2;
|
||||||
|
this.stateValue = 1;
|
||||||
|
break;
|
||||||
|
case 'DIM':
|
||||||
|
this.state = 16;
|
||||||
|
this.stateValue = device.status.level;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TelldusAccessory.create = function (log, device, callback) {
|
||||||
|
|
||||||
|
callback(null, new TelldusAccessory(log, device));
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
TelldusAccessory.prototype = {
|
||||||
|
|
||||||
|
dimmerValue: function() {
|
||||||
|
|
||||||
|
if (this.state === 1) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === 16 && this.stateValue != "unde") {
|
||||||
|
return parseInt(this.stateValue * 100 / 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
informationCharacteristics = [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.manufacturer,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.model,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: function () {
|
||||||
|
telldus.turnOff(that.id, function(err){
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
telldus.turnOn(that.id, function(err){
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
telldus.turnOff(that.id, function(err){
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
telldus.turnOn(that.id, function(err){
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return informationCharacteristics;
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}]
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value) {
|
||||||
|
telldus.turnOn(that.id, function(err){
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message)
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated power state: ON");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
telldus.turnOff(that.id, function(err){
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message)
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated power state: OFF");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != 0)) ? 1 : 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (that.model === "selflearning-dimmer") {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function (value) {
|
||||||
|
if (that.dimTimeout) {
|
||||||
|
clearTimeout(that.dimTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
that.dimTimeout = setTimeout(function(){
|
||||||
|
telldus.dim(that.id, (255 * (value / 100)), function(err, result){
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message);
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated brightness: " + value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
that.dimTimeout = false;
|
||||||
|
}, 250);
|
||||||
|
},
|
||||||
|
perms: ["pw", "pr", "ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.dimmerValue(),
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
var services = [
|
||||||
|
{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: this.informationCharacteristics()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
characteristics: this.controlCharacteristics()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.platform = TelldusPlatform;
|
||||||
|
module.exports.accessory = TelldusAccessory;
|
||||||
265
platforms/TelldusLive.js
Normal file
265
platforms/TelldusLive.js
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var TellduAPI = require("telldus-live");
|
||||||
|
|
||||||
|
function TelldusLivePlatform(log, config) {
|
||||||
|
var that = this;
|
||||||
|
that.log = log;
|
||||||
|
|
||||||
|
that.isLoggedIn = false;
|
||||||
|
|
||||||
|
// Login to Telldus Live!
|
||||||
|
that.cloud = new TellduAPI.TelldusAPI({publicKey: config["public_key"], privateKey: config["private_key"]})
|
||||||
|
.login(config["token"], config["token_secret"], function(err, user) {
|
||||||
|
if (!!err) that.log("Login error: " + err.message);
|
||||||
|
that.log("User logged in: " + user.firstname + " " + user.lastname + ", " + user.email);
|
||||||
|
that.isLoggedIn = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TelldusLivePlatform.prototype = {
|
||||||
|
|
||||||
|
accessories: function(callback) {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
that.log("Fetching devices...");
|
||||||
|
|
||||||
|
that.cloud.getDevices(function(err, devices) {
|
||||||
|
|
||||||
|
if (!!err) return that.log('getDevices: ' + err.message);
|
||||||
|
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
// Clean non device
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
if (devices[i].type != 'device') {
|
||||||
|
devices.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < devices.length; i++) {
|
||||||
|
if (devices[i].type === 'device') {
|
||||||
|
TelldusLiveAccessory.create(that.log, devices[i], that.cloud, function(err, accessory) {
|
||||||
|
if (!!err) that.log("Couldn't load device info");
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
if (foundAccessories.length >= devices.length) {
|
||||||
|
callback(foundAccessories);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var TelldusLiveAccessory = function TelldusLiveAccessory(log, cloud, device) {
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
this.cloud = cloud;
|
||||||
|
|
||||||
|
var m = device.model ? device.model.split(':') : ['unknown', 'unknown'] ;
|
||||||
|
|
||||||
|
// Set accessory info
|
||||||
|
this.device = device;
|
||||||
|
this.id = device.id;
|
||||||
|
this.name = device.name;
|
||||||
|
this.manufacturer = m[1];
|
||||||
|
this.model = m[0];
|
||||||
|
this.state = device.state;
|
||||||
|
this.stateValue = device.stateValue;
|
||||||
|
this.status = device.status;
|
||||||
|
};
|
||||||
|
|
||||||
|
TelldusLiveAccessory.create = function (log, device, cloud, callback) {
|
||||||
|
|
||||||
|
cloud.getDeviceInfo(device, function(err, device) {
|
||||||
|
|
||||||
|
if (!!err) that.log("Couldn't load device info");
|
||||||
|
|
||||||
|
callback(err, new TelldusLiveAccessory(log, cloud, device));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
TelldusLiveAccessory.prototype = {
|
||||||
|
|
||||||
|
dimmerValue: function() {
|
||||||
|
|
||||||
|
if (this.state === 1) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state === 16 && this.stateValue != "unde") {
|
||||||
|
return parseInt(this.stateValue * 100 / 255);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
informationCharacteristics: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
informationCharacteristics = [
|
||||||
|
{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.manufacturer,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.model,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: function () {
|
||||||
|
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
that.cloud.onOffDevice(that.device, false, function(err, result) {
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
that.cloud.onOffDevice(that.device, false, function(err, result) {
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
that.cloud.onOffDevice(that.device, true, function(err, result) {
|
||||||
|
if (!!err) that.log("Error: " + err.message);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return informationCharacteristics;
|
||||||
|
},
|
||||||
|
|
||||||
|
controlCharacteristics: function() {
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
cTypes = [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: that.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
}]
|
||||||
|
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
if (value == 1) {
|
||||||
|
that.cloud.onOffDevice(that.device, value, function(err, result) {
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message)
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated power state: " + (value === true ? 'ON' : 'OFF'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
that.cloud.onOffDevice(that.device, value, function(err, result) {
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message)
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated power state: " + (value === true ? 'ON' : 'OFF'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != "0")) ? 1 : 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state",
|
||||||
|
designedMaxLength: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (that.model === "selflearning-dimmer") {
|
||||||
|
cTypes.push({
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function (value) {
|
||||||
|
that.cloud.dimDevice(that.device, (255 * (value / 100)), function (err, result) {
|
||||||
|
if (!!err) {
|
||||||
|
that.log("Error: " + err.message);
|
||||||
|
} else {
|
||||||
|
that.log(that.name + " - Updated brightness: " + value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw", "pr", "ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: that.dimmerValue(),
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cTypes
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
|
||||||
|
var services = [
|
||||||
|
{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: this.informationCharacteristics()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
characteristics: this.controlCharacteristics()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.platform = TelldusLivePlatform;
|
||||||
|
module.exports.accessory = TelldusLiveAccessory;
|
||||||
252
platforms/Wink.js
Normal file
252
platforms/Wink.js
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var wink = require('wink-js');
|
||||||
|
|
||||||
|
var model = {
|
||||||
|
light_bulbs: require('wink-js/lib/model/light')
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function WinkPlatform(log, config){
|
||||||
|
|
||||||
|
// auth info
|
||||||
|
this.client_id = config["client_id"];
|
||||||
|
this.client_secret = config["client_secret"];
|
||||||
|
this.username = config["username"];
|
||||||
|
this.password = config["password"];
|
||||||
|
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
WinkPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Fetching Wink devices.");
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
var foundAccessories = [];
|
||||||
|
|
||||||
|
wink.init({
|
||||||
|
"client_id": this.client_id,
|
||||||
|
"client_secret": this.client_secret,
|
||||||
|
"username": this.username,
|
||||||
|
"password": this.password
|
||||||
|
}, function(auth_return) {
|
||||||
|
if ( auth_return === undefined ) {
|
||||||
|
that.log("There was a problem authenticating with Wink.");
|
||||||
|
} else {
|
||||||
|
// success
|
||||||
|
wink.user().devices('light_bulbs', function(devices) {
|
||||||
|
for (var i=0; i<devices.data.length; i++){
|
||||||
|
device = model.light_bulbs(devices.data[i], wink)
|
||||||
|
accessory = new WinkAccessory(that.log, device);
|
||||||
|
foundAccessories.push(accessory);
|
||||||
|
}
|
||||||
|
callback(foundAccessories);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function WinkAccessory(log, device) {
|
||||||
|
// device info
|
||||||
|
this.name = device.name;
|
||||||
|
this.device = device;
|
||||||
|
this.log = log;
|
||||||
|
}
|
||||||
|
|
||||||
|
WinkAccessory.prototype = {
|
||||||
|
getPowerState: function(callback){
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '"+this.name+"' device found (yet?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("checking power state for: " + this.name);
|
||||||
|
wink.user().device(this.name, function(light_obj){
|
||||||
|
powerState = light_obj.desired_state.powered
|
||||||
|
that.log("power state for " + that.name + " is: " + powerState)
|
||||||
|
callback(powerState);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getBrightness: function(callback){
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '"+this.name+"' device found (yet?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("checking brightness level for: " + this.name);
|
||||||
|
wink.user().device(this.name, function(light_obj){
|
||||||
|
level = light_obj.desired_state.brightness * 100
|
||||||
|
that.log("brightness level for " + that.name + " is: " + level)
|
||||||
|
callback(level);
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setPowerState: function(powerOn) {
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '"+this.name+"' device found (yet?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to on");
|
||||||
|
this.device.power.on(function(response) {
|
||||||
|
if (response === undefined) {
|
||||||
|
that.log("Error setting power state on the '"+that.name+"'")
|
||||||
|
} else {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to on");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
this.log("Setting power state on the '"+this.name+"' to off");
|
||||||
|
this.device.power.off(function(response) {
|
||||||
|
if (response === undefined) {
|
||||||
|
that.log("Error setting power state on the '"+that.name+"'")
|
||||||
|
} else {
|
||||||
|
that.log("Successfully set power state on the '"+that.name+"' to off");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level) {
|
||||||
|
if (!this.device) {
|
||||||
|
this.log("No '"+this.name+"' device found (yet?)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
this.log("Setting brightness on the '"+this.name+"' to " + level);
|
||||||
|
this.device.brightness(level, function(response) {
|
||||||
|
if (response === undefined) {
|
||||||
|
that.log("Error setting brightness on the '"+that.name+"'")
|
||||||
|
} else {
|
||||||
|
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
return [{
|
||||||
|
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of the accessory",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MANUFACTURER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Wink",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Manufacturer",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.MODEL_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "Rev-1",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Model",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.SERIAL_NUMBER_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: "A1S2NASF88EW",
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "SN",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.IDENTIFY_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pw"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: false,
|
||||||
|
supportEvents: false,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Identify Accessory",
|
||||||
|
designedMaxLength: 1
|
||||||
|
}]
|
||||||
|
},{
|
||||||
|
sType: types.LIGHTBULB_STYPE,
|
||||||
|
characteristics: [{
|
||||||
|
cType: types.NAME_CTYPE,
|
||||||
|
onUpdate: null,
|
||||||
|
perms: ["pr"],
|
||||||
|
format: "string",
|
||||||
|
initialValue: this.name,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Name of service",
|
||||||
|
designedMaxLength: 255
|
||||||
|
},{
|
||||||
|
cType: types.POWER_STATE_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setPowerState(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getPowerState(function(powerState){
|
||||||
|
callback(powerState);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "bool",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Change the power state of the Bulb",
|
||||||
|
designedMaxLength: 1
|
||||||
|
},{
|
||||||
|
cType: types.BRIGHTNESS_CTYPE,
|
||||||
|
onUpdate: function(value) {
|
||||||
|
that.setBrightness(value);
|
||||||
|
},
|
||||||
|
onRead: function(callback) {
|
||||||
|
that.getBrightness(function(level){
|
||||||
|
callback(level);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
perms: ["pw","pr","ev"],
|
||||||
|
format: "int",
|
||||||
|
initialValue: 0,
|
||||||
|
supportEvents: true,
|
||||||
|
supportBonjour: false,
|
||||||
|
manfDescription: "Adjust Brightness of Light",
|
||||||
|
designedMinValue: 0,
|
||||||
|
designedMaxValue: 100,
|
||||||
|
designedMinStep: 1,
|
||||||
|
unit: "%"
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = WinkAccessory;
|
||||||
|
module.exports.platform = WinkPlatform;
|
||||||
238
platforms/YamahaAVR.js
Normal file
238
platforms/YamahaAVR.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var inherits = require('util').inherits;
|
||||||
|
var debug = require('debug')('YamahaAVR');
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var Yamaha = require('yamaha-nodejs');
|
||||||
|
var Q = require('q');
|
||||||
|
var mdns = require('mdns');
|
||||||
|
//workaround for raspberry pi
|
||||||
|
var sequence = [
|
||||||
|
mdns.rst.DNSServiceResolve(),
|
||||||
|
'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]}),
|
||||||
|
mdns.rst.makeAddressesUnique()
|
||||||
|
];
|
||||||
|
|
||||||
|
function YamahaAVRPlatform(log, config){
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.playVolume = config["play_volume"];
|
||||||
|
this.minVolume = config["min_volume"] || -50.0;
|
||||||
|
this.maxVolume = config["max_volume"] || -20.0;
|
||||||
|
this.gapVolume = this.maxVolume - this.minVolume;
|
||||||
|
this.setMainInputTo = config["setMainInputTo"];
|
||||||
|
this.expectedDevices = config["expected_devices"] || 100;
|
||||||
|
this.discoveryTimeout = config["discovery_timeout"] || 30;
|
||||||
|
this.manualAddresses = config["manual_addresses"] || {};
|
||||||
|
this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Characteristics and service...
|
||||||
|
|
||||||
|
YamahaAVRPlatform.AudioVolume = function() {
|
||||||
|
Characteristic.call(this, 'Audio Volume', '00001001-0000-1000-8000-135D67EC4377');
|
||||||
|
this.setProps({
|
||||||
|
format: Characteristic.Formats.UINT8,
|
||||||
|
unit: Characteristic.Units.PERCENTAGE,
|
||||||
|
maxValue: 100,
|
||||||
|
minValue: 0,
|
||||||
|
minStep: 1,
|
||||||
|
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||||
|
});
|
||||||
|
this.value = this.getDefaultValue();
|
||||||
|
};
|
||||||
|
inherits(YamahaAVRPlatform.AudioVolume, Characteristic);
|
||||||
|
|
||||||
|
YamahaAVRPlatform.Muting = function() {
|
||||||
|
Characteristic.call(this, 'Muting', '00001002-0000-1000-8000-135D67EC4377');
|
||||||
|
this.setProps({
|
||||||
|
format: Characteristic.Formats.UINT8,
|
||||||
|
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||||
|
});
|
||||||
|
this.value = this.getDefaultValue();
|
||||||
|
};
|
||||||
|
inherits(YamahaAVRPlatform.Muting, Characteristic);
|
||||||
|
|
||||||
|
YamahaAVRPlatform.AudioDeviceService = function(displayName, subtype) {
|
||||||
|
Service.call(this, displayName, '00000001-0000-1000-8000-135D67EC4377', subtype);
|
||||||
|
|
||||||
|
// Required Characteristics
|
||||||
|
this.addCharacteristic(YamahaAVRPlatform.AudioVolume);
|
||||||
|
|
||||||
|
// Optional Characteristics
|
||||||
|
this.addOptionalCharacteristic(YamahaAVRPlatform.Muting);
|
||||||
|
};
|
||||||
|
inherits(YamahaAVRPlatform.AudioDeviceService, Service);
|
||||||
|
|
||||||
|
|
||||||
|
YamahaAVRPlatform.prototype = {
|
||||||
|
accessories: function(callback) {
|
||||||
|
this.log("Getting Yamaha AVR devices.");
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
var browser = this.browser;
|
||||||
|
browser.stop();
|
||||||
|
browser.removeAllListeners('serviceUp'); // cleanup listeners
|
||||||
|
var accessories = [];
|
||||||
|
var timer, timeElapsed = 0, checkCyclePeriod = 5000;
|
||||||
|
|
||||||
|
// Hmm... seems we need to prevent double-listing via manual and Bonjour...
|
||||||
|
var sysIds = {};
|
||||||
|
|
||||||
|
var setupFromService = function(service){
|
||||||
|
var name = service.name;
|
||||||
|
//console.log('Found HTTP service "' + name + '"');
|
||||||
|
// We can't tell just from mdns if this is an AVR...
|
||||||
|
if (service.port != 80) return; // yamaha-nodejs assumes this, so finding one on another port wouldn't do any good anyway.
|
||||||
|
var yamaha = new Yamaha(service.host);
|
||||||
|
yamaha.getSystemConfig().then(
|
||||||
|
function(sysConfig){
|
||||||
|
var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0];
|
||||||
|
var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0];
|
||||||
|
if(sysIds[sysId]){
|
||||||
|
this.log("WARN: Got multiple systems with ID " + sysId + "! Omitting duplicate!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sysIds[sysId] = true;
|
||||||
|
this.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\"");
|
||||||
|
var accessory = new YamahaAVRAccessory(this.log, this.config, name, yamaha, sysConfig);
|
||||||
|
accessories.push(accessory);
|
||||||
|
if(accessories.length >= this.expectedDevices)
|
||||||
|
timeoutFunction(); // We're done, call the timeout function now.
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
}.bind(this);
|
||||||
|
|
||||||
|
// process manually specified devices...
|
||||||
|
for(var key in this.manualAddresses){
|
||||||
|
if(!this.manualAddresses.hasOwnProperty(key)) continue;
|
||||||
|
setupFromService({
|
||||||
|
name: key,
|
||||||
|
host: this.manualAddresses[key],
|
||||||
|
port: 80
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.on('serviceUp', setupFromService);
|
||||||
|
browser.start();
|
||||||
|
|
||||||
|
// The callback can only be called once...so we'll have to find as many as we can
|
||||||
|
// in a fixed time and then call them in.
|
||||||
|
var timeoutFunction = function(){
|
||||||
|
if(accessories.length >= that.expectedDevices){
|
||||||
|
clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
timeElapsed += checkCyclePeriod;
|
||||||
|
if(timeElapsed > that.discoveryTimeout * 1000){
|
||||||
|
that.log("Waited " + that.discoveryTimeout + " seconds, stopping discovery.");
|
||||||
|
} else {
|
||||||
|
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
browser.stop();
|
||||||
|
browser.removeAllListeners('serviceUp');
|
||||||
|
that.log("Discovery finished, found " + accessories.length + " Yamaha AVR devices.");
|
||||||
|
callback(accessories);
|
||||||
|
};
|
||||||
|
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function YamahaAVRAccessory(log, config, name, yamaha, sysConfig) {
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.yamaha = yamaha;
|
||||||
|
this.sysConfig = sysConfig;
|
||||||
|
|
||||||
|
this.nameSuffix = config["name_suffix"] || " Speakers";
|
||||||
|
this.name = name;
|
||||||
|
this.serviceName = name + this.nameSuffix;
|
||||||
|
this.setMainInputTo = config["setMainInputTo"];
|
||||||
|
this.playVolume = this.config["play_volume"];
|
||||||
|
this.minVolume = config["min_volume"] || -50.0;
|
||||||
|
this.maxVolume = config["max_volume"] || -20.0;
|
||||||
|
this.gapVolume = this.maxVolume - this.minVolume;
|
||||||
|
}
|
||||||
|
|
||||||
|
YamahaAVRAccessory.prototype = {
|
||||||
|
|
||||||
|
setPlaying: function(playing) {
|
||||||
|
var that = this;
|
||||||
|
var yamaha = this.yamaha;
|
||||||
|
|
||||||
|
if (playing) {
|
||||||
|
|
||||||
|
return yamaha.powerOn().then(function(){
|
||||||
|
if (that.playVolume) return yamaha.setVolumeTo(that.playVolume*10);
|
||||||
|
else return Q();
|
||||||
|
}).then(function(){
|
||||||
|
if (that.setMainInputTo) return yamaha.setMainInputTo(that.setMainInputTo);
|
||||||
|
else return Q();
|
||||||
|
}).then(function(){
|
||||||
|
if (that.setMainInputTo == "AirPlay") return yamaha.SendXMLToReceiver(
|
||||||
|
'<YAMAHA_AV cmd="PUT"><AirPlay><Play_Control><Playback>Play</Playback></Play_Control></AirPlay></YAMAHA_AV>'
|
||||||
|
);
|
||||||
|
else return Q();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return yamaha.powerOff();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var that = this;
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
var yamaha = this.yamaha;
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Name, this.name)
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "Yamaha")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0])
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0]);
|
||||||
|
|
||||||
|
var switchService = new Service.Switch("Power State");
|
||||||
|
switchService.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', function(callback, context){
|
||||||
|
yamaha.isOn().then(function(result){
|
||||||
|
callback(false, result);
|
||||||
|
}.bind(this));
|
||||||
|
}.bind(this))
|
||||||
|
.on('set', function(powerOn, callback){
|
||||||
|
this.setPlaying(powerOn).then(function(){
|
||||||
|
callback(false, powerOn);
|
||||||
|
}, function(error){
|
||||||
|
callback(error, !powerOn); //TODO: Actually determine and send real new status.
|
||||||
|
});
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
var audioDeviceService = new YamahaAVRPlatform.AudioDeviceService("Audio Functions");
|
||||||
|
audioDeviceService.getCharacteristic(YamahaAVRPlatform.AudioVolume)
|
||||||
|
.on('get', function(callback, context){
|
||||||
|
yamaha.getBasicInfo().done(function(basicInfo){
|
||||||
|
var v = basicInfo.getVolume()/10.0;
|
||||||
|
var p = 100 * ((v - that.minVolume) / that.gapVolume);
|
||||||
|
p = p < 0 ? 0 : p > 100 ? 100 : Math.round(p);
|
||||||
|
debug("Got volume percent of " + p + "%");
|
||||||
|
callback(false, p);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('set', function(p, callback){
|
||||||
|
var v = ((p / 100) * that.gapVolume) + that.minVolume;
|
||||||
|
v = Math.round(v*10.0);
|
||||||
|
debug("Setting volume to " + v);
|
||||||
|
yamaha.setVolumeTo(v).then(function(){
|
||||||
|
callback(false, p);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.getValue(null, null); // force an asynchronous get
|
||||||
|
|
||||||
|
|
||||||
|
return [informationService, switchService, audioDeviceService];
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.accessory = YamahaAVRAccessory;
|
||||||
|
module.exports.platform = YamahaAVRPlatform;
|
||||||
1012
platforms/ZWayServer.js
Normal file
1012
platforms/ZWayServer.js
Normal file
File diff suppressed because it is too large
Load Diff
763
platforms/isy-js.js
Normal file
763
platforms/isy-js.js
Normal file
@@ -0,0 +1,763 @@
|
|||||||
|
/*
|
||||||
|
ISY-JS
|
||||||
|
|
||||||
|
ISY-99 REST / WebSockets based HomeBridge shim.
|
||||||
|
|
||||||
|
Supports the following Insteon devices: Lights (dimmable and non-dimmable), Fans, Outlets, Door/Window Sensors, MorningLinc locks, Inline Lincs and I/O Lincs.
|
||||||
|
Also supports ZWave based locks. If elkEnabled is set to true then this will also expose your Elk Alarm Panel and all of your Elk Sensors.
|
||||||
|
|
||||||
|
Turns out that HomeBridge platforms can only return a maximum of 100 devices. So if you end up exposing more then 100 devices through HomeBridge the HomeKit
|
||||||
|
software will fail adding the HomeBridge to your HomeKit network. To address this issue this platform provides an option to screen out devices based on
|
||||||
|
criteria specified in the config.
|
||||||
|
|
||||||
|
Configuration sample:
|
||||||
|
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"platform": "isy-js",
|
||||||
|
"name": "isy-js",
|
||||||
|
"host": "10.0.1.12",
|
||||||
|
"username": "admin",
|
||||||
|
"password": "password",
|
||||||
|
"elkEnabled": true,
|
||||||
|
"ignoreDevices": [
|
||||||
|
{ "nameContains": "ApplianceLinc", "lastAddressDigit": "", "address": ""},
|
||||||
|
{ "nameContains": "Bedroom.Side Gate", "lastAddressDigit": "", "address": ""},
|
||||||
|
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" },
|
||||||
|
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
"platform" - Must be set to isy-js
|
||||||
|
"name" - Can be set to whatever you want
|
||||||
|
"host" - IP address of the ISY
|
||||||
|
"username" - Your ISY username
|
||||||
|
"password" - Your ISY password
|
||||||
|
"elkEnabled" - true if there is an elk alarm panel connected to your ISY
|
||||||
|
"ignoreDevices" - Array of objects specifying criteria for screening out devices from the network. nameContains is the only required criteria. If the other criteria
|
||||||
|
are blank all devices will match those criteria (providing they match the name criteria).
|
||||||
|
"nameContains" - Specifies a substring to check against the names of the ISY devices. Required field for the criteria.
|
||||||
|
"lastAddressDigit" - Specifies a single digit in the ISY address of a device which should be used to match the device. Example use of this is for composite
|
||||||
|
devices like keypads so you can screen out the non-main buttons.
|
||||||
|
"address" - ISY address to match.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" } - Ignore all devices which have the word Keypad in their name and whose last address digit is 2.
|
||||||
|
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" } - Ignore all devices which have the word Remote in their name
|
||||||
|
{ "nameContains": "", "lastAddressDigit": "", "address": "15 5 3 2"} - Ignore the device with an ISY address of 15 5 3 2.
|
||||||
|
|
||||||
|
TODOS: Implement identify functions (beep perhaps?) and more device types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
var types = require("hap-nodejs/accessories/types.js");
|
||||||
|
var isy = require('isy-js');
|
||||||
|
var Service = require("hap-nodejs").Service;
|
||||||
|
var Characteristic = require("hap-nodejs").Characteristic;
|
||||||
|
var inherits = require('util').inherits;
|
||||||
|
|
||||||
|
// Global device map. Needed to map incoming notifications to the corresponding HomeKit device for update.
|
||||||
|
var deviceMap = {};
|
||||||
|
|
||||||
|
// This function responds to changes in devices from the isy-js library. Uses the global device map to update
|
||||||
|
// the state.
|
||||||
|
// TODO: Move this to a member function of the ISYPlatform object so we don't need a global map.
|
||||||
|
function ISYChangeHandler(isy,device) {
|
||||||
|
var deviceToUpdate = deviceMap[device.address];
|
||||||
|
if(deviceToUpdate != null) {
|
||||||
|
deviceToUpdate.handleExternalChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to have ISYJSDEBUG control if debug output appears
|
||||||
|
function ISYJSDebugMessage(isy,message) {
|
||||||
|
if(process.env.ISYJSDEBUG != undefined) {
|
||||||
|
isy.log(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// PLATFORM
|
||||||
|
|
||||||
|
// Construct the ISY platform. log = Logger, config = homebridge cofnig
|
||||||
|
function ISYPlatform(log,config) {
|
||||||
|
this.log = log;
|
||||||
|
this.config = config;
|
||||||
|
this.host = config.host;
|
||||||
|
this.username = config.username;
|
||||||
|
this.password = config.password;
|
||||||
|
this.elkEnabled = config.elkEnabled;
|
||||||
|
this.isy = new isy.ISY(this.host, this.username,this.password, config.elkEnabled, ISYChangeHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks the device against the configuration to see if it should be ignored.
|
||||||
|
ISYPlatform.prototype.shouldIgnore = function(device) {
|
||||||
|
var deviceAddress = device.address;
|
||||||
|
var deviceName = device.name;
|
||||||
|
for(var index = 0; index < this.config.ignoreDevices.length; index++) {
|
||||||
|
var rule = this.config.ignoreDevices[index];
|
||||||
|
if(rule.nameContains != "") {
|
||||||
|
if(deviceName.indexOf(rule.nameContains) == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(rule.lastAddressDigit != "") {
|
||||||
|
if(deviceAddress.indexOf(rule.lastAddressDigit,deviceAddress.length-2) == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(rule.address != "") {
|
||||||
|
if(deviceAddress != rule.address) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ISYJSDebugMessage(this,"Ignoring device: "+deviceName+" ["+deviceAddress+"] because of rule ["+rule.nameContains+"] ["+rule.lastAddressDigit+"] ["+rule.address+"]");
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calls the isy-js library, retrieves the list of devices, and maps them to appropriate ISYXXXXAccessory devices.
|
||||||
|
ISYPlatform.prototype.accessories = function(callback) {
|
||||||
|
var that = this;
|
||||||
|
this.isy.initialize(function() {
|
||||||
|
var results = [];
|
||||||
|
var deviceList = that.isy.getDeviceList();
|
||||||
|
for(var index = 0; index < deviceList.length; index++) {
|
||||||
|
var device = deviceList[index];
|
||||||
|
var homeKitDevice = null;
|
||||||
|
if(!that.shouldIgnore(device)) {
|
||||||
|
|
||||||
|
if(device.deviceType == that.isy.DEVICE_TYPE_LIGHT || device.deviceType == that.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||||
|
homeKitDevice = new ISYLightAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_LOCK || device.deviceType == that.isy.DEVICE_TYPE_SECURE_LOCK) {
|
||||||
|
homeKitDevice = new ISYLockAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_OUTLET) {
|
||||||
|
homeKitDevice = new ISYOutletAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_FAN) {
|
||||||
|
homeKitDevice = new ISYFanAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_DOOR_WINDOW_SENSOR) {
|
||||||
|
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_DOOR_WINDOW_SENSOR) {
|
||||||
|
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||||
|
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_PANEL) {
|
||||||
|
homeKitDevice = new ISYElkAlarmPanelAccessory(that.log,device);
|
||||||
|
}
|
||||||
|
if(homeKitDevice != null) {
|
||||||
|
// Make sure the device is address to the global map
|
||||||
|
deviceMap[device.address] = homeKitDevice;
|
||||||
|
results.push(homeKitDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(that.isy.elkEnabled) {
|
||||||
|
var panelDevice = that.isy.getElkAlarmPanel();
|
||||||
|
var panelDeviceHK = new ISYElkAlarmPanelAccessory(that.log,panelDevice);
|
||||||
|
deviceMap[panelDevice.address] = panelDeviceHK;
|
||||||
|
results.push(panelDeviceHK);
|
||||||
|
}
|
||||||
|
ISYJSDebugMessage(that,"Filtered device has: "+results.length+" devices");
|
||||||
|
callback(results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// BASE FOR ALL DEVICES
|
||||||
|
|
||||||
|
// Provides common constructor tasks
|
||||||
|
function ISYAccessoryBaseSetup(accessory,log,device) {
|
||||||
|
accessory.log = log;
|
||||||
|
accessory.device = device;
|
||||||
|
accessory.address = device.address;
|
||||||
|
accessory.name = device.name;
|
||||||
|
accessory.uuid_base = device.isy.address+":"+device.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// FANS - ISYFanAccessory
|
||||||
|
// Implemetnts the fan service for an isy fan device.
|
||||||
|
|
||||||
|
// Constructs a fan accessory object. device is the isy-js device object and log is the logger.
|
||||||
|
function ISYFanAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
}
|
||||||
|
|
||||||
|
ISYFanAccessory.prototype.identify = function(callback) {
|
||||||
|
// Do the identify action
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates the fan speed as an isy-js string into the corresponding homekit constant level.
|
||||||
|
// Homekit doesn't have steps for the fan speed and needs to have a value from 0 to 100. We
|
||||||
|
// split the range into 4 steps and map them to the 4 isy-js levels.
|
||||||
|
ISYFanAccessory.prototype.translateFanSpeedToHK = function(fanSpeed) {
|
||||||
|
if(fanSpeed == this.device.FAN_LEVEL_OFF) {
|
||||||
|
return 0;
|
||||||
|
} else if(fanSpeed == this.device.FAN_LEVEL_LOW) {
|
||||||
|
return 32;
|
||||||
|
} else if(fanSpeed == this.device.FAN_LEVEL_MEDIUM) {
|
||||||
|
return 67;
|
||||||
|
} else if(fanSpeed == this.device.FAN_LEVEL_HIGH) {
|
||||||
|
return 100;
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"!!!! ERROR: Unknown fan speed: "+fanSpeed);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates the fan level from homebridge into the isy-js level. Maps from the 0-100
|
||||||
|
// to the four isy-js fan speed levels.
|
||||||
|
ISYFanAccessory.prototype.translateHKToFanSpeed = function(fanStateHK) {
|
||||||
|
if(fanStateHK == 0) {
|
||||||
|
return this.device.FAN_LEVEL_OFF;
|
||||||
|
} else if(fanStateHK > 0 && fanStateHK <=32) {
|
||||||
|
return this.device.FAN_LEVEL_LOW;
|
||||||
|
} else if(fanStateHK >= 33 && fanStateHK <= 67) {
|
||||||
|
return this.device.FAN_LEVEL_MEDIUM;
|
||||||
|
} else if(fanStateHK > 67) {
|
||||||
|
return this.device.FAN_LEVEL_HIGH;
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"ERROR: Unknown fan state!");
|
||||||
|
return this.device.FAN_LEVEL_OFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the current state of the fan from the isy-js level to the 0-100 level of HK.
|
||||||
|
ISYFanAccessory.prototype.getFanRotationSpeed = function(callback) {
|
||||||
|
callback(null,this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the current state of the fan from the 0-100 level of HK to the isy-js level.
|
||||||
|
ISYFanAccessory.prototype.setFanRotationSpeed = function(fanStateHK,callback) {
|
||||||
|
var newFanState = this.translateHKToFanSpeed(fanStateHK);
|
||||||
|
ISYJSDebugMessage(this,"Sending command to set fan state to: "+newFanState);
|
||||||
|
if(newFanState != this.device.getCurrentFanState()) {
|
||||||
|
this.device.sendFanCommand(newFanState, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Fan command does not change actual speed");
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the fan is on
|
||||||
|
ISYFanAccessory.prototype.getIsFanOn = function() {
|
||||||
|
return (this.device.getCurrentFanState() != "Off");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the state of the fan to the homebridge system for the On characteristic
|
||||||
|
ISYFanAccessory.prototype.getFanOnState = function(callback) {
|
||||||
|
callback(null,this.getIsFanOn());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the fan state based on the value of the On characteristic. Default to Medium for on.
|
||||||
|
ISYFanAccessory.prototype.setFanOnState = function(onState,callback) {
|
||||||
|
if(onState != this.getIsFanOn()) {
|
||||||
|
if(onState) {
|
||||||
|
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_MEDIUM), callback);
|
||||||
|
} else {
|
||||||
|
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_OFF), callback);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Fan command does not change actual state");
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYFanAccessory.prototype.handleExternalChange = function() {
|
||||||
|
this.fanService
|
||||||
|
.setCharacteristic(Characteristic.On, this.getIsFanOn());
|
||||||
|
|
||||||
|
this.fanService
|
||||||
|
.setCharacteristic(Characteristic.RotationSpeed, this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the services supported by the fan device.
|
||||||
|
ISYFanAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var fanService = new Service.Fan();
|
||||||
|
|
||||||
|
this.fanService = fanService;
|
||||||
|
this.informationService = informationService;
|
||||||
|
|
||||||
|
fanService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setFanOnState.bind(this));
|
||||||
|
|
||||||
|
fanService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getFanOnState.bind(this));
|
||||||
|
|
||||||
|
fanService
|
||||||
|
.addCharacteristic(new Characteristic.RotationSpeed())
|
||||||
|
.on('get', this.getFanRotationSpeed.bind(this));
|
||||||
|
|
||||||
|
fanService
|
||||||
|
.getCharacteristic(Characteristic.RotationSpeed)
|
||||||
|
.on('set', this.setFanRotationSpeed.bind(this));
|
||||||
|
|
||||||
|
return [informationService, fanService];
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// OUTLETS - ISYOutletAccessory
|
||||||
|
// Implements the Outlet service for ISY devices.
|
||||||
|
|
||||||
|
// Constructs an outlet. log = HomeBridge logger, device = isy-js device to wrap
|
||||||
|
function ISYOutletAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the identify command
|
||||||
|
ISYOutletAccessory.prototype.identify = function(callback) {
|
||||||
|
// Do the identify action
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a request to set the outlet state. Ignores redundant sets based on current states.
|
||||||
|
ISYOutletAccessory.prototype.setOutletState = function(outletState,callback) {
|
||||||
|
ISYJSDebugMessage(this,"Sending command to set outlet state to: "+outletState);
|
||||||
|
if(outletState != this.device.getCurrentOutletState()) {
|
||||||
|
this.device.sendOutletCommand(outletState, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a request to get the current outlet state based on underlying isy-js device object.
|
||||||
|
ISYOutletAccessory.prototype.getOutletState = function(callback) {
|
||||||
|
callback(null,this.device.getCurrentOutletState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a request to get the current in use state of the outlet. We set this to true always as
|
||||||
|
// there is no way to deterine this through the isy.
|
||||||
|
ISYOutletAccessory.prototype.getOutletInUseState = function(callback) {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYOutletAccessory.prototype.handleExternalChange = function() {
|
||||||
|
this.outletService
|
||||||
|
.setCharacteristic(Characteristic.On, this.device.getCurrentOutletState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of services supported by this object.
|
||||||
|
ISYOutletAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var outletService = new Service.Outlet();
|
||||||
|
|
||||||
|
this.outletService = outletService;
|
||||||
|
this.informationService = informationService;
|
||||||
|
|
||||||
|
outletService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setOutletState.bind(this));
|
||||||
|
|
||||||
|
outletService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getOutletState.bind(this));
|
||||||
|
|
||||||
|
outletService
|
||||||
|
.getCharacteristic(Characteristic.OutletInUse)
|
||||||
|
.on('get', this.getOutletInUseState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, outletService];
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// LOCKS - ISYLockAccessory
|
||||||
|
// Implements the lock service for isy-js devices.
|
||||||
|
|
||||||
|
// Constructs a lock accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||||
|
function ISYLockAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles an identify request
|
||||||
|
ISYLockAccessory.prototype.identify = function(callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a set to the target lock state. Will ignore redundant commands.
|
||||||
|
ISYLockAccessory.prototype.setTargetLockState = function(lockState,callback) {
|
||||||
|
ISYJSDebugMessage(this,"Sending command to set lock state to: "+lockState);
|
||||||
|
if(lockState != this.getDeviceCurrentStateAsHK()) {
|
||||||
|
var targetLockValue = (lockState == 0) ? false : true;
|
||||||
|
this.device.sendLockCommand(targetLockValue, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates underlying lock state into the corresponding homekit state
|
||||||
|
ISYLockAccessory.prototype.getDeviceCurrentStateAsHK = function() {
|
||||||
|
return (this.device.getCurrentLockState() ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to get the current lock state for homekit
|
||||||
|
ISYLockAccessory.prototype.getLockCurrentState = function(callback) {
|
||||||
|
callback(null, this.getDeviceCurrentStateAsHK());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to get the target lock state for homekit
|
||||||
|
ISYLockAccessory.prototype.getTargetLockState = function(callback) {
|
||||||
|
this.getLockCurrentState(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYLockAccessory.prototype.handleExternalChange = function() {
|
||||||
|
this.lockService
|
||||||
|
.setCharacteristic(Characteristic.LockTargetState, this.getDeviceCurrentStateAsHK());
|
||||||
|
this.lockService
|
||||||
|
.setCharacteristic(Characteristic.LockCurrentState, this.getDeviceCurrentStateAsHK());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of services supported by this object.
|
||||||
|
ISYLockAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var lockMechanismService = new Service.LockMechanism();
|
||||||
|
|
||||||
|
this.lockService = lockMechanismService;
|
||||||
|
this.informationService = informationService;
|
||||||
|
|
||||||
|
lockMechanismService
|
||||||
|
.getCharacteristic(Characteristic.LockTargetState)
|
||||||
|
.on('set', this.setTargetLockState.bind(this));
|
||||||
|
|
||||||
|
lockMechanismService
|
||||||
|
.getCharacteristic(Characteristic.LockTargetState)
|
||||||
|
.on('get', this.getTargetLockState.bind(this));
|
||||||
|
|
||||||
|
lockMechanismService
|
||||||
|
.getCharacteristic(Characteristic.LockCurrentState)
|
||||||
|
.on('get', this.getLockCurrentState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, lockMechanismService];
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// LIGHTS
|
||||||
|
// Implements the Light service for homekit based on an underlying isy-js device. Is dimmable or not depending
|
||||||
|
// on if the underlying device is dimmable.
|
||||||
|
|
||||||
|
// Constructs the light accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||||
|
function ISYLightAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
this.dimmable = (this.device.deviceType == "DimmableLight");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the identify command
|
||||||
|
ISYLightAccessory.prototype.identify = function(callback) {
|
||||||
|
this.device.sendLightCommand(true, function(result) {
|
||||||
|
this.device.sendLightCommand(false, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to set the current powerstate from homekit. Will ignore redundant commands.
|
||||||
|
ISYLightAccessory.prototype.setPowerState = function(powerOn,callback) {
|
||||||
|
ISYJSDebugMessage(this,"Setting powerstate to %s", powerOn);
|
||||||
|
if(powerOn != this.device.getCurrentLightState()) {
|
||||||
|
ISYJSDebugMessage(this,"Changing powerstate to "+powerOn);
|
||||||
|
this.device.sendLightCommand(powerOn, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Ignoring redundant setPowerState");
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYLightAccessory.prototype.handleExternalChange = function() {
|
||||||
|
ISYJSDebugMessage(this,"Handling external change for light");
|
||||||
|
this.lightService
|
||||||
|
.setCharacteristic(Characteristic.On, this.device.getCurrentLightState());
|
||||||
|
if(this.device.deviceType == this.device.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||||
|
this.lightService
|
||||||
|
.setCharacteristic(Characteristic.Brightness, this.device.getCurrentLightDimState() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to get the current on state
|
||||||
|
ISYLightAccessory.prototype.getPowerState = function(callback) {
|
||||||
|
callback(null,this.device.getCurrentLightState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to set the brightness level of dimmable lights. Ignore redundant commands.
|
||||||
|
ISYLightAccessory.prototype.setBrightness = function(level,callback) {
|
||||||
|
ISYJSDebugMessage(this,"Setting brightness to %s", level);
|
||||||
|
if(level != this.device.getCurrentLightDimState()) {
|
||||||
|
ISYJSDebugMessage(this,"Changing Brightness to "+level);
|
||||||
|
this.device.sendLightDimCommand(level, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Ignoring redundant setBrightness");
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles a request to get the current brightness level for dimmable lights.
|
||||||
|
ISYLightAccessory.prototype.getBrightness = function(callback) {
|
||||||
|
callback(null,this.device.getCurrentLightDimState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of services supported by this object.
|
||||||
|
ISYLightAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var lightBulbService = new Service.Lightbulb();
|
||||||
|
|
||||||
|
this.informationService = informationService;
|
||||||
|
this.lightService = lightBulbService;
|
||||||
|
|
||||||
|
lightBulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
lightBulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('get', this.getPowerState.bind(this));
|
||||||
|
|
||||||
|
if(this.dimmable) {
|
||||||
|
lightBulbService
|
||||||
|
.addCharacteristic(new Characteristic.Brightness())
|
||||||
|
.on('get', this.getBrightness.bind(this));
|
||||||
|
|
||||||
|
lightBulbService
|
||||||
|
.getCharacteristic(Characteristic.Brightness)
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [informationService, lightBulbService];
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CONTACT SENSOR - ISYDoorWindowSensorAccessory
|
||||||
|
// Implements the ContactSensor service.
|
||||||
|
|
||||||
|
// Constructs a Door Window Sensor (contact sensor) accessory. log = HomeBridge logger, device = wrapped isy-js device.
|
||||||
|
function ISYDoorWindowSensorAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
this.doorWindowState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the identify command.
|
||||||
|
ISYDoorWindowSensorAccessory.prototype.identify = function(callback) {
|
||||||
|
// Do the identify action
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates the state of the underlying device object into the corresponding homekit compatible state
|
||||||
|
ISYDoorWindowSensorAccessory.prototype.translateCurrentDoorWindowState = function() {
|
||||||
|
return (this.device.getCurrentDoorWindowState()) ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the request to get he current door window state.
|
||||||
|
ISYDoorWindowSensorAccessory.prototype.getCurrentDoorWindowState = function(callback) {
|
||||||
|
callback(null,this.translateCurrentDoorWindowState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYDoorWindowSensorAccessory.prototype.handleExternalChange = function() {
|
||||||
|
this.sensorService
|
||||||
|
.setCharacteristic(Characteristic.ContactSensorState, this.translateCurrentDoorWindowState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of services supported by this object.
|
||||||
|
ISYDoorWindowSensorAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var sensorService = new Service.ContactSensor();
|
||||||
|
|
||||||
|
this.sensorService = sensorService;
|
||||||
|
this.informationService = informationService;
|
||||||
|
|
||||||
|
sensorService
|
||||||
|
.getCharacteristic(Characteristic.ContactSensorState)
|
||||||
|
.on('get', this.getCurrentDoorWindowState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, sensorService];
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// ELK SENSOR PANEL - ISYElkAlarmPanelAccessory
|
||||||
|
// Implements the SecuritySystem service for an elk security panel connected to the isy system
|
||||||
|
|
||||||
|
// Constructs the alarm panel accessory. log = HomeBridge logger, device = underlying isy-js device being wrapped
|
||||||
|
function ISYElkAlarmPanelAccessory(log,device) {
|
||||||
|
ISYAccessoryBaseSetup(this,log,device);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the identify command
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.identify = function(callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the request to set the alarm target state
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.setAlarmTargetState = function(targetStateHK,callback) {
|
||||||
|
ISYJSDebugMessage(this,"Sending command to set alarm panel state to: "+targetStateHK);
|
||||||
|
var targetState = this.translateHKToAlarmTargetState(targetStateHK);
|
||||||
|
ISYJSDebugMessage(this,"Would send the target state of: "+targetState);
|
||||||
|
if(this.device.getAlarmMode() != targetState) {
|
||||||
|
this.device.sendSetAlarmModeCommand(targetState, function(result) {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Redundant command, already in that state.");
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates from the current state of the elk alarm system into a homekit compatible state. The elk panel has a lot more
|
||||||
|
// possible states then can be directly represented by homekit so we map them. If the alarm is going off then it is tripped.
|
||||||
|
// If it is arming or armed it is considered armed. Stay maps to the state state, away to the away state, night to the night
|
||||||
|
// state.
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.translateAlarmCurrentStateToHK = function() {
|
||||||
|
var tripState = this.device.getAlarmTripState();
|
||||||
|
var sourceAlarmState = this.device.getAlarmState();
|
||||||
|
var sourceAlarmMode = this.device.getAlarmMode();
|
||||||
|
|
||||||
|
if(tripState >= this.device.ALARM_TRIP_STATE_TRIPPED) {
|
||||||
|
return Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
||||||
|
} else if(sourceAlarmState == this.device.ALARM_STATE_NOT_READY_TO_ARM ||
|
||||||
|
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM ||
|
||||||
|
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM_VIOLATION) {
|
||||||
|
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||||
|
} else {
|
||||||
|
if(sourceAlarmMode == this.device.ALARM_MODE_STAY || sourceAlarmMode == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||||
|
return Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
||||||
|
} else if(sourceAlarmMode == this.device.ALARM_MODE_AWAY || sourceAlarmMode == this.device.ALARM_MODE_VACATION) {
|
||||||
|
return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
||||||
|
} else if(sourceAlarmMode == this.device.ALARM_MODE_NIGHT || sourceAlarmMode == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||||
|
return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
||||||
|
} else {
|
||||||
|
ISYJSDebugMessage(this,"Setting to disarmed because sourceAlarmMode is "+sourceAlarmMode);
|
||||||
|
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates the current target state of hthe underlying alarm into the appropriate homekit value
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.translateAlarmTargetStateToHK = function() {
|
||||||
|
var sourceAlarmState = this.device.getAlarmMode();
|
||||||
|
if(sourceAlarmState == this.device.ALARM_MODE_STAY || sourceAlarmState == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||||
|
return Characteristic.SecuritySystemTargetState.STAY_ARM;
|
||||||
|
} else if(sourceAlarmState == this.device.ALARM_MODE_AWAY || sourceAlarmState == this.device.ALARM_MODE_VACATION) {
|
||||||
|
return Characteristic.SecuritySystemTargetState.AWAY_ARM;
|
||||||
|
} else if(sourceAlarmState == this.device.ALARM_MODE_NIGHT || sourceAlarmState == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||||
|
return Characteristic.SecuritySystemTargetState.NIGHT_ARM;
|
||||||
|
} else {
|
||||||
|
return Characteristic.SecuritySystemTargetState.DISARM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Translates the homekit version of the alarm target state into the appropriate elk alarm panel state
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.translateHKToAlarmTargetState = function(state) {
|
||||||
|
if(state == Characteristic.SecuritySystemTargetState.STAY_ARM) {
|
||||||
|
return this.device.ALARM_MODE_STAY;
|
||||||
|
} else if(state == Characteristic.SecuritySystemTargetState.AWAY_ARM) {
|
||||||
|
return this.device.ALARM_MODE_AWAY;
|
||||||
|
} else if(state == Characteristic.SecuritySystemTargetState.NIGHT_ARM) {
|
||||||
|
return this.device.ALARM_MODE_NIGHT;
|
||||||
|
} else {
|
||||||
|
return this.device.ALARM_MODE_DISARMED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to get the target alarm state
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.getAlarmTargetState = function(callback) {
|
||||||
|
callback(null,this.translateAlarmTargetStateToHK());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles request to get the current alarm state
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.getAlarmCurrentState = function(callback) {
|
||||||
|
callback(null,this.translateAlarmCurrentStateToHK());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mirrors change in the state of the underlying isj-js device object.
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.handleExternalChange = function() {
|
||||||
|
ISYJSDebugMessage(this,"Source device. Currenty state locally -"+this.device.getAlarmStatusAsText());
|
||||||
|
ISYJSDebugMessage(this,"Got alarm change notification. Setting HK target state to: "+this.translateAlarmTargetStateToHK()+" Setting HK Current state to: "+this.translateAlarmCurrentStateToHK());
|
||||||
|
this.alarmPanelService
|
||||||
|
.setCharacteristic(Characteristic.SecuritySystemTargetState, this.translateAlarmTargetStateToHK());
|
||||||
|
this.alarmPanelService
|
||||||
|
.setCharacteristic(Characteristic.SecuritySystemCurrentState, this.translateAlarmCurrentStateToHK());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the set of services supported by this object.
|
||||||
|
ISYElkAlarmPanelAccessory.prototype.getServices = function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||||
|
|
||||||
|
var alarmPanelService = new Service.SecuritySystem();
|
||||||
|
|
||||||
|
this.alarmPanelService = alarmPanelService;
|
||||||
|
this.informationService = informationService;
|
||||||
|
|
||||||
|
alarmPanelService
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||||
|
.on('set', this.setAlarmTargetState.bind(this));
|
||||||
|
|
||||||
|
alarmPanelService
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||||
|
.on('get', this.getAlarmTargetState.bind(this));
|
||||||
|
|
||||||
|
alarmPanelService
|
||||||
|
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||||
|
.on('get', this.getAlarmCurrentState.bind(this));
|
||||||
|
|
||||||
|
return [informationService, alarmPanelService];
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports.platform = ISYPlatform;
|
||||||
|
module.exports.accessory = ISYFanAccessory;
|
||||||
|
module.exports.accessory = ISYLightAccessory;
|
||||||
|
module.exports.accessory = ISYLockAccessory;
|
||||||
|
module.exports.accessory = ISYOutletAccessory;
|
||||||
|
module.exports.accessory = ISYDoorWindowSensorAccessory;
|
||||||
|
module.exports.accessory = ISYElkAlarmPanelAccessory;
|
||||||
33
script/bootstrap
Executable file
33
script/bootstrap
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if ! test $(which forever)
|
||||||
|
then
|
||||||
|
echo
|
||||||
|
echo "!!!!"
|
||||||
|
echo "You don't have forever installed. You need to install it first."
|
||||||
|
echo
|
||||||
|
echo "Just install it with this command: "
|
||||||
|
echo 'sudo npm install forever -g'
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p log
|
||||||
|
|
||||||
|
echo "Installing packages..."
|
||||||
|
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||||
|
echo "This might take a while on a Raspberry Pi..."
|
||||||
|
fi
|
||||||
|
npm install > /dev/null 2>&1
|
||||||
|
|
||||||
|
if [ ! -f config.json ]
|
||||||
|
then
|
||||||
|
echo
|
||||||
|
echo "==> Creating your config. Please edit config.json."
|
||||||
|
echo
|
||||||
|
cp config-sample.json config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Finished setting up Homebridge! run it with script/server or install it with script/install."
|
||||||
44
script/install
Executable file
44
script/install
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Installing Homebridge..."
|
||||||
|
|
||||||
|
APP_PATH=`pwd`
|
||||||
|
FOREVER_PATH=`which forever || true`
|
||||||
|
USER_NAME=`whoami`
|
||||||
|
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||||
|
## install for linux
|
||||||
|
|
||||||
|
# copy SysVinit script to the correct path.
|
||||||
|
sudo cp startup/homebridge /etc/init.d/homebridge
|
||||||
|
# set exec permissions on script.
|
||||||
|
sudo chmod +x /etc/init.d/homebridge
|
||||||
|
|
||||||
|
# Set the current path for where the app lives.
|
||||||
|
sudo sed -i '' -e "s#%PATH%#$APP_PATH#g" /etc/init.d/homebridge
|
||||||
|
# Set the path for where the forever lives.
|
||||||
|
sudo sed -i '' -e "s#%FOREVER_PATH%#$FOREVER_PATH#g" /etc/init.d/homebridge
|
||||||
|
|
||||||
|
# Start it.
|
||||||
|
sudo /etc/init.d/homebridge start
|
||||||
|
|
||||||
|
# tell it to launch at boot
|
||||||
|
sudo update-rc.d homebridge defaults
|
||||||
|
|
||||||
|
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
## install for OS X
|
||||||
|
|
||||||
|
# copy template plist to the correct path.
|
||||||
|
cp startup/org.homebridge.plist ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
|
||||||
|
# Set the current user for the script to run as.
|
||||||
|
sed -i '' -e "s#%USER%#$USER_NAME#g" ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
# Set the current path for where the app lives.
|
||||||
|
sed -i '' -e "s#%PATH%#$APP_PATH#g" ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
|
||||||
|
# Tell launchd to start it.
|
||||||
|
launchctl load -w -F ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
fi
|
||||||
4
script/restart
Executable file
4
script/restart
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
script/uninstall
|
||||||
|
script/install
|
||||||
12
script/server
Executable file
12
script/server
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
FOREVER_PATH=`which forever`
|
||||||
|
|
||||||
|
test -z "$NODE_ENV" &&
|
||||||
|
export NODE_ENV='development'
|
||||||
|
|
||||||
|
if [ "$NODE_ENV" = "development" ]; then
|
||||||
|
$FOREVER_PATH -f startup/forever/development.json
|
||||||
|
else
|
||||||
|
$FOREVER_PATH start startup/forever/production.json
|
||||||
|
fi
|
||||||
22
script/uninstall
Executable file
22
script/uninstall
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Uninstalling Homebridge..."
|
||||||
|
# make forever stop the app.
|
||||||
|
forever stop homebridge > /dev/null 2>&1
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == "linux*" ]]; then
|
||||||
|
## uninstall for linux
|
||||||
|
|
||||||
|
# stop the service.
|
||||||
|
sudo /etc/init.d/homebridge stop
|
||||||
|
|
||||||
|
# remove the service from launching at boot.
|
||||||
|
sudo update-rc.d -f homebridge remove
|
||||||
|
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
## uninstall for OS X
|
||||||
|
|
||||||
|
# tell launchd to stop the process.
|
||||||
|
launchctl unload ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
# remove the launchd script.
|
||||||
|
rm ~/Library/LaunchAgents/org.homebridge.plist
|
||||||
|
fi
|
||||||
8
script/upgrade
Executable file
8
script/upgrade
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
echo "Updating from GitHub..."
|
||||||
|
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
git pull origin $BRANCH
|
||||||
|
|
||||||
|
npm update
|
||||||
|
|
||||||
|
script/restart
|
||||||
4
startup/forever/development.json
Normal file
4
startup/forever/development.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"append": true,
|
||||||
|
"script": "app.js"
|
||||||
|
}
|
||||||
7
startup/forever/production.json
Normal file
7
startup/forever/production.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"uid": "homebridge",
|
||||||
|
"append": true,
|
||||||
|
"script": "app.js",
|
||||||
|
"outFile": "log/logs.log",
|
||||||
|
"errFile": "log/error.log"
|
||||||
|
}
|
||||||
88
startup/homebridge
Normal file
88
startup/homebridge
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
### BEGIN INIT INFO
|
||||||
|
# If you wish the Daemon to be lauched at boot / stopped at shutdown :
|
||||||
|
#
|
||||||
|
# On Debian-based distributions:
|
||||||
|
# INSTALL : update-rc.d scriptname defaults
|
||||||
|
# (UNINSTALL : update-rc.d -f scriptname remove)
|
||||||
|
#
|
||||||
|
# On RedHat-based distributions (CentOS, OpenSUSE...):
|
||||||
|
# INSTALL : chkconfig --level 35 scriptname on
|
||||||
|
# (UNINSTALL : chkconfig --level 35 scriptname off)
|
||||||
|
#
|
||||||
|
# chkconfig: 2345 90 60
|
||||||
|
# Provides: %PATH%/app.js
|
||||||
|
# Required-Start: $remote_fs $syslog
|
||||||
|
# Required-Stop: $remote_fs $syslog
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: forever running %PATH%/app.js
|
||||||
|
# Description: %PATH%/app.js
|
||||||
|
### END INIT INFO
|
||||||
|
#
|
||||||
|
# initd a node app
|
||||||
|
# Based on a script posted by https://gist.github.com/jinze at https://gist.github.com/3748766
|
||||||
|
#
|
||||||
|
|
||||||
|
if [ -e /lib/lsb/init-functions ]; then
|
||||||
|
# LSB source function library.
|
||||||
|
. /lib/lsb/init-functions
|
||||||
|
fi;
|
||||||
|
|
||||||
|
pidFile="/var/run/homebridge.pid"
|
||||||
|
logFile="%PATH%/log/logs.log"
|
||||||
|
|
||||||
|
command="node"
|
||||||
|
nodeApp="%PATH%/app.js"
|
||||||
|
foreverApp="%FOREVER_PATH%"
|
||||||
|
|
||||||
|
start() {
|
||||||
|
echo "Starting $nodeApp"
|
||||||
|
|
||||||
|
# Notice that we change the PATH because on reboot
|
||||||
|
# the PATH does not include the path to node.
|
||||||
|
# Launching forever with a full path
|
||||||
|
# does not work unless we set the PATH.
|
||||||
|
PATH=/usr/local/bin:$PATH
|
||||||
|
export NODE_ENV=production
|
||||||
|
#PORT=80
|
||||||
|
$foreverApp start --pidFile $pidFile -l $logFile -a -d -c "$command" $nodeApp
|
||||||
|
RETVAL=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
echo -n "Restarting $nodeApp"
|
||||||
|
$foreverApp restart $nodeApp
|
||||||
|
RETVAL=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
echo -n "Shutting down $nodeApp"
|
||||||
|
$foreverApp stop $nodeApp
|
||||||
|
RETVAL=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
echo -n "Status $nodeApp"
|
||||||
|
$foreverApp list
|
||||||
|
RETVAL=$?
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
start)
|
||||||
|
start
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
stop
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
status
|
||||||
|
;;
|
||||||
|
restart)
|
||||||
|
restart
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: {start|stop|status|restart}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
exit $RETVAL
|
||||||
41
startup/org.homebridge.plist
Normal file
41
startup/org.homebridge.plist
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>org.homebridge</string>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>PATH</key>
|
||||||
|
<string>/usr/local/bin/:/usr/bin:$PATH</string>
|
||||||
|
<key>NODE_ENV</key>
|
||||||
|
<string>production</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>Program</key>
|
||||||
|
<string>script/server</string>
|
||||||
|
|
||||||
|
<key>AbandonProcessGroup</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<dict>
|
||||||
|
<key>SuccessfulExit</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>WorkingDirectory</key>
|
||||||
|
<string>%PATH%</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/Users/%USER%/Library/Logs/homebridge.log</string>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/Users/%USER%/Library/Logs/homebridge.log</string>
|
||||||
|
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
Reference in New Issue
Block a user