diff --git a/README.md b/README.md index 3932307..4dece24 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,16 @@ # Automation Switches Platform -A platform that provides configurable switches for automation purposes. This platform can be created to provide time delayed responses in HomeKit rules or to simulate security systems. +A platform that provides configurable switches for automation purposes. This platform can be created to provide time delayed responses in HomeKit rules or to simulate security systems. ## Why do we need this plugin? -HomeKit (as of iOS 11.1) does not provide a capability to delay the execution of rules. This platform provides switches that can be used to build a set of rules that are delayed in execution by a configurable period of time on each switch. The period as well as the repetetiveness and the response can be configured via HomeKit (while the configuration provides sane defaults) and can also be changed in response to rules. +This platform provides software based, optionally persistent, switches to create DIY HomeKit solutions. +Each switch has specific purposes that are illustrated in their respective documents linked below. -The security system switches enable an easier creation of home alarm systems using already available -accessories. +The plugin provides four different types of switches: A basic on/off switch, a lock mechanism, an automation switch with advanced properties and a security system. All of them are configured ahead +of their use through the configuration file and each one of them potentially saves their state to storage +to keep their state even across crashes, reboots and such. ## Installation instructions @@ -18,7 +20,7 @@ After [Homebridge](https://github.com/nfarina/homebridge) has been installed: ## Example config.json: - ``` +```json { "bridge": { ... @@ -45,61 +47,30 @@ After [Homebridge](https://github.com/nfarina/homebridge) has been installed: The platform can provide any number of switches that have to be predefined in the homebridge config.json. -### Automation Switch Type +### Switch types -Each automation switch provides a regular switch, a motion sensor and a configuration service. Activating the switch starts a timer, which will trigger the motion sensor for 1s when the timer elapses. This can be used as a trigger for HomeKit rules. +Please see the documentation for each type of switch this plugin is able to create: -The switch support two modes: An automatic shut off mode, where the motion sensor will only be tripped once and the switch is automatically shut off. The other mode is a repeating mode, where the motion sensor will be tripped repeatedly until the switch is shut off again. +- [Automation switch](docs/AutomationSwitch.md) +- [Lock mechanism](docs/LockMechanism.md) +- [Security system](docs/SecuritySystem.md) +- [Switch](docs/Switch.md) -| Attributes | Required | Usage | -|------------|----------|-------| -| type | No | If not set, creates an "automation" switch. If specified, this must be "automation" for automation switches. | -| name | Yes | A unique name for the switch. Will be used as the accessory name. | -| period | Yes | The default delay of the switch in seconds. | -| autoOff | Yes | Determines if the switch automatically shuts off after the period has elapsed. | +An advanced configuration example containing all four switch types can be found [here](docs/Configuration.md). -### Security System Switch Type +### Storage -The security system switch type enables the creation of security systems. The switch can be armed in night, away and stay modes. Additionally there's an option to trigger an alarm using an On/Off characteristic. +Every type of switch is able to store every state change to disk. This is useful if homebridge is restarted for whatever reason: The switches created by this plugin will retain the state they had before the restart. -The value of the characteristics is persisted if homebridge is restarted. +For that the switches create individual files in the persist subfolder of your homebridge configuration folder. -| Attributes | Required | Usage | -|------------|----------|-------| -| type | Yes | Must be set to "security" for this type of switch. | -| name | Yes | A unique name for the switch. Will be used as the accessory name. | +## Developer Information -The settings of this switch are persisted to files in the homebridge configuration folder in the persist subfolder. - -## Accessory Services - -Each automation switch will expose four services: - -* Accessory Information Service -* Switch Service -* Motion Sensor Service -* Switch Program Service - -Each security switch will expose the following service: - -* Accessory Information Service -* Security System Service - -## Switch Program Service Characteristics - -The exposed switch service supports the following characteristics: - -| Characteristic | UUID | Permissions | Usage | -|---|---|---|---| -| Period | `B469181F-D796-46B4-8D99-5FBE4BA9DC9C` | READ, WRITE | The period of the switch in seconds. This value can be changed between 1s and 3600s. A change will only take effect the next time the switch is turned on. | -| AutomaticOff | `72227266-CA42-4442-AB84-0A7D55A0F08D` | READ, WRITE | Determines if the switch is shut off after the period has elapsed. If the switch is not automatically shut off, the timer will be restarted and the motion sensor will be triggered again until the switch is shut off externally. | -| Alarm | `72227266-CA42-4442-AB84-0A7D55A0F08D` | READ, WRITE, EVENTS | For security switches this characteristic enables the triggering of an alarm. Can also be used for additional rules. | - -See [HomeKitTypes.js](src/HomeKitTypes.js) for details. +There's [documentation](docs/CustomCharacteristics.md) of the custom services and characteristics exposed by the switches. ## Supported clients -This platform and the delayed switches it creates have been verified to work with the following apps on iOS 11 +This platform and the switches it creates have been verified to work with the following apps on iOS 11: * Home * Elgato Eve @@ -112,9 +83,7 @@ This plugin was initially forked from and inspired by [homebridge-delay-switch]( If you use this and like it - please leave a note by staring this package here or on GitHub. -If you use it and have a -problem, file an issue at [GitHub](https://github.com/grover/homebridge-telegram/issues) - I'll try -to help. +If you use it and have a problem, file an issue at [GitHub](https://github.com/grover/homebridge-telegram/issues) - I'll try to help. If you tried this, but don't like it: tell me about it in an issue too. I'll try my best to address these in my spare time. @@ -143,4 +112,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/docs/AutomationSwitch.md b/docs/AutomationSwitch.md new file mode 100644 index 0000000..ac7f5eb --- /dev/null +++ b/docs/AutomationSwitch.md @@ -0,0 +1,72 @@ +# Automation Switch + +HomeKit (as of iOS 11.1) does not provide a capability to delay the execution of rules. This plugin provides an automation switch that can be used to build a set of rules that are delayed in execution by a configurable period of time on each switch. The period as well as the repetetiveness and the response can be configured via HomeKit (while the configuration provides sane defaults) and can also be changed in response to rules. + +This switch optionally supports being saved to storage on every state change, the switch will save the settings and restore them when homebridge is restarted. + +> Please note that while the plugin works with the Home app, some features, such as dynamic +> reconfiguration can only be used with other HomeKit apps, such as [Elgato Eve](https://www.elgato.com/en/eve/eve-app). + +## Appearance + +Each automation switch provides a regular switch, a motion sensor and a configuration service. Activating the switch starts a timer, which will trigger the motion sensor for 1s when the timer elapses. This can be used as a trigger for HomeKit rules. + +The switch support two modes: An automatic shut off mode, where the motion sensor will only be tripped once and the switch is automatically shut off. The other mode is a repeating mode, where the motion sensor will be tripped repeatedly until the switch is shut off again. + +![Preview](AutomationSwitch.png "Preview") + +(Screenshot: Elgato Eve) + +## Configuration + +```json +{ + "bridge": { + ... + }, + "platforms": [ + { + "platform": "AutomationSwitches", + "switches": [ + { + "type": "automation", + "name": "Automation Switch #1", + "period": 1800, + "autoOff": false + } + ] + } + ] +} +``` + +## Options + +| Attributes | Required | Usage | +|------------|----------|-------| +| type | Yes | Set this to ```automation``` to make this entry an automation switch. | +| name | Yes | A unique name for the automation switch. Will be used as the accessory name. | +| period | Yes | The default time delay of the switch in seconds, before the sensor is triggered. | +| autoOff | Yes | Set this to ```true``` to automatically turn the switch off after the period has expired once, ```false``` to keep the triggering the sensor until it has expired. | +| default | No | Specifies the default state of the switch. This is used if the switch is not yet stored, not stored or the storage has become faulty. The default state is ```false``` if not specified, which means the switch is off. Setting this to ```true``` turns the switch on by default. | +| stored | No | Set this to true if you want the switch to retain its on/off state across restarts. The default setting for the ```switch``` type is ```false```. | + +See [configuration](Configuration.md) for more advanced configuration examples. + +## Usage + +This type of switch is best used if you need to delay the execution of a scene or if you need to execute the scene multiple times until something else has happened. + +I personally have used this to send me notifications until I close the bathroom window, reminding me every 10 minutes that the window is open and I might freeze to the toilet seat. For that I combine this switch type with my [homebridge-telegram](https://www.npmjs.com/packages/homebridge-telegram) plugin. (Shameless plug.) + +## Notes + +### Stored switch states + +This switch has logic, which is tied to the state of the switch (e.g. 'On' or 'Off'.) The initial state of the switch is determined either by the "default" field in the configuration or by the value saved just before homebridge terminated the last time. + +If the initial value is `true` the timer controlled by the switch will immediately start as soon as the plugin is initialized. This can also serve to delay initialization just after homebridge has been loaded. + +### Editing the timeout period + +If the automation switch was stored the default period from the configuration file is also not used any longer. To edit the timeout in those cases, either edit the file that belongs to the switch (located in your homebridge configuration directory under the persist folder) or delete it and then edit the default again. diff --git a/docs/AutomationSwitch.png b/docs/AutomationSwitch.png new file mode 100755 index 0000000..e710db7 Binary files /dev/null and b/docs/AutomationSwitch.png differ diff --git a/docs/Configuration.md b/docs/Configuration.md new file mode 100644 index 0000000..51d2119 --- /dev/null +++ b/docs/Configuration.md @@ -0,0 +1,53 @@ +# Advanced configuration + +The following configuration example shows all switches in combination. The configuration was used to make the screenshots in the detailed documentation pages. The configuration shows examples of the +[automation switch](AutomationSwitch.md), the [lock mechanism](LockMechanism.md), the [security system](SecuritySystem.md) and the regular [switch](Switch.md). + +## Example + +```json +{ + "bridge": { + ... + }, + "platforms": [ + { + "platform": "AutomationSwitches", + "switches": [ + { + "type": "automation", + "name": "WindowOpenAlert", + "period": 600, + "autoOff": false, + "stored": false + }, + { + "type": "automation", + "name": "AutoOff", + "period": 900, + "autoOff": true, + "stored": false + }, + { + "type": "lock", + "name": "ScreenLock", + "default": "unlocked", + "stored": true + }, + { + "type": "security", + "name": "Home Mode", + "default": "armed-stay", + "stored": true + }, + { + "type": "security", + "name": "My DIY security system", + "default": "unarmed", + "stored": true + } + ] + } + ] +} +``` diff --git a/docs/CustomCharacteristics.md b/docs/CustomCharacteristics.md new file mode 100644 index 0000000..359f6b9 --- /dev/null +++ b/docs/CustomCharacteristics.md @@ -0,0 +1,60 @@ +# Custom services and characteristics + +Most of the switch types provided by this plugin use standard HomeKit characteristics. In some cases there was no way (or it made no sense) to bend the standard services/characteristics to enable the use cases supported by this plugin. + +All custom services and characteristics used are listed below. + +## Custom characteristics + +All custom characteristics are defined in [HomeKitTypes.js](../src/HomeKitTypes.js). + +### Custom automation switch characteristics + +The automation switch provides a programming service, which allows dynamic changes to the time period between triggers and also control of the automatic shut off. These characteristics can be used in scenes to change the default behavior of the switch. + +| Characteristic | UUID | Service | Permissions | Usage | +|---|---|---|---| +| Period | `B469181F-D796-46B4-8D99-5FBE4BA9DC9C` | READ, WRITE | `FD92B7CF-A343-4D7E-9467-FD251E22C374` | The period of the switch in seconds. This value can be changed between 1s and 3600s. A change will only take effect the next time the switch is turned on. | +| AutomaticOff | `72227266-CA42-4442-AB84-0A7D55A0F08D` | `FD92B7CF-A343-4D7E-9467-FD251E22C374` | READ, WRITE | Determines if the switch is shut off after the period has elapsed. If the switch is not automatically shut off, the timer will be restarted and the motion sensor will be triggered again until the switch is shut off externally. | + +Both of these characteristics are provided on the custom SwitchProgramService. + +### Custom security system characteristics + +The security system provides an additional characteristic to trigger the Alarm state: + +| Characteristic | UUID | Service | Properties | Description | +| Alarm | `72227266-CA42-4442-AB84-0A7D55A0F08D` | `0000008E-0000-1000-8000-0026BB765291` | READ, WRITE, EVENTS | This characteristic enables the triggering of an alarm. Can also be used for additional rules. | + +This characteristic is provided as an extension on the standard security system service. + +## Services + +Each switch created by this accessory will provide the standard HomeKit services: + +- AccessoryInformation service +- BridgingState service + +### Automation switch + +The automation switch additionally provides: + +- Switch service +- MotionSensor service +- SwitchProgram service (see above) + +### Lock mechanism + +The lock mechanism service provides: + +- LockMechanism service + +### Security system + +The security system switch provides: + +- SecuritySystem service + +### Switch + +- Switch Service diff --git a/docs/LockMechanism.md b/docs/LockMechanism.md new file mode 100644 index 0000000..b9e1db6 --- /dev/null +++ b/docs/LockMechanism.md @@ -0,0 +1,55 @@ +# Lock mechanism + +This switch type represents a lock. A lock can be locked or unlocked and changing the state of the lock triggers built-in HomeKit notifications. Use it for things that can best be simulated by a lock. + +## Appearance + +The lock mechanism only provides means to lock/unlock via specifically labelled buttons. + +![Preview](LockMechanism.png "Preview") + +(Screenshot: Elgato Eve) + +## Configuration + +```json +{ + "bridge": { + ... + }, + "platforms": [ + { + "platform": "AutomationSwitches", + "switches": [ + { + "type": "lock", + "name": "My simulated door lock", + "default": "unlocked", + "stored": false + } + ] + } + ] +} +``` + +## Options + +| Field | Required | Description | +|---|---|---| +| type | Yes | Set this to ```lock``` to make this entry a lock mechanism. | +| name | Yes | Set this to the name of the lock as you want it to appear in HomeKit apps. | +| default | No | This configures the default state of the lock if it is not yet stored, never stored or the storage has become faulty. Set this to ```unlocked``` or ```locked``` depending on your needs. By default a lock is ```unlocked``` if this is not specified. | +| stored | No | Set this to true if you want the lock to retain its locked/unlocked state across restarts. The default setting for the ```lock``` type is ```false```. | + +See [configuration](Configuration.md) for more advanced configuration examples. + +## Usage + +This type is best used to simulate a physical lock or things that are best described by a lock. An example use case for the lock is in conditions for HomeKit rules and thus use it to enable or disable the rules based on other conditions. + +## HomeKit Notifications + +HomeKit, by default, enables notifications for lock mechanisms. Once enabled you automatically get built-in notifications for this lock too. You can disable these notifications in the Home.app if you do not care for the notifications. To disable/enable the notifications, open the Home app, select the tile that represents the lock and long-press on it, choose Details and scroll down until you get to Notifications. You can disable them there. + +There's unfortunately no way to change the notification text in HomeKit. If you're looking for something to send customized notifications I'd recommend one of the [IFTT plugins](https://www.npmjs.com/search?q=homebridge+ifttt) or my [homebridge-telegram](https://www.npmjs.com/packages/homebridge-telegram) plugin (shameless plug.) diff --git a/docs/LockMechanism.png b/docs/LockMechanism.png new file mode 100755 index 0000000..17c0dd1 Binary files /dev/null and b/docs/LockMechanism.png differ diff --git a/docs/SecuritySystem.md b/docs/SecuritySystem.md new file mode 100644 index 0000000..ac70c84 --- /dev/null +++ b/docs/SecuritySystem.md @@ -0,0 +1,58 @@ +# Security System + +The security system switches enable an easier creation of home alarm systems using already available accessories. Security system switches have default HomeKit notifications associated with them. + +## Appearance + +The security system provides labelled buttons for the various armed/unarmed states as well +as a button to trigger an alarm. + +![Preview](SecuritySystem.png "Preview") + +(Screenshot: Elgato Eve) + +## Configuration + +```json +{ + "bridge": { + ... + }, + "platforms": [ + { + "platform": "AutomationSwitches", + "switches": [ + { + "type": "security", + "name": "My DIY security system", + "default": "unarmed", + "stored": false + } + ] + } + ] +} +``` + +## Options + +| Field | Required | Description | +|---|---|---| +| type | Yes | Set this to ```security``` to make this entry a security system. | +| name | Yes | Set this to the name of the security system as you want it to appear in HomeKit apps. | +| default | No | This configures the default state of the security system if it is not yet stored, never stored or the storage has become faulty. Set this to ```unarmed```, or ```armed-away```, ```armed-stay``` or ```armed-night``` depending on your needs. By default a security system is ```unarmed``` if this is not specified. | +| stored | No | The state of security systems are by default stored. Set this to ```false``` if you do not want the security system to retain its armed/unarmed and alarm state across restarts. | + +See [configuration](Configuration.md) for more advanced configuration examples. + +## Usage + +This is best used if you want to build your own home security system. Another use of this is to set up rules and conditions for smoke, leak or gas detectors and have different responses on the state of your home. + +An additional use I have for this switch is to integrate with heating solutions and have centralized control over lighting, heating and security based on the mode of your home. + +## HomeKit Notifications + +HomeKit, by default, enables notifications for security system mechanisms. Once enabled you automatically get built-in notifications for this security system switch too. You can disable these notifications in the Home.app if you do not care for the notifications. To disable/enable the notifications, open the Home app, select the tile that represents the lock and long-press on it, choose Details and scroll down until you get to Notifications. You can disable them there. + +There's unfortunately no way to change the notification text in HomeKit. If you're looking for something to send customized notifications I'd recommend one of the [IFTT plugins](https://www.npmjs.com/search?q=homebridge+ifttt) or my [homebridge-telegram](https://www.npmjs.com/packages/homebridge-telegram) plugin (shameless plug.) diff --git a/docs/SecuritySystem.png b/docs/SecuritySystem.png new file mode 100755 index 0000000..57d9a73 Binary files /dev/null and b/docs/SecuritySystem.png differ diff --git a/docs/Switch.md b/docs/Switch.md new file mode 100644 index 0000000..1f90386 --- /dev/null +++ b/docs/Switch.md @@ -0,0 +1,50 @@ +# Switch + +This provides a basic on/off style switch without any of the features provided by other switches. It is designed to be this simple on purpose. If you're looking for advanced features you should take a look at +the [automation switch](AutomationSwitch.md). + +## Appearance + +A switch is a switch, what did you expect? + +![Preview](Switch.png "Preview") + +(Screenshot: Elgato Eve) + +## Configuration + +```json +{ + "bridge": { + ... + }, + "platforms": [ + { + "platform": "AutomationSwitches", + "switches": [ + { + "type": "switch", + "name": "My basic switch", + "stored": true, + "default": false + } + ] + } + ] +} +``` + +## Options + +| Field | Required | Description | +|---|---|---| +| type | Yes | Set this to ```switch``` to make this entry a basic switch. | +| name | Yes | Set this to the name of the switch as you want it to appear in HomeKit apps. | +| default | No | Specifies the default state of the switch. This is used if the switch is not yet stored, not stored or the storage has become faulty. The default state is ```false``` if not specified, which means the switch is off. Setting this to ```true``` turns the switch on by default. | +| stored | No | Set this to true if you want the switch to retain its on/off state across restarts. The default setting for the ```switch``` type is ```false```. | + +See [configuration](Configuration.md) for more advanced configuration examples. + +## Usage + +This switch can be used like any real power switch. You can set rules based on the state of the switch and notifications will be sent for each change of the switch state. diff --git a/docs/Switch.png b/docs/Switch.png new file mode 100755 index 0000000..4574c4d Binary files /dev/null and b/docs/Switch.png differ diff --git a/package.json b/package.json index 3c2d92f..983bdde 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "homebridge": ">=0.2.0" }, "dependencies": { + "clone": "^2.1.1", "node-persist": "^0.0.8" }, "devDependencies": { diff --git a/src/AutomationSwitchAccessory.js b/src/AutomationSwitchAccessory.js index fbf9c32..63fd82d 100644 --- a/src/AutomationSwitchAccessory.js +++ b/src/AutomationSwitchAccessory.js @@ -1,13 +1,14 @@ "use strict"; const version = require('../package.json').version; +const clone = require('clone'); +const inherits = require('util').inherits; -var inherits = require('util').inherits; -var Accessory, Characteristic, Service; +let Accessory, Characteristic, Service; class SwitchAccessory { - constructor(api, log, config) { + constructor(api, log, config, storage) { Accessory = api.hap.Accessory; Characteristic = api.hap.Characteristic; Service = api.hap.Service; @@ -20,8 +21,25 @@ class SwitchAccessory { this._periodInSeconds = config.period; this._autoOff = config.autoOff; + this._storage = storage; + + const defaultValue = { + autoOff: config.autoOff, + period: config.period, + state: config.default === undefined ? false : config.default + }; + + storage.retrieve(defaultValue, (error, value) => { + this._state = value; + }); + + this._services = this.createServices(); this._timer = undefined; + + if (this._state.state) { + this._startTimer(); + } } getServices() { @@ -60,7 +78,8 @@ class SwitchAccessory { const switchSvc = new Service.Switch(this.name); switchSvc.getCharacteristic(Characteristic.On) .on('set', this._setOn.bind(this)) - .on('get', this._getOn.bind(this)); + .updateValue(this._state.state); + return switchSvc; } @@ -68,10 +87,12 @@ class SwitchAccessory { const program = new Service.SwitchProgram(this.name); program.getCharacteristic(Characteristic.PeriodInSeconds) .on('set', this._setPeriod.bind(this)) - .on('get', this._getPeriod.bind(this)); + .updateValue(this._state.period); + program.getCharacteristic(Characteristic.AutomaticOff) .on('set', this._setAutoOff.bind(this)) - .on('get', this._getAutoOff.bind(this)); + .updateValue(this._state.autoOff); + return program; } @@ -88,38 +109,39 @@ class SwitchAccessory { this.log("Setting switch state to " + on); this._resetTimer(); - if (on) { + + const data = clone(this._state); + data.state = on; + + this._persist(data, (error) => { + if (error) { + this.log('Storing the state change has failed.'); + callback(error); + return; + } + this._startTimer(); - } + }); callback(); } - _getOn(callback) { - this.log("Returning current power status value: s=" + (this._timer !== undefined)); - callback(undefined, this._timer !== undefined); - } - _setPeriod(value, callback) { this.log("Setting period value: d=" + value + "s"); - this._periodInSeconds = value; - callback(); - } - _getPeriod(callback) { - this.log("Returning current period value: d=" + this._periodInSeconds + "s"); - callback(undefined, this._periodInSeconds); + const data = clone(this._state); + data.period = value; + + this._persist(data, callback); } _setAutoOff(value, callback) { this.log("Setting auto off value " + value); - this._autoOff = value; - callback(); - } - _getAutoOff(callback) { - this.log("Returning current auto off value " + this._autoOff); - callback(undefined, this._autoOff); + const data = clone(this._state); + data.autoOff = value; + + this._persist(data, callback); } _startTimer() { @@ -165,6 +187,18 @@ class SwitchAccessory { this._startTimer(); } } + + _persist(data, callback) { + this._storage.store(data, (error) => { + if (error) { + callback(error); + return; + } + + this._state = data; + callback(); + }); + } } module.exports = SwitchAccessory; \ No newline at end of file diff --git a/src/LockMechanismAccessory.js b/src/LockMechanismAccessory.js index 9efd9a9..3fe5f93 100644 --- a/src/LockMechanismAccessory.js +++ b/src/LockMechanismAccessory.js @@ -1,6 +1,7 @@ "use strict"; const version = require('../package.json').version; +const clone = require('clone'); const inherits = require('util').inherits; const NameFactory = require('./util/NameFactory'); @@ -17,7 +18,7 @@ const LockMechanismStates = [ class LockMechanismAccessory { - constructor(api, log, config) { + constructor(api, log, config, storage) { Accessory = api.hap.Accessory; Characteristic = api.hap.Characteristic; Service = api.hap.Service; @@ -26,13 +27,31 @@ class LockMechanismAccessory { this.name = config.name; this.version = config.version; - this._state = { - targetState: Characteristic.LockTargetState.UNSECURED + this._storage = storage; + + const defaultValue = { + targetState: this._pickDefault(config.default) }; + storage.retrieve(defaultValue, (error, value) => { + this._state = value; + }); + this._services = this.createServices(); } + _pickDefault(value) { + if (value === 'locked') { + return 1; + } + + if (value === 'unlocked' || value === undefined) { + return 0; + } + + throw new Error('Unsupported default value in configuration of lock.'); + } + getServices() { return this._services; } @@ -84,9 +103,20 @@ class LockMechanismAccessory { _setTargetState(value, callback) { this.log(`Change target state of ${this.name} to ${LockMechanismStates[value]}`); - this._state.targetState = value; - this._updateCurrentState(); - callback(); + + const data = clone(this._state); + data.targetState = value; + + this._persist(data, (error) => { + if (error) { + callback(error); + return; + } + + this._state = data; + this._updateCurrentState(); + callback(); + }); } _updateCurrentState() { @@ -96,6 +126,18 @@ class LockMechanismAccessory { .getCharacteristic(Characteristic.LockCurrentState) .updateValue(currentState); } + + _persist(data, callback) { + this._storage.store(data, (error) => { + if (error) { + callback(error); + return; + } + + this._state = data; + callback(); + }); + } } -module.exports = LockMechanismAccessory; \ No newline at end of file +module.exports = LockMechanismAccessory; diff --git a/src/SecuritySystemAccessory.js b/src/SecuritySystemAccessory.js index 6baea0a..3ed5cae 100644 --- a/src/SecuritySystemAccessory.js +++ b/src/SecuritySystemAccessory.js @@ -1,12 +1,9 @@ "use strict"; const version = require('../package.json').version; -const securitySystemStorage = require('node-persist').create(); - +const clone = require('clone'); const inherits = require('util').inherits; -const NameFactory = require('./util/NameFactory'); - let Accessory, Characteristic, Service; const SecuritySystemStates = [ @@ -19,7 +16,7 @@ const SecuritySystemStates = [ class SecuritySystemAccessory { - constructor(api, log, config) { + constructor(api, log, config, storage) { Accessory = api.hap.Accessory; Characteristic = api.hap.Characteristic; Service = api.hap.Service; @@ -28,17 +25,32 @@ class SecuritySystemAccessory { this.name = config.name; this.version = config.version; - this._persistKey = `SecuritySystem.${NameFactory.generate(this.name)}.json`; + this._storage = storage; - securitySystemStorage.initSync({ dir: api.user.persistPath() }); - this._state = securitySystemStorage.getItemSync(this._persistKey) || { - targetState: Characteristic.SecuritySystemCurrentState.DISARMED, + const defaultValue = { + targetState: this._pickDefault(config.default), alarm: false }; + storage.retrieve(defaultValue, (error, value) => { + this._state = value; + }); + this._services = this.createServices(); } + _pickDefault(value) { + switch (value) { + case 'armed-stay': return 0; + case 'armed-away': return 1; + case 'armed-night': return 2; + case 'unarmed': return 3; + case undefined: return 3; + } + + throw new Error('Unsupported default value in configuration of security system.'); + } + getServices() { return this._services; } @@ -94,32 +106,30 @@ class SecuritySystemAccessory { _setState(value, callback) { this.log(`Change target state of ${this.name} to ${SecuritySystemStates[value]}`); - this._state.targetState = value; - this._persist(callback); + + const data = clone(this._state); + data.targetState = value; + this._persist(data, callback); } _setAlarm(value, callback) { this.log(`Change alarm state of ${this.name} to ${value}`); - this._state.alarm = value; - this._persist(callback); + + const data = clone(this._state); + data.alarm = value; + this._persist(data, callback); } - _persist(callback) { - securitySystemStorage.setItem(this._persistKey, this._state, (error, result) => { + _persist(data, callback) { + this._storage.store(data, (error) => { if (error) { callback(error); return; } - securitySystemStorage.persistKey(this._persistKey, (error) => { - if (error) { - callback(error); - return; - } - - this._updateCurrentState(); - callback(); - }); + this._state = data; + this._updateCurrentState(); + callback(); }); } diff --git a/src/SwitchAccessory.js b/src/SwitchAccessory.js index 29b137c..2cbd34a 100644 --- a/src/SwitchAccessory.js +++ b/src/SwitchAccessory.js @@ -1,12 +1,9 @@ "use strict"; const version = require('../package.json').version; -const securitySystemStorage = require('node-persist').create(); - +const clone = require('clone'); const inherits = require('util').inherits; -const NameFactory = require('./util/NameFactory'); - let Accessory, Characteristic, Service; const SwitchStates = [ @@ -16,7 +13,7 @@ const SwitchStates = [ class SwitchAccessory { - constructor(api, log, config) { + constructor(api, log, config, storage) { Accessory = api.hap.Accessory; Characteristic = api.hap.Characteristic; Service = api.hap.Service; @@ -25,10 +22,16 @@ class SwitchAccessory { this.name = config.name; this.version = config.version; - this._state = { - state: false + this._storage = storage; + + const defaultValue = { + state: config.default === undefined ? false : config.default }; + storage.retrieve(defaultValue, (error, value) => { + this._state = value; + }); + this._services = this.createServices(); } @@ -79,10 +82,25 @@ class SwitchAccessory { } _setState(value, callback) { - this.log(`Change target state of ${this.name} to ${SwitchStates[value]}`); - this._state.targetState = value; - callback(); + this.log(`Change target state of ${this.name} to ${value}`); + + const data = clone(this._state); + data.state = value; + + this._persist(data, callback); + } + + _persist(data, callback) { + this._storage.store(data, (error) => { + if (error) { + callback(error); + return; + } + + this._state = data; + callback(); + }); } } -module.exports = SwitchAccessory; \ No newline at end of file +module.exports = SwitchAccessory; diff --git a/src/index.js b/src/index.js index f356128..9dec30f 100644 --- a/src/index.js +++ b/src/index.js @@ -8,6 +8,9 @@ const SecuritySystemAccessory = require('./SecuritySystemAccessory'); const LockMechanismAccessory = require('./LockMechanismAccessory'); const SwitchAccessory = require('./SwitchAccessory'); +const StorageWrapper = require('./util/StorageWrapper'); +const FakeStorageWrapper = require('./util/FakeStorageWrapper'); + const HomeKitTypes = require('./HomeKitTypes'); @@ -70,34 +73,58 @@ const AutomationSwitchesPlatform = class { return; } - const accessory = factory(sw); + const storage = this._createStorage(sw); + + const accessory = factory(sw, storage); _accessories.push(accessory); }); callback(_accessories); } - _createAutomationSwitch(sw) { + _createStorage(sw) { + if (this._shouldStoreSwitchState(sw)) { + const type = this._sanitizeTypeForStorage(sw.type); + return new StorageWrapper(this.api, type, sw.name); + } + + return new FakeStorageWrapper(); + } + + _shouldStoreSwitchState(sw) { + return sw.stored === true + || (sw.type === 'security' && sw.stored !== false); + } + + _sanitizeTypeForStorage(type) { + if (type === 'security') { + type = 'SecuritySystem'; + } + + return type; + } + + _createAutomationSwitch(sw, storage) { // Make sure minimal configuration is set sw.autoOff = typeof sw.autoOff !== "undefined" ? sw.autoOff : true; sw.period = sw.period || 60; sw.version = version; - return new AutomationSwitchAccessory(this.api, this.log, sw); + return new AutomationSwitchAccessory(this.api, this.log, sw, storage); } - _createSecuritySwitch(sw) { + _createSecuritySwitch(sw, storage) { sw.version = version; - return new SecuritySystemAccessory(this.api, this.log, sw); + return new SecuritySystemAccessory(this.api, this.log, sw, storage); } - _createLockMechanism(sw) { + _createLockMechanism(sw, storage) { sw.version = version; - return new LockMechanismAccessory(this.api, this.log, sw); + return new LockMechanismAccessory(this.api, this.log, sw, storage); } - _createSwitch(sw) { + _createSwitch(sw, storage) { sw.version = version; - return new SwitchAccessory(this.api, this.log, sw); + return new SwitchAccessory(this.api, this.log, sw, storage); } } \ No newline at end of file diff --git a/src/util/FakeStorageWrapper.js b/src/util/FakeStorageWrapper.js new file mode 100644 index 0000000..1dc3fd0 --- /dev/null +++ b/src/util/FakeStorageWrapper.js @@ -0,0 +1,13 @@ +"use strict"; + +class FakeStorageWrapper { + store(value, callback) { + callback(undefined); + } + + retrieve(defaultValue, callback) { + callback(undefined, defaultValue); + } +}; + +module.exports = FakeStorageWrapper; diff --git a/src/util/StorageWrapper.js b/src/util/StorageWrapper.js new file mode 100644 index 0000000..a68413b --- /dev/null +++ b/src/util/StorageWrapper.js @@ -0,0 +1,43 @@ +"use strict"; + +const Storage = require('node-persist').create(); +const NameFactory = require('./NameFactory'); + +class StorageWrapper { + constructor(api, log, type, name) { + this._key = `${type}.${NameFactory.generate(name)}.json`; + log(`Switch ${name} is stored in file ${this._key}`); + + Storage.initSync({ dir: api.user.persistPath() }); + } + + store(value, callback) { + Storage.setItem(this._key, value, (error, result) => { + if (error) { + callback(error); + return; + } + + Storage.persistKey(this._key, (error) => { + callback(error); + }); + }); + } + + retrieve(defaultValue, callback) { + Storage.getItem(this._key, (error, data) => { + if (error) { + callback(error, defaultValue); + return; + } + + if (data === undefined) { + data = defaultValue; + } + + callback(undefined, data); + }); + } +}; + +module.exports = StorageWrapper;