diff --git a/README.md b/README.md index 620cc5b..a05b88d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ This app adds support for the Sensative Strips family of products in Homey. Release notes ------------- +**2.0.1:** +* Fixed a number of issues with Strips Drip and Strips Comfort **2.0.0:** * Support for notification command class for Strips Guard. diff --git a/app.json b/app.json index bbff8d5..2fa8a74 100644 --- a/app.json +++ b/app.json @@ -18,8 +18,8 @@ "large": "/assets/images/large.png", "small": "/assets/images/small.png" }, - "version": "2.0.0", - "compatibility": "1.x >=1.5.0", + "version": "2.0.1", + "compatibility": ">=1.5.0", "tags": { "en": [ "security", @@ -111,12 +111,20 @@ "settings": [ { "id": "report_type", - "value": "1", - "type": "dropdown", "zwave": { "index": 1, "size": 1 }, + "label": { + "en": "Notification type", + "nl": "Notificatietype" + }, + "hint": { + "en": "Z-Wave command class used for reporting contact sensor events. Notification is typically best.", + "nl": "Z-Wave command class die gebruikt wordt voor rapportages van de contactsensor. Notification heeft meestal de voorkeur." + }, + "type": "dropdown", + "value": "1", "values": [ { "id": "0", @@ -132,20 +140,10 @@ "nl": "Notification" } } - ], - "label": { - "en": "Notification type", - "nl": "Notificatietype" - }, - "hint": { - "en": "Z-Wave command class used for reporting contact sensor events. Notification is typically best.", - "nl": "Z-Wave command class die gebruikt wordt voor rapportages van de contactsensor. Notification heeft meestal de voorkeur." - } + ] }, { "id": "led_indication", - "type": "checkbox", - "value": true, "zwave": { "index": 2, "size": 1 @@ -157,7 +155,9 @@ "hint": { "en": "When enabled, the LED on Strips will flash shortly when the door/window is opened.", "nl": "Wanneer ingeschakeld zal de led op Strips kort oplichten als de deur of het raam wordt geopened." - } + }, + "type": "checkbox", + "value": true } ] }, @@ -205,186 +205,292 @@ }, "settings": [ { - "id": 10, - "value": 40000, - "label": { - "en": "High ambient light report level", - "nl": "High ambient light report level" + "id": "led_indication", + "zwave": { + "index": 2, + "size": 1 }, - "hint": { - "en": "3 to 64000", - "nl": "3 tot 64000" + "label": { + "en": "LED alarm event reporting", + "nl": "LED alarm event rapportage" }, - "_size": 4, - "type": "number" + "type": "checkbox", + "value": true }, { - "id": 11, - "value": 5000, + "id": "reporting_frequency", + "zwave": { + "index": 3, + "size": 1 + }, "label": { - "en": "Low ambient light report level (Must be significantly lower than parameter 10)", - "nl": "Low ambient light report level (Must be significantly lower than parameter 10)" + "en": "Temperature & Light reporting frequency", + "nl": "Temperatuur & Light rapportage frequency" }, "hint": { - "en": "1 to 42000", - "nl": "1 tot 42000" + "en": "Refer to the Sensative support site for details on how this affects the accuracy, number of reports and battery life", + "nl": "Kijk op de Sensative support site voor informatie over hoe dit accuraatheid, aantal rapportages en levensduur van de batterij beïnvloedt" }, - "_size": 4, - "type": "number" + "type": "dropdown", + "value": "1", + "values": [ + { + "id": "1", + "label": { + "en": "Normal", + "nl": "Normaal" + } + }, + { + "id": "2", + "label": { + "en": "Frequent", + "nl": "Frequent" + } + } + ] }, { - "id": 12, - "value": true, + "id": "temperature_reporting", + "zwave": { + "index": 4, + "size": 1 + }, "label": { - "en": "Leakage alarm", - "nl": "Lekkage-alarm" + "en": "Temperature reporting", + "nl": "Temperatuur rapportage" }, "hint": { - "en": "0: Off\r\n1: On", - "nl": "0: Uit\r\n1: Aan" + "en": "Does not affect temperature alarms", + "nl": "Heeft geen invloed op temperatuuralarmering" }, - "_size": 1, - "type": "checkbox" + "type": "checkbox", + "value": true }, { - "id": 13, - "value": 10, + "id": "temperature_unit", + "zwave": { + "index": 5, + "size": 1 + }, "label": { - "en": "Leakage alarm level", - "nl": "Lekkage alarm level" + "en": "Temperature reporting unit", + "nl": "Temperatuur rapportage-eenheid" }, "hint": { - "en": "1 to 100\r\n(1= almost dry, 100= wet)", - "nl": "1 tot 100\r\n(1= Bijna droog, 100= nat)" + "en": "This has no effect on how Homey displays the temperature", + "nl": "Dit heeft geen invloed op hoe Homey de temperatuur weergeeft" }, - "_size": 1, - "type": "number" + "type": "dropdown", + "value": "0", + "values": [ + { + "id": "0", + "label": { + "en": "Celcius", + "nl": "Celcius" + } + }, + { + "id": "1", + "label": { + "en": "Fahrenheit", + "nl": "Fahrenheit" + } + } + ] }, { - "id": 14, - "value": 0, - "label": { - "en": "Moisture reporting period", - "nl": "Vochtigheid rapportageperiode" + "id": "temperature_alarms", + "zwave": { + "index": 6, + "size": 1 }, - "hint": { - "en": "0-120: Number of hours between moisture reports", - "nl": "0-120: Aantal uren tussen vochtigheidsrapportage" + "label": { + "en": "Temperature alarms", + "nl": "Temperatuuralarmering" }, - "_size": 1, - "type": "number" + "type": "checkbox", + "value": false }, { - "id": 2, - "value": true, + "id": "temperature_high", + "zwave": { + "index": 7, + "size": 1 + }, "label": { - "en": "LED alarm event reporting", - "nl": "LED alarm event rapportage" + "en": "High temperature alarm level", + "nl": "Hoge temperatuur alarm niveau" }, "hint": { - "en": "0: Off\r\n1: On", - "nl": "0: Uit\r\n1: Aan" + "en": "-20 to +60 degrees Celcius", + "nl": "-20 tot +60 graden Celsius" }, - "_size": 1, - "type": "checkbox" + "type": "number", + "value": 60, + "attr": { + "min": -20, + "max": 60 + } }, { - "id": 3, - "value": 1, + "id": "temperature_low", + "zwave": { + "index": 8, + "size": 1 + }, "label": { - "en": "Temperature & Light reporting frequency", - "nl": "Temperatuur & Light rapportage frequency" + "en": "Low temperature alarm level", + "nl": "Lage temperatuur alarm niveau" }, "hint": { - "en": "1: Normal\r\n2: Frequent", - "nl": "1: Normaal\r\n2: Frequent" + "en": "-20 to +60 degrees Celcius", + "nl": "-20 tot +60 graden Celcius" }, - "_size": 1, - "type": "number" + "type": "number", + "value": -20, + "attr": { + "min": -20, + "max": 60 + } }, { - "id": 4, - "value": true, + "id": "light_reporting", + "zwave": { + "index": 9, + "size": 1 + }, + "value": "1", "label": { - "en": "Temperature reporting (Does not affect temperature alarms)", - "nl": "Temperatuur rapportage (Does not affect temperature alarms)" + "en": "Ambient light reporting", + "nl": "Omgevingslicht rapportage" }, "hint": { - "en": "0: Off\r\n1: On", - "nl": "0: Uit\r\n1: Aan" + "en": "Bounds are configured by high and low ambient light report level.", + "nl": "Bounds are configured by high and low ambient light report level." }, - "_size": 1, - "type": "checkbox" + "type": "dropdown", + "values": [ + { + "id": "0", + "label": { + "en": "Off", + "nl": "Uit" + } + }, + { + "id": "1", + "label": { + "en": "On", + "nl": "Aan" + } + }, + { + "id": "2", + "label": { + "en": "Only outside configured bounds", + "nl": "Alleen buiten ingestelde grenzen" + } + } + ] }, { - "id": 5, - "value": true, + "id": "light_high", + "zwave": { + "index": 10, + "size": 4 + }, "label": { - "en": "Temperature reporting unit", - "nl": "Temperatuur rapportage eenheid" + "en": "High ambient light report level", + "nl": "High ambient light report level" }, "hint": { - "en": "0: Celcius\r\n1: Fahrenheit", - "nl": "0: Celcius\r\n1: Fahrenheit" + "en": "3 to 64000", + "nl": "3 tot 64000" }, - "_size": 1, - "type": "checkbox" + "type": "number", + "value": 40000, + "attr": { + "min": 3, + "max": 64000 + } }, { - "id": 6, - "value": false, + "id": "light_low", + "zwave": { + "index": 11, + "size": 4 + }, "label": { - "en": "Temperature alarms", - "nl": "temperatuur alarms" + "en": "Low ambient light report level", + "nl": "Low ambient light report level" }, "hint": { - "en": "0: Off\r\n1: On", - "nl": "0: Uit\r\n1: Aan" + "en": "1 to 42000. Must be significantly lower than high ambient light report level.", + "nl": "1 tot 42000. Must be significantly lower than high ambient light report level." }, - "_size": 1, - "type": "checkbox" + "type": "number", + "value": 5000, + "attr": { + "min": 1, + "max": 42000 + } }, { - "id": 7, - "value": 60, - "label": { - "en": "High Temperature alarm level", - "nl": "Hoge Temperatuur alarm niveau" + "id": "leakage_alarm", + "zwave": { + "index": 12, + "size": 1 }, - "hint": { - "en": "-20 to +60 degrees Celcius", - "nl": "-20 tot +60 graden Celsius" + "label": { + "en": "Leakage alarm", + "nl": "Lekkage-alarm" }, - "_size": 1, - "type": "number" + "type": "checkbox", + "value": true }, { - "id": 8, - "value": -20, + "id": "leakage_alarm_level", + "zwave": { + "index": 13, + "size": 1 + }, "label": { - "en": "Low Temperature alarm level", - "nl": "Laag Temperatuur alarm nivo" + "en": "Leakage alarm level", + "nl": "Lekkage-alarm level" }, "hint": { - "en": "-20 to +60 degrees Celcius", - "nl": "-20 tot +60 graden Celcius" + "en": "1 to 100\r\n(1=almost dry, 100=wet)", + "nl": "1 tot 100\r\n(1=Bijna droog, 100=nat)" }, - "_size": 1, - "type": "number" + "type": "number", + "value": 10, + "attr": { + "min": 1, + "max": 100 + } }, { - "id": 9, - "value": 1, + "id": "humidity_reporting_interval", + "zwave": { + "index": 14, + "size": 1 + }, "label": { - "en": "Ambient light reporting", - "nl": "Omgevingslicht rapportage" + "en": "Moisture reporting period", + "nl": "Vochtigheid rapportageperiode" }, "hint": { - "en": "0: Off\r\n1: On\r\n2: Report only when levels defined in parameter 10 & 11 are passed.", - "nl": "0: Uit\r\n1: Aan\r\n2: Report only when levels defined in parameter 10 & 11 are passed." + "en": "0-120: Number of hours between moisture reports (0=off)", + "nl": "0-120: Aantal uren tussen vochtigheidsrapportage (0=uit)" }, - "_size": 1, - "type": "number" + "type": "number", + "value": 0, + "attr": { + "min": 0, + "max": 120 + } } ] } diff --git a/drivers/1101011/device.js b/drivers/1101011/device.js index 2ed2442..8da6621 100644 --- a/drivers/1101011/device.js +++ b/drivers/1101011/device.js @@ -30,7 +30,7 @@ class StripsMaZw extends ZwaveDevice { const settings = this.getSettings(); this.registerAlarmContactCapability(settings.report_type); - this.registerCapability('measure_battery', 'BATTERY'); + this.registerCapability('measure_battery', 'BATTERY', { getOpts: { getOnOnline: true } }); this.registerOptionalCapabilities(); } diff --git a/drivers/1102011/device.js b/drivers/1102011/device.js index fa400ad..c90ecf5 100644 --- a/drivers/1102011/device.js +++ b/drivers/1102011/device.js @@ -2,17 +2,71 @@ const ZwaveDevice = require('homey-meshdriver').ZwaveDevice; +function luminanceReportParser(report) { + const isLuminanceReport = + report && + report.hasOwnProperty('Sensor Type') && + report.hasOwnProperty('Sensor Value (Parsed)') && + report['Sensor Type'] === 'Luminance (version 1)'; + + if (!isLuminanceReport) return null; + + const sensorValue = report['Sensor Value (Parsed)']; + + if (sensorValue < 0) { + // Early firmwares of Strips Comfort mistakenly use a 16-bit integer to represent the luminance value. + // Z-Wave only supports signed integers, but the value was intended as unsigned. + // This should work around that issue, since a lux value can never be negative anyway. + return 65536 + sensorValue; + } + + return sensorValue; +} + class StripsMultiSensor extends ZwaveDevice { - onMeshInit() { - //this.enableDebug(); - //this.printNode(); - this.registerCapability('measure_temperature', 'SENSOR_MULTILEVEL'); - this.registerCapability('measure_luminance', 'SENSOR_MULTILEVEL'); - this.registerCapability('measure_moisture', 'SENSOR_MULTILEVEL'); - this.registerCapability('alarm_heat', 'NOTIFICATION'); - this.registerCapability('alarm_water', 'NOTIFICATION'); - this.registerCapability('alarm_battery', 'BATTERY'); - this.registerCapability('measure_battery', 'BATTERY'); - } + onMeshInit() { + this.registerCapability('measure_temperature', 'SENSOR_MULTILEVEL', { + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('measure_luminance', 'SENSOR_MULTILEVEL', { + reportParser: luminanceReportParser, + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('measure_humidity', 'SENSOR_MULTILEVEL', { + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('alarm_heat', 'NOTIFICATION', { + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('alarm_water', 'NOTIFICATION', { + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('alarm_battery', 'BATTERY', { + getOpts: { + getOnOnline: true, + }, + }); + + this.registerCapability('measure_battery', 'BATTERY', { + getOpts: { + getOnOnline: true, + }, + }); + } } module.exports = StripsMultiSensor; \ No newline at end of file diff --git a/node_modules/homey-log/.npmignore b/node_modules/homey-log/.npmignore deleted file mode 100644 index bb5c8c1..0000000 --- a/node_modules/homey-log/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -/node_modules -.idea \ No newline at end of file diff --git a/node_modules/homey-log/build/.npmignore b/node_modules/homey-log/build/.npmignore deleted file mode 100644 index bb5c8c1..0000000 --- a/node_modules/homey-log/build/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -/node_modules -.idea \ No newline at end of file diff --git a/node_modules/homey-log/build/CONTRIBUTING.md b/node_modules/homey-log/build/CONTRIBUTING.md deleted file mode 100644 index 1319dd9..0000000 --- a/node_modules/homey-log/build/CONTRIBUTING.md +++ /dev/null @@ -1,53 +0,0 @@ -# Contributing to Athom and Homey - -First off all, thank you for taking the time to contribute! - -The following is a set of guidelines for contributing to Athom and its packages, which are hosted in the [Athom Organization](https://github.com/athombv) on GitHub. These are just guidelines, not rules. Use your best judgment, and feel free to contact us if you have any questions. - -Please join our [community slack](https://slack.athom.com), if you have not done so already. -We also have a [forum](https://forum.athom.com) for general discussions. - - -## Before submitting a bug or feature request - -* **Have you actually read the error message**? -* Have you searched for similar issues? -* Have you updated homey, all apps, and the development tools (if applicable)? -* Have you checked that it's not a problem with one of the apps you're using, rather than Homey itself? -* Have you looked at what's involved in fixing/implementing this? - -Capable programmers should always attempt to investigate and fix problems themselves before asking for others to help. Submit a pull request instead of an issue! - -Regular support is provided through our [support staff](support@athom.com). - -## A great bug report contains - -* Context – what were you trying to achieve? -* Detailed steps to reproduce the error from scratch. Try isolating the minimal amount of code needed to reproduce the error. -* Any applicable log files or ID's. -* Evidence you've looked into solving the problem and ideally, a theory on the cause and a possible solution. - -## A great feature request contains - -* The current situation. -* How and why the current situation is problematic. -* A detailed proposal or pull request that demonstrates how the problem could be solved. -* A use case – who needs this feature and why? -* Any caveats. - -## A great pull request contains - -* Minimal changes. Only submit code relevant to the current issue. Other changes should go in new pull requests. -* Minimal commits. Please squash to a single commit before sending your pull request. -* No conflicts. Please rebase off the latest master before submitting. -* Code conforming to the existing conventions and formats. i.e. Please don't reformat whitespace. -* Passing tests in the test folder (if applicable). Use existing tests as a reference. -* Relevant documentation. - -## Speeding up your pull request -Merging pull requests takes time. While we always try to merge your pull request as soon as possible, there are certain things you can do to speed up this process. - -* Ask developers to review your code changes and post their feedback. -* Ask users to test your changes and post their feedback. -* Keep your changes to the minimal required amount, and dedicated to one issue/feature only. -* If your PR introduces new features or more than just a small fix, please sign our [Contributor License Agreement](https://go.athom.com/cla). diff --git a/node_modules/homey-log/build/LICENSE b/node_modules/homey-log/build/LICENSE deleted file mode 100644 index 9cecc1d..0000000 --- a/node_modules/homey-log/build/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - {one line to give the program's name and a brief idea of what it does.} - Copyright (C) {year} {name of author} - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - {project} Copyright (C) {year} {fullname} - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/node_modules/homey-log/build/README.md b/node_modules/homey-log/build/README.md deleted file mode 100644 index 0194c2d..0000000 --- a/node_modules/homey-log/build/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# homey-log - -This module can be plugged into a Homey app to send logs to [Sentry](http://sentry.io/). - -## Installation - -``` -npm install homey-log -``` - -## Usage - -In your `env.json`, add your Sentry URL: - -```javascript -{ - "HOMEY_LOG_URL": "https://foo:bar@sentry.io/123456" -} -``` - -In your app.js, include the library: - -```javascript -const Log = require('homey-log').Log; -... -throw new Error("Whoops"); -``` - -### Notes - -* When your app crashes due to an uncaughtException, this will automatically be sent to Sentry. -* As of Homey v1.0.3, when running your app using `athom project --run`, logging is disabled. - -### Methods - -#### Log.init( String url ); -Set the URL manually, when not provided using your `env.json`. Not recommended due to security! - -#### Log.setTags( Object tags ); -Set a custom object of 'tags'. Tags that are already set are `appId`, `appVersion` and `homeyVersion` - -#### Log.setExtra( Object extra ); -Set a custom object of 'extra' parameters - -#### Log.setUser( Object user ); -Set a custom object of 'user' data - do not include sensitive data! - -#### Log.captureMessage( String message ) -Send a message to Sentry - -#### Log.captureException( Error err ) -Send an Error object to Sentry diff --git a/node_modules/homey-log/build/index.js b/node_modules/homey-log/build/index.js deleted file mode 100644 index d948f55..0000000 --- a/node_modules/homey-log/build/index.js +++ /dev/null @@ -1,3 +0,0 @@ -'use strict'; - -module.exports.Log = require('./lib/Log.js'); \ No newline at end of file diff --git a/node_modules/homey-log/build/lib/Log.js b/node_modules/homey-log/build/lib/Log.js deleted file mode 100644 index 6003da4..0000000 --- a/node_modules/homey-log/build/lib/Log.js +++ /dev/null @@ -1,143 +0,0 @@ -'use strict'; - -const raven = require('raven'); - -class Log { - - constructor() { - - this._capturedExceptions = []; - this._capturedMessages = []; - - if( typeof global.Homey === 'undefined' ) { - try { - this._homey = require('homey'); - } catch( err ) { - console.error(err) - return console.error('Error: Homey not found'); - } - } else { - this._homey = global.Homey; - } - - if( typeof this._homey === 'undefined' ) - return console.error('Error: Homey not found'); - - if( typeof this._homey.env.HOMEY_LOG_URL === 'string' ) { - this.init( this._homey.env.HOMEY_LOG_URL ); - } - - } - - _log() { - console.log.bind( null, Log.logTime(), '[homey-log]' ).apply( null, arguments ); - } - - init( url, opts ) { - - if( process.env.DEBUG === '1' && this._homey.env.HOMEY_LOG_FORCE !== '1' ) - return this._log('App is running in debug mode, disabling log'); - - this._client = new raven.Client( url, opts ); - - this._client.patchGlobal(); - - this.setTags({ - appId : this._homey.manifest.id, - appVersion : this._homey.manifest.version, - homeyVersion : this._homey.version - }); - - this._log(`App ${this._homey.manifest.id} v${this._homey.manifest.version} logging...`); - - return this; - - } - - setTags( tags ) { - if( this._client ) { - this._client.setTagsContext(tags); - } - - return this; - } - - setExtra( extra ) { - if( this._client ) { - this._client.setExtraContext(extra); - } - - return this; - } - - setUser( user ) { - if( this._client ) { - this._client.setUserContext(user); - } - - return this; - } - - captureMessage( message, options, callback) { - this._log('captureMessage:', message); - - if( this._capturedMessages.indexOf( message ) > -1 ) { - this._log('Prevented sending a duplicate message'); - return this; - } - - this._capturedMessages.push( message ) - - if( this._client ) { - this._client.captureMessage( - message, - options && options.constructor.name === 'Object' ? Object.assign({}, options) : options, - callback - ); - } - - return this; - } - - captureException( err, options, callback) { - this._log('captureException:', err); - - if( this._capturedExceptions.indexOf( err ) > -1 ) { - this._log('Prevented sending a duplicate log'); - return this; - } - - this._capturedExceptions.push( err ) - - if( this._client ) { - this._client.captureException( - err, - options && options.constructor.name === 'Object' ? Object.assign({}, options) : options, - callback - ); - } - - return this; - } - - static logTime() { - - let date = new Date(); - - let mm = date.getMonth()+1; - mm = (mm<10?"0"+mm:mm); - let dd = date.getDate(); - dd = (dd<10?"0"+dd:dd); - let hh = date.getHours(); - hh = (hh<10?"0"+hh:hh); - let min = date.getMinutes(); - min = (min<10?"0"+min:min); - let sec = date.getSeconds(); - sec = (sec<10?"0"+sec:sec); - - return `${date.getFullYear()}-${mm}-${dd} ${hh}:${min}:${sec}`; - } - -} - -module.exports = new Log(); diff --git a/node_modules/homey-log/build/package.json b/node_modules/homey-log/build/package.json deleted file mode 100644 index 65fd00d..0000000 --- a/node_modules/homey-log/build/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "homey-log", - "version": "1.0.4", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://WeeJewel@github.com/athombv/node-homey-log.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/athombv/node-homey-log/issues" - }, - "homepage": "https://github.com/athombv/node-homey-log#readme", - "dependencies": { - "raven": "^0.12.3" - }, - "config": { - "npmPublishTagProduction": "latest", - "npmPublishTagStaging": "beta" - } -} diff --git a/node_modules/homey-log/lib/Log.js b/node_modules/homey-log/lib/Log.js index 6003da4..964376b 100644 --- a/node_modules/homey-log/lib/Log.js +++ b/node_modules/homey-log/lib/Log.js @@ -8,7 +8,7 @@ class Log { this._capturedExceptions = []; this._capturedMessages = []; - + if( typeof global.Homey === 'undefined' ) { try { this._homey = require('homey'); @@ -22,7 +22,7 @@ class Log { if( typeof this._homey === 'undefined' ) return console.error('Error: Homey not found'); - + if( typeof this._homey.env.HOMEY_LOG_URL === 'string' ) { this.init( this._homey.env.HOMEY_LOG_URL ); } @@ -34,7 +34,7 @@ class Log { } init( url, opts ) { - + if( process.env.DEBUG === '1' && this._homey.env.HOMEY_LOG_FORCE !== '1' ) return this._log('App is running in debug mode, disabling log'); @@ -48,12 +48,24 @@ class Log { homeyVersion : this._homey.version }); + if (this._homey.hasOwnProperty('ManagerCloud')) { // SDKv2 + this._homey.ManagerCloud.getHomeyId(this.setHomeyIdTag.bind(this)) + } else if (this._homey.hasOwnProperty('manager')) { // SDKv1 + this._homey.manager('cloud').getHomeyId(this.setHomeyIdTag.bind(this)) + } + this._log(`App ${this._homey.manifest.id} v${this._homey.manifest.version} logging...`); return this; } + setHomeyIdTag(err, result) { + if (!err && typeof result === 'string') { + this.setTags({ homeyId: result }) + } + } + setTags( tags ) { if( this._client ) { this._client.setTagsContext(tags); diff --git a/node_modules/homey-log/package.json b/node_modules/homey-log/package.json index 40d7934..2e51f8f 100644 --- a/node_modules/homey-log/package.json +++ b/node_modules/homey-log/package.json @@ -1,8 +1,8 @@ { "_from": "homey-log@^1.0.1", - "_id": "homey-log@1.0.5", + "_id": "homey-log@1.0.6", "_inBundle": false, - "_integrity": "sha1-k2RU5iM5u3wxO3+m5htZasohSL8=", + "_integrity": "sha512-LFOaByu/E2krWNSGwmoazL4HCSSyLVQMoaBo9mgMxV2YC4lEB15SMov1bqZxUclEJCbnR+YUX9xsnFW5brmIbw==", "_location": "/homey-log", "_phantomChildren": {}, "_requested": { @@ -16,10 +16,11 @@ "fetchSpec": "^1.0.1" }, "_requiredBy": [ + "#USER", "/" ], - "_resolved": "https://registry.npmjs.org/homey-log/-/homey-log-1.0.5.tgz", - "_shasum": "936454e62339bb7c313b7fa6e61b596aca2148bf", + "_resolved": "https://registry.npmjs.org/homey-log/-/homey-log-1.0.6.tgz", + "_shasum": "228e5b126b7dd68e5485b0d41f9fef2d7a46024f", "_spec": "homey-log@^1.0.1", "_where": "D:\\Projects\\Homey\\com.sensative", "author": "", @@ -47,5 +48,5 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "version": "1.0.5" + "version": "1.0.6" } diff --git a/node_modules/homey-meshdriver/assets/driver/zigbee/device.js b/node_modules/homey-meshdriver/assets/driver/zigbee/device.js index 285ef2f..c2913d1 100644 --- a/node_modules/homey-meshdriver/assets/driver/zigbee/device.js +++ b/node_modules/homey-meshdriver/assets/driver/zigbee/device.js @@ -1,13 +1,13 @@ 'use strict'; -const ZwaveDevice = require('homey-meshdriver').ZigBeeDevice; +const { ZigBeeDevice } = require('homey-meshdriver'); -class MyDevice extends ZigBeeDevice { +class MyZigBeeDevice extends ZigBeeDevice { onMeshInit() { - this.log('MyDevice has been inited'); + this.log('MyZigBeeDevice has been inited'); } } -module.exports = MyDevice; \ No newline at end of file +module.exports = MyZigBeeDevice; diff --git a/node_modules/homey-meshdriver/assets/driver/zwave/device.js b/node_modules/homey-meshdriver/assets/driver/zwave/device.js index 35ecace..ec6fc72 100644 --- a/node_modules/homey-meshdriver/assets/driver/zwave/device.js +++ b/node_modules/homey-meshdriver/assets/driver/zwave/device.js @@ -1,13 +1,13 @@ 'use strict'; -const ZwaveDevice = require('homey-meshdriver').ZwaveDevice; +const { ZwaveDevice } = require('homey-meshdriver'); -class MyDevice extends ZwaveDevice { +class MyZWaveDevice extends ZwaveDevice { onMeshInit() { - this.log('MyDevice has been inited'); + this.log('MyZWaveDevice has been inited'); } } -module.exports = MyDevice; \ No newline at end of file +module.exports = MyZWaveDevice; diff --git a/node_modules/homey-meshdriver/index.js b/node_modules/homey-meshdriver/index.js index efebc02..0b08377 100644 --- a/node_modules/homey-meshdriver/index.js +++ b/node_modules/homey-meshdriver/index.js @@ -5,6 +5,7 @@ module.exports.Util = require('./lib/util'); module.exports.ZwaveDevice = require('./lib/zwave/ZwaveDevice.js'); module.exports.ZwaveLockDevice = require('./lib/zwave/ZwaveLockDevice.js'); module.exports.ZwaveMeteringDevice = require('./lib/zwave/ZwaveMeteringDevice.js'); +module.exports.ZwaveLightDevice = require('./lib/zwave/ZwaveLightDevice.js'); module.exports.ZigBeeDevice = require('./lib/zigbee/ZigBeeDevice.js'); module.exports.ZigBeeLightDevice = require('./lib/zigbee/ZigBeeLightDevice.js'); diff --git a/node_modules/homey-meshdriver/lib/MeshDevice.js b/node_modules/homey-meshdriver/lib/MeshDevice.js index 027103c..439995d 100644 --- a/node_modules/homey-meshdriver/lib/MeshDevice.js +++ b/node_modules/homey-meshdriver/lib/MeshDevice.js @@ -88,11 +88,13 @@ class MeshDevice extends Homey.Device { if (this.node) this.node.removeAllListeners(); // Clear all pollIntervals - Object.keys(this._pollIntervals).forEach(capabilityId => { - Object.values(this._pollIntervals[capabilityId]).forEach(interval => { - clearInterval(interval); + if (this._pollIntervals) { // Sometimes it is null/undefined for some reason + Object.keys(this._pollIntervals).forEach(capabilityId => { + Object.values(this._pollIntervals[capabilityId]).forEach(interval => { + clearInterval(interval); + }); }); - }); + } } } diff --git a/node_modules/homey-meshdriver/lib/util/index.js b/node_modules/homey-meshdriver/lib/util/index.js index f9669c1..ca5a14b 100644 --- a/node_modules/homey-meshdriver/lib/util/index.js +++ b/node_modules/homey-meshdriver/lib/util/index.js @@ -37,6 +37,28 @@ function calculateZwaveDimDuration(duration) { return 254; } +/** + * Calculate a transtime value for ZigBee clusters, it takes two parameters, opts and settings. Opts is the opts object + * provided by a capbilityListener which can hold a duration property (in miliseconds), settings is an object which can + * hold a 'transition_time' property (in seconds). If none are available, the default is 0. The valid value range is + * 0 - 6553. + * @param opts {object} + * @param opts.duration {number} - Duration property in miliseconds (preferred over 'transition_time') + * @param settings {object} + * @param settings.transition_time {number} - Transition time property in seconds + * @returns {number} + */ +function calculateZigBeeDimDuration(opts = {}, settings = {}) { + let transtime = 0; + if (opts.hasOwnProperty('duration')) { + transtime = opts.duration / 100; + } else if (typeof settings.transition_time === 'number') { + transtime = Math.round(settings.transition_time * 10); + } + // Cap the range between 0 and 6553 + return Math.max(Math.min(transtime, 6553), 0); +} + /** * Utility class with several color and range conversion methods. @@ -49,4 +71,5 @@ module.exports = { convertRGBToHSV: color.convertRGBToHSV, mapValueRange, calculateZwaveDimDuration, + calculateZigBeeDimDuration, }; diff --git a/node_modules/homey-meshdriver/lib/zigbee/ZigBeeDevice.js b/node_modules/homey-meshdriver/lib/zigbee/ZigBeeDevice.js index 97c4006..446d379 100644 --- a/node_modules/homey-meshdriver/lib/zigbee/ZigBeeDevice.js +++ b/node_modules/homey-meshdriver/lib/zigbee/ZigBeeDevice.js @@ -11,6 +11,10 @@ const i18n = { en: 'Unknown error', nl: 'Onbekend probleem', }, + could_not_reach_device: { + en: 'Could not reach device', + nl: 'Kon apparaat niet bereiken', + }, invalid_ieeeaddr: { en: 'Device not found in network', nl: 'Apparaat niet gevonden in netwerk', @@ -240,7 +244,7 @@ class ZigBeeDevice extends MeshDevice { return cluster.do(commandId, parsedPayload) .catch(err => { this.error(`Error: could not perform ${commandId} on ${capabilitySetObj.clusterId}`, err); - throw err; + throw new Error(this.__(i18n.error.could_not_reach_device)); }); } catch (err) { return Promise.reject(err); @@ -279,6 +283,7 @@ class ZigBeeDevice extends MeshDevice { else if (typeof userOpts === 'undefined') userOpts = { endpoint: this.getClusterEndpoint(clusterId) }; this._capabilities[capabilityId][clusterId] = Object.assign( + {}, systemOpts || {}, userOpts || {} ); diff --git a/node_modules/homey-meshdriver/lib/zigbee/system/capabilities/dim/genLevelCtrl.js b/node_modules/homey-meshdriver/lib/zigbee/system/capabilities/dim/genLevelCtrl.js index 802c869..cf4b548 100644 --- a/node_modules/homey-meshdriver/lib/zigbee/system/capabilities/dim/genLevelCtrl.js +++ b/node_modules/homey-meshdriver/lib/zigbee/system/capabilities/dim/genLevelCtrl.js @@ -1,25 +1,21 @@ 'use strict'; +const util = require('./../../../../util'); + const maxDim = 254; module.exports = { - set: 'moveToLevel', - setParser(value) { + set: 'moveToLevelWithOnOff', + setParser(value, opts = {}) { if (value === 0) { - return this.triggerCapabilityListener('onoff', false) - .then(() => null) - .catch(err => new Error('failed_to_trigger_onoff')); + this.setCapabilityValue('onoff', false); } else if (this.getCapabilityValue('onoff') === false && value > 0) { - return this.triggerCapabilityListener('onoff', true) - .then(() => ({ - level: Math.round(value * maxDim), - transtime: this.getSetting('transition_time') ? Math.round(this.getSetting('transition_time') * 10) : 0, - })) - .catch(err => new Error('failed_to_trigger_onoff`', err)); + this.setCapabilityValue('onoff', true); } + return { level: Math.round(value * maxDim), - transtime: this.getSetting('transition_time') ? Math.round(this.getSetting('transition_time') * 10) : 0, + transtime: util.calculateZigBeeDimDuration(opts, this.getSettings()), }; }, get: 'currentLevel', diff --git a/node_modules/homey-meshdriver/lib/zwave/ZwaveDevice.js b/node_modules/homey-meshdriver/lib/zwave/ZwaveDevice.js index 92b1588..2fe253a 100644 --- a/node_modules/homey-meshdriver/lib/zwave/ZwaveDevice.js +++ b/node_modules/homey-meshdriver/lib/zwave/ZwaveDevice.js @@ -4,19 +4,19 @@ const Homey = require('homey'); const MeshDevice = require('../MeshDevice.js'); const commandClassParsers = { - NOTIFICATION: payload => require('./system/commandclasses/NOTIFICATION')(payload), - METER: payload => require('./system/commandclasses/METER')(payload), - SENSOR_ALARM: payload => require('./system/commandclasses/SENSOR_ALARM')(payload), - SENSOR_MULTILEVEL: payload => require('./system/commandclasses/SENSOR_MULTILEVEL')(payload), + NOTIFICATION: payload => require('./system/commandclasses/NOTIFICATION')(payload), + METER: payload => require('./system/commandclasses/METER')(payload), + SENSOR_ALARM: payload => require('./system/commandclasses/SENSOR_ALARM')(payload), + SENSOR_MULTILEVEL: payload => require('./system/commandclasses/SENSOR_MULTILEVEL')(payload), }; const i18n = { - settings: { - offlineNodeSaveMessage: { - en: 'Settings will be saved during the next wakeup of this battery device.', - nl: 'Instelling zullen worden opgeslagen bij volgende wakeup van dit apparaat.', - }, - }, + settings: { + offlineNodeSaveMessage: { + en: 'Settings will be saved during the next wakeup of this battery device.', + nl: 'Instelling zullen worden opgeslagen bij volgende wakeup van dit apparaat.', + }, + }, }; // TODO alarm_fire capability parser @@ -30,903 +30,954 @@ const i18n = { /** * @extends MeshDevice + * @desc {@link https://developer.athom.com/docs/apps/Device.html#getSetting Device settings} used by system capabilities: + * - `invertWindowCoveringsDirection {boolean}` - Used by several windowcoverings capabilities, if true it will invert the up/down direction + * - `invertWindowCoveringsTiltDirection {boolean}` - Used by several windowcoverings capabilities, if true it will invert the tilt direction + * @property {string} thermostatSetpointType - The 'Setpoint Type' used in the THERMOSTAT_SETPOINT commandclass for the target_temperature capability */ class ZwaveDevice extends MeshDevice { - /* - * Homey methods - */ - - /** - * @private - */ - onInit() { - super.onInit('zwave'); - - this._capabilities = {}; - this._settings = {}; - this._reportListeners = {}; - this._pollIntervals = {}; - this._pollIntervalsKeys = {}; - - this.once('__meshInit', () => { - this.log('ZwaveDevice has been inited'); - this.onMeshInit && this.onMeshInit(); - }); - } - - /** - * Remove all listeners and intervals from node - */ - onDeleted() { - super.onDeleted(); - - // Remove all report listeners on command classes - if (this.node.CommandClass) { - Object.keys(this.node.CommandClass).forEach(commandClassId => { - this.node.CommandClass[commandClassId].removeAllListeners(); - }); - } - - - // Remove all report listeners on multi channel nodes - if (this.node.MultiChannelNodes) { - Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { - Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { - this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].removeAllListeners(); - }); - }); - } - } - - /** - * Method that flattens possibly nested settings and returns a flat settings array. - * @returns {Array} - */ - getManifestSettings() { - if (!this.manifestSettings) { - const manifest = this.getDriver().getManifest(); - if (!manifest || !manifest.settings) return this.manifestSettings = []; - - const flattenSettings = (settings) => settings.reduce((manifestSettings, setting) => { - if (setting.type === 'group') { - return manifestSettings.concat(flattenSettings(setting.children)); - } - manifestSettings.push(setting); - return manifestSettings; - }, []); - - this.manifestSettings = flattenSettings(manifest.settings); - } - return this.manifestSettings; - } - - /** - * Method that refreshes the capability value once. If you want to poll this value please use - * the parameter getOpts.pollInterval at {@link ZwaveDevice#registerCapability} - * @param {String} capabilityId, the string id of the Homey capability - * @param {String} commandClassId, the Z-Wave command class used for this request - */ - refreshCapabilityValue(capabilityId, commandClassId) { - return this._getCapabilityValue(capabilityId, commandClassId); - } - - /** - * Get a specific setting object from the manifest - * @param id - Setting id to retrieve - * @returns {Object|Error} - */ - getManifestSetting(id) { - const settings = this.getManifestSettings(); - if (Array.isArray(settings)) return settings.find(setting => setting.id === id); - return new Error(`missing_setting_id_${id}`); - } - - /** - * Method that handles changing settings for Z-Wave devices. It iterates over the changed settings and executes - * a CONFIGURATION_SET in sync. If all succeed, it will resolve, if one or more fail it will reject with an error - * of concatenated error messages (to see which settings failed if more than one). - * @param oldSettings - * @param newSettings - * @param changedKeysArr - * @returns {Promise.} - */ - async onSettings(oldSettings, newSettings, changedKeysArr = []) { - let changeSettingError = ''; - - // Loop all changed settings - for (const changedKey of changedKeysArr) { - const newValue = newSettings[changedKey]; - - // check for poll interval - if (this._pollIntervalsKeys[changedKey]) { - this._setPollInterval( - this._pollIntervalsKeys[changedKey].capabilityId, - this._pollIntervalsKeys[changedKey].commandClassId, - newValue - ); - continue; - } - - // Get manifest setting object and execute configuration set - const manifestSetting = (this.getManifestSettings().find(setting => setting.id === changedKey) || {}).zwave; - - // Non z-wave settings: see if there is a function to execute, otherwise do nothing. - if (typeof manifestSetting === 'undefined') { - - if (this._settings.hasOwnProperty(changedKey)) { - const parser = this._settings[changedKey]; - - if (typeof parser === 'function') parser.call(this, newValue); - } - continue; - } - - try { - this.log(`configurationSet() -> ${changedKey}: ${newSettings[changedKey]}, ${manifestSetting.index}, ${manifestSetting.size}`); - await this.configurationSet({ - id: changedKey, - index: manifestSetting.index, - size: manifestSetting.size, - signed: (manifestSetting.hasOwnProperty('signed')) ? manifestSetting.signed : true, - }, newSettings[changedKey]); - } catch (err) { - this.error(`failed_to_set_${changedKey}_to_${newValue}_size_${manifestSetting.size}`, err); - let errorString = `${changeSettingError}failed_to_set_${changedKey}_to_${newValue}_size_${manifestSetting.size}`; - if (changeSettingError.length > 0) errorString = `_${errorString}`; - changeSettingError = errorString; - } - } - - // If one or more of the settings failed to set, reject - if (changeSettingError.length > 0) return Promise.reject(new Error(changeSettingError)); - - // Compose save message - const saveMessage = this._composeCustomSaveMessage(oldSettings, newSettings, changedKeysArr); - return Promise.resolve(saveMessage); - } - - /** - * @private - */ - _composeCustomSaveMessage(oldSettings, newSettings, changedKeysArr) { - - // Provide user with proper feedback after clicking save - let saveMessage = null; - if (this.node.battery === true && this.node.online === false) saveMessage = i18n.settings.offlineNodeSaveMessage; - if (typeof this.customSaveMessage === 'function') { - const message = this.customSaveMessage(oldSettings, newSettings, changedKeysArr); - - if (typeof message !== 'object' && typeof message !== 'string') { - this._debug('Save message\'s return value is not an object nor a string'); - } else if (typeof message === 'object' && !message.hasOwnProperty('en')) { - this._debug('A custom save message needs at least the english translation'); - } else { - saveMessage = message; - } - } else if (typeof this.customSaveMessage === 'object') { - if (!this.customSaveMessage.hasOwnProperty('en')) { - this._debug('A custom save message needs at least the english translation'); - } else { - saveMessage = this.customSaveMessage; - } - } - return saveMessage; - } - - /** - * Wrapper for CONFIGURATION_SET. Provide options.id and/or options.index and options.size. By default - * options.useSettingParser is true, then the value will first be parsed by the registered setting parser or the - * system parser before sending. It will only be able to use the registered setting parser if options.id is provided. - * @param options - * @param options.index - * @param options.size - * @param options.id - * @param [options.signed] - * @param [options.useSettingParser=true] - * @param value - * @returns {Promise.<*>} - */ - async configurationSet(options = {}, value) { - if (!options.hasOwnProperty('index') && !options.hasOwnProperty('id')) return Promise.reject(new Error('missing_setting_index_or_id')); - if (options.hasOwnProperty('index') && !options.hasOwnProperty('size')) return Promise.reject(new Error('missing_setting_size')); - if (options.hasOwnProperty('id') && (!options.hasOwnProperty('size') || !options.hasOwnProperty('index') || !options.hasOwnProperty('signed'))) { - - // Fetch information from manifest by setting id - const settingObj = this.getManifestSetting(options.id); - if (settingObj instanceof Error) return Promise.reject(new Error('invalid_setting_id')); - if (!settingObj.hasOwnProperty('zwave') || !settingObj.zwave.hasOwnProperty('index') || - !settingObj.zwave.hasOwnProperty('size') || typeof settingObj.zwave.index !== 'number' || - typeof settingObj.zwave.size !== 'number') { - - return new Promise.reject(new Error('missing_valid_zwave_setting_object')); - } - options.index = settingObj.zwave.index; - options.size = settingObj.zwave.size; - - if (!options.hasOwnProperty('signed')) { - options.signed = (settingObj.zwave.hasOwnProperty('signed')) ? settingObj.zwave.signed : true; - } - } - - // Check if device has command class - const commandClassConfiguration = this.getCommandClass('CONFIGURATION'); - if (commandClassConfiguration instanceof Error || - typeof commandClassConfiguration.CONFIGURATION_SET !== 'function') { - this.error('Missing COMMAND_CLASS_CONFIGURATION'); - return Promise.reject(new Error('missing_command_class_configuration')); - } - - // If desired the input value can be parsed by the provided parser or the system parser - let parsedValue = null; - if (!options.hasOwnProperty('useSettingParser') || options.useSettingParser === true) { - parsedValue = this._parseSetting(options, value); - if (parsedValue instanceof Error) return Promise.reject(parsedValue); - } else if (!Buffer.isBuffer(value)) { - return Promise.reject(new Error('invalid_value_type')); - } - - return new Promise((resolve, reject) => { - commandClassConfiguration.CONFIGURATION_SET({ - 'Parameter Number': options.index, - Level: { - Size: options.size, - Default: false, - }, - 'Configuration Value': parsedValue || value, - }, (err, result) => { - const parsedBufValue = parsedValue.toString('hex').toUpperCase(); - let parsedDecValue; - - try { - if (!options.hasOwnProperty('signed') || options.signed === true) parsedDecValue = parsedValue.readIntBE(0, options.size); - else parsedDecValue = parsedValue.readUIntBE(0, options.size); - } catch (error) { - this.error('failed to read the buffer value', error); - parsedDecValue = 'N/A'; - } - - if (err) { - this.error(`configurationSet() -> failed to set configuration parameter ${options.index}, size: ${options.size} to ${value} (parsed: ${parsedDecValue} / 0x${parsedBufValue})`); - return reject(err); - } - this.log(`configurationSet() -> successfully set ${options.index}, size: ${options.size} to ${value} (parsed: ${parsedDecValue} / 0x${parsedBufValue})`); - return resolve(result); - }); - - // If battery device which is offline, setting will be saved later, continue - if (this.node.battery === true && this.node.online === false) return resolve(); - }); - } - - /** - * Method that retrieves the value of a configuration parameter from the node. - * @param {Object} options - * @param {number} options.index - Parameter index - * @returns {*} - */ - async configurationGet(options = {}) { - if (!options.hasOwnProperty('index')) return Promise.reject(new Error('missing_index')); - if (this.node.battery === true && this.node.online === false) return Promise.reject(new Error('cannot_get_parameter_from_battery_node')); - - // Check if device has command class - const commandClassConfiguration = this.getCommandClass('CONFIGURATION'); - if (commandClassConfiguration instanceof Error || - typeof commandClassConfiguration.CONFIGURATION_GET !== 'function') { - this.error('Missing COMMAND_CLASS_CONFIGURATION'); - return Promise.reject(new Error('missing_command_class_configuration')); - } - - return commandClassConfiguration.CONFIGURATION_GET({ - 'Parameter Number': options.index, - }); - } - - /* - Private methods - */ - - /** - * Parses a given setting uses the registered setting parser or the system parser and returns the parsed value. - * @param {Object} settingObj - * @param {string} [settingObj.id] - Optional setting id (key) if provided in manifest - * @param settingObj.index - Parameter index - * @param settingObj.size - Parameter size - * @param settingObj.signed - Parameter signed or not - * @param value - Input value to parse - * @returns {Buffer|Error} - * @private - */ - _parseSetting(settingObj = {}, value) { - let parser, - customParser; - - // get the parser - if (typeof this._settings[settingObj.id] !== 'undefined') { - parser = this._settings[settingObj.id]; - customParser = true; - } else { - parser = this._systemSettingParser; - } - - if (typeof parser !== 'function') return new Error('invalid_parser'); - - // Parse and check value - let parsedValue = parser.call(this, value, settingObj); - if (parsedValue instanceof Error) return parsedValue; - if (!Buffer.isBuffer(parsedValue)) { - - if (customParser) { - parsedValue = this._systemSettingParser(parsedValue, settingObj); - - if (!Buffer.isBuffer(parsedValue)) { - return new Error('invalid_buffer'); - } - } else { - return new Error('invalid_buffer'); - } - } - - if (parsedValue.length !== settingObj.size) return new Error('invalid_buffer_length'); - return parsedValue; - } - - /** - * @private - */ - _systemSettingParser(newValue, manifestSetting) { - - if (typeof newValue === 'boolean') { - return new Buffer([(newValue === true) ? 1 : 0]); - } - - if (typeof newValue === 'number' || parseInt(newValue, 10).toString() === newValue) { - if (manifestSetting.signed === false) { - - try { - const buf = new Buffer(manifestSetting.size); - buf.writeUIntBE(newValue, 0, manifestSetting.size); - return buf; - } catch (err) { - return err; - } - - } else { - - try { - const buf = new Buffer(manifestSetting.size); - buf.writeIntBE(newValue, 0, manifestSetting.size); - return buf; - } catch (err) { - return err; - } - - } - } - - if (Buffer.isBuffer(newValue)) return newValue; - } - - - /** - * @private - */ - _registerCapabilityGet(capabilityId, commandClassId) { - - const capabilityGetObj = this._getCapabilityObj('get', capabilityId, commandClassId); - if (capabilityGetObj instanceof Error) return capabilityGetObj; - - // Get capability value on device init - if (capabilityGetObj.opts.getOnStart) { - - // But not for battery devices - if (this.node.battery === false) this._getCapabilityValue(capabilityId, commandClassId); - else this.error('do not use getOnStart for battery devices, use getOnOnline instead'); - } - - // Perform get on online, also when device is initing and device is still online (replacing the getOnStart - // functionality) - if (capabilityGetObj.opts.getOnOnline) { - - // Get immediately if node is still online (right after pairing for example) - if (this.node.battery === true && this.node.online === true) { - this.log(`Node online, getting commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); - this._getCapabilityValue(capabilityId, commandClassId); - } - - // Bind online listener for future events - this.node.on('online', online => { - if (online) { - this.log(`Node online, getting commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); - this._getCapabilityValue(capabilityId, commandClassId); - } - }); - } - - if (capabilityGetObj.opts.pollInterval) { - - let pollInterval; - const pollMultiplication = capabilityGetObj.opts.pollMultiplication || 1; - - if (typeof capabilityGetObj.opts.pollInterval === 'number') { - pollInterval = (capabilityGetObj.opts.pollInterval * pollMultiplication); - } - - if (typeof capabilityGetObj.opts.pollInterval === 'string') { - pollInterval = (this.getSetting(capabilityGetObj.opts.pollInterval) * pollMultiplication); - this._pollIntervalsKeys[capabilityGetObj.opts.pollInterval] = { - capabilityId, - commandClassId, - }; - } - - this._setPollInterval(capabilityId, commandClassId, pollInterval); - - } - - } - - - /** - * @private - */ - _setPollInterval(capabilityId, commandClassId, pollInterval) { - - this._pollIntervals[capabilityId] = this._pollIntervals[capabilityId] || {}; - - if (this._pollIntervals[capabilityId][commandClassId]) { - clearInterval(this._pollIntervals[capabilityId][commandClassId]); - } - - if (pollInterval < 1) return; - - this._pollIntervals[capabilityId][commandClassId] = setInterval(() => { - this._debug(`Polling commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); - this._getCapabilityValue(capabilityId, commandClassId); - }, pollInterval); - - } - - - /** - * @private - */ - _getCapabilityValue(capabilityId, commandClassId) { - - const capabilityGetObj = this._getCapabilityObj('get', capabilityId, commandClassId); - if (capabilityGetObj instanceof Error) return capabilityGetObj; - - let parsedPayload = {}; - - if (typeof capabilityGetObj.parser === 'function') { - parsedPayload = capabilityGetObj.parser.call(this); - if (parsedPayload instanceof Error) return this.error(parsedPayload); - } - - try { - const commandClass = capabilityGetObj.node.CommandClass[`COMMAND_CLASS_${capabilityGetObj.commandClassId}`]; - const command = commandClass[capabilityGetObj.commandId]; - - return command.call(command, parsedPayload, (err, payload) => { - if (err) return this.error(err); - - const result = this._onReport(capabilityId, commandClassId, payload); - if (result instanceof Error) return this.error(result); - }); - } catch (err) { - return this.error(err); - } - } - - /** - * @private - */ - _registerCapabilitySet(capabilityId, commandClassId) { - - const capabilitySetObj = this._getCapabilityObj('set', capabilityId, commandClassId); - if (capabilitySetObj instanceof Error) return capabilitySetObj; - - this.registerCapabilityListener(capabilityId, (value, opts) => { - - if (typeof capabilitySetObj.parser !== 'function') return Promise.reject(new Error('missing_parser')); - - const parsedPayload = capabilitySetObj.parser.call(this, value, opts); - if (parsedPayload instanceof Error) return Promise.reject(parsedPayload); - - try { - const commandClass = capabilitySetObj.node.CommandClass[`COMMAND_CLASS_${capabilitySetObj.commandClassId}`]; - const command = commandClass[capabilitySetObj.commandId]; - const commandSetPromise = command.call(command, parsedPayload); - if (this.node.battery === true && this.node.online === false) return Promise.resolve('TRANSMIT_QUEUED'); - return commandSetPromise; - } catch (err) { - return Promise.reject(err); - } - }); - } - - /** - * @private - */ - _registerCapabilityRealtime(capabilityId, commandClassId) { - - const capabilityReportObj = this._getCapabilityObj('report', capabilityId, commandClassId); - if (capabilityReportObj instanceof Error) return capabilityReportObj; - - const commandClass = capabilityReportObj.node.CommandClass[`COMMAND_CLASS_${capabilityReportObj.commandClassId}`]; - if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', capabilityReportObj.commandClassId); - - commandClass.on('report', (command, payload) => { - if (command.name !== capabilityReportObj.commandId) return; - - const parsedPayload = this._onReport(capabilityId, commandClassId, payload); - if (parsedPayload instanceof Error) return; - if (parsedPayload === null) return; - - if (this._reportListeners[commandClassId] && - this._reportListeners[commandClassId][command.name]) { - this._reportListeners[commandClassId][command.name](payload, parsedPayload); - } - }); - } - - /** - * @private - */ - _onReport(capabilityId, commandClassId, payload) { - - const capabilityReportObj = this._getCapabilityObj('report', capabilityId, commandClassId); - if (capabilityReportObj instanceof Error) return capabilityReportObj; - if (typeof capabilityReportObj.parser !== 'function') return new Error('Missing report parser'); - - // parse the payload using a built-in Command Class parser - const commandClassParsedPayload = this._parseCommandClassPayload(commandClassId, payload); - - const parsedPayload = capabilityReportObj.parser.call(this, commandClassParsedPayload); - if (parsedPayload instanceof Error) return parsedPayload; - if (parsedPayload === null) return parsedPayload; - - this.setCapabilityValue(capabilityId, parsedPayload); - - return parsedPayload; - } - - /** - * Extend a Command Class payload with parsed values, as provided by the Z-Wave specification - * @private - */ - _parseCommandClassPayload(commandClassId, payload) { - const parser = commandClassParsers[commandClassId]; - if (parser) return parser(payload); - return payload; - } - - /** - * @private - */ - _getCapabilityObj(commandType, capabilityId, commandClassId) { - - const capability = this._capabilities[capabilityId]; - let commandClass; - - if (typeof commandClassId !== 'undefined') { - commandClass = capability[commandClassId]; - } else { - for (const commandClassId in capability) { - commandClass = capability[commandClassId]; - } - } - - if (typeof commandClass === 'undefined') { - return new Error('missing_zwave_capability'); - } - - const commandId = commandClass[commandType]; - const opts = commandClass[`${commandType}Opts`] || {}; - let node = this.node; - - if (typeof commandClass.multiChannelNodeId === 'number') { - node = this.node.MultiChannelNodes[commandClass.multiChannelNodeId]; - if (typeof node === 'undefined') { - throw new Error(`Invalid multiChannelNodeId ${commandClass.multiChannelNodeId} for capabilityId ${capabilityId} and commandClassId ${commandClassId}`); - } - } - - let parser = null; - const nodeCommandClass = node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; - if (typeof nodeCommandClass === 'undefined') return new Error(`missing_command_class_${commandClassId}`); - const nodeCommandClassVersion = nodeCommandClass.version; - - for (let i = nodeCommandClassVersion; i > 0; i--) { - const fn = commandClass[`${commandType}ParserV${i}`]; - if (typeof fn === 'function') { - parser = fn; - break; - } - } - - if (parser === null && typeof commandClass[`${commandType}Parser`] === 'function') { - parser = commandClass[`${commandType}Parser`]; - } - - if (typeof commandId === 'string') { - return { - commandClassId, - commandId, - parser, - opts, - node, - }; - } - - return new Error('missing_zwave_capability'); - - } - - /* - * Public methods - */ - - /** - * Register a Homey Capability with a Command Class. - * Multiple `parser` methods can be provided by appending a version, e.g. `getParserV3`. This will make sure that the highest matching version will be used, falling back to `getParser`. - * @param {string} capabilityId - The Homey capability id (e.g. `onoff`) - * @param {string} commandClassId - The command class id (e.g. `BASIC`) - * @param {Object} [opts] - The object with options for this capability/commandclass combination. These will extend system options, if available (`/lib/zwave/system/`) - * @param {String} [opts.get] - The command to get a value (e.g. `BASIC_GET`) - * @param {String} [opts.getParser] - The function that is called when a GET request is made. Should return an Object. - * @param {Object} [opts.getOpts - * @param {Boolean} [opts.getOpts.getOnStart] - Get the value on App start - * @param {Boolean} [opts.getOpts.getOnOnline] - Get the value when the device is marked as online - * @param {Number|String} [opts.getOpts.pollInterval] - Interval (in ms) to poll with a GET request. When provided a string, the device's setting with the string as ID will be used (e.g. `poll_interval`) - * @param {Number} [opts.getOpts.pollMultiplication] - Multiplication factor for the pollInterval key, must be a number. (e.g. 1000 to convert to seconds, 60.000 for minutes, 3600000 for hours) - * @param {String} [opts.set] - The command to set a value (e.g. `BASIC_SET`) - * @param {Function} [opts.setParser] - The function that is called when a SET request is made. Should return an Object. - * @param {Mixed} [opts.setParser.value] - The value of the Homey capability - * @param {Object} [opts.setParser.opts] - Options for the capability command - * @param {String} [opts.report] - The command to report a value (e.g. `BASIC_REPORT`) - * @param {Function} [opts.reportParser] - The function that is called when a REPORT request is made. Should return an Object. - * @param {Object} [opts.reportParser.report] - The report object - * @param {Number} [opts.multiChannelNodeId] - An ID to use a MultiChannel Node for this capability - */ - registerCapability(capabilityId, commandClassId, userOpts) { - - // Check if device has the command class we're trying to register, if not, abort - if (typeof this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`] === 'undefined') { - return this.error('Invalid commandClass:', commandClassId); - } - - // register the Z-Wave capability listener - this._capabilities[capabilityId] = this._capabilities[capabilityId] || {}; - this._capabilities[capabilityId][commandClassId] = this._capabilities[capabilityId][commandClassId] || {}; - - // merge systemOpts & userOpts - let systemOpts = {}; - try { - - // First try get device class specific system capability - systemOpts = this._getDeviceClassSpecificSystemCapability(capabilityId, commandClassId); - - // If not available use general system capability - if (!systemOpts) systemOpts = require(`./system/capabilities/${capabilityId}/${commandClassId}.js`); - - } catch (err) { - if (err.code !== 'MODULE_NOT_FOUND') { - process.nextTick(() => { - throw err; - }); - } - } - - this._capabilities[capabilityId][commandClassId] = Object.assign( - systemOpts || {}, - userOpts || {} - ); - - // register get/set/realtime - this._registerCapabilityRealtime(capabilityId, commandClassId); - this._registerCapabilitySet(capabilityId, commandClassId); - this._registerCapabilityGet(capabilityId, commandClassId); - } - - /** - * Method that checks if a device class specific system capability is available and returns it if possible. Else it - * will return null. - * @param {string} capabilityId - * @param {string} commandClassId - * @returns {Object|null} - * @private - */ - _getDeviceClassSpecificSystemCapability(capabilityId, commandClassId) { - try { - return require(`./system/capabilities/${capabilityId}/${this.getClass()}/${commandClassId}.js`); - } catch (err) { - if (err.code !== 'MODULE_NOT_FOUND') this.error(err); - return null; - } - } - - /** - * Register a setting parser, which is called when a setting has changed. - * @param {string} settingId - The setting ID, as specified in `/app.json` - * @param {Function} parserFn - The parser function, must return a Buffer, number or boolean - * @param {Mixed} parserFn.value - The setting value - * @param {Mixed} parserFn.zwaveObj - The setting's `zwave` object as defined in `/app.json` - */ - registerSetting(settingId, parserFn) { - this._settings[settingId] = parserFn; - } - - /** - * Register a multi channel report listener, which is called when a report has been received. - * @param {number} multiChannelNodeId - The multi channel node id - * @param {string} commandClassId - The ID of the Command Class (e.g. `BASIC`) - * @param {string} commandId - The ID of the Command (e.g. `BASIC_REPORT`) - * @param {Function} triggerFn - * @param {Object} triggerFn.report - The received report - */ - registerMultiChannelReportListener(multiChannelNodeId, commandClassId, commandId, triggerFn) { - // Check for valid multi channel nodes - if (!this.node.MultiChannelNodes || - !this.node.MultiChannelNodes[multiChannelNodeId] || - (Array.isArray(this.node.MultiChannelNodes) && this.node.MultiChannelNodes.length === 0)) { - return this.error('Invalid multi channel node', multiChannelNodeId); - } - - const commandClass = this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[`COMMAND_CLASS_${commandClassId}`]; - if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', commandClassId); - - this._reportListeners[commandClassId] = this._reportListeners[commandClassId] || {}; - this._reportListeners[commandClassId][`${multiChannelNodeId}_${commandId}`] = triggerFn; - - commandClass.on('report', (command, payload) => { - if (command.name !== commandId) return; - if (typeof this._reportListeners[commandClassId][`${multiChannelNodeId}_${command.name}`] === 'function') { - this._reportListeners[commandClassId][`${multiChannelNodeId}_${command.name}`](payload); - } - }); - } - - /** - * Register a report listener, which is called when a report has been received. - * @param {string} commandClassId - The ID of the Command Class (e.g. `BASIC`) - * @param {string} commandId - The ID of the Command (e.g. `BASIC_REPORT`) - * @param {Function} triggerFn - * @param {Object} triggerFn.report - The received report - */ - registerReportListener(commandClassId, commandId, triggerFn) { - const commandClass = this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; - if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', commandClassId); - let previousSequence; - - this._reportListeners[commandClassId] = this._reportListeners[commandClassId] || {}; - this._reportListeners[commandClassId][commandId] = triggerFn; - - commandClass.on('report', (command, payload) => { - if (command.name !== commandId) return; - - // Catch central scene echos and (sometimes) failing parser - if (command.name === 'CENTRAL_SCENE_NOTIFICATION') { - - if (typeof previousSequence !== 'undefined' && payload.hasOwnProperty('Sequence Number') && payload['Sequence Number'] === previousSequence) return; - previousSequence = payload['Sequence Number']; - - if (payload.hasOwnProperty('Properties1') && - payload.Properties1.hasOwnProperty('Key Attributes') && - typeof payload.Properties1['Key Attributes'] === 'number') { - switch (payload.Properties1['Key Attributes']) { - case 0: - payload.Properties1['Key Attributes'] = 'Key Pressed 1 time'; - break; - case 1: - payload.Properties1['Key Attributes'] = 'Key Released'; - break; - case 2: - payload.Properties1['Key Attributes'] = 'Key Held Down'; - break; - case 3: - payload.Properties1['Key Attributes'] = 'Key Pressed 2 times'; - break; - case 4: - payload.Properties1['Key Attributes'] = 'Key Pressed 3 times'; - break; - case 5: - payload.Properties1['Key Attributes'] = 'Key Pressed 4 times'; - break; - case 6: - payload.Properties1['Key Attributes'] = 'Key Pressed 5 times'; - break; - } - } - } - - if (this._reportListeners[commandClassId] && - this._reportListeners[commandClassId][command.name]) { - this._reportListeners[commandClassId][command.name](payload); - } - }); - } - - /** - * Method that will check if the node has the provided command class - * @param {string} commandClassId - For example: SWITCH_BINARY - * @returns {boolean} - */ - hasCommandClass(commandClassId) { - return !(typeof this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`] === 'undefined'); - } - - getCommandClass(commandClassId) { - if (!this.hasCommandClass(commandClassId)) return new Error(`missing_command_class_${commandClassId}`); - return this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; - } - - /** - * Print the current Node information with Command Classes and their versions - */ - printNode() { - this.log('------------------------------------------'); - - // log the entire Node - this.log('Node:', this.getData().token); - this.log('- Battery:', this.node.battery); - this.log('- DeviceClassGeneric:', this.node.deviceClassGeneric); - - Object.keys(this.node.CommandClass).forEach(commandClassId => { - this.log('- CommandClass:', commandClassId); - this.log('-- Version:', this.node.CommandClass[commandClassId].version); - this.log('-- Commands:'); - - Object.keys(this.node.CommandClass[commandClassId]).forEach(key => { - if (typeof this.node.CommandClass[commandClassId][key] === 'function' && key === key.toUpperCase()) { - this.log('---', key); - } - }); - }); - - if (this.node.MultiChannelNodes) { - Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { - this.log('- MultiChannelNode:', multiChannelNodeId); - this.log('- DeviceClassGeneric:', this.node.MultiChannelNodes[multiChannelNodeId].deviceClassGeneric); - - Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { - this.log('-- CommandClass:', commandClassId); - this.log('--- Version:', - this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].version); - this.log('--- Commands:'); - - Object - .keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId]) - .forEach(key => { - if (typeof this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId][key] === - 'function' && key === key.toUpperCase()) { - this.log('----', key); - } - }); - }); - }); - } - - this.log('------------------------------------------'); - this.log(''); - - Object.keys(this.node.CommandClass).forEach(commandClassId => { - this.node.CommandClass[commandClassId].on('report', function () { - this.log(`node.CommandClass['${commandClassId}'].on('report')`, 'arguments:', arguments); - }.bind(this)); - }); - - if (this.node.MultiChannelNodes) { - Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { - Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { - this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].on('report', function () { - this.log(`node.MultiChannelNodes['${multiChannelNodeId}']. + /* + * Homey methods + */ + + /** + * @private + */ + onInit() { + super.onInit('zwave'); + + this._capabilities = {}; + this._settings = {}; + this._reportListeners = {}; + this._pollIntervals = {}; + this._pollIntervalsKeys = {}; + + this.once('__meshInit', () => { + this.log('ZwaveDevice has been inited'); + this.onMeshInit && this.onMeshInit(); + }); + } + + /** + * Remove all listeners and intervals from node + */ + onDeleted() { + super.onDeleted(); + + if(!this.node) return; + + // Remove all report listeners on command classes + if (this.node.CommandClass) { + Object.keys(this.node.CommandClass).forEach(commandClassId => { + this.node.CommandClass[commandClassId].removeAllListeners(); + }); + } + + + // Remove all report listeners on multi channel nodes + if (this.node.MultiChannelNodes) { + Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { + Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { + this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].removeAllListeners(); + }); + }); + } + } + + /** + * Method that flattens possibly nested settings and returns a flat settings array. + * @returns {Array} + */ + getManifestSettings() { + if (!this.manifestSettings) { + const manifest = this.getDriver().getManifest(); + if (!manifest || !manifest.settings) return this.manifestSettings = []; + + const flattenSettings = (settings) => settings.reduce((manifestSettings, setting) => { + if (setting.type === 'group') { + return manifestSettings.concat(flattenSettings(setting.children)); + } + manifestSettings.push(setting); + return manifestSettings; + }, []); + + this.manifestSettings = flattenSettings(manifest.settings); + } + return this.manifestSettings; + } + + /** + * Method that refreshes the capability value once. If you want to poll this value please use + * the parameter getOpts.pollInterval at {@link ZwaveDevice#registerCapability} + * @param {String} capabilityId, the string id of the Homey capability + * @param {String} commandClassId, the Z-Wave command class used for this request + */ + refreshCapabilityValue(capabilityId, commandClassId) { + return this._getCapabilityValue(capabilityId, commandClassId); + } + + /** + * Get a specific setting object from the manifest + * @param id - Setting id to retrieve + * @returns {Object|Error} + */ + getManifestSetting(id) { + const settings = this.getManifestSettings(); + if (Array.isArray(settings)) return settings.find(setting => setting.id === id); + return new Error(`missing_setting_id_${id}`); + } + + /** + * Method that handles changing settings for Z-Wave devices. It iterates over the changed settings and executes + * a CONFIGURATION_SET in sync. If all succeed, it will resolve, if one or more fail it will reject with an error + * of concatenated error messages (to see which settings failed if more than one). + * @param oldSettings + * @param newSettings + * @param changedKeysArr + * @returns {Promise.} + */ + async onSettings(oldSettings, newSettings, changedKeysArr = []) { + let changeSettingError = ''; + + // Loop all changed settings + for (const changedKey of changedKeysArr) { + const newValue = newSettings[changedKey]; + + // check for poll interval + if (this._pollIntervalsKeys[changedKey]) { + const capabilityGetObj = this._getCapabilityObj('get', this._pollIntervalsKeys[changedKey].capabilityId, this._pollIntervalsKeys[changedKey].commandClassId); + let pollInterval; + const pollMultiplication = capabilityGetObj.opts.pollMultiplication || 1; + pollInterval = newValue * pollMultiplication + + this._setPollInterval( + this._pollIntervalsKeys[changedKey].capabilityId, + this._pollIntervalsKeys[changedKey].commandClassId, + pollInterval + ); + continue; + } + + // Get manifest setting object and execute configuration set + const manifestSetting = (this.getManifestSettings().find(setting => setting.id === changedKey) || {}).zwave; + + // Non z-wave settings: see if there is a function to execute, otherwise do nothing. + if (typeof manifestSetting === 'undefined') { + + if (this._settings.hasOwnProperty(changedKey)) { + const parser = this._settings[changedKey]; + + if (typeof parser === 'function') parser.call(this, newValue); + } + continue; + } + + try { + this.log(`configurationSet() -> ${changedKey}: ${newSettings[changedKey]}, ${manifestSetting.index}, ${manifestSetting.size}`); + await this.configurationSet({ + id: changedKey, + index: manifestSetting.index, + size: manifestSetting.size, + signed: (manifestSetting.hasOwnProperty('signed')) ? manifestSetting.signed : true, + }, newSettings[changedKey]); + } catch (err) { + this.error(`failed_to_set_${changedKey}_to_${newValue}_size_${manifestSetting.size}`, err); + let errorString = `${changeSettingError}failed_to_set_${changedKey}_to_${newValue}_size_${manifestSetting.size}`; + if (changeSettingError.length > 0) errorString = `_${errorString}`; + changeSettingError = errorString; + } + } + + // If one or more of the settings failed to set, reject + if (changeSettingError.length > 0) return Promise.reject(new Error(changeSettingError)); + + // Compose save message + const saveMessage = this._composeCustomSaveMessage(oldSettings, newSettings, changedKeysArr); + return Promise.resolve(saveMessage); + } + + /** + * @private + */ + _composeCustomSaveMessage(oldSettings, newSettings, changedKeysArr) { + + // Provide user with proper feedback after clicking save + let saveMessage = null; + if (this.node.battery === true && this.node.online === false) saveMessage = i18n.settings.offlineNodeSaveMessage; + if (typeof this.customSaveMessage === 'function') { + const message = this.customSaveMessage(oldSettings, newSettings, changedKeysArr); + + if (typeof message !== 'object' && typeof message !== 'string') { + this._debug('Save message\'s return value is not an object nor a string'); + } else if (typeof message === 'object' && !message.hasOwnProperty('en')) { + this._debug('A custom save message needs at least the english translation'); + } else { + saveMessage = message; + } + } else if (typeof this.customSaveMessage === 'object') { + if (!this.customSaveMessage.hasOwnProperty('en')) { + this._debug('A custom save message needs at least the english translation'); + } else { + saveMessage = this.customSaveMessage; + } + } + return saveMessage; + } + + /** + * Wrapper for CONFIGURATION_SET. Provide options.id and/or options.index and options.size. By default + * options.useSettingParser is true, then the value will first be parsed by the registered setting parser or the + * system parser before sending. It will only be able to use the registered setting parser if options.id is provided. + * @param options + * @param options.index + * @param options.size + * @param options.id + * @param [options.signed] + * @param [options.useSettingParser=true] + * @param value + * @returns {Promise.<*>} + */ + async configurationSet(options = {}, value) { + if (!options.hasOwnProperty('index') && !options.hasOwnProperty('id')) return Promise.reject(new Error('missing_setting_index_or_id')); + if (options.hasOwnProperty('index') && !options.hasOwnProperty('size')) return Promise.reject(new Error('missing_setting_size')); + if (options.hasOwnProperty('id') && (!options.hasOwnProperty('size') || !options.hasOwnProperty('index') || !options.hasOwnProperty('signed'))) { + + // Fetch information from manifest by setting id + const settingObj = this.getManifestSetting(options.id); + if (settingObj instanceof Error) return Promise.reject(new Error('invalid_setting_id')); + if (!settingObj.hasOwnProperty('zwave') || !settingObj.zwave.hasOwnProperty('index') || + !settingObj.zwave.hasOwnProperty('size') || typeof settingObj.zwave.index !== 'number' || + typeof settingObj.zwave.size !== 'number') { + + return new Promise.reject(new Error('missing_valid_zwave_setting_object')); + } + options.index = settingObj.zwave.index; + options.size = settingObj.zwave.size; + + if (!options.hasOwnProperty('signed')) { + options.signed = (settingObj.zwave.hasOwnProperty('signed')) ? settingObj.zwave.signed : true; + } + } + + // Check if device has command class + const commandClassConfiguration = this.getCommandClass('CONFIGURATION'); + if (commandClassConfiguration instanceof Error || + typeof commandClassConfiguration.CONFIGURATION_SET !== 'function') { + this.error('Missing COMMAND_CLASS_CONFIGURATION'); + return Promise.reject(new Error('missing_command_class_configuration')); + } + + // If desired the input value can be parsed by the provided parser or the system parser + let parsedValue = null; + if (!options.hasOwnProperty('useSettingParser') || options.useSettingParser === true) { + parsedValue = this._parseSetting(options, value); + if (parsedValue instanceof Error) return Promise.reject(parsedValue); + } else if (!Buffer.isBuffer(value)) { + return Promise.reject(new Error('invalid_value_type')); + } + + return new Promise((resolve, reject) => { + commandClassConfiguration.CONFIGURATION_SET({ + 'Parameter Number': options.index, + Level: { + Size: options.size, + Default: false, + }, + 'Configuration Value': parsedValue || value, + }, (err, result) => { + const parsedBufValue = parsedValue.toString('hex').toUpperCase(); + let parsedDecValue; + + try { + if (!options.hasOwnProperty('signed') || options.signed === true) parsedDecValue = parsedValue.readIntBE(0, options.size); + else parsedDecValue = parsedValue.readUIntBE(0, options.size); + } catch (error) { + this.error('failed to read the buffer value', error); + parsedDecValue = 'N/A'; + } + + if (err) { + this.error(`configurationSet() -> failed to set configuration parameter ${options.index}, size: ${options.size} to ${value} (parsed: ${parsedDecValue} / 0x${parsedBufValue})`); + return reject(err); + } + this.log(`configurationSet() -> successfully set ${options.index}, size: ${options.size} to ${value} (parsed: ${parsedDecValue} / 0x${parsedBufValue})`); + return resolve(result); + }); + + // If battery device which is offline, setting will be saved later, continue + if (this.node.battery === true && this.node.online === false) return resolve(); + }); + } + + /** + * Method that retrieves the value of a configuration parameter from the node. + * @param {Object} options + * @param {number} options.index - Parameter index + * @returns {*} + */ + async configurationGet(options = {}) { + if (!options.hasOwnProperty('index')) return Promise.reject(new Error('missing_index')); + if (this.node.battery === true && this.node.online === false) return Promise.reject(new Error('cannot_get_parameter_from_battery_node')); + + // Check if device has command class + const commandClassConfiguration = this.getCommandClass('CONFIGURATION'); + if (commandClassConfiguration instanceof Error || + typeof commandClassConfiguration.CONFIGURATION_GET !== 'function') { + this.error('Missing COMMAND_CLASS_CONFIGURATION'); + return Promise.reject(new Error('missing_command_class_configuration')); + } + + return commandClassConfiguration.CONFIGURATION_GET({ + 'Parameter Number': options.index, + }); + } + + /* + Private methods + */ + + /** + * Parses a given setting uses the registered setting parser or the system parser and returns the parsed value. + * @param {Object} settingObj + * @param {string} [settingObj.id] - Optional setting id (key) if provided in manifest + * @param settingObj.index - Parameter index + * @param settingObj.size - Parameter size + * @param settingObj.signed - Parameter signed or not + * @param value - Input value to parse + * @returns {Buffer|Error} + * @private + */ + _parseSetting(settingObj = {}, value) { + let parser, + customParser; + + // get the parser + if (typeof this._settings[settingObj.id] !== 'undefined') { + parser = this._settings[settingObj.id]; + customParser = true; + } else { + parser = this._systemSettingParser; + } + + if (typeof parser !== 'function') return new Error('invalid_parser'); + + // Parse and check value + let parsedValue = parser.call(this, value, settingObj); + if (parsedValue instanceof Error) return parsedValue; + if (!Buffer.isBuffer(parsedValue)) { + + if (customParser) { + parsedValue = this._systemSettingParser(parsedValue, settingObj); + + if (!Buffer.isBuffer(parsedValue)) { + return new Error('invalid_buffer'); + } + } else { + return new Error('invalid_buffer'); + } + } + + if (parsedValue.length !== settingObj.size) return new Error('invalid_buffer_length'); + return parsedValue; + } + + /** + * @private + */ + _systemSettingParser(newValue, manifestSetting) { + + if (typeof newValue === 'boolean') { + return new Buffer([(newValue === true) ? 1 : 0]); + } + + if (typeof newValue === 'number' || parseInt(newValue, 10).toString() === newValue) { + if (manifestSetting.signed === false) { + + try { + const buf = new Buffer(manifestSetting.size); + buf.writeUIntBE(newValue, 0, manifestSetting.size); + return buf; + } catch (err) { + return err; + } + + } else { + + try { + const buf = new Buffer(manifestSetting.size); + buf.writeIntBE(newValue, 0, manifestSetting.size); + return buf; + } catch (err) { + return err; + } + + } + } + + if (Buffer.isBuffer(newValue)) return newValue; + } + + + /** + * @private + */ + _registerCapabilityGet(capabilityId, commandClassId) { + + const capabilityGetObj = this._getCapabilityObj('get', capabilityId, commandClassId); + if (capabilityGetObj instanceof Error) return capabilityGetObj; + + // Get capability value on device init + if (capabilityGetObj.opts.getOnStart) { + + // But not for battery devices + if (this.node.battery === false) this._getCapabilityValue(capabilityId, commandClassId); + else this.error('do not use getOnStart for battery devices, use getOnOnline instead'); + } + + // Perform get on online, also when device is initing and device is still online (replacing the getOnStart + // functionality) + if (capabilityGetObj.opts.getOnOnline) { + + // Get immediately if node is still online (right after pairing for example) + if (this.node.battery === true && this.node.online === true) { + this.log(`Node online, getting commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); + this._getCapabilityValue(capabilityId, commandClassId); + } + + // Bind online listener for future events + this.node.on('online', online => { + if (online) { + this.log(`Node online, getting commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); + this._getCapabilityValue(capabilityId, commandClassId); + } + }); + } + + if (capabilityGetObj.opts.pollInterval) { + + let pollInterval; + const pollMultiplication = capabilityGetObj.opts.pollMultiplication || 1; + + if (typeof capabilityGetObj.opts.pollInterval === 'number') { + pollInterval = (capabilityGetObj.opts.pollInterval * pollMultiplication); + } + + if (typeof capabilityGetObj.opts.pollInterval === 'string') { + pollInterval = (this.getSetting(capabilityGetObj.opts.pollInterval) * pollMultiplication); + this._pollIntervalsKeys[capabilityGetObj.opts.pollInterval] = { + capabilityId, + commandClassId, + }; + } + + this._setPollInterval(capabilityId, commandClassId, pollInterval); + + } + + } + + + /** + * @private + */ + _setPollInterval(capabilityId, commandClassId, pollInterval) { + + this._pollIntervals[capabilityId] = this._pollIntervals[capabilityId] || {}; + + if (this._pollIntervals[capabilityId][commandClassId]) { + clearInterval(this._pollIntervals[capabilityId][commandClassId]); + } + + if (pollInterval < 1) return; + + this._pollIntervals[capabilityId][commandClassId] = setInterval(() => { + this._debug(`Polling commandClassId '${commandClassId}' for capabilityId '${capabilityId}'`); + this._getCapabilityValue(capabilityId, commandClassId); + }, pollInterval); + + } + + + /** + * @private + */ + _getCapabilityValue(capabilityId, commandClassId) { + + const capabilityGetObj = this._getCapabilityObj('get', capabilityId, commandClassId); + if (capabilityGetObj instanceof Error) return capabilityGetObj; + + let parsedPayload = {}; + + if (typeof capabilityGetObj.parser === 'function') { + parsedPayload = capabilityGetObj.parser.call(this); + if (parsedPayload instanceof Error) return this.error(parsedPayload); + } + + try { + const commandClass = capabilityGetObj.node.CommandClass[`COMMAND_CLASS_${capabilityGetObj.commandClassId}`]; + const command = commandClass[capabilityGetObj.commandId]; + + return command.call(command, parsedPayload, (err, payload) => { + if (err) return this.error(err); + + const result = this._onReport(capabilityId, commandClassId, payload); + if (result instanceof Error) return this.error(result); + }); + } catch (err) { + return this.error(err); + } + } + + /** + * @private + */ + _registerCapabilitySet(capabilityId, commandClassId) { + + const capabilitySetObj = this._getCapabilityObj('set', capabilityId, commandClassId); + if (capabilitySetObj instanceof Error) return capabilitySetObj; + + this.registerCapabilityListener(capabilityId, (value, opts) => { + return (async () => { + if (typeof capabilitySetObj.parser !== 'function') return Promise.reject(new Error('missing_parser')); + + const parsedPayload = capabilitySetObj.parser.call(this, value, opts); + if (parsedPayload instanceof Error) return Promise.reject(parsedPayload); + + try { + const commandClass = capabilitySetObj.node.CommandClass[`COMMAND_CLASS_${capabilitySetObj.commandClassId}`]; + const command = commandClass[capabilitySetObj.commandId]; + const commandSetPromise = command.call(command, parsedPayload); + if (this.node.battery === true && this.node.online === false) return Promise.resolve('TRANSMIT_QUEUED'); + return commandSetPromise; + } catch (err) { + return Promise.reject(err); + } + })().then(result => { + if( typeof capabilitySetObj.opts.fn === 'function' ) { + process.nextTick(() => { + try { + capabilitySetObj.opts.fn.call(this, value, opts); + } catch( err ) { + this.error(err); + } + }); + } + return result; + }); + }); + } + + /** + * @private + */ + _registerCapabilityRealtime(capabilityId, commandClassId) { + + const capabilityReportObj = this._getCapabilityObj('report', capabilityId, commandClassId); + if (capabilityReportObj instanceof Error) return capabilityReportObj; + + const commandClass = capabilityReportObj.node.CommandClass[`COMMAND_CLASS_${capabilityReportObj.commandClassId}`]; + if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', capabilityReportObj.commandClassId); + + commandClass.on('report', (command, payload) => { + if (command.name !== capabilityReportObj.commandId) return; + + const parsedPayload = this._onReport(capabilityId, commandClassId, payload); + if (parsedPayload instanceof Error) return; + if (parsedPayload === null) return; + + if (this._reportListeners[commandClassId] && + this._reportListeners[commandClassId][command.name]) { + this._reportListeners[commandClassId][command.name](payload, parsedPayload); + } + }); + } + + /** + * @private + */ + _onReport(capabilityId, commandClassId, payload) { + + const capabilityReportObj = this._getCapabilityObj('report', capabilityId, commandClassId); + if (capabilityReportObj instanceof Error) return capabilityReportObj; + if (typeof capabilityReportObj.parser !== 'function') return new Error('Missing report parser'); + + // parse the payload using a built-in Command Class parser + const commandClassParsedPayload = this._parseCommandClassPayload(commandClassId, payload); + + const parsedPayload = capabilityReportObj.parser.call(this, commandClassParsedPayload); + if (parsedPayload instanceof Error) return parsedPayload; + if (parsedPayload === null) return parsedPayload; + + this.setCapabilityValue(capabilityId, parsedPayload); + + try { + if( typeof capabilityReportObj.opts.fn === 'function' ) + capabilityReportObj.opts.fn.call(this, parsedPayload); + } catch( err ) { + this.error(err); + } + + return parsedPayload; + } + + /** + * Extend a Command Class payload with parsed values, as provided by the Z-Wave specification + * @private + */ + _parseCommandClassPayload(commandClassId, payload) { + const parser = commandClassParsers[commandClassId]; + if (parser) return parser(payload); + return payload; + } + + /** + * @private + */ + _getCapabilityObj(commandType, capabilityId, commandClassId) { + + // get capability and command class from the _capabilities object + const capability = this._capabilities[capabilityId]; + let commandClass; + + if (typeof commandClassId !== 'undefined') { + commandClass = capability[commandClassId]; + } else { + for (const commandClassId in capability) { + commandClass = capability[commandClassId]; + } + } + + if (typeof commandClass === 'undefined') { + return new Error('missing_zwave_capability'); + } + + const commandId = commandClass[commandType]; + const opts = commandClass[`${commandType}Opts`] || {}; + let node = this.node; + + if (typeof commandClass.multiChannelNodeId === 'number') { + node = this.node.MultiChannelNodes[commandClass.multiChannelNodeId]; + if (typeof node === 'undefined') { + throw new Error(`Invalid multiChannelNodeId ${commandClass.multiChannelNodeId} for capabilityId ${capabilityId} and commandClassId ${commandClassId}`); + } + } + + let parser = null; + if (commandType === 'report' && commandClass.reportParserOverride && typeof commandClass[`${commandType}Parser`] === 'function') { + parser = commandClass[`${commandType}Parser`]; + } else { + const nodeCommandClass = node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; + if (typeof nodeCommandClass === 'undefined') return new Error(`missing_command_class_${commandClassId}`); + const nodeCommandClassVersion = nodeCommandClass.version; + + for (let i = nodeCommandClassVersion; i > 0; i--) { + const fn = commandClass[`${commandType}ParserV${i}`]; + if (typeof fn === 'function') { + parser = fn; + break; + } + } + + if (parser === null && typeof commandClass[`${commandType}Parser`] === 'function') { + parser = commandClass[`${commandType}Parser`]; + } + } + + if (typeof commandId === 'string') { + return { + commandClassId, + commandId, + parser, + opts, + node, + }; + } + + return new Error('missing_zwave_capability'); + + } + + /* + * Public methods + */ + + /** + * Register a Homey Capability with a Command Class. + * Multiple `parser` methods can be provided by appending a version, e.g. `getParserV3`. This will make sure that the highest matching version will be used, falling back to `getParser`. + * @param {string} capabilityId - The Homey capability id (e.g. `onoff`) + * @param {string} commandClassId - The command class id (e.g. `BASIC`) + * @param {Object} [opts] - The object with options for this capability/commandclass combination. These will extend system options, if available (`/lib/zwave/system/`) + * @param {String} [opts.get] - The command to get a value (e.g. `BASIC_GET`) + * @param {String} [opts.getParser] - The function that is called when a GET request is made. Should return an Object. + * @param {Object} [opts.getOpts + * @param {Boolean} [opts.getOpts.getOnStart] - Get the value on App start + * @param {Boolean} [opts.getOpts.getOnOnline] - Get the value when the device is marked as online + * @param {Number|String} [opts.getOpts.pollInterval] - Interval (in ms) to poll with a GET request. When provided a string, the device's setting with the string as ID will be used (e.g. `poll_interval`) + * @param {Number} [opts.getOpts.pollMultiplication] - Multiplication factor for the pollInterval key, must be a number. (e.g. 1000 to convert to seconds, 60.000 for minutes, 3600000 for hours) + * @param {String} [opts.set] - The command to set a value (e.g. `BASIC_SET`) + * @param {Function} [opts.setParser] - The function that is called when a SET request is made. Should return an Object. + * @param {Mixed} [opts.setParser.value] - The value of the Homey capability + * @param {Object} [opts.setParser.opts] - Options for the capability command + * @param {Function} [opts.fn] - This function is called after a setCapabilityValue has been called + * @param {Object} [opts.fn.value] - The capability value + * @param {Object} [opts.fn.opts] - The capability opts + * @param {String} [opts.report] - The command to report a value (e.g. `BASIC_REPORT`) + * @param {Boolean} [opts.reportParserOverride] - Boolean flag to determine if the `reportParser` method should override all report parsers. (Assumed false when not specified) + * @param {Function} [opts.reportParser] - The function that is called when a REPORT request is made. Should return an Object. + * @param {Object} [opts.reportParser.report] - The report object + * @param {Number} [opts.multiChannelNodeId] - An ID to use a MultiChannel Node for this capability + * @param {Function} [opts.fn] - This function is called after a setCapabilityValue has been executed + * @param {Object} [opts.fn.value] - The capability value + */ + registerCapability(capabilityId, commandClassId, userOpts) { + + // Check if device has the command class we're trying to register, if not, abort + if (userOpts && typeof userOpts.multiChannelNodeId === 'number') { + if (!this.node.MultiChannelNodes || !this.node.MultiChannelNodes[userOpts.multiChannelNodeId] || this.node.MultiChannelNodes[userOpts.multiChannelNodeId].CommandClass[`COMMAND_CLASS_${commandClassId}`] === 'undefined') { + return this.error(`CommandClass: ${commandClassId} in multi channel node ${userOpts.multiChannelNodeId} undefined`); + } + } else if (typeof this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`] === 'undefined') { + return this.error(`CommandClass: ${commandClassId} in main node undefined`); + } + + // register the Z-Wave capability listener + this._capabilities[capabilityId] = this._capabilities[capabilityId] || {}; + this._capabilities[capabilityId][commandClassId] = this._capabilities[capabilityId][commandClassId] || {}; + + // merge systemOpts & userOpts + let systemOpts = {}; + try { + + // First try get device class specific system capability + systemOpts = this._getDeviceClassSpecificSystemCapability(capabilityId, commandClassId); + + // If not available use general system capability + if (!systemOpts) systemOpts = require(`./system/capabilities/${capabilityId}/${commandClassId}.js`); + + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + process.nextTick(() => { + throw err; + }); + } + } + + // add implicit override for this capability's reportParser + if (userOpts) { + userOpts.reportParserOverride = typeof userOpts.reportParser === 'function' && userOpts.reportParserOverride === true; + } + + this._capabilities[capabilityId][commandClassId] = Object.assign( + {}, + systemOpts || {}, + userOpts || {} + ); + + // register get/set/realtime + this._registerCapabilityRealtime(capabilityId, commandClassId); + this._registerCapabilitySet(capabilityId, commandClassId); + this._registerCapabilityGet(capabilityId, commandClassId); + } + + /** + * Method that checks if a device class specific system capability is available and returns it if possible. Else it + * will return null. + * @param {string} capabilityId + * @param {string} commandClassId + * @returns {Object|null} + * @private + */ + _getDeviceClassSpecificSystemCapability(capabilityId, commandClassId) { + try { + return require(`./system/capabilities/${capabilityId}/${this.getClass()}/${commandClassId}.js`); + } catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') this.error(err); + return null; + } + } + + /** + * Register a setting parser, which is called when a setting has changed. + * @param {string} settingId - The setting ID, as specified in `/app.json` + * @param {Function} parserFn - The parser function, must return a Buffer, number or boolean + * @param {Mixed} parserFn.value - The setting value + * @param {Mixed} parserFn.zwaveObj - The setting's `zwave` object as defined in `/app.json` + */ + registerSetting(settingId, parserFn) { + this._settings[settingId] = parserFn; + } + + /** + * Register a multi channel report listener, which is called when a report has been received. + * @param {number} multiChannelNodeId - The multi channel node id + * @param {string} commandClassId - The ID of the Command Class (e.g. `BASIC`) + * @param {string} commandId - The ID of the Command (e.g. `BASIC_REPORT`) + * @param {Function} triggerFn + * @param {Object} triggerFn.report - The received report + */ + registerMultiChannelReportListener(multiChannelNodeId, commandClassId, commandId, triggerFn) { + // Check for valid multi channel nodes + if (!this.node.MultiChannelNodes || + !this.node.MultiChannelNodes[multiChannelNodeId] || + (Array.isArray(this.node.MultiChannelNodes) && this.node.MultiChannelNodes.length === 0)) { + return this.error('Invalid multi channel node', multiChannelNodeId); + } + + const commandClass = this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[`COMMAND_CLASS_${commandClassId}`]; + if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', commandClassId); + + this._reportListeners[commandClassId] = this._reportListeners[commandClassId] || {}; + this._reportListeners[commandClassId][`${multiChannelNodeId}_${commandId}`] = triggerFn; + + commandClass.on('report', (command, payload) => { + if (command.name !== commandId) return; + if (typeof this._reportListeners[commandClassId][`${multiChannelNodeId}_${command.name}`] === 'function') { + this._reportListeners[commandClassId][`${multiChannelNodeId}_${command.name}`](payload); + } + }); + } + + /** + * Register a report listener, which is called when a report has been received. + * @param {string} commandClassId - The ID of the Command Class (e.g. `BASIC`) + * @param {string} commandId - The ID of the Command (e.g. `BASIC_REPORT`) + * @param {Function} triggerFn + * @param {Object} triggerFn.report - The received report + */ + registerReportListener(commandClassId, commandId, triggerFn) { + const commandClass = this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; + if (typeof commandClass === 'undefined') return this.error('Invalid commandClass:', commandClassId); + let previousSequence; + + this._reportListeners[commandClassId] = this._reportListeners[commandClassId] || {}; + this._reportListeners[commandClassId][commandId] = triggerFn; + + commandClass.on('report', (command, payload) => { + if (command.name !== commandId) return; + + // Catch central scene echos and (sometimes) failing parser + if (command.name === 'CENTRAL_SCENE_NOTIFICATION') { + + if (typeof previousSequence !== 'undefined' && payload.hasOwnProperty('Sequence Number') && payload['Sequence Number'] === previousSequence) return; + previousSequence = payload['Sequence Number']; + + if (payload.hasOwnProperty('Properties1') && + payload.Properties1.hasOwnProperty('Key Attributes') && + typeof payload.Properties1['Key Attributes'] === 'number') { + switch (payload.Properties1['Key Attributes']) { + case 0: + payload.Properties1['Key Attributes'] = 'Key Pressed 1 time'; + break; + case 1: + payload.Properties1['Key Attributes'] = 'Key Released'; + break; + case 2: + payload.Properties1['Key Attributes'] = 'Key Held Down'; + break; + case 3: + payload.Properties1['Key Attributes'] = 'Key Pressed 2 times'; + break; + case 4: + payload.Properties1['Key Attributes'] = 'Key Pressed 3 times'; + break; + case 5: + payload.Properties1['Key Attributes'] = 'Key Pressed 4 times'; + break; + case 6: + payload.Properties1['Key Attributes'] = 'Key Pressed 5 times'; + break; + } + } + } + + if (this._reportListeners[commandClassId] && + this._reportListeners[commandClassId][command.name]) { + this._reportListeners[commandClassId][command.name](payload); + } + }); + } + + /** + * Method that will check if the node has the provided command class + * @param {string} commandClassId - For example: SWITCH_BINARY + * @returns {boolean} + */ + hasCommandClass(commandClassId) { + return !(typeof this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`] === 'undefined'); + } + + getCommandClass(commandClassId) { + if (!this.hasCommandClass(commandClassId)) return new Error(`missing_command_class_${commandClassId}`); + return this.node.CommandClass[`COMMAND_CLASS_${commandClassId}`]; + } + + /** + * Print the current Node information with Command Classes and their versions + */ + printNode() { + this.log('------------------------------------------'); + + // log the entire Node + this.log('Node:', this.getData().token); + this.log('- Battery:', this.node.battery); + this.log('- DeviceClassGeneric:', this.node.deviceClassGeneric); + + Object.keys(this.node.CommandClass).forEach(commandClassId => { + this.log('- CommandClass:', commandClassId); + this.log('-- Version:', this.node.CommandClass[commandClassId].version); + this.log('-- Commands:'); + + Object.keys(this.node.CommandClass[commandClassId]).forEach(key => { + if (typeof this.node.CommandClass[commandClassId][key] === 'function' && key === key.toUpperCase()) { + this.log('---', key); + } + }); + }); + + if (this.node.MultiChannelNodes) { + Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { + this.log('- MultiChannelNode:', multiChannelNodeId); + this.log('- DeviceClassGeneric:', this.node.MultiChannelNodes[multiChannelNodeId].deviceClassGeneric); + + Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { + this.log('-- CommandClass:', commandClassId); + this.log('--- Version:', + this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].version); + this.log('--- Commands:'); + + Object + .keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId]) + .forEach(key => { + if (typeof this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId][key] === + 'function' && key === key.toUpperCase()) { + this.log('----', key); + } + }); + }); + }); + } + + this.log('------------------------------------------'); + this.log(''); + + Object.keys(this.node.CommandClass).forEach(commandClassId => { + this.node.CommandClass[commandClassId].on('report', function () { + this.log(`node.CommandClass['${commandClassId}'].on('report')`, 'arguments:', arguments); + }.bind(this)); + }); + + if (this.node.MultiChannelNodes) { + Object.keys(this.node.MultiChannelNodes).forEach(multiChannelNodeId => { + Object.keys(this.node.MultiChannelNodes[multiChannelNodeId].CommandClass).forEach(commandClassId => { + this.node.MultiChannelNodes[multiChannelNodeId].CommandClass[commandClassId].on('report', function () { + this.log(`node.MultiChannelNodes['${multiChannelNodeId}']. CommandClass['${commandClassId}'].on('report')`, 'arguments:', arguments); - }.bind(this)); - }); - }); - } - } + }.bind(this)); + }); + }); + } + } } diff --git a/node_modules/homey-meshdriver/lib/zwave/ZwaveLightDevice.js b/node_modules/homey-meshdriver/lib/zwave/ZwaveLightDevice.js new file mode 100644 index 0000000..cb36f89 --- /dev/null +++ b/node_modules/homey-meshdriver/lib/zwave/ZwaveLightDevice.js @@ -0,0 +1,281 @@ +'use strict'; + +const ZwaveDevice = require('homey-meshdriver').ZwaveDevice; +const Utils = require('homey-meshdriver').Util; +const FACTORY_DEFAULT_COLOR_DURATION = 255; +let debounceColorMode; + +/** + * ZwaveLightDevice takes care of all commands used by XY Lighting devices (COMMAND_CLASS_SWITCH_COLOR). + * Set the capabilitiesOptions "setOnDim" to false for the onoff capability + * + * Duration(s) can be given for: + * - dim (SWITCH_MULTILEVEL >= V2) + * - light_hue (SWITCH_COLOR >= V2) + * - light_saturation (SWITCH_COLOR >= V2) + * - light_temperature (SWITCH_COLOR >= V2) + * + * @extends ZwaveDevice + * @example + * + * // app.json + * { + * "id": YOUR_APP_ID, + * ... + * "drivers": [ + * { + * "id": "YOUR_DRIVER_ID", + * "capabilitiesOptions": { + * "onoff": { + * "setOnDim": false + * }, + * "dim": { + * "opts": { + * "duration": true + * } + * }, + * "light_hue": { + * "opts": { + * "duration": true + * } + * }, + * "light_saturation": { + * "opts": { + * "duration": true + * } + * }, + * "light_temperature": { + * "opts": { + * "duration": true + * } + * } + * } + * ] + * } + * + * // device.js + * const ZwaveLightDevice = require('homey-meshdriver').ZwaveLightDevice; + * + * class myDevice extends ZwaveLightDevice { + * + * async onMeshInit() { + * + * await super.onMeshInit(); + * // YOUR CODE COMES HERE + * } + * } + */ + +class ZwaveLightDevice extends ZwaveDevice { + + onMeshInit() { + + // Check if all used capabilities are present + if (!this.hasCapability('onoff')) return this.error('Missing capability: onoff'); + if (!this.hasCapability('dim')) return this.error('Missing capability: dim'); + if (!this.hasCapability('light_mode')) return this.error('Missing capability: light_mode'); + if (!this.hasCapability('light_hue')) return this.error('Missing capability: light_hue'); + if (!this.hasCapability('light_saturation')) return this.error('Missing capability: light_saturation'); + if (!this.hasCapability('light_temperature')) return this.error('Missing capability: light_temperature'); + + // Register capabilities + if (this.hasCommandClass('SWITCH_MULTILEVEL')) { + this.registerCapability('onoff', 'SWITCH_MULTILEVEL'); + this.registerCapability('dim', 'SWITCH_MULTILEVEL'); + + // If Multilevel Switch is not available fall back to basic + } else if (this.hasCommandClass('BASIC')) { + this.registerCapability('onoff', 'BASIC'); + this.registerCapability('dim', 'BASIC'); + + } + + this.registerMultipleCapabilityListener(['light_hue', 'light_saturation'], async (values, options) => { + let hue; + let saturation; + + typeof values.light_hue === 'number' ? hue = values.light_hue : hue = this.getCapabilityValue('light_hue'); + typeof values.light_saturation === 'number' ? saturation = values.light_saturation : saturation = this.getCapabilityValue('light_saturation'); + const value = 1; // brightness value is not determined in SWITCH_COLOR but with SWITCH_MULTILEVEL, changing this throws the dim value vs real life brightness out of sync + + const rgb = Utils.convertHSVToRGB({ hue, saturation, value }); + + debounceColorMode = setTimeout(() => { + debounceColorMode = false; + }, 200); + + let duration = null; + if (options.hasOwnProperty('light_hue') && options.light_hue.hasOwnProperty('duration')) { + duration = options.light_hue.duration; + } + if (!duration && options.hasOwnProperty('light_saturation') && options.light_saturation.hasOwnProperty('duration')) { + duration = options.light_saturation.duration; + } + + return await this._sendColors({ + warm: 0, + cold: 0, + red: rgb.red, + green: rgb.green, + blue: rgb.blue, + duration, + }); + }); + + this.registerCapabilityListener(['light_temperature'], async (value, options) => { + const warm = Math.floor(value * 255); + const cold = Math.floor((1 - value) * 255); + + debounceColorMode = setTimeout(() => { + debounceColorMode = false; + }, 200); + + let duration = null; + if (options.hasOwnProperty('duration')) duration = options.duration; + + return await this._sendColors({ + warm, + cold, + red: 0, + green: 0, + blue: 0, + duration, + }); + }); + + this.registerCapability('light_mode', 'SWITCH_COLOR', { + set: 'SWITCH_COLOR_SET', + setParser: (value, options) => { + + // set light_mode is always triggered with the set color/temperature flow cards, timeout is needed because of homey's async nature surpassing the debounce + setTimeout(async () => { + if (debounceColorMode) { + clearTimeout(debounceColorMode); + debounceColorMode = false; + return this.setCapabilityValue('light_mode', value); + } + + if (value === 'color') { + const hue = this.getCapabilityValue('light_hue') || 1; + const saturation = this.getCapabilityValue('light_saturation') || 1; + const _value = 1; // brightness value is not determined in SWITCH_COLOR but with SWITCH_MULTILEVEL, changing this throws the dim value vs real life brightness out of sync + + const rgb = Utils.convertHSVToRGB({ hue, saturation, _value }); + + return await this._sendColors({ + warm: 0, + cold: 0, + red: rgb.red, + green: rgb.green, + blue: rgb.blue, + duration: options.duration || null, + }); + + } else if (value === 'temperature') { + const temperature = this.getCapabilityValue('light_temperature') || 1; + const warm = temperature * 255; + const cold = (1 - temperature) * 255; + + return await this._sendColors({ + warm, + cold, + red: 0, + green: 0, + blue: 0, + duration: options.duration || null, + }); + } + }, 50); + }, + }); + + // Getting all color values during boot + const commandClassColorSwitch = this.getCommandClass('SWITCH_COLOR'); + if (!(commandClassColorSwitch instanceof Error) && typeof commandClassColorSwitch.SWITCH_COLOR_GET === 'function') { + + // Timeout mandatory for stability, often fails getting 1 (or more) value without it + setTimeout(() => { + + // Wait for all color values to arrive + Promise.all([this._getColorValue(0), this._getColorValue(1), this._getColorValue(2), this._getColorValue(3), this._getColorValue(4)]) + .then(result => { + if (result[0] === 0 && result[1] === 0) { + const hsv = Utils.convertRGBToHSV({ + red: result[2], + green: result[3], + blue: result[4], + }); + + this.setCapabilityValue('light_mode', 'color'); + this.setCapabilityValue('light_hue', hsv.hue); + this.setCapabilityValue('light_saturation', hsv.saturation); + } else { + const temperature = Math.round(result[0] / 255 * 100) / 100; + this.setCapabilityValue('light_mode', 'temperature'); + this.setCapabilityValue('light_temperature', temperature); + } + }); + }, 500); + } + } + + async _getColorValue(colorComponentID) { + const commandClassColorSwitch = this.getCommandClass('SWITCH_COLOR'); + if (!(commandClassColorSwitch instanceof Error) && typeof commandClassColorSwitch.SWITCH_COLOR_GET === 'function') { + + try { + const result = await commandClassColorSwitch.SWITCH_COLOR_GET({ 'Color Component ID': colorComponentID }); + return (result && typeof result.Value === 'number') ? result.Value : 0; + } catch (err) { + this.error(err); + return 0; + } + } + return 0; + } + + async _sendColors({ warm, cold, red, green, blue, duration }) { + const SwitchColorVersion = this.getCommandClass('SWITCH_COLOR').version || 1; + + let setCommand = { + Properties1: { + 'Color Component Count': 5, + }, + vg1: [ + { + 'Color Component ID': 0, + Value: Math.round(warm), + }, + { + 'Color Component ID': 1, + Value: Math.round(cold), + }, + { + 'Color Component ID': 2, + Value: Math.round(red), + }, + { + 'Color Component ID': 3, + Value: Math.round(green), + }, + { + 'Color Component ID': 4, + Value: Math.round(blue), + }, + ], + }; + + if (SwitchColorVersion > 1) { + setCommand.duration = typeof duration !== 'number' ? FACTORY_DEFAULT_COLOR_DURATION : Utils.calculateZwaveDimDuration(duration); + } + + // Fix broken CC_SWITCH_COLOR_V2 parser + if (SwitchColorVersion === 2) { + setCommand = new Buffer([setCommand.Properties1['Color Component Count'], 0, setCommand.vg1[0].Value, 1, setCommand.vg1[1].Value, 2, setCommand.vg1[2].Value, 3, setCommand.vg1[3].Value, 4, setCommand.vg1[4].Value, setCommand.duration]); + } + + return this.node.CommandClass.COMMAND_CLASS_SWITCH_COLOR.SWITCH_COLOR_SET(setCommand); + } +} + +module.exports = ZwaveLightDevice; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/dim/windowcoverings/SWITCH_MULTILEVEL.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/dim/windowcoverings/SWITCH_MULTILEVEL.js index 2c5c23d..9a694a6 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/dim/windowcoverings/SWITCH_MULTILEVEL.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/dim/windowcoverings/SWITCH_MULTILEVEL.js @@ -14,33 +14,30 @@ module.exports = { }, setParserV1(value) { const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_DIRECTION); + if (invertDirection) value = 1 - value; - if (value >= 1) { - if (invertDirection) value = 0; - else value = 0.99; - } return { - Value: invertDirection ? Math.round((1 - value) * 100) : Math.round(value * 100), + Value: Math.round(value * 99), }; }, setParserV2(value, options) { const duration = (options.hasOwnProperty('duration') ? util.calculateZwaveDimDuration(options.duration) : FACTORY_DEFAULT_DIMMING_DURATION_V2); const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_DIRECTION); - - if (value >= 1) { - if (invertDirection) value = 0; - else value = 0.99; - } + if (invertDirection) value = 1 - value; return { - Value: invertDirection ? Math.round((1 - value) * 100) : Math.round(value * 100), + Value: Math.round(value * 99), 'Dimming Duration': duration, }; }, setParserV4(value, options) { const duration = (options.hasOwnProperty('duration') ? util.calculateZwaveDimDuration(options.duration) : FACTORY_DEFAULT_DIMMING_DURATION_V4); if (this.hasCapability('onoff')) this.setCapabilityValue('onoff', value > 0); + + const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_DIRECTION); + if (invertDirection) value = 1 - value; + return { Value: Math.round(value * 99), 'Dimming Duration': duration, @@ -52,7 +49,7 @@ module.exports = { if (report && report.hasOwnProperty('Value (Raw)')) { if (report['Value (Raw)'][0] === 255) return invertDirection ? 0 : 1; - return invertDirection ? (100 - report['Value (Raw)'][0]) / 99 : report['Value (Raw)'][0] / 99; + return invertDirection ? (99 - report['Value (Raw)'][0]) / 99 : report['Value (Raw)'][0] / 99; } return null; }, @@ -61,7 +58,7 @@ module.exports = { if (report && report.hasOwnProperty('Current Value (Raw)')) { if (report['Current Value (Raw)'][0] === 255) return invertDirection ? 0 : 1; - return invertDirection ? (100 - report['Current Value (Raw)'][0]) / 99 : report['Current Value (Raw)'][0] / 99; + return invertDirection ? (99 - report['Current Value (Raw)'][0]) / 99 : report['Current Value (Raw)'][0] / 99; } return null; }, diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_current/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_current/METER.js index 9a62d03..be5db0c 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_current/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_current/METER.js @@ -69,8 +69,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 1 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_power/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_power/METER.js index 6d8431a..1aa6dd3 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_power/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_power/METER.js @@ -32,7 +32,7 @@ module.exports = { getOpts: { getOnStart: true, }, - getParserV3: () => ({ + getParserV2: () => ({ Properties1: { Scale: 2, }, @@ -92,8 +92,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 2 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_voltage/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_voltage/METER.js index 1efed80..fe2ee3b 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_voltage/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/measure_voltage/METER.js @@ -68,8 +68,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 0 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/meter_power/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/meter_power/METER.js index 6c4c421..f39d31d 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/meter_power/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/meter_power/METER.js @@ -31,7 +31,7 @@ module.exports = { getOpts: { getOnStart: true, }, - getParserV3: () => ({ + getParserV2: () => ({ Properties1: { Scale: 0, }, @@ -91,8 +91,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 0 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerFactor/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerFactor/METER.js index 478a7be..2cc4cfd 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerFactor/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerFactor/METER.js @@ -68,8 +68,9 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 2 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + report.hasOwnProperty('Scale 2') === false || + report.hasOwnProperty('Scale 2') + && report['Scale 2'] === 0) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerReactive/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerReactive/METER.js index 0eb8ec5..96ca818 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerReactive/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerReactive/METER.js @@ -69,8 +69,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 3 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerTotalApparent/METER.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerTotalApparent/METER.js index 6f1a21e..530c22d 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerTotalApparent/METER.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/powerTotalApparent/METER.js @@ -31,7 +31,7 @@ module.exports = { getOpts: { getOnStart: true, }, - getParserV3: () => ({ + getParserV2: () => ({ Properties1: { Scale: 1, }, @@ -68,8 +68,8 @@ module.exports = { report.hasOwnProperty('Properties2') && report.Properties2.hasOwnProperty('Scale bits 10') && report.Properties2['Scale bits 10'] === 1 && - report.hasOwnProperty('Scale 2') && - report['Scale 2'] === 0) { + (report.hasOwnProperty('Scale 2') === false || report['Scale 2'] === 0) + ) { return report['Meter Value (Parsed)']; } return null; diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/target_temperature/THERMOSTAT_SETPOINT.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/target_temperature/THERMOSTAT_SETPOINT.js index fa9f165..f24cf85 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/target_temperature/THERMOSTAT_SETPOINT.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/target_temperature/THERMOSTAT_SETPOINT.js @@ -1,17 +1,23 @@ 'use strict'; +/** + * Capability target_temperature i.c.w. the THERMOSTAT_SETPOINT command class takes an additional variable (this.thermostatSetpointType) + * which can determine on runtime the setpoint type to use. + */ module.exports = { get: 'THERMOSTAT_SETPOINT_GET', getOpts: { getOnStart: true, }, - getParser: () => ({ - Level: { - 'Setpoint Type': 'Heating 1', - }, - }), + getParser() { + return { + Level: { + 'Setpoint Type': this.thermostatSetpointType || 'Heating 1', + }, + }; + }, set: 'THERMOSTAT_SETPOINT_SET', - setParser: value => { + setParser(value) { // Create value buffer const bufferValue = new Buffer(2); @@ -19,7 +25,7 @@ module.exports = { return { Level: { - 'Setpoint Type': 'Heating 1', + 'Setpoint Type': this.thermostatSetpointType || 'Heating 1', }, Level2: { Size: 2, diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_state/SWITCH_BINARY.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_state/SWITCH_BINARY.js index 1c3cd45..d2f6b32 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_state/SWITCH_BINARY.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_state/SWITCH_BINARY.js @@ -65,6 +65,7 @@ function determineAndSaveState(value) { break; } - // Save latest known position state + // Save latest known position state and return it this.windowCoveringsPosition = result; + return result; } \ No newline at end of file diff --git a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_tilt_set/SWITCH_MULTILEVEL.js b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_tilt_set/SWITCH_MULTILEVEL.js index a4d66d4..7be2b2d 100644 --- a/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_tilt_set/SWITCH_MULTILEVEL.js +++ b/node_modules/homey-meshdriver/lib/zwave/system/capabilities/windowcoverings_tilt_set/SWITCH_MULTILEVEL.js @@ -1,6 +1,6 @@ 'use strict'; -const util = require('./../../../../../util'); +const util = require('./../../../../util'); const INVERT_WINDOW_COVERINGS_TILT_DIRECTION = 'invertWindowCoveringsTiltDirection'; const FACTORY_DEFAULT_DIMMING_DURATION_V2 = 'Factory default'; @@ -14,33 +14,30 @@ module.exports = { }, setParserV1(value) { const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_TILT_DIRECTION); + if (invertDirection) value = 1 - value; - if (value >= 1) { - if (invertDirection) value = 0; - else value = 0.99; - } return { - Value: invertDirection ? Math.round((1 - value) * 100) : Math.round(value * 100), + Value: Math.round(value * 99), }; }, setParserV2(value, options) { const duration = (options.hasOwnProperty('duration') ? util.calculateZwaveDimDuration(options.duration) : FACTORY_DEFAULT_DIMMING_DURATION_V2); const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_TILT_DIRECTION); - - if (value >= 1) { - if (invertDirection) value = 0; - else value = 0.99; - } + if (invertDirection) value = 1 - value; return { - Value: invertDirection ? Math.round((1 - value) * 100) : Math.round(value * 100), + Value: Math.round(value * 99), 'Dimming Duration': duration, }; }, setParserV4(value, options) { const duration = (options.hasOwnProperty('duration') ? util.calculateZwaveDimDuration(options.duration) : FACTORY_DEFAULT_DIMMING_DURATION_V4); if (this.hasCapability('onoff')) this.setCapabilityValue('onoff', value > 0); + + const invertDirection = !!this.getSetting(INVERT_WINDOW_COVERINGS_TILT_DIRECTION); + if (invertDirection) value = 1 - value; + return { Value: Math.round(value * 99), 'Dimming Duration': duration, @@ -52,7 +49,7 @@ module.exports = { if (report && report.hasOwnProperty('Value (Raw)')) { if (report['Value (Raw)'][0] === 255) return invertDirection ? 0 : 1; - return invertDirection ? (100 - report['Value (Raw)'][0]) / 99 : report['Value (Raw)'][0] / 99; + return invertDirection ? (99 - report['Value (Raw)'][0]) / 99 : report['Value (Raw)'][0] / 99; } return null; }, @@ -61,7 +58,7 @@ module.exports = { if (report && report.hasOwnProperty('Current Value (Raw)')) { if (report['Current Value (Raw)'][0] === 255) return invertDirection ? 0 : 1; - return invertDirection ? (100 - report['Current Value (Raw)'][0]) / 99 : report['Current Value (Raw)'][0] / 99; + return invertDirection ? (99 - report['Current Value (Raw)'][0]) / 99 : report['Current Value (Raw)'][0] / 99; } return null; }, diff --git a/node_modules/homey-meshdriver/package.json b/node_modules/homey-meshdriver/package.json index 9875c3a..7366ab9 100644 --- a/node_modules/homey-meshdriver/package.json +++ b/node_modules/homey-meshdriver/package.json @@ -1,6 +1,6 @@ { "_from": "git+https://github.com/athombv/node-homey-meshdriver.git", - "_id": "homey-meshdriver@1.2.7", + "_id": "homey-meshdriver@1.2.28", "_inBundle": false, "_location": "/homey-meshdriver", "_phantomChildren": {}, @@ -18,7 +18,7 @@ "#USER", "/" ], - "_resolved": "git+https://github.com/athombv/node-homey-meshdriver.git#588a09ed1caf91bbe9e1a048bd30b1c328baa73b", + "_resolved": "git+https://github.com/athombv/node-homey-meshdriver.git#35da9d9948ab7fc16399c558d68ba800d72e23c1", "_spec": "homey-meshdriver@git+https://github.com/athombv/node-homey-meshdriver.git", "_where": "D:\\Projects\\Homey\\com.sensative", "author": "", @@ -44,5 +44,5 @@ "test": "echo \"Error: no test specified\" && exit 1", "updateDocs": "npm install --only=dev; grunt jsdoc" }, - "version": "1.2.7" + "version": "1.2.28" } diff --git a/package-lock.json b/package-lock.json index 93fd5be..4478382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,15 +24,15 @@ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" }, "homey-log": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/homey-log/-/homey-log-1.0.5.tgz", - "integrity": "sha1-k2RU5iM5u3wxO3+m5htZasohSL8=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/homey-log/-/homey-log-1.0.6.tgz", + "integrity": "sha512-LFOaByu/E2krWNSGwmoazL4HCSSyLVQMoaBo9mgMxV2YC4lEB15SMov1bqZxUclEJCbnR+YUX9xsnFW5brmIbw==", "requires": { "raven": "0.12.3" } }, "homey-meshdriver": { - "version": "git+https://github.com/athombv/node-homey-meshdriver.git#588a09ed1caf91bbe9e1a048bd30b1c328baa73b", + "version": "git+https://github.com/athombv/node-homey-meshdriver.git#35da9d9948ab7fc16399c558d68ba800d72e23c1", "requires": { "color-space": "1.16.0" } diff --git a/package.json b/package.json index 1868113..354dea8 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "requires": true, "dependencies": { - "homey-log": "^1.0.1", + "homey-log": "^1.0.6", "homey-meshdriver": "git+https://github.com/athombv/node-homey-meshdriver.git" }, "devDependencies": {},