├── .eslintrc ├── .gitignore ├── .hound.yml ├── CLA.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── accessories ├── alarm_control_panel.js ├── binary_sensor.js ├── climate.js ├── cover.js ├── device_tracker.js ├── fan.js ├── light.js ├── lock.js ├── media_player.js ├── sensor.js └── switch.js ├── index.js └── package.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "airbnb-base", 7 | "rules": { 8 | "new-cap": 0, 9 | "prefer-template": 0, 10 | "object-shorthand": 0, 11 | "func-names": 0, 12 | "prefer-arrow-callback": 0, 13 | "no-underscore-dangle": 0, 14 | "no-var": 0, 15 | "strict": 0, 16 | "prefer-spread": 0, 17 | "no-plusplus": 0, 18 | "no-bitwise": 0, 19 | "comma-dangle": 0, 20 | "vars-on-top": 0, 21 | "no-continue": 0, 22 | "no-param-reassign": 0, 23 | "no-multi-assign": 0, 24 | "class-methods-use-this": [ 25 | "error", 26 | { 27 | "exceptMethods": [ 28 | "transformData" 29 | ] 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .idea/ 4 | homebridge-homeassistant.iml 5 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | jshint: 2 | enabled: false 3 | eslint: 4 | enabled: true 5 | config_file: .eslintrc 6 | -------------------------------------------------------------------------------- /CLA.md: -------------------------------------------------------------------------------- 1 | # Contributor License Agreement 2 | 3 | ``` 4 | By making a contribution to this project, I certify that: 5 | 6 | (a) The contribution was created in whole or in part by me and I 7 | have the right to submit it under the Apache 2.0 license; or 8 | 9 | (b) The contribution is based upon previous work that, to the best 10 | of my knowledge, is covered under an appropriate open source 11 | license and I have the right under that license to submit that 12 | work with modifications, whether created in whole or in part 13 | by me, under the Apache 2.0 license; or 14 | 15 | (c) The contribution was provided directly to me by some other 16 | person who certified (a), (b) or (c) and I have not modified 17 | it. 18 | 19 | (d) I understand and agree that this project and the contribution 20 | are public and that a record of the contribution (including all 21 | personal information I submit with it) is maintained indefinitely 22 | and may be redistributed consistent with this project or the open 23 | source license(s) involved. 24 | ``` 25 | 26 | ## Attribution 27 | 28 | The text of this license is available under the [Creative Commons Attribution-ShareAlike 3.0 Unported License](http://creativecommons.org/licenses/by-sa/3.0/). It is based on the Linux [Developer Certificate Of Origin](http://elinux.org/Developer_Certificate_Of_Origin), but is modified to explicitly use the Apache 2.0 license 29 | and not mention sign-off. 30 | 31 | ## Signing 32 | 33 | To sign this CLA you must first submit a pull request to a repository under the Home Assistant organization. 34 | 35 | ## Adoption 36 | 37 | This Contributor License Agreement (CLA) was first announced on January 21st, 2017 in [this][cla-blog] blog post and adopted January 28th, 2017. 38 | 39 | [cla-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [safety@home-assistant.io][email]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available [here][version]. 72 | 73 | ## Adoption 74 | 75 | This Code of Conduct was first adopted January 21st, 2017 and announced in [this][coc-blog] blog post. 76 | 77 | [homepage]: http://contributor-covenant.org 78 | [version]: http://contributor-covenant.org/version/1/4/ 79 | [email]: mailto:safety@home-assistant.io 80 | [coc-blog]: https://home-assistant.io/blog/2017/01/21/home-assistant-governance/ 81 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :rotating_light: DEPRECATED :rotating_light: 2 | homebridge-homeassistant has been deprecated since Home Assistant has natively supported 3 | HomeKit since version 0.64. For more information on how to configure native HomeKit support 4 | please see the documentation [here](https://www.home-assistant.io/components/homekit/). 5 | We **STRONGLY** suggest to migrate to the Home Assistant HomeKit component. 6 | -------------------------------------------------------------------------------- /accessories/alarm_control_panel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function HomeAssistantAlarmControlPanel(log, data, client, firmware) { 8 | // device info 9 | this.domain = 'alarm_control_panel'; 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_model) { 25 | this.model = String(data.attributes.homebridge_model); 26 | } else { 27 | this.model = 'Alarm Control Panel'; 28 | } 29 | if (data.attributes && data.attributes.homebridge_serial) { 30 | this.serial = String(data.attributes.homebridge_serial); 31 | } else { 32 | this.serial = data.entity_id; 33 | } 34 | this.client = client; 35 | this.log = log; 36 | this.alarmCode = data.attributes.homebridge_alarm_code; 37 | } 38 | 39 | HomeAssistantAlarmControlPanel.prototype = { 40 | onEvent(oldState, newState) { 41 | if (newState.state) { 42 | let alarmState; 43 | if (newState.state === 'armed_home') { 44 | alarmState = 0; 45 | } else if (newState.state === 'armed_away') { 46 | alarmState = 1; 47 | } else if (newState.state === 'armed_night') { 48 | alarmState = 2; 49 | } else if (newState.state === 'disarmed') { 50 | alarmState = 3; 51 | } else if (newState.state === 'triggered') { 52 | alarmState = 4; 53 | } else { 54 | alarmState = 3; 55 | } 56 | this.alarmService.getCharacteristic(Characteristic.SecuritySystemCurrentState) 57 | .setValue(alarmState, null, 'internal'); 58 | this.alarmService.getCharacteristic(Characteristic.SecuritySystemTargetState) 59 | .setValue(alarmState, null, 'internal'); 60 | } 61 | }, 62 | getAlarmState(callback) { 63 | this.client.fetchState(this.entity_id, (data) => { 64 | if (data) { 65 | if (data.state === 'armed_home') { 66 | callback(null, 0); 67 | } else if (data.state === 'armed_away') { 68 | callback(null, 1); 69 | } else if (data.state === 'armed_night') { 70 | callback(null, 2); 71 | } else if (data.state === 'disarmed') { 72 | callback(null, 3); 73 | } else if (data.state === 'triggered') { 74 | callback(null, 4); 75 | } else { 76 | callback(null, 3); 77 | } 78 | } else { 79 | callback(communicationError); 80 | } 81 | }); 82 | }, 83 | 84 | setAlarmState(targetState, callback, context) { 85 | if (context === 'internal') { 86 | callback(); 87 | return; 88 | } 89 | 90 | const that = this; 91 | const serviceData = {}; 92 | serviceData.entity_id = this.entity_id; 93 | if (this.alarmCode) { 94 | serviceData.code = this.alarmCode; 95 | } 96 | 97 | if (targetState === Characteristic.SecuritySystemCurrentState.STAY_ARM) { 98 | this.log(`Setting alarm state on the '${this.name}' to armed stay`); 99 | 100 | this.client.callService(this.domain, 'alarm_arm_home', serviceData, (data) => { 101 | if (data) { 102 | that.log(`Successfully set alarm state on the '${that.name}' to armed stay`); 103 | callback(); 104 | } else { 105 | callback(communicationError); 106 | } 107 | }); 108 | } else if (targetState === Characteristic.SecuritySystemCurrentState.AWAY_ARM) { 109 | this.log(`Setting alarm state on the '${this.name}' to armed away`); 110 | 111 | this.client.callService(this.domain, 'alarm_arm_away', serviceData, (data) => { 112 | if (data) { 113 | that.log(`Successfully set alarm state on the '${that.name}' to armed away`); 114 | callback(); 115 | } else { 116 | callback(communicationError); 117 | } 118 | }); 119 | } else if (targetState === Characteristic.SecuritySystemCurrentState.NIGHT_ARM) { 120 | this.log(`Setting alarm state on the '${this.name}' to armed night`); 121 | 122 | this.client.callService(this.domain, 'alarm_arm_night', serviceData, (data) => { 123 | if (data) { 124 | that.log(`Successfully set alarm state on the '${that.name}' to armed night`); 125 | callback(); 126 | } else { 127 | callback(communicationError); 128 | } 129 | }); 130 | } else if (targetState === Characteristic.SecuritySystemCurrentState.DISARMED) { 131 | this.log(`Setting alarm state on the '${this.name}' to disarmed`); 132 | 133 | this.client.callService(this.domain, 'alarm_disarm', serviceData, (data) => { 134 | if (data) { 135 | that.log(`Successfully set alarm state on the '${that.name}' to disarmed`); 136 | callback(); 137 | } else { 138 | callback(communicationError); 139 | } 140 | }); 141 | } else { 142 | this.log(`Setting alarm state on the '${this.name}' to disarmed`); 143 | 144 | this.client.callService(this.domain, 'alarm_disarm', serviceData, (data) => { 145 | if (data) { 146 | that.log(`Successfully set alarm state on the '${that.name}' to disarmed`); 147 | callback(); 148 | } else { 149 | callback(communicationError); 150 | } 151 | }); 152 | } 153 | }, 154 | getServices() { 155 | this.alarmService = new Service.SecuritySystem(); 156 | const informationService = new Service.AccessoryInformation(); 157 | 158 | informationService 159 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 160 | .setCharacteristic(Characteristic.Model, this.model) 161 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 162 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 163 | 164 | this.alarmService 165 | .getCharacteristic(Characteristic.SecuritySystemCurrentState) 166 | .on('get', this.getAlarmState.bind(this)); 167 | 168 | this.alarmService 169 | .getCharacteristic(Characteristic.SecuritySystemTargetState) 170 | .on('get', this.getAlarmState.bind(this)) 171 | .on('set', this.setAlarmState.bind(this)); 172 | 173 | return [informationService, this.alarmService]; 174 | }, 175 | 176 | }; 177 | 178 | function HomeAssistantAlarmControlPanelPlatform(oService, oCharacteristic, oCommunicationError) { 179 | Service = oService; 180 | Characteristic = oCharacteristic; 181 | communicationError = oCommunicationError; 182 | 183 | return HomeAssistantAlarmControlPanel; 184 | } 185 | 186 | module.exports = HomeAssistantAlarmControlPanelPlatform; 187 | module.exports.HomeAssistantAlarmControlPanel = HomeAssistantAlarmControlPanel; 188 | -------------------------------------------------------------------------------- /accessories/binary_sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function toTitleCase(str) { 8 | return str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); 9 | } 10 | 11 | class HomeAssistantBinarySensor { 12 | constructor(log, data, client, service, characteristic, onValue, offValue, firmware) { 13 | // device info 14 | this.data = data; 15 | this.entity_id = data.entity_id; 16 | this.uuid_base = data.entity_id; 17 | this.firmware = firmware; 18 | if (data.attributes && data.attributes.friendly_name) { 19 | this.name = data.attributes.friendly_name; 20 | } else { 21 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 22 | } 23 | if (data.attributes && data.attributes.homebridge_manufacturer) { 24 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 25 | } else { 26 | this.manufacturer = 'Home Assistant'; 27 | } 28 | if (data.attributes && data.attributes.homebridge_model) { 29 | this.model = String(data.attributes.homebridge_model); 30 | } else { 31 | this.model = `${toTitleCase(data.attributes.device_class)} Binary Sensor`; 32 | } 33 | if (data.attributes && data.attributes.homebridge_serial) { 34 | this.serial = String(data.attributes.homebridge_serial); 35 | } else { 36 | this.serial = data.entity_id; 37 | } 38 | this.entity_type = data.entity_id.split('.')[0]; 39 | this.client = client; 40 | this.log = log; 41 | this.service = service; 42 | this.characteristic = characteristic; 43 | this.onValue = onValue; 44 | this.offValue = offValue; 45 | this.batterySource = data.attributes.homebridge_battery_source; 46 | this.chargingSource = data.attributes.homebridge_charging_source; 47 | } 48 | 49 | onEvent(oldState, newState) { 50 | if (newState.state) { 51 | this.sensorService.getCharacteristic(this.characteristic) 52 | .setValue(newState.state === 'on' ? this.onValue : this.offValue, null, 'internal'); 53 | } 54 | } 55 | identify(callback) { 56 | this.log(`identifying: ${this.name}`); 57 | callback(); 58 | } 59 | getState(callback) { 60 | this.log(`fetching state for: ${this.name}`); 61 | this.client.fetchState(this.entity_id, (data) => { 62 | if (data) { 63 | callback(null, data.state === 'on' ? this.onValue : this.offValue); 64 | } else { 65 | callback(communicationError); 66 | } 67 | }); 68 | } 69 | getBatteryLevel(callback) { 70 | this.client.fetchState(this.batterySource, (data) => { 71 | if (data) { 72 | callback(null, parseFloat(data.state)); 73 | } else { 74 | callback(communicationError); 75 | } 76 | }); 77 | } 78 | getChargingState(callback) { 79 | if (this.batterySource && this.chargingSource) { 80 | this.client.fetchState(this.chargingSource, (data) => { 81 | if (data) { 82 | callback(null, data.state.toLowerCase() === 'charging' ? 1 : 0); 83 | } else { 84 | callback(communicationError); 85 | } 86 | }); 87 | } else { 88 | callback(null, 2); 89 | } 90 | } 91 | getLowBatteryStatus(callback) { 92 | this.client.fetchState(this.batterySource, (data) => { 93 | if (data) { 94 | callback(null, parseFloat(data.state) > 20 ? 0 : 1); 95 | } else { 96 | callback(communicationError); 97 | } 98 | }); 99 | } 100 | getServices() { 101 | this.sensorService = new this.service(); // eslint-disable-line new-cap 102 | this.sensorService 103 | .getCharacteristic(this.characteristic) 104 | .on('get', this.getState.bind(this)); 105 | 106 | const informationService = new Service.AccessoryInformation(); 107 | 108 | informationService 109 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 110 | .setCharacteristic(Characteristic.Model, this.model) 111 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 112 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 113 | 114 | if (this.batterySource) { 115 | this.batteryService = new Service.BatteryService(); 116 | this.batteryService 117 | .getCharacteristic(Characteristic.BatteryLevel) 118 | .setProps({ maxValue: 100, minValue: 0, minStep: 1 }) 119 | .on('get', this.getBatteryLevel.bind(this)); 120 | this.batteryService 121 | .getCharacteristic(Characteristic.ChargingState) 122 | .setProps({ maxValue: 2 }) 123 | .on('get', this.getChargingState.bind(this)); 124 | this.batteryService 125 | .getCharacteristic(Characteristic.StatusLowBattery) 126 | .on('get', this.getLowBatteryStatus.bind(this)); 127 | return [informationService, this.batteryService, this.sensorService]; 128 | } 129 | return [informationService, this.sensorService]; 130 | } 131 | } 132 | 133 | function HomeAssistantBinarySensorFactory(log, data, client, firmware) { 134 | if (!(data.attributes && data.attributes.device_class)) { 135 | return null; 136 | } 137 | switch (data.attributes.device_class) { 138 | case 'door': 139 | case 'garage_door': 140 | case 'opening': 141 | case 'window': 142 | return new HomeAssistantBinarySensor( 143 | log, data, client, 144 | Service.ContactSensor, 145 | Characteristic.ContactSensorState, 146 | Characteristic.ContactSensorState.CONTACT_NOT_DETECTED, 147 | Characteristic.ContactSensorState.CONTACT_DETECTED, 148 | firmware 149 | ); 150 | case 'gas': 151 | if (!(data.attributes.homebridge_gas_type)) { 152 | return new HomeAssistantBinarySensor( 153 | log, data, client, 154 | Service.CarbonMonoxideSensor, 155 | Characteristic.CarbonMonoxideDetected, 156 | Characteristic.LeakDetected.CO_LEVELS_ABNORMAL, 157 | Characteristic.LeakDetected.CO_LEVELS_NORMAL, 158 | firmware 159 | ); 160 | } 161 | switch (data.attributes.homebridge_gas_type) { 162 | case 'co2': 163 | return new HomeAssistantBinarySensor( 164 | log, data, client, 165 | Service.CarbonDioxideSensor, 166 | Characteristic.CarbonDioxideDetected, 167 | Characteristic.LeakDetected.CO2_LEVELS_ABNORMAL, 168 | Characteristic.LeakDetected.CO2_LEVELS_NORMAL, 169 | firmware 170 | ); 171 | case 'co': 172 | return new HomeAssistantBinarySensor( 173 | log, data, client, 174 | Service.CarbonMonoxideSensor, 175 | Characteristic.CarbonMonoxideDetected, 176 | Characteristic.LeakDetected.CO_LEVELS_ABNORMAL, 177 | Characteristic.LeakDetected.CO_LEVELS_NORMAL, 178 | firmware 179 | ); 180 | default: 181 | return new HomeAssistantBinarySensor( 182 | log, data, client, 183 | Service.CarbonMonoxideSensor, 184 | Characteristic.CarbonMonoxideDetected, 185 | Characteristic.LeakDetected.CO_LEVELS_ABNORMAL, 186 | Characteristic.LeakDetected.CO_LEVELS_NORMAL, 187 | firmware 188 | ); 189 | } 190 | case 'moisture': 191 | return new HomeAssistantBinarySensor( 192 | log, data, client, 193 | Service.LeakSensor, 194 | Characteristic.LeakDetected, 195 | Characteristic.LeakDetected.LEAK_DETECTED, 196 | Characteristic.LeakDetected.LEAK_NOT_DETECTED, 197 | firmware 198 | ); 199 | case 'motion': 200 | return new HomeAssistantBinarySensor( 201 | log, data, client, 202 | Service.MotionSensor, 203 | Characteristic.MotionDetected, 204 | true, 205 | false, 206 | firmware 207 | ); 208 | case 'occupancy': 209 | return new HomeAssistantBinarySensor( 210 | log, data, client, 211 | Service.OccupancySensor, 212 | Characteristic.OccupancyDetected, 213 | Characteristic.OccupancyDetected.OCCUPANCY_DETECTED, 214 | Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED, 215 | firmware 216 | ); 217 | case 'smoke': 218 | return new HomeAssistantBinarySensor( 219 | log, data, client, 220 | Service.SmokeSensor, 221 | Characteristic.SmokeDetected, 222 | Characteristic.SmokeDetected.SMOKE_DETECTED, 223 | Characteristic.SmokeDetected.SMOKE_NOT_DETECTED, 224 | firmware 225 | ); 226 | default: 227 | log.error(`'${data.entity_id}' has a device_class of '${data.attributes.device_class}' which is not supported by ` + 228 | 'homebridge-homeassistant. Supported classes are \'gas\', \'moisture\', \'motion\', \'occupancy\', \'opening\' and \'smoke\'. ' + 229 | 'See the README.md for more information.'); 230 | return null; 231 | } 232 | } 233 | 234 | function HomeAssistantBinarySensorPlatform(oService, oCharacteristic, oCommunicationError) { 235 | Service = oService; 236 | Characteristic = oCharacteristic; 237 | communicationError = oCommunicationError; 238 | 239 | return HomeAssistantBinarySensorFactory; 240 | } 241 | 242 | module.exports = HomeAssistantBinarySensorPlatform; 243 | module.exports.HomeAssistantBinarySensorFactory = HomeAssistantBinarySensorFactory; 244 | -------------------------------------------------------------------------------- /accessories/climate.js: -------------------------------------------------------------------------------- 1 | var Service; 2 | var Characteristic; 3 | var communicationError; 4 | 5 | 6 | function fahrenheitToCelsius(temperature) { 7 | return (temperature - 32) / 1.8; 8 | } 9 | 10 | function celsiusToFahrenheit(temperature) { 11 | return Math.round((temperature * 1.8) + 32); 12 | } 13 | 14 | function getTempUnits(data) { 15 | // determine HomeAssistant temp. units (celsius vs. fahrenheit) 16 | // defaults to celsius 17 | return (data.attributes && data.attributes.unit_of_measurement && data.attributes.unit_of_measurement === '°F') ? 'FAHRENHEIT' : 'CELSIUS'; 18 | } 19 | 20 | function HomeAssistantClimate(log, data, client, firmware) { 21 | // device info 22 | 23 | this.domain = 'climate'; 24 | this.data = data; 25 | this.entity_id = data.entity_id; 26 | this.uuid_base = data.entity_id; 27 | this.firmware = firmware; 28 | if (data.attributes && data.attributes.friendly_name) { 29 | this.name = data.attributes.friendly_name; 30 | } else { 31 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 32 | } 33 | if (data.attributes && data.attributes.homebridge_manufacturer) { 34 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 35 | } else { 36 | this.manufacturer = 'Home Assistant'; 37 | } 38 | if (data.attributes && data.attributes.homebridge_model) { 39 | this.model = String(data.attributes.homebridge_model); 40 | } else { 41 | this.model = 'Climate'; 42 | } 43 | if (data.attributes && data.attributes.homebridge_serial) { 44 | this.serial = String(data.attributes.homebridge_serial); 45 | } else { 46 | this.serial = data.entity_id; 47 | } 48 | this.client = client; 49 | this.log = log; 50 | 51 | var fanList = data.attributes.fan_list; 52 | if (fanList) { 53 | this.maxFanRotationValue = fanList.length - 1; 54 | } else { 55 | this.maxFanRotationValue = 100; 56 | } 57 | } 58 | HomeAssistantClimate.prototype = { 59 | onEvent: function (oldState, newState) { 60 | if (newState.state) { 61 | const list = { 62 | idle: 0, heat: 1, cool: 2, auto: 3, off: 0 63 | }; 64 | this.ThermostatService.getCharacteristic(Characteristic.CurrentTemperature) 65 | .setValue(newState.attributes.current_temperature || newState.attributes.temperature, null, 'internal'); 66 | this.ThermostatService.getCharacteristic(Characteristic.TargetTemperature) 67 | .setValue(newState.attributes.temperature, null, 'internal'); 68 | this.ThermostatService.getCharacteristic(Characteristic.TargetHeatingCoolingState) 69 | .setValue(list[newState.state], null, 'internal'); 70 | } 71 | }, 72 | getCurrentTemp: function (callback) { 73 | this.client.fetchState(this.entity_id, function (data) { 74 | if (data) { 75 | if (getTempUnits(data) === 'FAHRENHEIT') { 76 | callback(null, fahrenheitToCelsius(data.attributes.current_temperature)); 77 | } else { 78 | callback(null, data.attributes.current_temperature); 79 | } 80 | } else { 81 | callback(communicationError); 82 | } 83 | }); 84 | }, 85 | getTargetTemp: function (callback) { 86 | this.client.fetchState(this.entity_id, function (data) { 87 | if (data) { 88 | if (getTempUnits(data) === 'FAHRENHEIT') { 89 | callback(null, fahrenheitToCelsius(data.attributes.temperature)); 90 | } else { 91 | callback(null, data.attributes.temperature); 92 | } 93 | } else { 94 | callback(communicationError); 95 | } 96 | }); 97 | }, 98 | setTargetTemp: function (value, callback, context) { 99 | if (context === 'internal') { 100 | callback(); 101 | return; 102 | } 103 | 104 | var that = this; 105 | var serviceData = {}; 106 | serviceData.entity_id = this.entity_id; 107 | serviceData.temperature = value; 108 | 109 | if (getTempUnits(this.data) === 'FAHRENHEIT') { 110 | serviceData.temperature = celsiusToFahrenheit(serviceData.temperature); 111 | } 112 | 113 | this.log(`Setting temperature on the '${this.name}' to ${serviceData.temperature}`); 114 | 115 | this.client.callService(this.domain, 'set_temperature', serviceData, function (data) { 116 | if (data) { 117 | that.log(`Successfully set temperature of '${that.name}'`); 118 | callback(); 119 | } else { 120 | callback(communicationError); 121 | } 122 | }); 123 | }, 124 | getTargetHeatingCoolingState: function (callback) { 125 | this.log('fetching Current Heating Cooling state for: ' + this.name); 126 | 127 | this.client.fetchState(this.entity_id, function (data) { 128 | if (data && data.attributes && data.attributes.operation_mode) { 129 | var state; 130 | switch (data.attributes.operation_mode) { 131 | case 'auto': 132 | state = Characteristic.TargetHeatingCoolingState.AUTO; 133 | break; 134 | case 'cool': 135 | state = Characteristic.TargetHeatingCoolingState.COOL; 136 | break; 137 | case 'heat': 138 | state = Characteristic.TargetHeatingCoolingState.HEAT; 139 | break; 140 | case 'off': 141 | default: 142 | state = Characteristic.TargetHeatingCoolingState.OFF; 143 | break; 144 | } 145 | callback(null, state); 146 | } else { 147 | callback(communicationError); 148 | } 149 | }); 150 | }, 151 | 152 | setTargetHeatingCoolingState: function (value, callback, context) { 153 | if (context === 'internal') { 154 | callback(); 155 | return; 156 | } 157 | var serviceData = {}; 158 | serviceData.entity_id = this.entity_id; 159 | 160 | var mode = ''; 161 | switch (value) { 162 | case Characteristic.TargetHeatingCoolingState.AUTO: 163 | mode = 'auto'; 164 | break; 165 | case Characteristic.TargetHeatingCoolingState.COOL: 166 | mode = 'cool'; 167 | break; 168 | case Characteristic.TargetHeatingCoolingState.HEAT: 169 | mode = 'heat'; 170 | break; 171 | case Characteristic.TargetHeatingCoolingState.OFF: 172 | default: 173 | mode = 'off'; 174 | break; 175 | } 176 | 177 | serviceData.operation_mode = mode; 178 | this.log(`Setting Current Heating Cooling state on the '${this.name}' to ${mode}`); 179 | 180 | var that = this; 181 | 182 | if (mode === 'idle') { 183 | this.fanService.getCharacteristic(Characteristic.On) 184 | .setValue(false, null, 'internal'); 185 | } else { 186 | this.fanService.getCharacteristic(Characteristic.On) 187 | .setValue(true, null, 'internal'); 188 | } 189 | 190 | this.client.callService(this.domain, 'set_operation_mode', serviceData, function (data) { 191 | if (data) { 192 | that.log(`Successfully set current heating cooling state of '${that.name}'`); 193 | callback(); 194 | } else { 195 | callback(communicationError); 196 | } 197 | }); 198 | }, 199 | 200 | getRotationSpeed(callback) { 201 | this.client.fetchState(this.entity_id, (data) => { 202 | if (data) { 203 | if (data.attributes.operation_mode === 'idle') { 204 | callback(null, 0); 205 | } else { 206 | var fanList = data.attributes.fan_list; 207 | if (fanList) { 208 | if (fanList.length > 2) { 209 | var index = fanList.indexOf(data.attributes.current_fan_mode); 210 | callback(null, index); 211 | } 212 | } else { 213 | switch (data.attributes.current_fan_mode) { 214 | case 'low': 215 | callback(null, 25); 216 | break; 217 | case 'mid': 218 | callback(null, 50); 219 | break; 220 | case 'high': 221 | callback(null, 75); 222 | break; 223 | case 'highest': 224 | callback(null, 100); 225 | break; 226 | default: 227 | callback(null, 0); 228 | } 229 | } 230 | } 231 | } else { 232 | callback(communicationError); 233 | } 234 | }); 235 | }, 236 | setRotationSpeed(speed, callback, context) { 237 | if (context === 'internal') { 238 | callback(); 239 | return; 240 | } 241 | 242 | const that = this; 243 | const serviceData = {}; 244 | serviceData.entity_id = this.entity_id; 245 | 246 | this.client.fetchState(this.entity_id, (data) => { 247 | if (data) { 248 | var fanList = data.attributes.fan_list; 249 | if (fanList) { 250 | for (var index = 0; index < fanList.length - 1; index += 1) { 251 | if (speed === index) { 252 | serviceData.fan_mode = fanList[index]; 253 | break; 254 | } 255 | } 256 | if (!serviceData.fan_mode) { 257 | serviceData.fan_mode = fanList[fanList.length - 1]; 258 | } 259 | } else if (speed <= 25) { 260 | serviceData.fan_mode = 'low'; 261 | } else if (speed <= 50) { 262 | serviceData.fan_mode = 'medium'; 263 | } else if (speed <= 75) { 264 | serviceData.fan_mode = 'high'; 265 | } else if (speed <= 100) { 266 | serviceData.fan_mode = 'highest'; 267 | } 268 | this.log(`Setting fan mode on the '${this.name}' to ${serviceData.fan_mode}`); 269 | 270 | this.client.callService(this.domain, 'set_fan_mode', serviceData, (data2) => { 271 | if (data2) { 272 | that.log(`Successfully set fan mode on the '${that.name}' to ${serviceData.fan_mode}`); 273 | callback(); 274 | } else { 275 | callback(communicationError); 276 | } 277 | }); 278 | } else { 279 | callback(communicationError); 280 | } 281 | }); 282 | }, 283 | 284 | getServices: function () { 285 | this.ThermostatService = new Service.Thermostat(); 286 | var informationService = new Service.AccessoryInformation(); 287 | 288 | informationService 289 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 290 | .setCharacteristic(Characteristic.Model, this.model) 291 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 292 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 293 | 294 | // get our unit var -- default to celsius 295 | var units = (getTempUnits(this.data) === 'FAHRENHEIT') ? Characteristic.TemperatureDisplayUnits.FAHRENHEIT : Characteristic.TemperatureDisplayUnits.CELSIUS; 296 | 297 | this.ThermostatService 298 | .getCharacteristic(Characteristic.CurrentTemperature) 299 | .on('get', this.getCurrentTemp.bind(this)); 300 | 301 | // default min/max/step for temperature 302 | var minTemp = 7.0; 303 | var maxTemp = 35.0; 304 | var tempStep = 0.5; 305 | 306 | if (units === Characteristic.TemperatureDisplayUnits.FAHRENHEIT) { 307 | if (this.data && this.data.attributes) { 308 | if (this.data.attributes.min_temp) { 309 | minTemp = fahrenheitToCelsius(this.data.attributes.min_temp); 310 | } 311 | if (this.data.attributes.max_temp) { 312 | maxTemp = fahrenheitToCelsius(this.data.attributes.max_temp); 313 | } 314 | if (this.data.attributes.target_temp_step) { 315 | tempStep = this.data.attributes.target_temp_step; 316 | } 317 | } 318 | } else if (this.data && this.data.attributes) { 319 | if (this.data.attributes.min_temp) { 320 | minTemp = this.data.attributes.min_temp; 321 | } 322 | if (this.data.attributes.max_temp) { 323 | maxTemp = this.data.attributes.max_temp; 324 | } 325 | if (this.data.attributes.target_temp_step) { 326 | tempStep = this.data.attributes.target_temp_step; 327 | } 328 | } 329 | 330 | this.ThermostatService 331 | .getCharacteristic(Characteristic.TargetTemperature) 332 | .setProps({ minValue: minTemp, maxValue: maxTemp, minStep: tempStep }) 333 | .on('get', this.getTargetTemp.bind(this)) 334 | .on('set', this.setTargetTemp.bind(this)); 335 | 336 | this.ThermostatService 337 | .getCharacteristic(Characteristic.TargetHeatingCoolingState) 338 | .on('get', this.getTargetHeatingCoolingState.bind(this)) 339 | .on('set', this.setTargetHeatingCoolingState.bind(this)); 340 | 341 | this.ThermostatService.setCharacteristic(Characteristic.TemperatureDisplayUnits, units); 342 | 343 | this.fanService = new Service.Fan(); 344 | this.fanService 345 | .getCharacteristic(Characteristic.RotationSpeed) 346 | .setProps({ 347 | minValue: 0, 348 | maxValue: this.maxFanRotationValue, 349 | minStep: 1 350 | }) 351 | .on('get', this.getRotationSpeed.bind(this)) 352 | .on('set', this.setRotationSpeed.bind(this)); 353 | 354 | return [informationService, this.ThermostatService, this.fanService]; 355 | } 356 | 357 | 358 | }; 359 | 360 | function HomeAssistantClimatePlatform(oService, oCharacteristic, oCommunicationError) { 361 | Service = oService; 362 | Characteristic = oCharacteristic; 363 | communicationError = oCommunicationError; 364 | 365 | return HomeAssistantClimate; 366 | } 367 | 368 | module.exports = HomeAssistantClimatePlatform; 369 | module.exports.HomeAssistantClimate = HomeAssistantClimate; 370 | -------------------------------------------------------------------------------- /accessories/cover.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | class HomeAssistantCover { 8 | constructor(log, data, client, firmware) { 9 | this.client = client; 10 | this.log = log; 11 | // device info 12 | this.domain = 'cover'; 13 | this.data = data; 14 | this.entity_id = data.entity_id; 15 | this.uuid_base = data.entity_id; 16 | this.firmware = firmware; 17 | if (data.attributes && data.attributes.friendly_name) { 18 | this.name = data.attributes.friendly_name; 19 | } else { 20 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 21 | } 22 | if (data.attributes && data.attributes.homebridge_manufacturer) { 23 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 24 | } else { 25 | this.manufacturer = 'Home Assistant'; 26 | } 27 | if (data.attributes && data.attributes.homebridge_serial) { 28 | this.serial = String(data.attributes.homebridge_serial); 29 | } else { 30 | this.serial = data.entity_id; 31 | } 32 | } 33 | 34 | onEvent(oldState, newState) { 35 | if (newState.state) { 36 | const state = this.transformData(newState); 37 | 38 | this.service.getCharacteristic(this.stateCharacteristic) 39 | .setValue(state, null, 'internal'); 40 | this.service.getCharacteristic(this.targetCharacteristic) 41 | .setValue(state, null, 'internal'); 42 | } 43 | } 44 | 45 | getState(callback) { 46 | this.client.fetchState(this.entity_id, (data) => { 47 | if (data) { 48 | callback(null, this.transformData(data)); 49 | } else { 50 | callback(communicationError); 51 | } 52 | }); 53 | } 54 | 55 | getServices() { 56 | const informationService = new Service.AccessoryInformation(); 57 | informationService 58 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 59 | .setCharacteristic(Characteristic.Model, this.model) 60 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 61 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 62 | 63 | this.service 64 | .getCharacteristic(this.stateCharacteristic) 65 | .on('get', this.getState.bind(this)); 66 | 67 | this.service 68 | .getCharacteristic(this.targetCharacteristic) 69 | .on('get', this.getState.bind(this)) 70 | .on('set', this.setTargetState.bind(this)); 71 | 72 | return [informationService, this.service]; 73 | } 74 | 75 | doChangeState(service, callback) { 76 | const serviceData = { 77 | entity_id: this.entity_id, 78 | }; 79 | 80 | this.log(`Calling service ${service} on ${this.name}`); 81 | 82 | this.client.callService(this.domain, service, serviceData, (data) => { 83 | if (data) { 84 | callback(); 85 | } else { 86 | callback(communicationError); 87 | } 88 | }); 89 | } 90 | } 91 | 92 | class HomeAssistantGarageDoor extends HomeAssistantCover { 93 | constructor(log, data, client, firmware) { 94 | super(log, data, client, firmware); 95 | if (data.attributes && data.attributes.homebridge_model) { 96 | this.model = String(data.attributes.homebridge_model); 97 | } else { 98 | this.model = 'Garage Door'; 99 | } 100 | this.service = new Service.GarageDoorOpener(); 101 | this.stateCharacteristic = Characteristic.CurrentDoorState; 102 | this.targetCharacteristic = Characteristic.TargetDoorState; 103 | } 104 | 105 | transformData(data) { 106 | return data.state === 'closed' ? this.stateCharacteristic.CLOSED : this.stateCharacteristic.OPEN; 107 | } 108 | 109 | setTargetState(targetState, callback, context) { 110 | if (context === 'internal') { 111 | callback(); 112 | return; 113 | } 114 | 115 | this.doChangeState(targetState === Characteristic.TargetDoorState.CLOSED ? 'close_cover' : 'open_cover', callback); 116 | } 117 | } 118 | 119 | class HomeAssistantRollershutter extends HomeAssistantCover { 120 | constructor(log, data, client, firmware) { 121 | super(log, data, client, firmware); 122 | if (data.attributes && data.attributes.homebridge_model) { 123 | this.model = String(data.attributes.homebridge_model); 124 | } else { 125 | this.model = 'Rollershutter'; 126 | } 127 | this.service = new Service.WindowCovering(); 128 | this.stateCharacteristic = Characteristic.CurrentPosition; 129 | this.targetCharacteristic = Characteristic.TargetPosition; 130 | } 131 | 132 | transformData(data) { 133 | return (data && data.attributes) ? data.attributes.current_position : null; 134 | } 135 | 136 | setTargetState(position, callback, context) { 137 | if (context === 'internal') { 138 | callback(); 139 | return; 140 | } 141 | 142 | const payload = { 143 | entity_id: this.entity_id, 144 | position, 145 | }; 146 | 147 | this.log(`Setting the state of the ${this.name} to ${payload.position}`); 148 | 149 | this.client.callService(this.domain, 'set_cover_position', payload, (data) => { 150 | if (data) { 151 | callback(); 152 | } else { 153 | callback(communicationError); 154 | } 155 | }); 156 | } 157 | } 158 | 159 | class HomeAssistantRollershutterBinary extends HomeAssistantRollershutter { 160 | transformData(data) { 161 | return (data && data.state) ? ((data.state === 'open') * 100) : null; 162 | } 163 | 164 | setTargetState(position, callback, context) { 165 | if (context === 'internal') { 166 | callback(); 167 | return; 168 | } 169 | 170 | if (!(position === 100 || position === 0)) { 171 | this.log('Cannot set this cover to positions other than 0 or 100'); 172 | callback(communicationError); // TODO 173 | } else { 174 | this.doChangeState(position === 100 ? 'open_cover' : 'close_cover', callback); 175 | } 176 | } 177 | } 178 | 179 | function HomeAssistantCoverFactory(log, data, client, firmware) { 180 | if (!data.attributes) { 181 | return null; 182 | } 183 | 184 | if (data.attributes.homebridge_cover_type === 'garage_door') { 185 | return new HomeAssistantGarageDoor(log, data, client, firmware); 186 | } else if (data.attributes.homebridge_cover_type === 'rollershutter') { 187 | if (data.attributes.current_position !== undefined) { 188 | return new HomeAssistantRollershutter(log, data, client, firmware); 189 | } 190 | return new HomeAssistantRollershutterBinary(log, data, client, firmware); 191 | } 192 | log.error(`'${data.entity_id}' is a cover but does not have a 'homebridge_cover_type' property set. ` + 193 | 'You must set it to either \'rollershutter\' or \'garage_door\' in the customize section ' + 194 | 'of your Home Assistant configuration. It will not be available to Homebridge until you do. ' + 195 | 'See the README.md for more information. ' + 196 | 'The attributes that were found are:', JSON.stringify(data.attributes)); 197 | } 198 | 199 | function HomeAssistantCoverPlatform(oService, oCharacteristic, oCommunicationError) { 200 | Service = oService; 201 | Characteristic = oCharacteristic; 202 | communicationError = oCommunicationError; 203 | 204 | return HomeAssistantCoverFactory; 205 | } 206 | 207 | module.exports = HomeAssistantCoverPlatform; 208 | 209 | module.exports.HomeAssistantCoverFactory = HomeAssistantCoverFactory; 210 | -------------------------------------------------------------------------------- /accessories/device_tracker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Service; 4 | var Characteristic; 5 | var communicationError; 6 | 7 | class HomeAssistantDeviceTracker { 8 | constructor(log, data, client, service, characteristic, onValue, offValue, firmware) { 9 | // device info 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_model) { 25 | this.model = String(data.attributes.homebridge_model); 26 | } else { 27 | this.model = 'Device Tracker'; 28 | } 29 | if (data.attributes && data.attributes.homebridge_serial) { 30 | this.serial = String(data.attributes.homebridge_serial); 31 | } else { 32 | this.serial = data.entity_id; 33 | } 34 | this.entity_type = data.entity_id.split('.')[0]; 35 | this.client = client; 36 | this.log = log; 37 | this.service = service; 38 | this.characteristic = characteristic; 39 | this.onValue = onValue; 40 | this.offValue = offValue; 41 | this.batterySource = data.attributes.homebridge_battery_source; 42 | this.chargingSource = data.attributes.homebridge_charging_source; 43 | } 44 | 45 | onEvent(oldState, newState) { 46 | if (newState.state) { 47 | this.sensorService.getCharacteristic(this.characteristic) 48 | .setValue(newState.state === 'home' ? this.onValue : this.offValue, null, 'internal'); 49 | } 50 | } 51 | identify(callback) { 52 | this.log('identifying: ' + this.name); 53 | callback(); 54 | } 55 | getState(callback) { 56 | this.log('fetching state for: ' + this.name); 57 | this.client.fetchState(this.entity_id, function (data) { 58 | if (data) { 59 | callback(null, data.state === 'home' ? this.onValue : this.offValue); 60 | } else { 61 | callback(communicationError); 62 | } 63 | }.bind(this)); 64 | } 65 | getBatteryLevel(callback) { 66 | this.client.fetchState(this.batterySource, (data) => { 67 | if (data) { 68 | callback(null, parseFloat(data.state)); 69 | } else { 70 | callback(communicationError); 71 | } 72 | }); 73 | } 74 | getChargingState(callback) { 75 | if (this.batterySource && this.chargingSource) { 76 | this.client.fetchState(this.chargingSource, (data) => { 77 | if (data) { 78 | callback(null, data.state.toLowerCase() === 'charging' ? 1 : 0); 79 | } else { 80 | callback(communicationError); 81 | } 82 | }); 83 | } else { 84 | callback(null, 2); 85 | } 86 | } 87 | getLowBatteryStatus(callback) { 88 | this.client.fetchState(this.batterySource, (data) => { 89 | if (data) { 90 | callback(null, parseFloat(data.state) > 20 ? 0 : 1); 91 | } else { 92 | callback(communicationError); 93 | } 94 | }); 95 | } 96 | getServices() { 97 | this.sensorService = new this.service(); 98 | this.sensorService 99 | .getCharacteristic(this.characteristic) 100 | .on('get', this.getState.bind(this)); 101 | 102 | var informationService = new Service.AccessoryInformation(); 103 | 104 | informationService 105 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 106 | .setCharacteristic(Characteristic.Model, this.model) 107 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 108 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 109 | 110 | if (this.batterySource) { 111 | this.batteryService = new Service.BatteryService(); 112 | this.batteryService 113 | .getCharacteristic(Characteristic.BatteryLevel) 114 | .setProps({ maxValue: 100, minValue: 0, minStep: 1 }) 115 | .on('get', this.getBatteryLevel.bind(this)); 116 | this.batteryService 117 | .getCharacteristic(Characteristic.ChargingState) 118 | .setProps({ maxValue: 2 }) 119 | .on('get', this.getChargingState.bind(this)); 120 | this.batteryService 121 | .getCharacteristic(Characteristic.StatusLowBattery) 122 | .on('get', this.getLowBatteryStatus.bind(this)); 123 | return [informationService, this.batteryService, this.sensorService]; 124 | } 125 | return [informationService, this.sensorService]; 126 | } 127 | } 128 | 129 | function HomeAssistantDeviceTrackerFactory(log, data, client, firmware) { 130 | if (!(data.attributes)) { 131 | return null; 132 | } 133 | return new HomeAssistantDeviceTracker( 134 | log, data, client, 135 | Service.OccupancySensor, 136 | Characteristic.OccupancyDetected, 137 | Characteristic.OccupancyDetected.OCCUPANCY_DETECTED, 138 | Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED, 139 | firmware 140 | ); 141 | } 142 | 143 | function HomeAssistantDeviceTrackerFactoryPlatform(oService, oCharacteristic, oCommunicationError) { 144 | Service = oService; 145 | Characteristic = oCharacteristic; 146 | communicationError = oCommunicationError; 147 | 148 | return HomeAssistantDeviceTrackerFactory; 149 | } 150 | 151 | module.exports = HomeAssistantDeviceTrackerFactoryPlatform; 152 | module.exports.HomeAssistantDeviceTrackerFactory = HomeAssistantDeviceTrackerFactory; 153 | -------------------------------------------------------------------------------- /accessories/fan.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function HomeAssistantFan(log, data, client, firmware) { 8 | // device info 9 | this.domain = 'fan'; 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_model) { 25 | this.model = String(data.attributes.homebridge_model); 26 | } else { 27 | this.model = 'Fan'; 28 | } 29 | if (data.attributes && data.attributes.homebridge_serial) { 30 | this.serial = String(data.attributes.homebridge_serial); 31 | } else { 32 | this.serial = data.entity_id; 33 | } 34 | if (data.attributes.speed_list) { 35 | var speedList = data.attributes.speed_list; 36 | this.maxValue = speedList.length - 1; 37 | } else { 38 | this.maxValue = 100; 39 | } 40 | this.client = client; 41 | this.log = log; 42 | } 43 | 44 | HomeAssistantFan.prototype = { 45 | onEvent(oldState, newState) { 46 | if (newState.state) { 47 | this.fanService.getCharacteristic(Characteristic.On) 48 | .setValue(newState.state === 'on', null, 'internal'); 49 | 50 | if (newState.state === 'on') { 51 | let speed; 52 | if (newState.attributes.speed_list) { 53 | var speedList = newState.attributes.speed_list; 54 | if (speedList.length > 2) { 55 | speed = speedList.indexOf(newState.attributes.speed); 56 | } 57 | } else { 58 | switch (newState.attributes.speed) { 59 | case 'low': 60 | speed = 25; 61 | break; 62 | case 'medium': 63 | speed = 50; 64 | break; 65 | case 'high': 66 | speed = 100; 67 | break; 68 | default: 69 | speed = 0; 70 | } 71 | } 72 | this.fanService.getCharacteristic(Characteristic.RotationSpeed) 73 | .setValue(speed, null, 'internal'); 74 | } 75 | } 76 | }, 77 | getPowerState(callback) { 78 | this.client.fetchState(this.entity_id, (data) => { 79 | if (data) { 80 | const powerState = data.state === 'on'; 81 | callback(null, powerState); 82 | } else { 83 | callback(communicationError); 84 | } 85 | }); 86 | }, 87 | setPowerState(powerOn, callback, context) { 88 | if (context === 'internal') { 89 | callback(); 90 | return; 91 | } 92 | 93 | const that = this; 94 | const serviceData = {}; 95 | serviceData.entity_id = this.entity_id; 96 | 97 | if (powerOn) { 98 | this.log(`Setting power state on the '${this.name}' to on`); 99 | 100 | this.client.callService(this.domain, 'turn_on', serviceData, (data) => { 101 | if (data) { 102 | that.log(`Successfully set power state on the '${that.name}' to on`); 103 | callback(); 104 | } else { 105 | callback(communicationError); 106 | } 107 | }); 108 | } else { 109 | this.log(`Setting power state on the '${this.name}' to off`); 110 | 111 | this.client.callService(this.domain, 'turn_off', serviceData, (data) => { 112 | if (data) { 113 | that.log(`Successfully set power state on the '${that.name}' to off`); 114 | callback(); 115 | } else { 116 | callback(communicationError); 117 | } 118 | }); 119 | } 120 | }, 121 | getRotationSpeed(callback) { 122 | this.client.fetchState(this.entity_id, (data) => { 123 | if (data) { 124 | if (data.state === 'off') { 125 | callback(null, 0); 126 | } else if (data.attributes.speed_list) { 127 | var speedList = data.attributes.speed_list; 128 | if (speedList.length > 2) { 129 | var index = speedList.indexOf(data.attributes.speed); 130 | callback(null, index); 131 | } 132 | } else { 133 | switch (data.attributes.speed) { 134 | case 'low': 135 | callback(null, 25); 136 | break; 137 | case 'medium': 138 | callback(null, 50); 139 | break; 140 | case 'high': 141 | callback(null, 100); 142 | break; 143 | default: 144 | callback(null, 0); 145 | } 146 | } 147 | } else { 148 | callback(communicationError); 149 | } 150 | }); 151 | }, 152 | setRotationSpeed(speed, callback, context) { 153 | if (context === 'internal') { 154 | callback(); 155 | return; 156 | } 157 | 158 | const that = this; 159 | const serviceData = {}; 160 | serviceData.entity_id = this.entity_id; 161 | 162 | if (speed === 0) { 163 | this.log(`Setting power state on the '${this.name}' to off`); 164 | 165 | this.client.callService(this.domain, 'turn_off', serviceData, (data) => { 166 | if (data) { 167 | that.log(`Successfully set power state on the '${that.name}' to off`); 168 | callback(); 169 | } else { 170 | callback(communicationError); 171 | } 172 | }); 173 | } else { 174 | this.client.fetchState(this.entity_id, (data) => { 175 | if (data) { 176 | if (data.attributes.speed_list) { 177 | var speedList = data.attributes.speed_list; 178 | for (var index = 0; index < speedList.length - 1; index += 1) { 179 | if (speed === index) { 180 | serviceData.speed = speedList[index]; 181 | break; 182 | } 183 | } 184 | if (!serviceData.speed) { 185 | serviceData.speed = speedList[speedList.length - 1]; 186 | } 187 | } else if (speed <= 25) { 188 | serviceData.speed = 'low'; 189 | } else if (speed <= 75) { 190 | serviceData.speed = 'medium'; 191 | } else if (speed <= 100) { 192 | serviceData.speed = 'high'; 193 | } 194 | this.log(`Setting speed on the '${this.name}' to ${serviceData.speed}`); 195 | 196 | this.client.callService(this.domain, 'set_speed', serviceData, (data2) => { 197 | if (data2) { 198 | that.log(`Successfully set power state on the '${that.name}' to on`); 199 | callback(); 200 | } else { 201 | callback(communicationError); 202 | } 203 | }); 204 | } else { 205 | callback(communicationError); 206 | } 207 | }); 208 | } 209 | }, 210 | getServices() { 211 | this.fanService = new Service.Fan(); 212 | const informationService = new Service.AccessoryInformation(); 213 | 214 | informationService 215 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 216 | .setCharacteristic(Characteristic.Model, this.model) 217 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 218 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 219 | 220 | this.fanService 221 | .getCharacteristic(Characteristic.On) 222 | .on('get', this.getPowerState.bind(this)) 223 | .on('set', this.setPowerState.bind(this)); 224 | 225 | this.fanService 226 | .getCharacteristic(Characteristic.RotationSpeed) 227 | .setProps({ 228 | minValue: 0, 229 | maxValue: this.maxValue, 230 | minStep: 1 231 | }) 232 | .on('get', this.getRotationSpeed.bind(this)) 233 | .on('set', this.setRotationSpeed.bind(this)); 234 | 235 | return [informationService, this.fanService]; 236 | }, 237 | 238 | }; 239 | 240 | function HomeAssistantFanPlatform(oService, oCharacteristic, oCommunicationError) { 241 | Service = oService; 242 | Characteristic = oCharacteristic; 243 | communicationError = oCommunicationError; 244 | 245 | return HomeAssistantFan; 246 | } 247 | 248 | module.exports = HomeAssistantFanPlatform; 249 | module.exports.HomeAssistantFan = HomeAssistantFan; 250 | -------------------------------------------------------------------------------- /accessories/light.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | /* eslint-disable */ 8 | const LightUtil = { 9 | hsvToRgb(h, s, v) { 10 | let r; 11 | let g; 12 | let b; 13 | let i; 14 | let f; 15 | let p; 16 | let q; 17 | let t; 18 | if (arguments.length === 1) { 19 | s = h.s, v = h.v, h = h.h; 20 | } 21 | i = Math.floor(h * 6); 22 | f = h * 6 - i; 23 | p = v * (1 - s); 24 | q = v * (1 - f * s); 25 | t = v * (1 - (1 - f) * s); 26 | switch (i % 6) { 27 | case 0: r = v, g = t, b = p; break; 28 | case 1: r = q, g = v, b = p; break; 29 | case 2: r = p, g = v, b = t; break; 30 | case 3: r = p, g = q, b = v; break; 31 | case 4: r = t, g = p, b = v; break; 32 | case 5: r = v, g = p, b = q; break; 33 | } 34 | return { 35 | r: Math.round(r * 255), 36 | g: Math.round(g * 255), 37 | b: Math.round(b * 255), 38 | }; 39 | }, 40 | rgbToHsv(r, g, b) { 41 | if (arguments.length === 1) { 42 | g = r.g, b = r.b, r = r.r; 43 | } 44 | let max = Math.max(r, g, b), 45 | min = Math.min(r, g, b), 46 | d = max - min, 47 | h, 48 | s = (max === 0 ? 0 : d / max), 49 | v = max / 255; 50 | 51 | switch (max) { 52 | case min: h = 0; break; 53 | case r: h = (g - b) + d * (g < b ? 6 : 0); h /= 6 * d; break; 54 | case g: h = (b - r) + d * 2; h /= 6 * d; break; 55 | case b: h = (r - g) + d * 4; h /= 6 * d; break; 56 | } 57 | 58 | return { 59 | h, 60 | s, 61 | v, 62 | }; 63 | }, 64 | rgbToCie(red, green, blue) { 65 | // Apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device 66 | red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92); 67 | green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92); 68 | blue = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92); 69 | 70 | // RGB values to XYZ using the Wide RGB D65 conversion formula 71 | const X = red * 0.664511 + green * 0.154324 + blue * 0.162028; 72 | const Y = red * 0.283881 + green * 0.668433 + blue * 0.047685; 73 | const Z = red * 0.000088 + green * 0.072310 + blue * 0.986039; 74 | 75 | // Calculate the xy values from the XYZ values 76 | let x = (X / (X + Y + Z)).toFixed(4); 77 | let y = (Y / (X + Y + Z)).toFixed(4); 78 | 79 | if (isNaN(x)) { 80 | x = 0; 81 | } 82 | 83 | if (isNaN(y)) { y = 0; } 84 | 85 | return [x, y]; 86 | }, 87 | }; 88 | /* eslint-enable */ 89 | 90 | function HomeAssistantLight(log, data, client, firmware) { 91 | // device info 92 | this.domain = 'light'; 93 | this.data = data; 94 | this.entity_id = data.entity_id; 95 | this.uuid_base = data.entity_id; 96 | this.firmware = firmware; 97 | if (data.attributes && data.attributes.friendly_name) { 98 | this.name = data.attributes.friendly_name; 99 | } else { 100 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 101 | } 102 | if (data.attributes && data.attributes.homebridge_manufacturer) { 103 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 104 | } else { 105 | this.manufacturer = 'Home Assistant'; 106 | } 107 | if (data.attributes && data.attributes.homebridge_model) { 108 | this.model = String(data.attributes.homebridge_model); 109 | } else { 110 | this.model = 'Light'; 111 | } 112 | if (data.attributes && data.attributes.homebridge_serial) { 113 | this.serial = String(data.attributes.homebridge_serial); 114 | } else { 115 | this.serial = data.entity_id; 116 | } 117 | this.client = client; 118 | this.log = log; 119 | 120 | this.maxTemp = 400; 121 | this.minTemp = 50; 122 | 123 | if (data.attributes.homebridge_max_mireds) { 124 | this.maxTemp = data.attributes.homebridge_max_mireds; 125 | } 126 | 127 | if (data.attributes.homebridge_min_mireds) { 128 | this.minTemp = data.attributes.homebridge_min_mireds; 129 | } 130 | 131 | this.cachedColor = false; 132 | } 133 | 134 | HomeAssistantLight.prototype = { 135 | features: Object.freeze({ 136 | BRIGHTNESS: 1, 137 | COLOR_TEMP: 2, 138 | EFFECT: 4, 139 | FLASH: 8, 140 | RGB_COLOR: 16, 141 | TRANSITION: 32, 142 | XY_COLOR: 64, 143 | }), 144 | is_supported(feature) { 145 | // If the supported_features attribute doesn't exist, assume not supported 146 | if (this.data.attributes.supported_features === undefined) { 147 | return false; 148 | } 149 | 150 | return (this.data.attributes.supported_features & feature) > 0; 151 | }, 152 | onEvent(oldState, newState) { 153 | if (newState.state) { 154 | this.lightbulbService.getCharacteristic(Characteristic.On) 155 | .setValue(newState.state === 'on', null, 'internal'); 156 | if (this.is_supported(this.features.BRIGHTNESS)) { 157 | const brightness = Math.round(((newState.attributes.brightness || 0) / 255) * 100); 158 | 159 | this.lightbulbService.getCharacteristic(Characteristic.Brightness) 160 | .setValue(brightness, null, 'internal'); 161 | 162 | this.data.attributes.brightness = newState.attributes.brightness; 163 | } 164 | 165 | if (this.is_supported(this.features.RGB_COLOR) && 166 | newState.attributes.rgb_color !== undefined) { 167 | const rgbColor = newState.attributes.rgb_color; 168 | const hsv = LightUtil.rgbToHsv(rgbColor[0], rgbColor[1], rgbColor[2]); 169 | const hue = hsv.h * 360; 170 | const saturation = hsv.s * 100; 171 | 172 | this.lightbulbService.getCharacteristic(Characteristic.Hue) 173 | .setValue(hue, null, 'internal'); 174 | this.lightbulbService.getCharacteristic(Characteristic.Saturation) 175 | .setValue(saturation, null, 'internal'); 176 | 177 | this.data.attributes.hue = hue; 178 | this.data.attributes.saturation = saturation; 179 | } 180 | 181 | if (this.is_supported(this.features.COLOR_TEMP)) { 182 | const colorTemperature = Math.round(newState.attributes.color_temp) || this.minTemp; 183 | 184 | this.lightbulbService.getCharacteristic(Characteristic.ColorTemperature) 185 | .setValue(colorTemperature, null, 'internal'); 186 | } 187 | } 188 | }, 189 | identify(callback) { 190 | this.log(`identifying: ${this.name}`); 191 | 192 | const that = this; 193 | const serviceData = {}; 194 | serviceData.entity_id = this.entity_id; 195 | let service = 'toggle'; 196 | if (this.is_supported(this.features.FLASH)) { 197 | service = 'turn_on'; 198 | serviceData.flash = 'short'; 199 | } 200 | this.client.callService(this.domain, service, serviceData, (data) => { 201 | if (data) { 202 | that.log(`Successfully identified '${that.name}'`); 203 | } 204 | callback(); 205 | }); 206 | }, 207 | getPowerState(callback) { 208 | this.log(`fetching power state for: ${this.name}`); 209 | 210 | this.client.fetchState(this.entity_id, (data) => { 211 | if (data) { 212 | const powerState = data.state === 'on'; 213 | callback(null, powerState); 214 | } else { 215 | callback(communicationError); 216 | } 217 | }); 218 | }, 219 | getBrightness(callback) { 220 | this.log(`fetching brightness for: ${this.name}`); 221 | 222 | this.client.fetchState(this.entity_id, (data) => { 223 | if (data) { 224 | const brightness = ((data.attributes.brightness || 0) / 255) * 100; 225 | callback(null, brightness); 226 | } else { 227 | callback(communicationError); 228 | } 229 | }); 230 | }, 231 | getHue(callback) { 232 | const that = this; 233 | this.client.fetchState(this.entity_id, (data) => { 234 | if (data) { 235 | const rgb = data.attributes.rgb_color || [0, 0, 0]; 236 | const hsv = LightUtil.rgbToHsv(rgb[0], rgb[1], rgb[2]); 237 | 238 | const hue = hsv.h * 360; 239 | that.data.attributes.hue = hue; 240 | 241 | callback(null, hue); 242 | } else { 243 | callback(communicationError); 244 | } 245 | }); 246 | }, 247 | getSaturation(callback) { 248 | const that = this; 249 | this.client.fetchState(this.entity_id, (data) => { 250 | if (data) { 251 | const rgb = data.attributes.rgb_color || [0, 0, 0]; 252 | const hsv = LightUtil.rgbToHsv(rgb[0], rgb[1], rgb[2]); 253 | 254 | const saturation = hsv.s * 100; 255 | that.data.attributes.saturation = saturation; 256 | 257 | callback(null, saturation); 258 | } else { 259 | callback(communicationError); 260 | } 261 | }); 262 | }, 263 | getColorTemperature(callback) { 264 | this.client.fetchState(this.entity_id, (data) => { 265 | if (data) { 266 | const colorTemp = Math.round(data.attributes.color_temp) || this.minTemp; 267 | callback(null, colorTemp); 268 | } else { 269 | callback(communicationError); 270 | } 271 | }); 272 | }, 273 | setPowerState(powerOn, callback, context) { 274 | if (context === 'internal') { 275 | callback(); 276 | return; 277 | } 278 | 279 | const that = this; 280 | const serviceData = {}; 281 | serviceData.entity_id = this.entity_id; 282 | 283 | if (powerOn) { 284 | this.log(`Setting power state on the '${this.name}' to on`); 285 | 286 | this.client.callService(this.domain, 'turn_on', serviceData, (data) => { 287 | if (data) { 288 | that.log(`Successfully set power state on the '${that.name}' to on`); 289 | callback(); 290 | } else { 291 | callback(communicationError); 292 | } 293 | }); 294 | } else { 295 | this.log(`Setting power state on the '${this.name}' to off`); 296 | 297 | this.client.callService(this.domain, 'turn_off', serviceData, (data) => { 298 | if (data) { 299 | that.log(`Successfully set power state on the '${that.name}' to off`); 300 | callback(); 301 | } else { 302 | callback(communicationError); 303 | } 304 | }); 305 | } 306 | }, 307 | setBrightness(level, callback, context) { 308 | if (context === 'internal') { 309 | callback(); 310 | return; 311 | } 312 | 313 | const that = this; 314 | const serviceData = {}; 315 | serviceData.entity_id = this.entity_id; 316 | 317 | serviceData.brightness = 255 * (level / 100.0); 318 | 319 | // To make sure setBrightness is done after the setPowerState 320 | setTimeout(() => { 321 | this.log(`Setting brightness on the '${this.name}' to ${level}`); 322 | this.client.callService(this.domain, 'turn_on', serviceData, (data) => { 323 | if (data) { 324 | that.log(`Successfully set brightness on the '${that.name}' to ${level}`); 325 | callback(); 326 | } else { 327 | callback(communicationError); 328 | } 329 | }); 330 | }, 800); 331 | }, 332 | setHue(level, callback, context) { 333 | if (context === 'internal') { 334 | callback(); 335 | return; 336 | } 337 | 338 | this.data.attributes.hue = level; 339 | 340 | if (this.cachedColor) { 341 | this._setColor(callback); 342 | } else { 343 | this.cachedColor = true; 344 | callback(); 345 | } 346 | }, 347 | setSaturation(level, callback, context) { 348 | if (context === 'internal') { 349 | callback(); 350 | return; 351 | } 352 | this.data.attributes.saturation = level; 353 | 354 | if (this.cachedColor) { 355 | this._setColor(callback); 356 | } else { 357 | this.cachedColor = true; 358 | callback(); 359 | } 360 | }, 361 | _setColor(callback) { 362 | const that = this; 363 | this.cachedColor = false; 364 | const serviceData = {}; 365 | serviceData.entity_id = this.entity_id; 366 | const rgb = LightUtil.hsvToRgb( 367 | (this.data.attributes.hue || 0) / 360, 368 | (this.data.attributes.saturation || 0) / 100, 369 | (this.data.attributes.brightness || 0) / 255 370 | ); 371 | 372 | if (this.data.attributes.hue !== undefined) { 373 | if (this.is_supported(this.features.XY_COLOR)) { 374 | serviceData.xy_color = LightUtil.rgbToCie(rgb.r, rgb.g, rgb.b); 375 | } else { 376 | serviceData.rgb_color = [rgb.r, rgb.g, rgb.b]; 377 | } 378 | } 379 | 380 | this.client.callService(this.domain, 'turn_on', serviceData, (data) => { 381 | if (data) { 382 | if (that.is_supported(that.features.XY_COLOR)) { 383 | that.log(`Successfully set xy on the '${that.name}' to ${serviceData.xy_color}`); 384 | } else { 385 | that.log(`Successfully set rgb on the '${that.name}' to ${serviceData.rgb_color}`); 386 | } 387 | callback(); 388 | } else { 389 | callback(communicationError); 390 | } 391 | }); 392 | }, 393 | setColorTemperature(level, callback, context) { 394 | if (context === 'internal') { 395 | callback(); 396 | return; 397 | } 398 | 399 | const that = this; 400 | const serviceData = {}; 401 | serviceData.entity_id = this.entity_id; 402 | serviceData.color_temp = Math.round(level); 403 | 404 | this.client.callService(this.domain, 'turn_on', serviceData, (data) => { 405 | if (data) { 406 | that.log(`Successfully set color temperature on the '${that.name}' to ${serviceData.color_temp}`); 407 | callback(); 408 | } else { 409 | callback(communicationError); 410 | } 411 | }); 412 | }, 413 | getServices() { 414 | this.lightbulbService = new Service.Lightbulb(); 415 | const informationService = new Service.AccessoryInformation(); 416 | 417 | informationService 418 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 419 | .setCharacteristic(Characteristic.Model, this.model) 420 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 421 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 422 | 423 | informationService 424 | .setCharacteristic(Characteristic.Identify) 425 | .on('set', this.identify.bind(this)); 426 | 427 | this.lightbulbService 428 | .getCharacteristic(Characteristic.On) 429 | .on('get', this.getPowerState.bind(this)) 430 | .on('set', this.setPowerState.bind(this)); 431 | 432 | if (this.is_supported(this.features.BRIGHTNESS)) { 433 | this.lightbulbService 434 | .addCharacteristic(Characteristic.Brightness) 435 | .on('get', this.getBrightness.bind(this)) 436 | .on('set', this.setBrightness.bind(this)); 437 | } 438 | 439 | if (this.is_supported(this.features.RGB_COLOR)) { 440 | this.lightbulbService 441 | .addCharacteristic(Characteristic.Hue) 442 | .on('get', this.getHue.bind(this)) 443 | .on('set', this.setHue.bind(this)); 444 | 445 | this.lightbulbService 446 | .addCharacteristic(Characteristic.Saturation) 447 | .on('get', this.getSaturation.bind(this)) 448 | .on('set', this.setSaturation.bind(this)); 449 | } 450 | 451 | if (this.is_supported(this.features.COLOR_TEMP)) { 452 | this.lightbulbService 453 | .addCharacteristic(Characteristic.ColorTemperature) 454 | .setProps({ maxValue: this.maxTemp, minValue: this.minTemp }) 455 | .on('get', this.getColorTemperature.bind(this)) 456 | .on('set', this.setColorTemperature.bind(this)); 457 | } 458 | 459 | return [informationService, this.lightbulbService]; 460 | }, 461 | 462 | }; 463 | 464 | function HomeAssistantLightPlatform(oService, oCharacteristic, oCommunicationError) { 465 | Service = oService; 466 | Characteristic = oCharacteristic; 467 | communicationError = oCommunicationError; 468 | 469 | return HomeAssistantLight; 470 | } 471 | 472 | module.exports = HomeAssistantLightPlatform; 473 | module.exports.HomeAssistantLight = HomeAssistantLight; 474 | -------------------------------------------------------------------------------- /accessories/lock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function HomeAssistantLock(log, data, client, firmware) { 8 | // device info 9 | this.domain = 'lock'; 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_model) { 25 | this.model = String(data.attributes.homebridge_model); 26 | } else { 27 | this.model = 'Lock'; 28 | } 29 | if (data.attributes && data.attributes.homebridge_serial) { 30 | this.serial = String(data.attributes.homebridge_serial); 31 | } else { 32 | this.serial = data.entity_id; 33 | } 34 | this.client = client; 35 | this.log = log; 36 | this.lockCode = data.attributes.homebridge_lock_code; 37 | this.batterySource = data.attributes.homebridge_battery_source; 38 | this.chargingSource = data.attributes.homebridge_charging_source; 39 | } 40 | 41 | HomeAssistantLock.prototype = { 42 | onEvent(oldState, newState) { 43 | if (newState.state) { 44 | const lockState = newState.state === 'unlocked' ? 0 : 1; 45 | this.lockService.getCharacteristic(Characteristic.LockCurrentState) 46 | .setValue(lockState, null, 'internal'); 47 | this.lockService.getCharacteristic(Characteristic.LockTargetState) 48 | .setValue(lockState, null, 'internal'); 49 | } 50 | }, 51 | getLockState(callback) { 52 | this.client.fetchState(this.entity_id, (data) => { 53 | if (data) { 54 | const lockState = data.state === 'locked'; 55 | callback(null, lockState); 56 | } else { 57 | callback(communicationError); 58 | } 59 | }); 60 | }, 61 | getBatteryLevel(callback) { 62 | this.client.fetchState(this.batterySource, (data) => { 63 | if (data) { 64 | callback(null, parseFloat(data.state)); 65 | } else { 66 | callback(communicationError); 67 | } 68 | }); 69 | }, 70 | getChargingState(callback) { 71 | if (this.batterySource && this.chargingSource) { 72 | this.client.fetchState(this.chargingSource, (data) => { 73 | if (data) { 74 | callback(null, data.state.toLowerCase() === 'charging' ? 1 : 0); 75 | } else { 76 | callback(communicationError); 77 | } 78 | }); 79 | } else { 80 | callback(null, 2); 81 | } 82 | }, 83 | getLowBatteryStatus(callback) { 84 | this.client.fetchState(this.batterySource, (data) => { 85 | if (data) { 86 | callback(null, parseFloat(data.state) > 20 ? 0 : 1); 87 | } else { 88 | callback(communicationError); 89 | } 90 | }); 91 | }, 92 | setLockState(lockOn, callback, context) { 93 | if (context === 'internal') { 94 | callback(); 95 | return; 96 | } 97 | 98 | const that = this; 99 | const serviceData = {}; 100 | serviceData.entity_id = this.entity_id; 101 | if (this.lockCode) { 102 | serviceData.code = this.lockCode; 103 | } 104 | 105 | if (lockOn) { 106 | this.log(`Setting lock state on the '${this.name}' to locked`); 107 | 108 | this.client.callService(this.domain, 'lock', serviceData, (data) => { 109 | if (data) { 110 | that.log(`Successfully set lock state on the '${that.name}' to locked`); 111 | callback(); 112 | } else { 113 | callback(communicationError); 114 | } 115 | }); 116 | } else { 117 | this.log(`Setting lock state on the '${this.name}' to unlocked`); 118 | 119 | this.client.callService(this.domain, 'unlock', serviceData, (data) => { 120 | if (data) { 121 | that.log(`Successfully set lock state on the '${that.name}' to unlocked`); 122 | callback(); 123 | } else { 124 | callback(communicationError); 125 | } 126 | }); 127 | } 128 | }, 129 | getServices() { 130 | this.lockService = new Service.LockMechanism(); 131 | const informationService = new Service.AccessoryInformation(); 132 | 133 | informationService 134 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 135 | .setCharacteristic(Characteristic.Model, this.model) 136 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 137 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 138 | 139 | this.lockService 140 | .getCharacteristic(Characteristic.LockCurrentState) 141 | .on('get', this.getLockState.bind(this)); 142 | 143 | this.lockService 144 | .getCharacteristic(Characteristic.LockTargetState) 145 | .on('get', this.getLockState.bind(this)) 146 | .on('set', this.setLockState.bind(this)); 147 | 148 | if (this.batterySource) { 149 | this.batteryService = new Service.BatteryService(); 150 | this.batteryService 151 | .getCharacteristic(Characteristic.BatteryLevel) 152 | .setProps({ maxValue: 100, minValue: 0, minStep: 1 }) 153 | .on('get', this.getBatteryLevel.bind(this)); 154 | this.batteryService 155 | .getCharacteristic(Characteristic.ChargingState) 156 | .setProps({ maxValue: 2 }) 157 | .on('get', this.getChargingState.bind(this)); 158 | this.batteryService 159 | .getCharacteristic(Characteristic.StatusLowBattery) 160 | .on('get', this.getLowBatteryStatus.bind(this)); 161 | return [informationService, this.lockService, this.batteryService]; 162 | } 163 | return [informationService, this.lockService]; 164 | }, 165 | 166 | }; 167 | 168 | function HomeAssistantLockPlatform(oService, oCharacteristic, oCommunicationError) { 169 | Service = oService; 170 | Characteristic = oCharacteristic; 171 | communicationError = oCommunicationError; 172 | 173 | return HomeAssistantLock; 174 | } 175 | 176 | module.exports = HomeAssistantLockPlatform; 177 | module.exports.HomeAssistantLock = HomeAssistantLock; 178 | -------------------------------------------------------------------------------- /accessories/media_player.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function HomeAssistantMediaPlayer(log, data, client, firmware) { 8 | /* eslint-disable no-unused-vars */ 9 | const SUPPORT_PAUSE = 1; 10 | const SUPPORT_SEEK = 2; 11 | const SUPPORT_VOLUME_SET = 4; 12 | const SUPPORT_VOLUME_MUTE = 8; 13 | const SUPPORT_PREVIOUS_TRACK = 16; 14 | const SUPPORT_NEXT_TRACK = 32; 15 | const SUPPORT_TURN_ON = 128; 16 | const SUPPORT_TURN_OFF = 256; 17 | const SUPPORT_VOLUME_STEP = 1024; 18 | const SUPPORT_STOP = 4096; 19 | const SUPPORT_PLAY = 16384; 20 | /* eslint-enable no-unused-vars */ 21 | 22 | // device info 23 | this.domain = 'media_player'; 24 | this.data = data; 25 | this.entity_id = data.entity_id; 26 | this.uuid_base = data.entity_id; 27 | this.firmware = firmware; 28 | this.supportedFeatures = data.attributes.supported_features; 29 | this.stateLogicCompareWithOn = true; 30 | 31 | if (data.attributes && data.attributes.friendly_name) { 32 | this.name = data.attributes.friendly_name; 33 | } else { 34 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 35 | } 36 | 37 | const supportPause = (this.supportedFeatures | SUPPORT_PAUSE) === this.supportedFeatures; 38 | const supportStop = (this.supportedFeatures | SUPPORT_STOP) === this.supportedFeatures; 39 | const supportOnOff = ((this.supportedFeatures | SUPPORT_TURN_ON) === this.supportedFeatures && 40 | (this.supportedFeatures | SUPPORT_TURN_OFF) === this.supportedFeatures); 41 | this.supportMute = (this.supportedFeatures | SUPPORT_VOLUME_MUTE) === this.supportedFeatures; 42 | this.supportVolume = (this.supportedFeatures | SUPPORT_VOLUME_SET) === this.supportedFeatures; 43 | 44 | if (this.data && this.data.attributes && this.data.attributes.homebridge_media_player_switch === 'on_off' && supportOnOff) { 45 | this.onState = 'on'; 46 | this.offState = 'off'; 47 | this.onService = 'turn_on'; 48 | this.offService = 'turn_off'; 49 | this.stateLogicCompareWithOn = false; 50 | } else if (this.data && this.data.attributes && this.data.attributes.homebridge_media_player_switch === 'play_stop' && supportStop) { 51 | this.onState = 'playing'; 52 | this.offState = 'idle'; 53 | this.onService = 'media_play'; 54 | this.offService = 'media_stop'; 55 | } else if (supportPause) { 56 | this.onState = 'playing'; 57 | this.offState = 'paused'; 58 | this.onService = 'media_play'; 59 | this.offService = 'media_pause'; 60 | } else if (supportStop) { 61 | this.onState = 'playing'; 62 | this.offState = 'idle'; 63 | this.onService = 'media_play'; 64 | this.offService = 'media_stop'; 65 | } else if (supportOnOff) { 66 | this.onState = 'on'; 67 | this.offState = 'off'; 68 | this.onService = 'turn_on'; 69 | this.offService = 'turn_off'; 70 | } 71 | if (data.attributes && data.attributes.homebridge_manufacturer) { 72 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 73 | } else { 74 | this.manufacturer = 'Home Assistant'; 75 | } 76 | if (data.attributes && data.attributes.homebridge_model) { 77 | this.model = String(data.attributes.homebridge_model); 78 | } else { 79 | this.model = 'Media Player'; 80 | } 81 | if (data.attributes && data.attributes.homebridge_serial) { 82 | this.serial = String(data.attributes.homebridge_serial); 83 | } else { 84 | this.serial = data.entity_id; 85 | } 86 | this.client = client; 87 | this.log = log; 88 | } 89 | 90 | HomeAssistantMediaPlayer.prototype = { 91 | onEvent(oldState, newState) { 92 | if (newState.state) { 93 | let powerState; 94 | if (this.stateLogicCompareWithOn) { 95 | powerState = newState.state === this.onState; 96 | } else { 97 | powerState = newState.state !== this.offState; 98 | } 99 | this.switchService.getCharacteristic(Characteristic.On) 100 | .setValue(powerState, null, 'internal'); 101 | } 102 | }, 103 | getPowerState(callback) { 104 | this.log(`fetching power state for: ${this.name}`); 105 | 106 | this.client.fetchState(this.entity_id, (data) => { 107 | if (data) { 108 | let powerState; 109 | if (this.stateLogicCompareWithOn) { 110 | powerState = data.state === this.onState; 111 | } else { 112 | powerState = data.state !== this.offState; 113 | } 114 | callback(null, powerState); 115 | } else { 116 | callback(communicationError); 117 | } 118 | }); 119 | }, 120 | setPowerState(powerOn, callback, context) { 121 | if (context === 'internal') { 122 | callback(); 123 | return; 124 | } 125 | 126 | const that = this; 127 | const serviceData = {}; 128 | serviceData.entity_id = this.entity_id; 129 | 130 | if (powerOn) { 131 | this.log(`Setting power state on the '${this.name}' to on`); 132 | 133 | this.client.callService(this.domain, this.onService, serviceData, (data) => { 134 | if (data) { 135 | that.log(`Successfully set power state on the '${that.name}' to on`); 136 | callback(); 137 | } else { 138 | callback(communicationError); 139 | } 140 | }); 141 | } else { 142 | this.log(`Setting power state on the '${this.name}' to off`); 143 | 144 | this.client.callService(this.domain, this.offService, serviceData, (data) => { 145 | if (data) { 146 | that.log(`Successfully set power state on the '${that.name}' to off`); 147 | callback(); 148 | } else { 149 | callback(communicationError); 150 | } 151 | }); 152 | } 153 | }, 154 | getMuteState(callback) { 155 | this.log(`fetching mute state for: ${this.name}`); 156 | 157 | this.client.fetchState(this.entity_id, (data) => { 158 | if (data) { 159 | callback(null, data.attributes.is_volume_muted); 160 | } else { 161 | callback(communicationError); 162 | } 163 | }); 164 | }, 165 | setMuteState(muteOn, callback, context) { 166 | if (context === 'internal') { 167 | callback(); 168 | return; 169 | } 170 | 171 | const that = this; 172 | const serviceData = {}; 173 | serviceData.entity_id = this.entity_id; 174 | serviceData.is_volume_muted = (muteOn) ? 'true' : 'false'; 175 | 176 | this.log(`Setting mute state on the '${this.name}' to ${serviceData.is_volume_muted}`); 177 | 178 | this.client.callService(this.domain, 'volume_mute', serviceData, (data) => { 179 | if (data) { 180 | that.log(`Successfully set mute state on the '${that.name}' to ${serviceData.is_volume_muted}`); 181 | callback(); 182 | } else { 183 | callback(communicationError); 184 | } 185 | }); 186 | }, 187 | getVolume(callback) { 188 | this.log(`fetching volume for: ${this.name}`); 189 | 190 | this.client.fetchState(this.entity_id, (data) => { 191 | if (data) { 192 | let volume; 193 | if (!(data.attributes.volume_level)) { 194 | volume = 0; 195 | } else { 196 | volume = (data.attributes.volume_level * 100); 197 | } 198 | callback(null, volume); 199 | } else { 200 | callback(communicationError); 201 | } 202 | }); 203 | }, 204 | setVolume(volume, callback, context) { 205 | if (context === 'internal') { 206 | callback(); 207 | return; 208 | } 209 | 210 | const that = this; 211 | const serviceData = {}; 212 | serviceData.entity_id = this.entity_id; 213 | serviceData.volume_level = volume / 100; 214 | 215 | this.log(`Setting volume on the '${this.name}' to ${volume}%`); 216 | 217 | this.client.callService(this.domain, 'volume_set', serviceData, (data) => { 218 | if (data) { 219 | that.log(`Successfully set volume on the '${that.name}' to ${serviceData.volume_level}`); 220 | callback(); 221 | } else { 222 | callback(communicationError); 223 | } 224 | }); 225 | }, 226 | getServices() { 227 | this.switchService = new Service.Switch(); 228 | const informationService = new Service.AccessoryInformation(); 229 | 230 | informationService 231 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 232 | .setCharacteristic(Characteristic.Model, this.model) 233 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 234 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 235 | 236 | this.switchService 237 | .getCharacteristic(Characteristic.On) 238 | .on('get', this.getPowerState.bind(this)) 239 | .on('set', this.setPowerState.bind(this)); 240 | 241 | if (this.supportMute) { 242 | this.speakerService = new Service.Speaker(); 243 | 244 | this.speakerService 245 | .setCharacteristic(Characteristic.Name, this.name); 246 | 247 | this.speakerService 248 | .getCharacteristic(Characteristic.Mute) 249 | .on('get', this.getMuteState.bind(this)) 250 | .on('set', this.setMuteState.bind(this)); 251 | 252 | if (this.supportVolume) { 253 | this.speakerService 254 | .getCharacteristic(Characteristic.Volume) 255 | .on('get', this.getVolume.bind(this)) 256 | .on('set', this.setVolume.bind(this)); 257 | } 258 | 259 | return [informationService, this.switchService, this.speakerService]; 260 | } 261 | 262 | return [informationService, this.switchService]; 263 | }, 264 | 265 | }; 266 | 267 | function HomeAssistantMediaPlayerPlatform(oService, oCharacteristic, oCommunicationError) { 268 | Service = oService; 269 | Characteristic = oCharacteristic; 270 | communicationError = oCommunicationError; 271 | 272 | return HomeAssistantMediaPlayer; 273 | } 274 | 275 | module.exports = HomeAssistantMediaPlayerPlatform; 276 | module.exports.HomeAssistantMediaPlayer = HomeAssistantMediaPlayer; 277 | -------------------------------------------------------------------------------- /accessories/sensor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | class HomeAssistantSensor { 8 | constructor(log, data, client, service, characteristic, transformData, firmware) { 9 | // device info 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_model) { 25 | this.model = String(data.attributes.homebridge_model); 26 | } else { 27 | this.model = 'Sensor'; 28 | } 29 | if (data.attributes && data.attributes.homebridge_serial) { 30 | this.serial = String(data.attributes.homebridge_serial); 31 | } else { 32 | this.serial = data.entity_id; 33 | } 34 | this.entity_type = data.entity_id.split('.')[0]; 35 | this.service = service; 36 | this.characteristic = characteristic; 37 | if (transformData) { 38 | this.transformData = transformData; 39 | } 40 | this.client = client; 41 | this.log = log; 42 | this.batterySource = data.attributes.homebridge_battery_source; 43 | this.chargingSource = data.attributes.homebridge_charging_source; 44 | } 45 | 46 | transformData(data) { 47 | return parseFloat(data.state); 48 | } 49 | 50 | onEvent(oldState, newState) { 51 | if (newState.state) { 52 | if (this.service === Service.CarbonDioxideSensor) { 53 | const transformed = this.transformData(newState); 54 | this.sensorService.getCharacteristic(this.characteristic) 55 | .setValue(transformed, null, 'internal'); 56 | 57 | const abnormal = Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL; 58 | const normal = Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL; 59 | const detected = (transformed > 1000 ? abnormal : normal); 60 | this.sensorService.getCharacteristic(Characteristic.CarbonDioxideDetected) 61 | .setValue(detected, null, 'internal'); 62 | } else { 63 | this.sensorService.getCharacteristic(this.characteristic) 64 | .setValue(this.transformData(newState), null, 'internal'); 65 | } 66 | } 67 | } 68 | 69 | identify(callback) { 70 | this.log(`identifying: ${this.name}`); 71 | callback(); 72 | } 73 | 74 | getState(callback) { 75 | this.log(`fetching state for: ${this.name}`); 76 | this.client.fetchState(this.entity_id, (data) => { 77 | if (data) { 78 | callback(null, this.transformData(data)); 79 | } else { 80 | callback(communicationError); 81 | } 82 | }); 83 | } 84 | 85 | getBatteryLevel(callback) { 86 | this.client.fetchState(this.batterySource, (data) => { 87 | if (data) { 88 | callback(null, parseFloat(data.state)); 89 | } else { 90 | callback(communicationError); 91 | } 92 | }); 93 | } 94 | getChargingState(callback) { 95 | if (this.batterySource && this.chargingSource) { 96 | this.client.fetchState(this.chargingSource, (data) => { 97 | if (data) { 98 | callback(null, data.state.toLowerCase() === 'charging' ? 1 : 0); 99 | } else { 100 | callback(communicationError); 101 | } 102 | }); 103 | } else { 104 | callback(null, 2); 105 | } 106 | } 107 | getLowBatteryStatus(callback) { 108 | this.client.fetchState(this.batterySource, (data) => { 109 | if (data) { 110 | callback(null, parseFloat(data.state) > 20 ? 0 : 1); 111 | } else { 112 | callback(communicationError); 113 | } 114 | }); 115 | } 116 | getServices() { 117 | this.sensorService = new this.service(); // eslint-disable-line new-cap 118 | const informationService = new Service.AccessoryInformation(); 119 | 120 | informationService 121 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 122 | .setCharacteristic(Characteristic.Model, this.model) 123 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 124 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 125 | 126 | this.sensorService 127 | .getCharacteristic(this.characteristic) 128 | .setProps({ minValue: -50 }) 129 | .on('get', this.getState.bind(this)); 130 | 131 | if (this.batterySource) { 132 | this.batteryService = new Service.BatteryService(); 133 | this.batteryService 134 | .getCharacteristic(Characteristic.BatteryLevel) 135 | .setProps({ maxValue: 100, minValue: 0, minStep: 1 }) 136 | .on('get', this.getBatteryLevel.bind(this)); 137 | this.batteryService 138 | .getCharacteristic(Characteristic.ChargingState) 139 | .setProps({ maxValue: 2 }) 140 | .on('get', this.getChargingState.bind(this)); 141 | this.batteryService 142 | .getCharacteristic(Characteristic.StatusLowBattery) 143 | .on('get', this.getLowBatteryStatus.bind(this)); 144 | return [informationService, this.batteryService, this.sensorService]; 145 | } 146 | return [informationService, this.sensorService]; 147 | } 148 | } 149 | 150 | function HomeAssistantSensorFactory(log, data, client, firmware) { 151 | if (!data.attributes) { 152 | return null; 153 | } 154 | let service; 155 | let characteristic; 156 | let transformData; 157 | if (data.attributes.unit_of_measurement === '°C' 158 | || data.attributes.unit_of_measurement === '℃' 159 | || data.attributes.unit_of_measurement === '°F' 160 | || data.attributes.unit_of_measurement === '℉') { 161 | service = Service.TemperatureSensor; 162 | characteristic = Characteristic.CurrentTemperature; 163 | transformData = function transformData(dataToTransform) { // eslint-disable-line no-shadow 164 | let value = parseFloat(dataToTransform.state); 165 | // HomeKit only works with Celsius internally 166 | if (dataToTransform.attributes.unit_of_measurement === '°F' 167 | || dataToTransform.attributes.unit_of_measurement === '℉') { 168 | value = (value - 32) / 1.8; 169 | } 170 | return value; 171 | }; 172 | } else if (data.attributes.unit_of_measurement === '%' && (data.entity_id.includes('humidity') || data.attributes.homebridge_sensor_type === 'humidity')) { 173 | service = Service.HumiditySensor; 174 | characteristic = Characteristic.CurrentRelativeHumidity; 175 | } else if ((typeof data.attributes.unit_of_measurement === 'string' && data.attributes.unit_of_measurement.toLowerCase() === 'lux') || data.attributes.homebridge_sensor_type === 'light') { 176 | service = Service.LightSensor; 177 | characteristic = Characteristic.CurrentAmbientLightLevel; 178 | transformData = function transformData(dataToTransform) { // eslint-disable-line no-shadow 179 | return Math.max(0.0001, parseFloat(dataToTransform.state)); 180 | }; 181 | } else if (typeof data.attributes.unit_of_measurement === 'string' && data.attributes.unit_of_measurement.toLowerCase() === 'ppm' && (data.entity_id.includes('co2') || data.attributes.homebridge_sensor_type === 'co2')) { 182 | service = Service.CarbonDioxideSensor; 183 | characteristic = Characteristic.CarbonDioxideLevel; 184 | } else if ((typeof data.attributes.unit_of_measurement === 'string' && data.attributes.unit_of_measurement.toLowerCase() === 'aqi') || data.attributes.homebridge_sensor_type === 'air_quality') { 185 | service = Service.AirQualitySensor; 186 | characteristic = Characteristic.AirQuality; 187 | transformData = function transformData(dataToTransform) { // eslint-disable-line no-shadow 188 | const value = parseFloat(dataToTransform.state); 189 | if (value <= 75) { 190 | return 1; 191 | } else if (value >= 76 && value <= 150) { 192 | return 2; 193 | } else if (value >= 151 && value <= 225) { 194 | return 3; 195 | } else if (value >= 226 && value <= 300) { 196 | return 4; 197 | } else if (value >= 301) { 198 | return 5; 199 | } 200 | return 0; 201 | }; 202 | } else { 203 | return null; 204 | } 205 | 206 | return new HomeAssistantSensor( 207 | log, data, client, 208 | service, 209 | characteristic, 210 | transformData, 211 | firmware 212 | ); 213 | } 214 | 215 | function HomeAssistantSensorPlatform(oService, oCharacteristic, oCommunicationError) { 216 | Service = oService; 217 | Characteristic = oCharacteristic; 218 | communicationError = oCommunicationError; 219 | 220 | return HomeAssistantSensorFactory; 221 | } 222 | 223 | module.exports = HomeAssistantSensorPlatform; 224 | 225 | module.exports.HomeAssistantSensorFactory = HomeAssistantSensorFactory; 226 | -------------------------------------------------------------------------------- /accessories/switch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | let communicationError; 6 | 7 | function HomeAssistantSwitch(log, data, client, type, firmware) { 8 | // device info 9 | this.domain = type; 10 | this.data = data; 11 | this.entity_id = data.entity_id; 12 | this.uuid_base = data.entity_id; 13 | this.firmware = firmware; 14 | if (data.attributes && data.attributes.friendly_name) { 15 | this.name = data.attributes.friendly_name; 16 | } else { 17 | this.name = data.entity_id.split('.').pop().replace(/_/g, ' '); 18 | } 19 | if (data.attributes && data.attributes.homebridge_manufacturer) { 20 | this.manufacturer = String(data.attributes.homebridge_manufacturer); 21 | } else { 22 | this.manufacturer = 'Home Assistant'; 23 | } 24 | if (data.attributes && data.attributes.homebridge_serial) { 25 | this.serial = String(data.attributes.homebridge_serial); 26 | } else { 27 | this.serial = data.entity_id; 28 | } 29 | this.client = client; 30 | this.log = log; 31 | } 32 | 33 | HomeAssistantSwitch.prototype = { 34 | onEvent(oldState, newState) { 35 | if (newState.state) { 36 | this.service.getCharacteristic(Characteristic.On) 37 | .setValue(newState.state === 'on', null, 'internal'); 38 | } 39 | }, 40 | getPowerState(callback) { 41 | this.client.fetchState(this.entity_id, (data) => { 42 | if (data) { 43 | const powerState = data.state === 'on'; 44 | callback(null, powerState); 45 | } else { 46 | callback(communicationError); 47 | } 48 | }); 49 | }, 50 | setPowerState(powerOn, callback, context) { 51 | if (context === 'internal') { 52 | callback(); 53 | return; 54 | } 55 | 56 | const that = this; 57 | const serviceData = {}; 58 | serviceData.entity_id = this.entity_id; 59 | var callDomain = this.domain === 'group' ? 'homeassistant' : this.domain; 60 | 61 | if (powerOn) { 62 | this.log(`Setting power state on the '${this.name}' to on`); 63 | 64 | this.client.callService(callDomain, 'turn_on', serviceData, (data) => { 65 | if (this.domain === 'scene' || (this.domain === 'script' && !(this.data.attributes.can_cancel))) { 66 | setTimeout(() => { 67 | this.service.getCharacteristic(Characteristic.On) 68 | .setValue(false, null, 'internal'); 69 | }, 500); 70 | } 71 | if (data) { 72 | that.log(`Successfully set power state on the '${that.name}' to on`); 73 | callback(); 74 | } else { 75 | callback(communicationError); 76 | } 77 | }); 78 | } else { 79 | this.log(`Setting power state on the '${this.name}' to off`); 80 | 81 | this.client.callService(callDomain, 'turn_off', serviceData, (data) => { 82 | if (data) { 83 | that.log(`Successfully set power state on the '${that.name}' to off`); 84 | callback(); 85 | } else { 86 | callback(communicationError); 87 | } 88 | }); 89 | } 90 | }, 91 | getServices() { 92 | let model; 93 | 94 | switch (this.domain) { 95 | case 'scene': 96 | if (this.data.attributes && this.data.attributes.homebridge_model) { 97 | model = String(this.data.attributes.homebridge_model); 98 | } else { 99 | model = 'Scene'; 100 | } 101 | break; 102 | case 'input_boolean': 103 | if (this.data.attributes && this.data.attributes.homebridge_model) { 104 | model = String(this.data.attributes.homebridge_model); 105 | } else { 106 | model = 'Input Boolean'; 107 | } 108 | break; 109 | case 'group': 110 | if (this.data.attributes && this.data.attributes.homebridge_model) { 111 | model = String(this.data.attributes.homebridge_model); 112 | } else { 113 | model = 'Group'; 114 | } 115 | break; 116 | case 'switch': 117 | if (this.data.attributes && this.data.attributes.homebridge_model) { 118 | model = String(this.data.attributes.homebridge_model); 119 | } else { 120 | model = 'Switch'; 121 | } 122 | break; 123 | case 'remote': 124 | if (this.data.attributes && this.data.attributes.homebridge_model) { 125 | model = String(this.data.attributes.homebridge_model); 126 | } else { 127 | model = 'Remote'; 128 | } 129 | break; 130 | case 'automation': 131 | if (this.data.attributes && this.data.attributes.homebridge_model) { 132 | model = String(this.data.attributes.homebridge_model); 133 | } else { 134 | model = 'Automation'; 135 | } 136 | break; 137 | case 'vacuum': 138 | if (this.data.attributes && this.data.attributes.homebridge_model) { 139 | model = String(this.data.attributes.homebridge_model); 140 | } else { 141 | model = 'Vacuum'; 142 | } 143 | break; 144 | case 'script': 145 | if (this.data.attributes && this.data.attributes.homebridge_model) { 146 | model = String(this.data.attributes.homebridge_model); 147 | } else { 148 | model = 'Script'; 149 | } 150 | break; 151 | default: 152 | model = 'Switch'; 153 | } 154 | 155 | this.service = new Service.Switch(); 156 | if (this.data && this.data.attributes && this.data.attributes.homebridge_switch_type === 'outlet') { 157 | this.service = new Service.Outlet(); 158 | if (this.data.attributes && this.data.attributes.homebridge_model) { 159 | model = String(this.data.attributes.homebridge_model); 160 | } else { 161 | model = 'Outlet'; 162 | } 163 | this.service 164 | .getCharacteristic(Characteristic.OutletInUse) 165 | .on('get', this.getPowerState.bind(this)); 166 | } 167 | const informationService = new Service.AccessoryInformation(); 168 | 169 | informationService 170 | .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) 171 | .setCharacteristic(Characteristic.Model, model) 172 | .setCharacteristic(Characteristic.SerialNumber, this.serial) 173 | .setCharacteristic(Characteristic.FirmwareRevision, this.firmware); 174 | 175 | if (this.domain === 'remote' || this.domain === 'switch' || this.domain === 'input_boolean' || this.domain === 'group' || this.domain === 'automation' || this.domain === 'vacuum' || (this.domain === 'script' && this.data.attributes.can_cancel)) { 176 | this.service 177 | .getCharacteristic(Characteristic.On) 178 | .on('get', this.getPowerState.bind(this)) 179 | .on('set', this.setPowerState.bind(this)); 180 | } else { 181 | this.service 182 | .getCharacteristic(Characteristic.On) 183 | .on('set', this.setPowerState.bind(this)); 184 | } 185 | 186 | return [informationService, this.service]; 187 | }, 188 | 189 | }; 190 | 191 | function HomeAssistantSwitchPlatform(oService, oCharacteristic, oCommunicationError) { 192 | Service = oService; 193 | Characteristic = oCharacteristic; 194 | communicationError = oCommunicationError; 195 | 196 | return HomeAssistantSwitch; 197 | } 198 | 199 | module.exports = HomeAssistantSwitchPlatform; 200 | module.exports.HomeAssistantSwitch = HomeAssistantSwitch; 201 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let Service; 4 | let Characteristic; 5 | const url = require('url'); 6 | const request = require('request'); 7 | const EventSource = require('eventsource'); 8 | /* eslint-disable import/no-unresolved */ 9 | const firmware = require('./package.json').version; 10 | /* eslint-enable import/no-unresolved */ 11 | 12 | const communicationError = new Error('Can not communicate with Home Assistant.'); 13 | 14 | let HomeAssistantAlarmControlPanel; 15 | let HomeAssistantBinarySensorFactory; 16 | let HomeAssistantCoverFactory; 17 | let HomeAssistantFan; 18 | let HomeAssistantLight; 19 | let HomeAssistantLock; 20 | let HomeAssistantMediaPlayer; 21 | let HomeAssistantSensorFactory; 22 | let HomeAssistantSwitch; 23 | let HomeAssistantDeviceTrackerFactory; 24 | let HomeAssistantClimate; 25 | 26 | function HomeAssistantPlatform(log, config, api) { 27 | // auth info 28 | this.host = config.host; 29 | this.password = config.password; 30 | this.supportedTypes = config.supported_types || ['alarm_control_panel', 'automation', 'binary_sensor', 'climate', 'cover', 'device_tracker', 'fan', 'group', 'input_boolean', 'light', 'lock', 'media_player', 'remote', 'scene', 'script', 'sensor', 'switch', 'vacuum']; 31 | this.foundAccessories = []; 32 | this.logging = config.logging !== undefined ? config.logging : true; 33 | this.verify_ssl = config.verify_ssl !== undefined ? config.verify_ssl : true; 34 | this.log = log; 35 | if (config.default_visibility === 'hidden' || config.default_visibility === 'visible') { 36 | this.defaultVisibility = config.default_visibility; 37 | } else { 38 | this.defaultVisibility = 'visible'; 39 | this.log.error('Please set default_visibility in config.json to "hidden" or "visible".'); 40 | } 41 | 42 | if (api) { 43 | // Save the API object as plugin needs to register new accessory via this object. 44 | this.api = api; 45 | } 46 | 47 | const es = new EventSource(`${config.host}/api/stream?api_password=${encodeURIComponent(this.password)}`); 48 | es.addEventListener('message', (e) => { 49 | if (this.logging) { 50 | this.log(`Received event: ${e.data}`); 51 | } 52 | if (e.data === 'ping') { 53 | return; 54 | } 55 | 56 | const data = JSON.parse(e.data); 57 | if (data.event_type !== 'state_changed') { 58 | return; 59 | } 60 | 61 | const numAccessories = this.foundAccessories.length; 62 | for (let i = 0; i < numAccessories; i++) { 63 | const accessory = this.foundAccessories[i]; 64 | 65 | if (accessory.entity_id === data.data.entity_id && accessory.onEvent) { 66 | accessory.onEvent(data.data.old_state, data.data.new_state); 67 | } 68 | } 69 | }); 70 | } 71 | 72 | HomeAssistantPlatform.prototype = { 73 | request(method, path, options, callback) { 74 | const requestURL = `${this.host}/api${path}`; 75 | /* eslint-disable no-param-reassign */ 76 | options = options || {}; 77 | options.query = options.query || {}; 78 | /* eslint-enable no-param-reassign */ 79 | 80 | const reqOpts = { 81 | url: url.parse(requestURL), 82 | method: method || 'GET', 83 | qs: options.query, 84 | body: JSON.stringify(options.body), 85 | headers: { 86 | Accept: 'application/json', 87 | 'Content-Type': 'application/json', 88 | 'x-ha-access': this.password, 89 | }, 90 | rejectUnauthorized: this.verify_ssl, 91 | }; 92 | 93 | request(reqOpts, (error, response, body) => { 94 | if (error) { 95 | callback(error, response); 96 | return; 97 | } 98 | 99 | if (response.statusCode === 401) { 100 | callback(new Error('You are not authenticated'), response); 101 | return; 102 | } 103 | 104 | callback(error, response, JSON.parse(body)); 105 | }); 106 | }, 107 | fetchState(entityID, callback) { 108 | this.request('GET', `/states/${entityID}`, {}, (error, response, data) => { 109 | if (error) { 110 | callback(null); 111 | } else { 112 | callback(data); 113 | } 114 | }); 115 | }, 116 | callService(domain, service, serviceData, callback) { 117 | const options = {}; 118 | options.body = serviceData; 119 | 120 | this.request('POST', `/services/${domain}/${service}`, options, (error, response, data) => { 121 | if (error) { 122 | callback(null); 123 | } else { 124 | callback(data); 125 | } 126 | }); 127 | }, 128 | accessories(callback) { 129 | this.log('Fetching HomeAssistant devices.'); 130 | 131 | const that = this; 132 | 133 | this.request('GET', '/states', {}, (error, response, data) => { 134 | if (error) { 135 | that.log(`Failed getting devices: ${error}. Retrying...`); 136 | setTimeout(() => { that.accessories(callback); }, 5000); 137 | return; 138 | } 139 | 140 | for (let i = 0; i < data.length; i++) { 141 | const entity = data[i]; 142 | const entityType = entity.entity_id.split('.')[0]; 143 | 144 | /* eslint-disable no-continue */ 145 | // ignore devices that are not in the list of supported types 146 | if (that.supportedTypes.indexOf(entityType) === -1) { 147 | continue; 148 | } 149 | 150 | // if default behavior is visible, then ignore hidden devices 151 | if (this.defaultVisibility === 'visible' && entity.attributes.homebridge_hidden) { 152 | continue; 153 | } 154 | /* eslint-enable no-continue */ 155 | 156 | // support providing custom names 157 | if (entity.attributes && entity.attributes.homebridge_name) { 158 | entity.attributes.friendly_name = entity.attributes.homebridge_name; 159 | } 160 | 161 | let accessory = null; 162 | 163 | if (this.defaultVisibility === 'visible' || (this.defaultVisibility === 'hidden' && entity.attributes.homebridge_visible)) { 164 | if (entityType === 'light') { 165 | accessory = new HomeAssistantLight(that.log, entity, that, firmware); 166 | } else if (entityType === 'switch') { 167 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'switch', firmware); 168 | } else if (entityType === 'lock') { 169 | accessory = new HomeAssistantLock(that.log, entity, that, firmware); 170 | } else if (entityType === 'garage_door') { 171 | that.log.error('Garage_doors are no longer supported by homebridge-homeassistant. Please upgrade to a newer version of Home Assistant to continue using this entity (with the new cover component).'); 172 | } else if (entityType === 'scene') { 173 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene', firmware); 174 | } else if (entityType === 'rollershutter') { 175 | that.log.error('Rollershutters are no longer supported by homebridge-homeassistant. Please upgrade to a newer version of Home Assistant to continue using this entity (with the new cover component).'); 176 | } else if (entityType === 'input_boolean') { 177 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'input_boolean', firmware); 178 | } else if (entityType === 'fan') { 179 | accessory = new HomeAssistantFan(that.log, entity, that, firmware); 180 | } else if (entityType === 'cover') { 181 | accessory = HomeAssistantCoverFactory(that.log, entity, that, firmware); 182 | } else if (entityType === 'sensor') { 183 | accessory = HomeAssistantSensorFactory(that.log, entity, that, firmware); 184 | } else if (entityType === 'device_tracker') { 185 | accessory = HomeAssistantDeviceTrackerFactory(that.log, entity, that, firmware); 186 | } else if (entityType === 'climate') { 187 | accessory = new HomeAssistantClimate(that.log, entity, that, firmware); 188 | } else if (entityType === 'media_player' && entity.attributes && entity.attributes.supported_features) { 189 | accessory = new HomeAssistantMediaPlayer(that.log, entity, that, firmware); 190 | } else if (entityType === 'binary_sensor' && entity.attributes && entity.attributes.device_class) { 191 | accessory = HomeAssistantBinarySensorFactory(that.log, entity, that, firmware); 192 | } else if (entityType === 'group') { 193 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'group', firmware); 194 | } else if (entityType === 'alarm_control_panel') { 195 | accessory = new HomeAssistantAlarmControlPanel(that.log, entity, that, firmware); 196 | } else if (entityType === 'remote') { 197 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'remote', firmware); 198 | } else if (entityType === 'automation') { 199 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'automation', firmware); 200 | } else if (entityType === 'vacuum') { 201 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'vacuum', firmware); 202 | } else if (entityType === 'script') { 203 | accessory = new HomeAssistantSwitch(that.log, entity, that, 'script', firmware); 204 | } 205 | } 206 | 207 | if (accessory) { 208 | that.foundAccessories.push(accessory); 209 | } 210 | } 211 | 212 | callback(that.foundAccessories); 213 | }); 214 | }, 215 | }; 216 | 217 | function HomebridgeHomeAssistant(homebridge) { 218 | Service = homebridge.hap.Service; 219 | Characteristic = homebridge.hap.Characteristic; 220 | 221 | /* eslint-disable global-require */ 222 | HomeAssistantLight = require('./accessories/light')(Service, Characteristic, communicationError); 223 | HomeAssistantSwitch = require('./accessories/switch')(Service, Characteristic, communicationError); 224 | HomeAssistantLock = require('./accessories/lock')(Service, Characteristic, communicationError); 225 | HomeAssistantMediaPlayer = require('./accessories/media_player')(Service, Characteristic, communicationError); 226 | HomeAssistantFan = require('./accessories/fan')(Service, Characteristic, communicationError); 227 | HomeAssistantCoverFactory = require('./accessories/cover')(Service, Characteristic, communicationError); 228 | HomeAssistantSensorFactory = require('./accessories/sensor')(Service, Characteristic, communicationError); 229 | HomeAssistantBinarySensorFactory = require('./accessories/binary_sensor')(Service, Characteristic, communicationError); 230 | HomeAssistantDeviceTrackerFactory = require('./accessories/device_tracker')(Service, Characteristic, communicationError); 231 | HomeAssistantClimate = require('./accessories/climate')(Service, Characteristic, communicationError); 232 | HomeAssistantAlarmControlPanel = require('./accessories/alarm_control_panel')(Service, Characteristic, communicationError); 233 | /* eslint-enable global-require */ 234 | 235 | homebridge.registerPlatform('homebridge-homeassistant', 'HomeAssistant', HomeAssistantPlatform, false); 236 | } 237 | 238 | module.exports = HomebridgeHomeAssistant; 239 | 240 | module.exports.platform = HomeAssistantPlatform; 241 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-homeassistant", 3 | "version": "3.1.0", 4 | "description": "Homebridge plugin for Home Assistant: https://home-assistant.io", 5 | "license": "Apache-2.0", 6 | "keywords": [ 7 | "homebridge-plugin", 8 | "homebridge", 9 | "plugin", 10 | "home-assistant", 11 | "home assistant", 12 | "home", 13 | "assistant", 14 | "homekit", 15 | "siri" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git://github.com/home-assistant/homebridge-homeassistant.git" 20 | }, 21 | "bugs": { 22 | "url": "http://github.com/home-assistant/homebridge-homeassistant/issues" 23 | }, 24 | "engines": { 25 | "node": ">=4.3.2", 26 | "homebridge": ">=0.3.0" 27 | }, 28 | "dependencies": { 29 | "eventsource": "^0.2.1", 30 | "request": "^2.69.0" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^3.13.1", 34 | "eslint-config-airbnb-base": "^11.1.0" 35 | }, 36 | "scripts": { 37 | "test": "eslint index.js accessories/*" 38 | } 39 | } 40 | --------------------------------------------------------------------------------