Repository: dubocr/homebridge-tahoma Branch: master Commit: 9d8e14f898a1 Files: 102 Total size: 224.9 KB Directory structure: gitextract_f_m6ihgp/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── config.yml │ └── workflows/ │ ├── new-release.yml │ └── stale-issue.yml ├── .gitignore ├── .npmignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config.schema.json ├── eslint.config.mjs ├── nodemon.json ├── package.json ├── platform.schema.json ├── src/ │ ├── CustomCharacteristics.ts │ ├── Mapper.ts │ ├── Platform.ts │ ├── SceneMapper.ts │ ├── colors.ts │ ├── index.ts │ ├── lang/ │ │ ├── en.json │ │ └── fr.json │ ├── mappers/ │ │ ├── AdjustableSlatsRollerShutter.ts │ │ ├── AirSensor/ │ │ │ ├── CO2Sensor.ts │ │ │ └── RelativeHumiditySensor.ts │ │ ├── AirSensor.ts │ │ ├── Alarm/ │ │ │ ├── MyFoxAlarmController.ts │ │ │ └── TSKAlarmController.ts │ │ ├── Alarm.ts │ │ ├── Awning/ │ │ │ └── PositionableHorizontalAwningUno.ts │ │ ├── Awning.ts │ │ ├── ConsumptionSensor.ts │ │ ├── ContactSensor.ts │ │ ├── Curtain.ts │ │ ├── DoorLock.ts │ │ ├── ElectricitySensor/ │ │ │ └── CumulativeElectricPowerConsumptionSensor.ts │ │ ├── ElectricitySensor.ts │ │ ├── EvoHome/ │ │ │ ├── DHWSetPoint.ts │ │ │ ├── EvoHomeController.ts │ │ │ └── HeatingSetPoint.ts │ │ ├── ExteriorHeatingSystem/ │ │ │ └── DimmerExteriorHeating.ts │ │ ├── ExteriorHeatingSystem.ts │ │ ├── ExteriorScreen.ts │ │ ├── ExteriorVenetianBlind.ts │ │ ├── GarageDoor.ts │ │ ├── Gate.ts │ │ ├── Generic/ │ │ │ ├── CyclicGeneric.ts │ │ │ ├── DimmerOnOff.ts │ │ │ ├── RTSGeneric.ts │ │ │ └── RTSGeneric4T.ts │ │ ├── HeatingSystem/ │ │ │ ├── AtlanticElectricalHeater.ts │ │ │ ├── AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint.ts │ │ │ ├── AtlanticElectricalTowelDryer.ts │ │ │ ├── AtlanticPassAPCBoiler.ts │ │ │ ├── AtlanticPassAPCHeatPump.ts │ │ │ ├── AtlanticPassAPCHeatingAndCoolingZone.ts │ │ │ ├── AtlanticPassAPCHeatingZone.ts │ │ │ ├── AtlanticPassAPCZoneControl.ts │ │ │ ├── ProgrammableAndProtectableThermostatSetPoint.ts │ │ │ ├── SomfyHeatingTemperatureInterface.ts │ │ │ ├── SomfyThermostat.ts │ │ │ ├── ThermostatSetPoint.ts │ │ │ └── ValveHeatingTemperatureInterface.ts │ │ ├── HeatingSystem.ts │ │ ├── HitachiHeatingSystem/ │ │ │ ├── HitachiAirToAirHeatPump.ts │ │ │ ├── HitachiAirToWaterHeatingZone.ts │ │ │ ├── HitachiAirToWaterMainComponent.ts │ │ │ └── HitachiDHW.ts │ │ ├── HumiditySensor/ │ │ │ └── WaterDetectionSensor.ts │ │ ├── HumiditySensor.ts │ │ ├── Light.ts │ │ ├── LightSensor.ts │ │ ├── OccupancySensor.ts │ │ ├── OnOff.ts │ │ ├── Pergola/ │ │ │ ├── BioclimaticPergola.ts │ │ │ └── PergolaHorizontalAwningUno.ts │ │ ├── Pergola.ts │ │ ├── RainSensor.ts │ │ ├── RemoteController.ts │ │ ├── RollerShutter/ │ │ │ ├── PositionableRollerShutterUno.ts │ │ │ └── PositionableRollerShutterWithLowSpeedManagement.ts │ │ ├── RollerShutter.ts │ │ ├── Screen.ts │ │ ├── Shutter.ts │ │ ├── Siren.ts │ │ ├── SmokeSensor.ts │ │ ├── SwingingShutter.ts │ │ ├── TemperatureSensor.ts │ │ ├── VenetianBlind.ts │ │ ├── VentilationSystem/ │ │ │ └── DimplexVentilationInletOutlet.ts │ │ ├── VentilationSystem.ts │ │ ├── WaterHeatingSystem/ │ │ │ ├── AtlanticPassAPCDHW.ts │ │ │ ├── DomesticHotWaterProduction/ │ │ │ │ └── AtlanticDomesticHotWaterProductionV2_SPLIT_IOComponent.ts │ │ │ ├── DomesticHotWaterProduction.ts │ │ │ └── DomesticHotWaterTank.ts │ │ ├── WaterHeatingSystem.ts │ │ ├── WaterSensor.ts │ │ ├── Window.ts │ │ └── WindowHandle.ts │ └── settings.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: Device integration and bug url: https://dev.duboc.pro/homebridge-tahoma about: Start here for unsupported device or any command,state unexpected behaviour - name: Gestion d'un équipement et anomalies de fonctionnement url: https://dev.duboc.pro/homebridge-tahoma about: Pour un équipement non pris en charge ou pour un problème de pilotage/affichage de l'état de votre produit, c'est par ici ================================================ FILE: .github/workflows/new-release.yml ================================================ name: Create Release on: push: tags: - v* jobs: publish: runs-on: ubuntu-latest steps: - name: Create GitHub release uses: Roang-zero1/github-create-release-action@master with: version_regex: ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+ env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/stale-issue.yml ================================================ name: Mark stale issues and pull requests on: schedule: - cron: "30 1 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v3 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: > There hasn't been any activity on this issue recently. Please make sure to update to the latest Homebridge TaHoma version to see if that solves the issue. This issue will be closed in case of inactivity. days-before-stale: 90 days-before-close: 120 stale-issue-label: "inactive" exempt-issue-labels: "blocked,waiting" ================================================ FILE: .gitignore ================================================ # Ignore compiled code dist old # ------------- Defaults ------------- # # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .pnp.* .DS_Store ================================================ FILE: .npmignore ================================================ # Ignore source code src # ------------- Defaults ------------- # # eslint .eslintrc # typescript tsconfig.json # vscode .vscode # nodemon nodemon.json # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage *.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env .env.test # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .pnp.* ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "dbaeumer.vscode-eslint" ] } ================================================ FILE: .vscode/settings.json ================================================ { "files.eol": "\n", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, "editor.rulers": [ 140 ], "eslint.enable": true } ================================================ FILE: CHANGELOG.md ================================================ ## 2.2.54 (2022-12-20) ### Bug Fixes * Fix nodejs compatibility to node v12.4.0 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: README.md ================================================ # Overkiz (Somfy) - Homebridge-TaHoma Homebridge plugin supporting Overkiz based platforms : | Service code | Vendor | Product compatibility | |---------------------------|---------------------------|-------------------------------------------------------------------| | `local` | Somfy local API | TaHoma, TaHoma Switch ([configure local API](#configure-local-api)) | | `somfy_europe` | Somfy Europe | TaHoma, TaHoma Switch, Connexoon IO, Kit de connectivité Orange | | `somfy_australia` | Somfy Australia | Connexoon RTS and other products in Australia | | `somfy_north_america` | Somfy North America | TaHoma and other product in North America | | `cozytouch` | Atlantic, Thermor, Sauter | Cozytouch | | `flexom` | Bouygues | Flexom | | `hi_kumo` | Hitachi | Hi hi_kumo | | `rexel` | Rexel | Energeasy connectivité | # Installation 1. Install homebridge using: `npm install -g homebridge` 2. Install this plugin using: `npm install -g homebridge-tahoma` 3. Update your configuration file. See bellow for a sample. # Configuration Minimal configuration sample: ``` { "bridge": { ... }, "description": "...", "accessories": [], "platforms":[ { "platform": "Tahoma", "name": "My TaHoma Box", "service": "somfy_europe", "user": "user@me.com", "password": "MyPassw0rd", } ] } ``` Configuration parameters: | Parameter | Type | Default | Note | |---------------------------|---------------|-------------------|-----------------------------------------------| | `service` | String | 'somfy_europe' | optional, service name (see above) | | `user` | String | null | mandatory, your service account username (or [gateway PIN / IP address](#configure-local-api)) | | `password` | String | null | mandatory, your service account password (or [API token](#configure-local-api)) | | `pollingPeriod` | Integer | 30 | optional, bridge polling period in seconds | | `refreshPeriod` | Integer | 30 | optional, device states refresh period in minutes | | `exclude` | String[] | [] | optional, protocol, ui, widget or device name to exclude | | `exposeScenarios` | Boolean | false | optional, expose scenarios as HomeKit switches. Could also specify a list of string corresponding to scenarios names to expose | | `devicesConfig` | Object[] | [] | optional list of device specific configuration (see below) | ### Configure Local API Local API service is available on TaHoma and TaHoma switch gateways. **WARNING: Switching to local API will break your HomeKit configuration (automations) as local API device identifiers actually differs.** To use Local API you will have to: 1. Activate `developer mode` ([www.somfy.com](https://www.somfy.com) > My Account > Activate developer mode) 2. Generate API credentials at [https://dev.duboc.pro/homebridge-tahoma](https://dev.duboc.pro/homebridge-tahoma) 3. Configure the plugin service to Local API: `"service":"local"` When using Local API service, please fill `user` with your gateway PIN number or IPv4 address and `password` with the token generated at [step 2](https://dev.duboc.pro/homebridge-tahoma) For more information, browse [https://developer.somfy.com/developer-mode](https://developer.somfy.com/developer-mode) # Specific device configuration This option allows you to apply a specific configuation to device or group of device. One configuration is composed of a `key` attribute containing device name, widget, uiClass, protocol or unique identifier and as many parameter depending of device type. ``` { "key": "Bedroom door", "param1": "value1" ... } ``` | Alarm parameters | Type | Default | Note | |-------------------|-----------|---------------|-----------------------| | `stayZones` | String | 'A' | optional, active zones (A,B,C) in 'Stay' mode | | `nightZones` | String | 'B' | optional, active zones (A,B,C) in 'Night' mode | | `occupancySensor` | Boolean | false | optional, add an occupancy widget linked to the alarm | | WindowCovering parameters | Type | Default | Note | |---------------------------|-----------|---------------|---------------| | `defaultPosition` | Integer | 0 | optional, final position for UpDown rollershutter after any command | | `reverse` | Boolean | false | optional, reverse up/down in case of bad mounting | | `lowSpeed` | Boolean / String | false | optional, use low speed for roller shutter supporting it. If string, specify slot time with format "HH:MM-HH:MM". Low speed will be enabled between them. | | `blindMode` | String | null | optional, change main slider action to orientation. By default, both closure and orientation will be set. When setting ``blindMode: true`` the blinds work in the following way: Opening the blinds or setting them to 100% will fully open them. Closing the blinds or setting them to 0% will fully close them. Setting the blinds to a value between 1% and 99% will first close the blinds and then adjust thier horizontal tilt in a way that 99% means fully horizonal = more light, and 1% means nearly closed = less light. | | `blindsOnRollerShutter` | Boolean | false | optional, when blinds are installed on roller shutter motors allow slats to stay horizontal (opened) at intermediate position | | `movementDuration` | Integer | 0 | optional, duration of a full shutter movement from 'open' to 'close' in seconds. Will be used to approximate shutter intermediate position. (0 = disable feature) | | GarageDoorOpener parameters | Type | Default | Note | |-------------------------------|---------------|---------------|-------------------| | `cyclic` | Boolean | false | optional, restore closed state after `cycleDuration` seconds for stateless devices with cyclic behaviour | | `cycleDuration` | Integer | false | optional, cycle duration (in seconds) for cyclic mode (default: 5 sec) | | `reverse` | Boolean | false | optional, reverse up/down in case of bad mounting | | `pedestrianDuration` | Integer | 0 | optional, duration for pedestrian position for RTS gates | | HeatingSystem parameters | Type | Default | Note | |-------------------------------|---------------|---------------|-------------------| | `derogationDuration` | Integer | 1 | optional, duration (in hours) for derogation orders | | `comfort` | Integer | 19 | optional, comfort temperature used as display for heaters controled by pilot wire | | `eco` | Integer | 17 | optional, comfort temperature used as display for heaters controled by pilot wire | Full configuration example: ``` { "bridge": { ... }, "description": "...", "accessories": [], "platforms":[ { "platform": "Tahoma", "name": "My Tahoma Bridge", "user": "user@me.com", "password": "MyPassw0rd", "service": "somfy_europe", "exclude": ["hue", "rts", "Main door", "Main door", "PositionableHorizontalAwning"], "devicesConfig": [ { "key": "Alarm", "stayZones": "A,C" }, { "key": "Bedroom blind", "blindMode": true }, { "key": "GarageDoor", "reverse": true }, { "key": "UpDownRollerShutter", "defaultPosition": 50 } ] } ] } ``` # Contribute You are welcome to contribute to this plugin development by opening an issue in case of unexpected behaviour or unsupported device. I do not expect any reward concerning this plugin, however, some users ask me for a [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L4X489MG7FUCN) button as sign of contribution. Feel free to use it. ================================================ FILE: config.schema.json ================================================ { "pluginAlias": "Tahoma", "pluginType": "platform", "singular": false, "schema": { "type": "object", "properties": { "name": { "title": "Name", "type": "string", "required": true, "default": "TaHoma" }, "service": { "title": "Service", "type": "string", "default": "somfy_europe", "oneOf": [ { "title": "Local API (TaHoma / Switch)", "enum": [ "local" ] }, { "title": "Somfy Europe (TaHoma / Switch / Connexoon IO)", "enum": [ "somfy_europe" ] }, { "title": "Somfy Australia (Connexoon RTS)", "enum": [ "somfy_australia" ] }, { "title": "Somfy North America", "enum": [ "somfy_north_america" ] }, { "title": "Cozytouch (Atlantic / Thermor / Sauter)", "enum": [ "cozytouch" ] }, { "title": "Energeasy Connect (Rexel)", "enum": [ "rexel" ] }, { "title": "Hi Kumo (Hitachi)", "enum": [ "hi_kumo" ] }, { "title": "Flexom (Bouygues)", "enum": [ "flexom" ] }, { "title": "Flexom (Bouygues)", "enum": [ "flexom" ] } ], "required": true }, "user": { "title": "User", "type": "string", "required": true, "description": "Your username for selected service (email, gateway Pin or IP)" }, "password": { "title": "Password", "type": "string", "required": true, "description": "Your password/token for selected service" }, "pollingPeriod": { "title": "Polling period", "type": "number", "minimum": 10, "placeholder": 60, "description": "Period (in seconds) for fetching device changes made from other controller (with TaHoma app for eg.)" }, "refreshPeriod": { "title": "Refresh period", "type": "number", "minimum": 10, "placeholder": 30, "description": "Period (in minutes) for refreshing device changes made locally (with remote control for eg.)" }, "exposeScenarios": { "title": "Expose scenarios", "type": "boolean", "description": "Expose scenarios as HomeKit switch to trigger them" }, "exclude": { "type": "array", "items": { "type": "string" } }, "devicesConfig": { "type": "array", "items": { "type": "object", "properties": { "key": { "title": "Device name", "type": "string", "required": true, "description": "Device name, widget or uiClass type" }, "blindMode": { "title": "Blind mode", "type": "boolean", "description": "Manage blind orientation with main slider" }, "blindsOnRollerShutter": { "title": "Blinds on roller shutter", "type": "boolean", "description": "Manage blinds installed on roller shutter motors" }, "reverse": { "title": "Reverse", "type": "boolean", "description": "Reverse behaviour for open/close commands" }, "lowSpeed": { "title": "Low speed mode", "type": "boolean", "description": "Low speed for supported roller shutter" }, "defaultPosition": { "title": "Default position", "type": "number", "minimum": 0, "maximum": 100, "description": "Restore specific default position for stateless covering" }, "movementDuration": { "title": "Movement Duration", "type": "integer", "minimum": 0, "description": "Duration from 'opened' to 'closed' position to estimate intermediate positions" }, "cyclic": { "title": "Cyclic", "type": "boolean", "description": "Emulate cyclic door" }, "cycleDuration": { "title": "Cycle Duration", "type": "integer", "description": "Cycle duration if cyclic mode enabled" }, "occupancySensor": { "title": "Occupancy sensor", "type": "boolean", "description": "Expose an occupancy sensor, active when alarm trigered" }, "stayZones": { "title": "Stay Zones", "type": "string", "description": "Zones to activate in Presence mode" }, "nightZones": { "title": "Night Zones", "type": "string", "description": "Zones to activate in Night mode" } } } } } }, "layout": [ "name", "service", "user", "password", { "type": "fieldset", "title": "What", "description": "Select what kind of ressources to expose.", "expandable": true, "expanded": false, "items": [ "exposeScenarios", { "title": "Exclude devices or scenarios", "description": "Exclude devices or scenarios from being exposed", "key": "exclude", "type": "array", "items": { "type": "string", "description": "Device or scenarios name, widget, uiClass or protocol." } } ] }, { "type": "fieldset", "title": "Device specific config", "description": "Apply specific config for some devices or kind of devices.", "expandable": true, "expanded": false, "items": [ { "key": "devicesConfig", "type": "array", "items": [ { "type": "div", "items": [ "devicesConfig[].key", { "title": "Window Covering", "type": "section", "expandable": true, "expanded": false, "items": [ "devicesConfig[].reverse", "devicesConfig[].defaultPosition", "devicesConfig[].blindMode", "devicesConfig[].lowSpeed", "devicesConfig[].blindsOnRollerShutter", "devicesConfig[].movementDuration" ] }, { "title": "Garage Door", "type": "section", "expandable": true, "expanded": false, "items": [ "devicesConfig[].reverse", "devicesConfig[].cyclic", "devicesConfig[].cycleDuration" ] }, { "title": "Alarm", "type": "section", "expandable": true, "expanded": false, "items": [ "devicesConfig[].occupancySensor", "devicesConfig[].stayZones", "devicesConfig[].nightZones" ] } ] } ] } ] }, { "type": "fieldset", "title": "Advanced Settings", "description": "Don't change these, unless you understand what you're doing.", "expandable": true, "expanded": false, "items": [ "pollingPeriod", "refreshPeriod" ] } ] } ================================================ FILE: eslint.config.mjs ================================================ import tsParser from "@typescript-eslint/parser"; import path from "node:path"; import { fileURLToPath } from "node:url"; import js from "@eslint/js"; import { FlatCompat } from "@eslint/eslintrc"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, allConfig: js.configs.all }); export default [{ ignores: ["**/dist"], }, ...compat.extends( "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", ), { languageOptions: { parser: tsParser, ecmaVersion: 2018, sourceType: "module", }, rules: { quotes: ["warn", "single"], indent: ["warn", 4, { SwitchCase: 1, }], semi: ["warn", "always"], "comma-dangle": ["warn", "always-multiline"], "dot-notation": "off", eqeqeq: "warn", curly: ["warn", "all"], "brace-style": ["warn"], "prefer-arrow-callback": ["warn"], "max-len": ["warn", 140], "no-console": ["warn"], "no-non-null-assertion": ["off"], "comma-spacing": ["error"], "no-multi-spaces": ["warn", { ignoreEOLComments: true, }], "lines-between-class-members": ["warn", "always", { exceptAfterSingleLine: true, }], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", }, }]; ================================================ FILE: nodemon.json ================================================ { "watch": [ "src" ], "ext": "ts", "ignore": [], "exec": "tsc && homebridge -I -D", "signal": "SIGTERM", "env": { "NODE_OPTIONS": "--trace-warnings" } } ================================================ FILE: package.json ================================================ { "name": "homebridge-tahoma", "displayName": "Homebridge TaHoma", "version": "2.2.61", "description": "Sample Platform plugin for TaHoma and Cozytouch services (Somfy,Atlantic,Thermor,Sauter): https://github.com/dubocr/homebridge-tahoma", "author": "Romain DUBOC ", "license": "Apache-2.0", "repository": { "type": "git", "url": "https://github.com/dubocr/homebridge-tahoma.git" }, "bugs": { "url": "https://github.com/dubocr/homebridge-tahoma/issues" }, "engines": { "node": ">=12.4.0", "homebridge": ">=1.3.0" }, "main": "dist/index.js", "scripts": { "lint": "eslint src/**.ts", "watch": "npm run build && npm link && nodemon", "clean": "rimraf ./dist", "build": "rimraf ./dist && tsc", "prepublishOnly": "npm run lint && npm run build && npm version patch --m 'Release %s'", "postpublish": "npm run clean" }, "keywords": [ "homebridge-plugin", "tahoma", "cozytouch", "somfy", "connexoon" ], "homepage": "https://github.com/dubocr/homebridge-tahoma#readme", "dependencies": { "moment": "^2.30.1", "overkiz-client": "^1.0.20" }, "devDependencies": { "@types/node": "^22.8.7", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "eslint": "^9.14.0", "homebridge": "^1.8.5", "nodemon": "^3.1.7", "rimraf": "^6.0.1", "ts-node": "^10.9.2", "typescript": "^5.6.3" }, "funding": { "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L4X489MG7FUCN" } } ================================================ FILE: platform.schema.json ================================================ { "plugin_alias": "Tahoma", "schema": { "type": "object", "properties": { "platform": { "title": "Platform", "type": "string", "const": "Tahoma", "readOnly": true }, "name": { "title": "Name", "type": "string", "required": true, "default": "TaHoma", "description": "The name of this platform in HomeKit" }, "service": { "title": "Service", "type": "string", "default": "somfy_europe", "oneOf": [ { "title": "Local API (TaHoma / Switch)", "enum": [ "local" ] }, { "title": "Somfy Europe (TaHoma / Switch / Connexoon IO)", "enum": [ "somfy_europe" ] }, { "title": "Somfy Australia (Connexoon RTS)", "enum": [ "somfy_australia" ] }, { "title": "Somfy North America", "enum": [ "somfy_north_america" ] }, { "title": "Cozytouch (Atlantic / Thermor / Sauter)", "enum": [ "cozytouch" ] }, { "title": "Energeasy Connect (Rexel)", "enum": [ "rexel" ] }, { "title": "Hi Kumo (Hitachi)", "enum": [ "hi_kumo" ] }, { "title": "Flexom (Bouygues)", "enum": [ "flexom" ] } ], "required": true, "description": "Service name" }, "user": { "title": "Username", "type": "string", "description": "Your username for selected service (email, gateway Pin or IP)" }, "password": { "title": "Password", "type": "string", "options": { "hidden": true }, "description": "Your password/token for selected service" }, "exposeScenarios": { "title": "Expose scenarios", "type": "boolean", "description": "Expose scenarios as HomeKit switch to trigger them" }, "exclude": { "type": "array", "items": { "type": "string" }, "description": "List of device or scenario to exclude (should be a name, widget, uiClass or protocol)" } } } } ================================================ FILE: src/CustomCharacteristics.ts ================================================ import { HAP, Characteristic, Perms, Formats, WithUUID } from 'homebridge'; export let CurrentShowerCharacteristic: WithUUID<{ new(): Characteristic }>; export let TargetShowerCharacteristic: WithUUID<{ new(): Characteristic }>; export let MyPositionCharacteristic: WithUUID<{ new(): Characteristic }>; export let ProgCharacteristic: WithUUID<{ new(): Characteristic }>; export let EcoCharacteristic: WithUUID<{ new(): Characteristic }>; export let TotalConsumptionCharacteristic: WithUUID<{ new(): Characteristic }>; export let CurrentConsumptionCharacteristic: WithUUID<{ new(): Characteristic }>; export class CustomCharacteristics { constructor(hap: HAP) { CurrentShowerCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = '10000001-0000-1000-8000-0026BB765291'; constructor() { super('Current Shower', CurrentShowerCharacteristic.UUID, { format: Formats.INT, minValue: 0, maxValue: 8, minStep: 1, perms: [Perms.NOTIFY, Perms.PAIRED_READ], }); this.value = this.getDefaultValue(); } }; TargetShowerCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = '10000002-0000-1000-8000-0026BB765291'; constructor() { super('Target Shower', TargetShowerCharacteristic.UUID, { format: Formats.INT, minValue: 0, maxValue: 8, minStep: 1, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], }); this.value = this.getDefaultValue(); } }; MyPositionCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = '10000003-0000-1000-8000-0026BB765291'; constructor() { super('My', MyPositionCharacteristic.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], }); this.value = this.getDefaultValue(); } }; ProgCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = '10000004-0000-1000-8000-0026BB765291'; constructor() { super('Prog', ProgCharacteristic.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], }); this.value = this.getDefaultValue(); } }; EcoCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = '10000005-0000-1000-8000-0026BB765291'; constructor() { super('Eco', EcoCharacteristic.UUID, { format: Formats.BOOL, perms: [Perms.NOTIFY, Perms.PAIRED_READ, Perms.PAIRED_WRITE], }); this.value = this.getDefaultValue(); } }; TotalConsumptionCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = 'E863F10C-079E-48FF-8F27-9C2605A29F52'; constructor() { super('Total Consumption', TotalConsumptionCharacteristic.UUID, { format: Formats.FLOAT, unit: 'kWh', minValue: 0, maxValue: 1000000, minStep: 0.1, perms: [Perms.NOTIFY, Perms.PAIRED_READ], }); this.value = this.getDefaultValue(); } }; CurrentConsumptionCharacteristic = class extends hap.Characteristic { public static readonly UUID: string = 'E863F10D-079E-48FF-8F27-9C2605A29F52'; constructor() { super('Current Consumption', CurrentConsumptionCharacteristic.UUID, { format: Formats.FLOAT, unit: 'W', minValue: 0, maxValue: 12000, minStep: 0.1, perms: [Perms.NOTIFY, Perms.PAIRED_READ], }); this.value = this.getDefaultValue(); } }; } } ================================================ FILE: src/Mapper.ts ================================================ import { Characteristics, Services } from './Platform'; import { CharacteristicValue, HAPStatus, Logger, PlatformAccessory, Service } from 'homebridge'; import { Device, State, Command, Action, ExecutionState } from 'overkiz-client'; import { Platform } from './Platform'; import { GREY } from './colors'; export default abstract class Mapper { protected log: Logger; private postponeTimer; private debounceTimer; protected stateless = false; //protected config: Record = {}; private executionId; private actionPromise; protected expectedStates: Array = []; constructor( protected readonly platform: Platform, protected readonly accessory: PlatformAccessory, protected readonly device: Device, ) { this.log = this.platform.log; } public build() { const config = Object.assign({}, this.platform.devicesConfig[this.device.definition.uiClass], this.platform.devicesConfig[this.device.definition.widgetName], this.platform.devicesConfig[this.device.label], this.platform.devicesConfig[this.device.uuid], ); this.stateless = this.device.states.length === 0 || (this.expectedStates.length > 0 && !this.expectedStates.some((state) => this.device.hasState(state))); this.applyConfig(config); if (Object.keys(config).length > 0) { delete config.key; if (this.platform.config.debug) { this.log.info(`${GREY} Config: `, JSON.stringify(config)); } else { this.log.debug(' Config: ', JSON.stringify(config)); } } const services = this.registerServices(); const info = this.accessory.getService(Services.AccessoryInformation); if (info) { info.setCharacteristic(Characteristics.Manufacturer, this.device.manufacturer); info.setCharacteristic(Characteristics.Model, this.device.model); info.setCharacteristic(Characteristics.SerialNumber, this.device.address.substring(0, 64)); services.push(info); } this.accessory.services.forEach((service) => { if (!services.find((s) => s.UUID === service.UUID && s.subtype === service.subtype)) { this.accessory.removeService(service); } }); if (!this.stateless) { // Init and register states changes this.onStatesChanged(this.device.states, true); this.device.on('states', states => this.onStatesChanged(states)); // Init and register sensors states changes this.device.sensors.forEach((sensor) => { this.onStatesChanged(sensor.states, true); sensor.on('states', states => this.onStatesChanged(states)); }); } // TODO: instanciate mapper for device sensors // Configure accessory sensors // this.device.sensors.forEach((sensor) => new mapper(platform, accessory, sensor))) } /** * Helper methods */ // eslint-disable-next-line @typescript-eslint/no-unused-vars protected applyConfig(config) { // } protected registerService(type: any, subtype?: string): Service { let service: Service; const name = subtype ? this.translate(subtype) : this.device.label; if (subtype) { service = this.accessory.getServiceById(type, subtype) || this.accessory.addService(type, name, subtype); } else { service = this.accessory.getService(type) || this.accessory.addService(type); } service.setCharacteristic(Characteristics.Name, name); /* service.getCharacteristic(Characteristics.Name) .updateValue(name) .onSet((value) => { this.debug('Will rename ' + name + ' to ' + value); this.platform.client.setDeviceName(this.device.deviceURL, value); }); */ return service; } private translate(value: string) { switch (value) { case 'boost': return 'Boost'; case 'drying': return 'Séchage'; default: return value.charAt(0).toUpperCase() + value.slice(1); } } protected debounce(task, immediate: Array = []) { return async (value: CharacteristicValue) => { if (this.debounceTimer !== null) { clearTimeout(this.debounceTimer); } if (immediate.includes(value)) { await task.bind(this, value)(); } else { this.debounceTimer = setTimeout(async () => { this.debounceTimer = null; task.bind(this, value)().catch(() => null); }, 500); } }; } protected postpone(task, ...args) { if (this.postponeTimer !== null) { clearTimeout(this.postponeTimer); } this.postponeTimer = setTimeout(() => { this.postponeTimer = null; task.bind(this, ...args)(); }, 500); } protected async executeCommands(commands: Command | Array | undefined, standalone = false): Promise { if (commands === undefined || (Array.isArray(commands) && commands.length === 0)) { this.error('No target command for', this.device.label); throw HAPStatus.RESOURCE_DOES_NOT_EXIST; } else if (Array.isArray(commands)) { for (const c of commands) { this.info(c.name + JSON.stringify(c.parameters)); } } else { this.info(commands.name + JSON.stringify(commands.parameters)); commands = [commands]; } const commandName = commands[0].name; const localizedName = this.platform.translate( commands[0].name + (commands[0].parameters.length > 0 ? '.' + commands[0].parameters[0] : ''), ); /* if (!this.isIdle) { this.cancelExecution(); } */ const highPriority = this.device.hasState('io:PriorityLockLevelState') ? true : false; const label = this.device.label + ' - ' + localizedName; if (this.actionPromise) { this.actionPromise.action.addCommands(commands); } else { this.actionPromise = new Promise((resolve, reject) => { setTimeout(async () => { try { this.executionId = await this.platform.executeAction(label, this.actionPromise.action, highPriority, standalone); resolve(this.actionPromise.action); } catch (error: any) { this.error(commandName + ' ' + error.message); reject(HAPStatus.SERVICE_COMMUNICATION_FAILURE); } this.actionPromise = null; }, 100); }); this.actionPromise.action = new Action(this.device.deviceURL, commands); this.actionPromise.action.on('update', (state, event) => { if (state === ExecutionState.FAILED) { this.error(commandName, event.failureType); } else if (state === ExecutionState.COMPLETED) { this.info(commandName, state); } else { this.debug(commandName, state); } }); } return this.actionPromise; } private async delay(duration) { return new Promise(resolve => setTimeout(resolve, duration)); } protected async requestStatesUpdate(defer?: number) { if (defer) { await this.delay(defer * 1000); } await this.platform.client.refreshDeviceStates(this.device.deviceURL); } /** * Logging methods */ protected debug(...args) { if (this.platform.config.debug) { this.platform.log.info(`${GREY}[${this.device.label}]`, ...args); } else { this.platform.log.debug(`[${this.device.label}]`, ...args); } } protected info(...args) { this.platform.log.info(`[${this.device.label}]`, ...args); } protected warn(...args) { this.platform.log.warn(`[${this.device.label}]`, ...args); } protected error(...args) { this.platform.log.error(`[${this.device.label}]`, ...args); } protected registerServices(): Array { if (typeof this.registerMainService === 'function') { try { return [this.registerMainService()]; } catch (error: any) { this.log.warn(error.message); } } else { this.log.warn(this.device.definition.widgetName + ' not supported.'); } return []; } protected onStatesChanged(states: Array, init = false) { states.forEach((state: State) => { if (!init) { this.debug(state.name + ' => ' + state.value); } if (typeof this.onStateChanged === 'function') { this.onStateChanged(state.name, state.value); } }); } // OLD get isIdle() { return !this.platform.client.hasExecution(this.executionId); } async cancelExecution() { await this.platform.client.cancelExecution(this.executionId); } /** * Abstract methods to be implemented */ /** * Build the main device service * @return the main service */ protected abstract registerMainService(): Service; /** * Triggered when device state change * @param name State name * @param value State value */ protected abstract onStateChanged(name: string, value); } ================================================ FILE: src/Platform.ts ================================================ import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { Client, Execution, Action } from 'overkiz-client'; import Mapper from './Mapper'; import SceneMapper from './SceneMapper'; import { CustomCharacteristics } from './CustomCharacteristics'; import { BLUE, GREY, RESET } from './colors'; export let Services: typeof Service; export let Characteristics: typeof Characteristic; const DEFAULT_RETRY_DELAY = 60; /** * HomebridgePlatform * This class is the main constructor for your plugin, this is where you should * parse the user config and discover/register accessories with Homebridge. */ export class Platform implements DynamicPlatformPlugin { // this is used to track restored cached accessories private readonly accessories: PlatformAccessory[] = []; public readonly client: Client; private readonly exclude: Array; private readonly exposeScenarios: boolean | Array; public readonly devicesConfig: Array = []; private translations; private executionPromise; private retryDelay = DEFAULT_RETRY_DELAY; constructor(public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API) { Services = this.api.hap.Service; Characteristics = this.api.hap.Characteristic; new CustomCharacteristics(this.api.hap); this.log.debug('Finished initializing platform:', this.config.name); process.on('unhandledRejection', (error: any) => this.log.error(error)); process.on('uncaughtException', (error: any) => this.log.error(error)); this.exclude = config.exclude || []; this.exclude.push('Pod', 'ConfigurationComponent', 'NetworkComponent', 'ProtocolGateway', 'ConsumptionSensor', 'OnOffHeatingSystem', 'Wifi', 'RemoteController', // AtlanticElectricalTowelDryer bad sensors 'io:LightIOSystemDeviceSensor', 'io:RelativeHumidityIOSystemDeviceSensor', 'WeatherForecastSensor', ); this.exposeScenarios = config.exposeScenarios; config.devicesConfig?.forEach(x => this.devicesConfig[x.key] = x); const logger = Object.assign({}, log, { debug: (...args) => { if (config['debug']) { log.info('\x1b[90m', ...args) } else { log.debug(args.shift(), ...args) } }, }); this.client = new Client(logger, config); // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', () => { log.debug('Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories this.discoverDevices(); if (this.config['service'] !== 'local') { this.loadLocation(); } }); } /** * This function is invoked when homebridge restores cached accessories from disk at startup. * It should be used to setup event handlers for characteristics and update respective values. */ async loadLocation() { let countryCode = 'en'; const location = await this.client.getSetupLocation().catch((error) => this.log.warn('Fail to load lang file:', error)); if (location?.countryCode) { countryCode = location.countryCode.toLowerCase().trim(); } this.translations = await import(`./lang/${countryCode}.json`) .catch(() => import('./lang/en.json')) .then((c) => c.default); } /** * This function is invoked when homebridge restores cached accessories from disk at startup. * It should be used to setup event handlers for characteristics and update respective values. */ async configureAccessory(accessory: PlatformAccessory) { if (!this.accessories.map((a) => a.UUID).includes(accessory.UUID)) { this.accessories.push(accessory); } } /** * This is an example method showing how to register discovered accessories. * Accessories must only be registered once, previously created accessories * must not be registered again to prevent "duplicate UUID" errors. */ async discoverDevices() { try { const uuids = Array(); const devices = await this.client.getDevices(); this.log.debug(devices.length + ' devices discovered'); // loop over the discovered devices and register each one if it has not already been registered for (const device of devices) { if ( this.exclude.includes(device.definition.uiClass) || this.exclude.includes(device.definition.widgetName) || this.exclude.includes(device.controllableName) || this.exclude.includes(device.label) || this.exclude.includes(device.protocol) ) { continue; } // see if an accessory with the same uuid has already been registered and restored from // the cached devices we stored in the `configureAccessory` method above let accessory = this.accessories.find(accessory => accessory.UUID === device.uuid); if (accessory) { // the accessory already exists //this.log.info('Updating accessory:', accessory.displayName); /* const newaccessory = new this.api.platformAccessory(device.label, device.uuid); newaccessory.context.device = device; await this.configureAccessory(newaccessory); const services = newaccessory.services.map((service) => service.UUID); accessory.services .filter((service) => !services.includes(service.UUID)) .forEach((services) => accessory?.removeService(services)); this.api.updatePlatformAccessories([accessory]); */ } else { // the accessory does not yet exist, so we need to create it this.log.info('Create accessory:', device.label); accessory = new this.api.platformAccessory(device.label, device.uuid); //accessory.context.device = device; await this.configureAccessory(accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } this.log.info(`Configure device ${BLUE}${accessory.displayName}${RESET}`); this.log.info(`${GREY} ${device.definition.uiClass} > ${device.definition.widgetName}`); const mapper = await import(`./mappers/${device.definition.uiClass}/${device.definition.widgetName}/${device.uniqueName}`) .catch(() => import(`./mappers/${device.definition.uiClass}/${device.definition.widgetName}`)) .catch(() => import(`./mappers/${device.definition.uiClass}`)) .then((c) => c.default) .catch(() => Mapper); new mapper(this, accessory, device).build(); uuids.push(device.uuid); } if (this.exposeScenarios) { const actionGroups = await this.client.getActionGroups(); for (const actionGroup of actionGroups) { if (this.exclude.includes(actionGroup.label) || actionGroup.label.startsWith('internal:') || actionGroup.label === '') { continue; } let accessory = this.accessories.find(accessory => accessory.UUID === actionGroup.oid); if (!accessory) { // the accessory does not yet exist, so we need to create it this.log.info('Create accessory', actionGroup.label); accessory = new this.api.platformAccessory(actionGroup.label, actionGroup.oid); await this.configureAccessory(accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } this.log.info('Map scene', accessory.displayName); new SceneMapper(this, accessory, actionGroup); uuids.push(actionGroup.oid); } } const deleted = this.accessories.filter((accessory) => !uuids.includes(accessory.UUID)); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, deleted); this.retryDelay = DEFAULT_RETRY_DELAY; } catch (error: any) { this.log.error(error); this.log.error('Retry in ' + this.retryDelay + ' sec...'); setTimeout(this.discoverDevices.bind(this), this.retryDelay * 1000); this.retryDelay *= 2; } } /* action: The action to execute */ public executeAction(label: string, action: Action, highPriority = false, standalone = false) { if (standalone) { // Run action in standalone execution return this.client.execute(highPriority ? 'apply/highPriority' : 'apply', new Execution(label + ' - HomeKit', action)); } else { if (this.executionPromise) { this.executionPromise.execution.addAction(action); this.executionPromise.execution.label = 'Execute scene (' + this.executionPromise.execution.actions.length + ' devices) - HomeKit'; } else { this.executionPromise = new Promise((resolve, reject) => { setTimeout(() => { this.client.execute(highPriority ? 'apply/highPriority' : 'apply', this.executionPromise.execution) .then(resolve) .catch(reject); this.executionPromise = null; }, 100); }); this.executionPromise.execution = new Execution(label + ' - HomeKit', action); } return this.executionPromise; } } /** * Translate * @param path * @returns string */ public translate(label: string): string | null { const path = label.split('.'); let translation = this.translations; for (const key of path) { if (typeof translation === 'object' && key in translation) { translation = translation[key]; } else if (typeof translation === 'string') { if (translation.includes(':param')) { translation = translation.replace(':param', key); } return translation; } } return label; } } ================================================ FILE: src/SceneMapper.ts ================================================ import { Characteristics, Services } from './Platform'; import { Characteristic, Logger, PlatformAccessory, Service } from 'homebridge'; import { ExecutionState, ActionGroup, Execution } from 'overkiz-client'; import { Platform } from './Platform'; export default class Mapper { protected log: Logger; protected services: Array = []; protected on: Characteristic | undefined; private lastExecId; constructor( protected readonly platform: Platform, protected readonly accessory: PlatformAccessory, protected readonly action: ActionGroup, ) { this.log = platform.log; const service = this.accessory.getService(Services.Switch) || this.accessory.addService(Services.Switch); this.on = service.getCharacteristic(Characteristics.On); this.on.onSet(this.setOn.bind(this)); this.on.updateValue(0); } private get isInProgress() { return this.platform.client.hasExecution(this.lastExecId); } protected async setOn(value) { if (value) { const execution = new Execution(''); this.lastExecId = await this.platform.client.execute(this.action.oid, execution); execution.on('update', (state, event) => { switch (state) { case ExecutionState.COMPLETED: case ExecutionState.FAILED: this.log.info('[Scene] ' + this.action.label + ' ' + (state === ExecutionState.FAILED ? event.failureType : state)); this.on?.updateValue(0); break; } }); } else if (this.isInProgress) { await this.platform.client.cancelExecution(this.lastExecId); } } } ================================================ FILE: src/colors.ts ================================================ export const RESET = '\x1b[0m'; export const BRIGHT = '\x1b[1m'; export const DIM = '\x1b[2m'; export const UNDERSCORE = '\x1b[4m'; export const BLINK = '\x1b[5m'; export const REVERSE = '\x1b[7m'; export const HIDDEN = '\x1b[8m'; export const BLACK = '\x1b[30m'; export const RED = '\x1b[31m'; export const GREEN = '\x1b[32m'; export const YELLOW = '\x1b[33m'; export const BLUE = '\x1b[34m'; export const MAGENTA = '\x1b[35m'; export const CYAN = '\x1b[36m'; export const LIGHT_GREY = '\x1b[37m'; export const GREY = '\x1b[90m'; export const WHITE = '\x1b[97m'; ================================================ FILE: src/index.ts ================================================ import { API } from 'homebridge'; import { PLATFORM_NAME } from './settings'; import { Platform } from './Platform'; /** * This method registers the platform with Homebridge */ export = (api: API) => { api.registerPlatform(PLATFORM_NAME, Platform); } ================================================ FILE: src/lang/en.json ================================================ { "others": ":param other(s)", "setClosure": "Close :param%", "setHeatingLevel": { "comfort": "Comfort mode", "eco": "Eco mode", "frostprotection": "Frost protection mode", "off": "Stop" }, "open": "Open", "close": "Close", "setPedestrianPosition": "Pedestrian position", "partialPosition": "Partial position" } ================================================ FILE: src/lang/fr.json ================================================ { "others": ":param autre(s)", "setClosure": "Fermeture :param%", "setHeatingLevel": { "comfort": "Mode confort", "eco": "Mode eco", "frostprotection": "Mode hors gel", "off": "Arrêt" }, "open": "Ouverture", "close": "Fermeture", "setPedestrianPosition": "Ouverture piéton", "partialPosition": "Ouverture partielle" } ================================================ FILE: src/mappers/AdjustableSlatsRollerShutter.ts ================================================ import { Command } from 'overkiz-client'; import VenetianBlind from './VenetianBlind'; export default class AdjustableSlatsRollerShutter extends VenetianBlind { protected getTargetCommands(value) { if(this.blindMode) { if(value === 100) { return new Command('setClosure', 0); } else { return new Command('setClosureOrOrientation', [100, this.reversedValue(value)]); } } else { return new Command('setClosureOrOrientation', [ this.reversedValue(value), this.angleToOrientation(this.targetAngle?.value), ]); } } protected getTargetAngleCommands(value) { return new Command('setClosureOrOrientation', [ this.reversedValue(this.targetPosition?.value), this.angleToOrientation(value), ]); } protected onStateChanged(name, value) { super.onStateChanged(name, value); switch(name) { case 'core:ClosureOrRockerPositionState': this.currentPosition?.updateValue(this.reversedValue(value)); if(!this.device.hasState('core:TargetClosureState')) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; default: break; } } } ================================================ FILE: src/mappers/AirSensor/CO2Sensor.ts ================================================ import { Characteristics } from '../../Platform'; import { Characteristic } from 'homebridge'; import AirSensor from '../AirSensor'; export default class RelativeHumiditySensor extends AirSensor { protected co2: Characteristic | undefined; protected registerMainService() { const service = super.registerMainService(); service.addOptionalCharacteristic(Characteristics.CarbonDioxideLevel); this.co2 = service.getCharacteristic(Characteristics.CarbonDioxideLevel); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:CO2ConcentrationState': this.co2?.updateValue(value); this.quality?.updateValue(this.co2ToQuality(value)); break; } } private co2ToQuality(value) { if (value < 350) { return Characteristics.AirQuality.EXCELLENT; } else if (value < 1000) { return Characteristics.AirQuality.GOOD; } else if (value < 2000) { return Characteristics.AirQuality.FAIR; } else if (value < 5000) { return Characteristics.AirQuality.INFERIOR; } else { return Characteristics.AirQuality.POOR; } } } ================================================ FILE: src/mappers/AirSensor/RelativeHumiditySensor.ts ================================================ import HumiditySensor from '../HumiditySensor'; export default class RelativeHumiditySensor extends HumiditySensor { } ================================================ FILE: src/mappers/AirSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, Service } from 'homebridge'; import Mapper from '../Mapper'; export default class AirSensor extends Mapper { protected quality: Characteristic | undefined; protected registerMainService(): Service { const service = this.registerService(Services.AirQualitySensor); this.quality = service.getCharacteristic(Characteristics.AirQuality); return service; } protected onStateChanged(name: string, value) { switch(name) { default: this.quality?.updateValue(value); } } } ================================================ FILE: src/mappers/Alarm/MyFoxAlarmController.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import Alarm from '../Alarm'; export default class MyFoxAlarmController extends Alarm { protected getTargetCommands(value): Command | Array { switch(value) { default: case Characteristics.SecuritySystemTargetState.STAY_ARM: return []; case Characteristics.SecuritySystemTargetState.NIGHT_ARM: return new Command('partial'); case Characteristics.SecuritySystemTargetState.AWAY_ARM: return new Command('arm'); case Characteristics.SecuritySystemTargetState.DISARM: return new Command('disarm'); } } protected onStateChanged(name: string, value) { switch(name) { case 'myfox:AlarmStatusState': switch(value) { default: case 'disarmed': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.DISARMED); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.DISARM); break; case 'armed': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.AWAY_ARM); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.AWAY_ARM); break; case 'partial': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.NIGHT_ARM); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.NIGHT_ARM); break; } break; } } } ================================================ FILE: src/mappers/Alarm/TSKAlarmController.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import Alarm from '../Alarm'; export default class TSKAlarmController extends Alarm { protected getTargetCommands(value): Command | Array { switch(value) { default: case Characteristics.SecuritySystemTargetState.STAY_ARM: return new Command('alarmPartial1'); case Characteristics.SecuritySystemTargetState.NIGHT_ARM: return new Command('alarmPartial2'); case Characteristics.SecuritySystemTargetState.AWAY_ARM: return new Command('alarmOn'); case Characteristics.SecuritySystemTargetState.DISARM: return new Command('alarmOff'); } } protected onStateChanged(name: string, value) { switch(name) { case 'internal:CurrentAlarmModeState': switch(value) { default: case 'off': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.DISARMED); break; case 'partial1': case 'zone1': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.STAY_ARM); break; case 'total': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.AWAY_ARM); break; case 'partial2': case 'zone2': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.NIGHT_ARM); break; } break; case 'internal:TargetAlarmModeState': switch(value) { default: case 'off': this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.DISARM); break; case 'partial1': case 'zone1': this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.STAY_ARM); break; case 'total': this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.AWAY_ARM); break; case 'partial2': case 'zone2': this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.NIGHT_ARM); break; } break; } } } ================================================ FILE: src/mappers/Alarm.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, CharacteristicSetCallback } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class Alarm extends Mapper { protected currentState: Characteristic | undefined; protected targetState: Characteristic | undefined; protected stayZones: unknown | undefined; protected nightZones: unknown | undefined; protected occupancySensor: unknown | undefined; protected applyConfig(config) { this.stayZones = config.stayZones || 'A'; this.nightZones = config.nightZones || 'B'; this.occupancySensor = config.occupancySensor || false; } protected registerMainService() { const service = this.registerService(Services.SecuritySystem); this.currentState = service.getCharacteristic(Characteristics.SecuritySystemCurrentState); this.targetState = service.getCharacteristic(Characteristics.SecuritySystemTargetState); this.targetState.onSet(this.setTargetState.bind(this)); return service; } protected getTargetCommands(value): Command | Array { switch (value) { default: case Characteristics.SecuritySystemTargetState.STAY_ARM: return new Command('alarmZoneOn', [this.stayZones]); case Characteristics.SecuritySystemTargetState.NIGHT_ARM: return new Command('alarmZoneOn', [this.nightZones]); case Characteristics.SecuritySystemTargetState.AWAY_ARM: return new Command('alarmOn'); case Characteristics.SecuritySystemTargetState.DISARM: return new Command('alarmOff'); } } async setTargetState(value) { const action = await this.executeCommands(this.getTargetCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: if (this.stateless) { this.currentState?.updateValue(value); } break; case ExecutionState.FAILED: if (this.currentState && this.currentState.value !== Characteristics.SecuritySystemCurrentState.ALARM_TRIGGERED) { this.targetState?.updateValue(this.currentState.value); } break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'core:ActiveZonesState': switch (value) { default: case '': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.DISARMED); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.DISARM); break; case this.stayZones: this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.STAY_ARM); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.STAY_ARM); break; case 'A,B,C': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.AWAY_ARM); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.AWAY_ARM); break; case this.nightZones: this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.NIGHT_ARM); this.targetState?.updateValue(Characteristics.SecuritySystemTargetState.NIGHT_ARM); break; case 'triggered': this.currentState?.updateValue(Characteristics.SecuritySystemCurrentState.ALARM_TRIGGERED); break; } break; } } } ================================================ FILE: src/mappers/Awning/PositionableHorizontalAwningUno.ts ================================================ import Awning from '../Awning'; export default class PositionableHorizontalAwningUno extends Awning { protected onStateChanged(name: string, value) { switch(name) { case 'core:TargetClosureState': if(this.isIdle) { this.targetPosition?.updateValue(this.reversedValue(value)); } this.currentPosition?.updateValue(this.reversedValue(value)); break; } } } ================================================ FILE: src/mappers/Awning.ts ================================================ import RollerShutter from './RollerShutter'; import { Command } from 'overkiz-client'; export default class Awning extends RollerShutter { /** * Triggered when Homekit try to modify the Characteristic.TargetPosition * HomeKit '0' (Close) => 0% Deployment * HomeKit '100' (Open) => 100% Deployment **/ protected getTargetCommands(value) { if(this.stateless) { if(value === 100) { return new Command(this.reverse ? 'undeploy' : 'deploy'); } else if(value === 0) { return new Command(this.reverse ? 'deploy' : 'undeploy'); } else { if(this.movementDuration > 0) { const delta = value - Number(this.currentPosition!.value); if(this.reverse) { return new Command(delta > 0 ? 'undeploy' : 'deploy'); } else { return new Command(delta > 0 ? 'deploy' : 'undeploy'); } } else { return new Command('my'); } } } else { return new Command('setDeployment', this.reversedValue(value)); } } protected reversedValue(value) { return this.reverse ? (100-value) : value; } protected onStateChanged(name: string, value) { switch(name) { case 'core:DeploymentState': this.currentPosition?.updateValue(this.reversedValue(value)); if(!this.device.hasState('core:TargetClosureState')) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; case 'core:ClosureState': this.currentPosition?.updateValue(this.reversedValue(value)); if(!this.device.hasState('core:TargetClosureState')) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; case 'core:TargetClosureState': this.targetPosition?.updateValue(this.reversedValue(value)); if(!this.device.hasState('core:ClosureState')) { this.currentPosition?.updateValue(this.reversedValue(value)); } break; } } } ================================================ FILE: src/mappers/ConsumptionSensor.ts ================================================ import { Service } from 'homebridge'; import Mapper from '../Mapper'; export default class ConsumptionSensor extends Mapper { protected registerMainService(): Service { throw new Error('ConsumptionSensor not implemented.'); } protected onStateChanged(name: string, value: any) { this.info(name + ' => ' + value); } } ================================================ FILE: src/mappers/ContactSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class ContactSensor extends Mapper { protected state: Characteristic | undefined; protected fault: Characteristic | undefined; protected battery: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.ContactSensor); this.state = service.getCharacteristic(Characteristics.ContactSensorState); if (this.device.hasState('core:SensorDefectState')) { this.fault = service.getCharacteristic(Characteristics.StatusFault); this.battery = service.getCharacteristic(Characteristics.StatusLowBattery); } return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:ContactState': switch (value) { case 'closed': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_DETECTED); break; case 'tilt': case 'open': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_NOT_DETECTED); break; } break; case 'core:SensorDefectState': switch (value) { case 'lowBattery': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_LOW); break; case 'maintenanceRequired': case 'dead': this.fault?.updateValue(Characteristics.StatusFault.GENERAL_FAULT); break; case 'noDefect': this.fault?.updateValue(Characteristics.StatusFault.NO_FAULT); this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_NORMAL); break; } break; } } } ================================================ FILE: src/mappers/Curtain.ts ================================================ import RollerShutter from './RollerShutter'; export default class Curtain extends RollerShutter { } ================================================ FILE: src/mappers/DoorLock.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class VentilationSystem extends Mapper { protected currentState: Characteristic | undefined; protected targetState: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.LockMechanism); this.currentState = service.getCharacteristic(Characteristics.LockCurrentState); this.targetState = service.getCharacteristic(Characteristics.LockTargetState); this.targetState?.onSet(this.setTargetState.bind(this)); return service; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.LockTargetState.SECURED: return new Command('setLockedUnlocked', 'locked'); case Characteristics.LockTargetState.UNSECURED: default: return new Command('setLockedUnlocked', 'unlocked'); } } protected async setTargetState(value) { const action = await this.executeCommands(this.getTargetStateCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.COMPLETED: if (this.stateless) { this.currentState?.updateValue(value); } break; case ExecutionState.FAILED: if (this.currentState) { this.targetState?.updateValue(this.currentState.value); } break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'core:LockedUnlockedState': switch (value) { case 'locked': this.currentState?.updateValue(Characteristics.LockCurrentState.SECURED); break; default: this.currentState?.updateValue(Characteristics.LockCurrentState.UNSECURED); break; } if (this.isIdle && this.currentState) { this.targetState?.updateValue(this.currentState.value); } break; } } } ================================================ FILE: src/mappers/ElectricitySensor/CumulativeElectricPowerConsumptionSensor.ts ================================================ import { Services } from '../../Platform'; import { Characteristic } from 'homebridge'; import { CurrentConsumptionCharacteristic, TotalConsumptionCharacteristic } from '../../CustomCharacteristics'; import ElectricitySensor from '../ElectricitySensor'; export default class CumulativeElectricPowerConsumptionSensor extends ElectricitySensor { protected consumption: Characteristic | undefined; protected power: Characteristic | undefined; protected registerMainService() { const service = super.registerMainService(); service.addOptionalCharacteristic(TotalConsumptionCharacteristic); this.consumption = service.getCharacteristic(TotalConsumptionCharacteristic); service.addOptionalCharacteristic(CurrentConsumptionCharacteristic); this.power = service.getCharacteristic(CurrentConsumptionCharacteristic); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:ElectricEnergyConsumptionState': this.consumption?.updateValue(value / 1000); break; case 'core:ElectricPowerConsumptionState': this.power?.updateValue(value); break; } } } ================================================ FILE: src/mappers/ElectricitySensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Service } from 'homebridge'; import Mapper from '../Mapper'; export default class ElectricitySensor extends Mapper { protected registerMainService(): Service { const service = this.registerService(Services.AccessoryMetrics); return service; } protected onStateChanged(name: string, value: any) { this.info(name + ' => ' + value); } } ================================================ FILE: src/mappers/EvoHome/DHWSetPoint.ts ================================================ import HeatingSystem from '../HeatingSystem'; import TemperatureSensor from '../TemperatureSensor'; export default class DHWSetPoint extends TemperatureSensor { } ================================================ FILE: src/mappers/EvoHome/EvoHomeController.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class EvoHomeController extends HeatingSystem { protected registerMainService() { const service = super.registerMainService(); this.targetState?.setProps({ validValues: [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ] }); return service; } protected getTargetStateCommands(value): Command | Array | undefined { switch(value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('setOperatingMode', 'auto'); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setOperatingMode', 'off'); } } } ================================================ FILE: src/mappers/EvoHome/HeatingSetPoint.ts ================================================ import { Characteristics } from '../../Platform'; import HeatingSystem from '../HeatingSystem'; export default class HeatingSetPoint extends HeatingSystem { protected registerMainService() { const service = super.registerMainService(); this.targetState?.setProps({ validValues: [ Characteristics.TargetHeatingCoolingState.AUTO, ] }); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); return service; } } ================================================ FILE: src/mappers/ExteriorHeatingSystem/DimmerExteriorHeating.ts ================================================ import { Characteristic, Service } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import { Characteristics, Services } from '../../Platform'; import ExteriorHeatingSystem from '../ExteriorHeatingSystem'; export default class DimmerExteriorHeating extends ExteriorHeatingSystem { protected level: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.Lightbulb); this.on = service.getCharacteristic(Characteristics.On); this.on.onSet(this.setOn.bind(this)); this.level = service.getCharacteristic(Characteristics.Brightness); this.level.onSet(this.debounce(this.setBrightness, [0, 100])); return service; } protected async setBrightness(value) { const action = await this.executeCommands(new Command('setLevel', 100 - value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected onStateChanged(name: string, value): boolean { value = 100 - value; switch (name) { case 'core:LevelState': this.level?.updateValue(value); this.on?.updateValue(value === 0 ? 0 : 1); break; } return false; } } ================================================ FILE: src/mappers/ExteriorHeatingSystem.ts ================================================ import { Command } from 'overkiz-client'; import HeatingSystem from './HeatingSystem'; export default class ExteriorHeatingSystem extends HeatingSystem { protected registerMainService() { return this.registerSwitchService(); } protected getOnCommands(value): Command | Array { return new Command(value ? 'on' : 'off'); } } ================================================ FILE: src/mappers/ExteriorScreen.ts ================================================ import RollerShutter from './RollerShutter'; export default class ExteriorScreen extends RollerShutter { } ================================================ FILE: src/mappers/ExteriorVenetianBlind.ts ================================================ import VenetianBlind from './VenetianBlind'; export default class ExteriorVenetianBlind extends VenetianBlind { } ================================================ FILE: src/mappers/GarageDoor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class GarageDoor extends Mapper { protected expectedStates = ['core:OpenClosedPartialState', 'core:OpenClosedUnknownState', 'core:OpenClosedState']; protected currentState: Characteristic | undefined; protected targetState: Characteristic | undefined; protected cyclic; protected cycleDuration; protected applyConfig(config) { this.cyclic = config['cyclic'] || false; this.cycleDuration = (config['cycleDuration'] || 5) * 1000; } protected registerMainService() { const service = this.registerService(Services.GarageDoorOpener); this.currentState = service.getCharacteristic(Characteristics.CurrentDoorState); this.targetState = service.getCharacteristic(Characteristics.TargetDoorState); this.targetState.onSet(this.setTargetState.bind(this)); this.cyclic = this.cyclic || this.device.hasCommand('cycle'); if (this.stateless) { this.currentState.updateValue(Characteristics.CurrentDoorState.CLOSED); this.targetState.updateValue(Characteristics.TargetDoorState.CLOSED); } return service; } protected getTargetCommands(value) { if (this.device.hasCommand('cycle')) { return new Command('cycle'); } else { return new Command(value ? 'close' : 'open'); } } protected async setTargetState(value) { const previousTarget = this.targetState?.value; const action = await this.executeCommands(this.getTargetCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.IN_PROGRESS: if (value === Characteristics.TargetDoorState.OPEN) { this.currentState?.updateValue(Characteristics.CurrentDoorState.OPENING); } else { this.currentState?.updateValue(Characteristics.CurrentDoorState.CLOSING); } break; case ExecutionState.COMPLETED: if (this.stateless) { this.onStateChanged( this.expectedStates[0], value === Characteristics.TargetDoorState.CLOSED ? 'closed' : 'open', ); if (this.cyclic) { setTimeout(() => { this.onStateChanged(this.expectedStates[0], 'closed'); }, this.cycleDuration); } } else if (this.cyclic) { this.requestStatesUpdate(60).catch((e) => this.warn(e)); } break; case ExecutionState.FAILED: if (previousTarget) { this.targetState?.updateValue(previousTarget); } break; } }); } protected onStateChanged(name: string, value) { let targetState; if (this.expectedStates.includes(name)) { switch (value) { case 'open': this.currentState?.updateValue(Characteristics.CurrentDoorState.OPEN); targetState = Characteristics.TargetDoorState.OPEN; break; case 'partial': this.currentState?.updateValue(Characteristics.CurrentDoorState.STOPPED); targetState = Characteristics.TargetDoorState.OPEN; break; case 'closed': this.currentState?.updateValue(Characteristics.CurrentDoorState.CLOSED); targetState = Characteristics.TargetDoorState.CLOSED; break; case 'unknown': break; } } if (this.targetState && targetState !== undefined) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/Gate.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, Service } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import GarageDoor from './GarageDoor'; export default class Gate extends GarageDoor { protected expectedStates = ['core:OpenClosedPedestrianState']; protected currentPedestrian: Characteristic | undefined; protected targetPedestrian: Characteristic | undefined; protected on: Characteristic | undefined; protected pedestrianCommand; protected pedestrianDuration; protected cancelTimeout; protected applyConfig(config) { super.applyConfig(config); this.pedestrianDuration = (config['pedestrianDuration'] || 0) * 1000; this.pedestrianCommand = ['setPedestrianPosition', 'partialPosition', 'my'] .find((command: string) => this.device.hasCommand(command)); } protected registerServices() { const services = super.registerServices(); if (this.pedestrianCommand || this.pedestrianDuration) { const pedestrian = this.registerLockService('pedestrian'); services.push(pedestrian); } if (this.stateless) { this.currentPedestrian?.updateValue(Characteristics.LockCurrentState.SECURED); this.targetPedestrian?.updateValue(Characteristics.LockCurrentState.SECURED); } return services; } protected registerSwitchService(subtype?: string): Service { const service = this.registerService(Services.Switch, subtype); this.on = service.getCharacteristic(Characteristics.On); this.on?.onSet(this.setOn.bind(this)); return service; } protected registerLockService(subtype?: string): Service { const service = this.registerService(Services.LockMechanism, subtype); this.currentPedestrian = service.getCharacteristic(Characteristics.LockCurrentState); this.targetPedestrian = service.getCharacteristic(Characteristics.LockTargetState); this.targetPedestrian?.onSet(this.setLock.bind(this)); return service; } protected getLockCommands(value): Command | Array { if (value === Characteristics.LockTargetState.UNSECURED && this.pedestrianCommand) { return new Command(this.pedestrianCommand); } else { return new Command(value === Characteristics.LockTargetState.UNSECURED ? 'open' : 'close'); } } protected async setLock(value) { if (this.cancelTimeout !== null) { clearTimeout(this.cancelTimeout); } const action = await this.executeCommands(this.getLockCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.IN_PROGRESS: if (this.stateless && !this.pedestrianCommand && this.pedestrianDuration) { this.info('Will stop movement in ' + this.pedestrianDuration + ' millisec'); this.cancelTimeout = setTimeout(() => { this.cancelTimeout = null; if (this.isIdle) { this.executeCommands(new Command('stop'), true); } else { this.cancelExecution().catch(this.error.bind(this)); } }, this.pedestrianDuration); } break; case ExecutionState.COMPLETED: if (this.stateless) { this.onStateChanged( 'core:OpenClosedPedestrianState', value === Characteristics.LockTargetState.SECURED ? 'closed' : 'pedestrian', ); if (this.cyclic) { setTimeout(() => { this.onStateChanged('core:OpenClosedPedestrianState', 'closed'); }, this.cycleDuration); } } break; } }); } protected getOnCommands(value): Command | Array { if (value && this.pedestrianCommand) { return new Command(this.pedestrianCommand); } else { return new Command(value ? 'open' : 'close'); } } protected async setOn(value) { const action = await this.executeCommands(this.getOnCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.FAILED: this.on?.updateValue(!value); break; } }); } protected onStateChanged(name: string, value) { let targetState; let targetPedestrian; if (this.expectedStates.includes(name)) { switch (value) { case 'unknown': case 'open': this.on?.updateValue(false); this.currentState?.updateValue(Characteristics.CurrentDoorState.OPEN); targetState = Characteristics.TargetDoorState.OPEN; this.currentPedestrian?.updateValue(Characteristics.LockCurrentState.UNKNOWN); targetPedestrian = Characteristics.LockTargetState.UNSECURED; break; case 'pedestrian': case 'partial': this.on?.updateValue(true); this.currentState?.updateValue(Characteristics.CurrentDoorState.STOPPED); targetState = Characteristics.TargetDoorState.OPEN; this.currentPedestrian?.updateValue(Characteristics.LockCurrentState.UNSECURED); targetPedestrian = Characteristics.LockTargetState.UNSECURED; break; case 'closed': this.on?.updateValue(false); this.currentState?.updateValue(Characteristics.CurrentDoorState.CLOSED); targetState = Characteristics.TargetDoorState.CLOSED; this.currentPedestrian?.updateValue(Characteristics.LockCurrentState.SECURED); targetPedestrian = Characteristics.LockTargetState.SECURED; break; } } if (this.targetState && targetState !== undefined) { this.targetState.updateValue(targetState); } if (this.targetPedestrian && targetPedestrian !== undefined) { this.targetPedestrian.updateValue(targetPedestrian); } } } ================================================ FILE: src/mappers/Generic/CyclicGeneric.ts ================================================ import GarageDoor from '../GarageDoor'; export default class CyclicGeneric extends GarageDoor { } ================================================ FILE: src/mappers/Generic/DimmerOnOff.ts ================================================ import Light from '../Light'; export default class DimmerOnOff extends Light { } ================================================ FILE: src/mappers/Generic/RTSGeneric.ts ================================================ import { Command } from 'overkiz-client'; import RollerShutter from '../RollerShutter'; export default class RTSGeneric extends RollerShutter { protected getTargetCommands(value) { if(value === 0) { return new Command('down'); } else { return new Command('up'); } } } ================================================ FILE: src/mappers/Generic/RTSGeneric4T.ts ================================================ import GarageDoor from '../GarageDoor'; export default class RTSGeneric extends GarageDoor { } ================================================ FILE: src/mappers/HeatingSystem/AtlanticElectricalHeater.ts ================================================ import { Characteristics } from '../../Platform'; import { Perms } from 'homebridge'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; const FROSTPROTECTION_TEMP = 7; export default class AtlanticElectricalHeater extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['eco']; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService() { const service = super.registerMainService(); this.targetTemperature?.setProps({ minValue: FROSTPROTECTION_TEMP, maxValue: this.comfortTemperature, minStep: 1, perms: [Perms.PAIRED_READ, Perms.EVENTS, Perms.PAIRED_WRITE], }); return service; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('setHeatingLevel', this?.eco?.value ? 'eco' : 'comfort'); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setHeatingLevel', 'comfort'); case Characteristics.TargetHeatingCoolingState.COOL: return new Command('setHeatingLevel', 'eco'); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setHeatingLevel', 'off'); } return []; } protected async setTargetTemperature(value) { if (this.targetState?.value === Characteristics.CurrentHeatingCoolingState.OFF) { return; } const frostEcoLimit = FROSTPROTECTION_TEMP + (this.ecoTemperature - FROSTPROTECTION_TEMP) / 2; const ecoComfortLimit = this.ecoTemperature + (this.comfortTemperature - this.ecoTemperature) / 2; let newValue = value; if (value <= frostEcoLimit) { newValue = FROSTPROTECTION_TEMP; } else if (value > frostEcoLimit && value <= this.ecoTemperature) { newValue = this.ecoTemperature; } else if (value > this.ecoTemperature && value <= ecoComfortLimit) { newValue = this.comfortTemperature; } if (newValue !== value) { this.targetTemperature?.updateValue(newValue); } await this.executeCommands(this.getTargetTemperatureCommands(newValue)); } protected getTargetTemperatureCommands(value): Command | Array | undefined { if (value === FROSTPROTECTION_TEMP) { this.targetState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); return new Command('setHeatingLevel', 'frostprotection'); } else if (value === this.ecoTemperature) { this.targetState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); return new Command('setHeatingLevel', 'eco'); } else if (value === this.comfortTemperature) { this.targetState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); return new Command('setHeatingLevel', 'comfort'); } } protected getProgCommands(): Command | Array | undefined { return new Command('setHeatingLevel', this?.eco?.value ? 'eco' : 'comfort'); } protected onStateChanged(name, value) { let targetState; switch (name) { case 'io:TargetHeatingLevelState': //targetState = Characteristics.TargetHeatingCoolingState.AUTO; switch (value) { case 'off': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); break; case 'frostprotection': targetState = Characteristics.TargetHeatingCoolingState.HEAT; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.currentTemperature?.updateValue(FROSTPROTECTION_TEMP); this.targetTemperature?.updateValue(FROSTPROTECTION_TEMP); break; case 'comfort': case 'comfort-1': case 'comfort-2': targetState = Characteristics.TargetHeatingCoolingState.HEAT; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.eco?.updateValue(false); this.currentTemperature?.updateValue(this.comfortTemperature); this.targetTemperature?.updateValue(this.comfortTemperature); break; case 'eco': targetState = Characteristics.TargetHeatingCoolingState.COOL; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.eco?.updateValue(true); this.currentTemperature?.updateValue(this.ecoTemperature); this.targetTemperature?.updateValue(this.ecoTemperature); break; } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } break; default: super.onStateChanged(name, value); break; } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticElectricalHeaterWithAdjustableTemperatureSetpoint extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['prog']; protected MIN_TEMP = 7; protected MAX_TEMP = 28; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService() { if (this.device.get('io:NativeFunctionalLevelState') === 'Top') { this.TARGET_MODES.push(Characteristics.TargetHeatingCoolingState.HEAT); } return super.registerMainService(); } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: if (this.device.get('io:NativeFunctionalLevelState') === 'Top') { return new Command('setOperatingMode', 'auto'); } else { return new Command('setOperatingMode', this.prog?.value ? 'internal' : 'basic'); } case Characteristics.TargetHeatingCoolingState.HEAT: if (this.device.get('io:NativeFunctionalLevelState') === 'Top') { return new Command('setOperatingMode', this.prog?.value ? 'internal' : 'basic'); } break; case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setOperatingMode', 'standby'); } return []; } protected getTargetTemperatureCommands(value): Command | Array | undefined { if (this.prog?.value) { return new Command('setDerogatedTargetTemperature', value); } else { return new Command('setTargetTemperature', value); } } protected onStateChanged(name: string, value) { switch (name) { case 'core:TemperatureState': this.onTemperatureUpdate(value); break; case 'io:EffectiveTemperatureSetpointState': case 'core:TargetTemperatureState': case 'io:TargetHeatingLevelState': case 'core:OperatingModeState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; let targetTemperature; targetState = Characteristics.TargetHeatingCoolingState.AUTO; switch (this.device.get('core:OperatingModeState')) { case 'off': case 'away': case 'frostprotection': case 'standby': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); targetTemperature = this.device.get('core:TargetTemperatureState'); break; case 'auto': this.prog?.updateValue(false); if (this.device.get('io:TargetHeatingLevelState') === 'eco') { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); } targetTemperature = this.device.get('io:EffectiveTemperatureSetpointState'); break; case 'prog': case 'program': case 'internal': case 'comfort': case 'eco': case 'manual': case 'basic': if (this.device.get('io:NativeFunctionalLevelState') === 'Top') { targetState = Characteristics.TargetHeatingCoolingState.HEAT; } this.prog?.updateValue(['prog', 'program', 'internal'].includes(this.device.get('core:OperatingModeState'))); if (this.device.get('io:TargetHeatingLevelState') === 'eco') { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); } targetTemperature = this.device.get('io:EffectiveTemperatureSetpointState'); break; } if (this.targetTemperature !== undefined && targetTemperature !== undefined && targetTemperature !== null) { this.targetTemperature.updateValue(targetTemperature); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticElectricalTowelDryer.ts ================================================ import { Characteristics, Services } from '../../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticElectricalTowelDryer extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['prog']; protected MIN_TEMP = 7; protected MAX_TEMP = 28; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; protected drying: Characteristic | undefined; protected registerServices() { const services = super.registerServices(); if (this.device.hasCommand('setTowelDryerBoostModeDuration')) { const boost = this.registerSwitchService('boost'); services.push(boost); } if (this.device.hasCommand('setDryingDuration')) { const drying = this.registerService(Services.Switch, 'drying'); this.drying = drying.getCharacteristic(Characteristics.On); this.drying?.onSet(this.setDrying.bind(this)); services.push(drying); } return services; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('setTowelDryerOperatingMode', this.prog?.value ? 'internal' : 'external'); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setTowelDryerOperatingMode', 'standby'); } return []; } protected getTargetTemperatureCommands(value): Command | Array | undefined { if (this.prog?.value) { return new Command('setDerogatedTargetTemperature', value); } else { return new Command('setTargetTemperature', value); } } protected getOnCommands(value): Command | Array { const commands = new Array(); commands.push(new Command('setTowelDryerTemporaryState', value ? 'boost' : 'permanentHeating')); if (value) { commands.push(new Command('setTowelDryerBoostModeDuration', 10)); } return commands; } protected async setDrying(value) { const commands = new Array(); commands.push(new Command('setTowelDryerTemporaryState', value ? 'drying' : 'permanentHeating')); if (value) { commands.push(new Command('setDryingDuration', 60)); } const action = await this.executeCommands(commands); action.on('update', (state) => { switch (state) { case ExecutionState.FAILED: this.drying?.updateValue(!value); break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'core:TemperatureState': this.onTemperatureUpdate(value); break; case 'io:TowelDryerTemporaryStateState': this.on?.updateValue(value === 'boost'); this.drying?.updateValue(value === 'drying'); break; case 'core:TargetTemperatureState': case 'core:DerogatedTargetTemperatureState': case 'core:ComfortRoomTemperatureState': case 'core:EcoRoomTemperatureState': case 'core:OperatingModeState': case 'io:TargetHeatingLevelState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetTemperature = Number(this.device.get('core:ComfortRoomTemperatureState')); switch (this.device.get('io:TargetHeatingLevelState')) { case 'off': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetTemperature?.updateValue(this.device.get('core:TargetTemperatureState')); break; case 'eco': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); targetTemperature = targetTemperature - Number(this.device.get('core:EcoRoomTemperatureState')); break; case 'comfort': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); break; } switch (this.device.get('core:OperatingModeState')) { case 'standby': this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.OFF); break; case 'internal': this.prog?.updateValue(true); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); if (Number(this.device.get('core:DerogatedTargetTemperatureState')) > 0) { this.targetTemperature?.updateValue(this.device.get('core:DerogatedTargetTemperatureState')); } else { this.targetTemperature?.updateValue(targetTemperature); } break; case 'external': this.prog?.updateValue(false); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); this.targetTemperature?.updateValue(targetTemperature); break; } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticPassAPCBoiler.ts ================================================ import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticPassAPCBoiler extends HeatingSystem { protected registerMainService() { return this.registerSwitchService(); } protected getOnCommands(value): Command | Array { return new Command('setPassAPCOperatingMode', value ? 'heating' : 'stop'); } protected onStateChanged(name, value) { switch (name) { case 'io:PassAPCOperatingModeState': switch (value) { case 'stop': this.on?.updateValue(false); break; case 'heating': case 'drying': case 'cooling': this.on?.updateValue(true); break; } break; default: super.onStateChanged(name, value); break; } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticPassAPCHeatPump.ts ================================================ import { Characteristic, Perms } from 'homebridge'; import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; import { TotalConsumptionCharacteristic } from '../../CustomCharacteristics'; export default class AtlanticPassAPCHeatPump extends HeatingSystem { protected MIN_TEMP = 0; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected consumption: Characteristic | undefined; protected registerMainService() { const service = super.registerMainService(); this.targetTemperature?.setProps({ perms: [Perms.PAIRED_READ, Perms.EVENTS] }); return service; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('setPassAPCOperatingMode', 'heating'); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setPassAPCOperatingMode', 'heating'); case Characteristics.TargetHeatingCoolingState.COOL: return new Command('setPassAPCOperatingMode', 'cooling'); default: case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setPassAPCOperatingMode', 'stop'); } } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getTargetTemperatureCommands(value): Command | Array { return []; } protected onStateChanged(name, value) { switch (name) { case 'io:PassAPCOperatingModeState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; switch (this.device.get('io:PassAPCOperatingModeState')) { case 'heating': targetState = Characteristics.TargetHeatingCoolingState.HEAT; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); break; case 'cooling': targetState = Characteristics.TargetHeatingCoolingState.COOL; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); break; case 'stop': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); break; } // eslint-disable-next-line eqeqeq if (this.targetState !== undefined && targetState != null && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticPassAPCHeatingAndCoolingZone.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticPassAPCHeatingAndCoolingZone extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['prog']; protected MIN_TEMP = 16; protected MAX_TEMP = 30; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; private refreshStatesTimeout; protected applyConfig(config) { super.applyConfig(config); } protected getTargetStateCommands(value): Command | Array { const heatingCooling = this.getHeatingCooling(); const commands: Array = []; switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: commands.push(new Command('set' + heatingCooling + 'OnOffState', 'on')); commands.push(new Command('setPassAPC' + heatingCooling + 'Mode', this.prog?.value ? 'internalScheduling' : 'manu')); break; case Characteristics.TargetHeatingCoolingState.OFF: commands.push(new Command('set' + heatingCooling + 'OnOffState', 'off')); break; } return commands; } protected getTargetTemperatureCommands(value): Command | Array { const heatingCooling = this.getHeatingCooling(); if (this.prog?.value) { if (this.device.hasCommand('setDerogatedTargetTemperature')) { // AtlanticPassAPCHeatPump return [ new Command('setDerogatedTargetTemperature', value), new Command('setDerogationTime', this.derogationDuration), new Command('setDerogationOnOffState', 'on'), ]; } else { const profile = this.getProfile(); return new Command(`set${profile}${heatingCooling}TargetTemperature`, value); } } else { if (this.device.hasCommand(`set${heatingCooling}TargetTemperature`)) { // AtlanticPassAPCZoneControl return new Command(`set${heatingCooling}TargetTemperature`, value); } else { // AtlanticPassAPCHeatPump return new Command(`setComfort${heatingCooling}TargetTemperature`, value); } } } protected onStateChanged(name, value) { switch (name) { case 'core:TemperatureState': this.onTemperatureUpdate(value); break; case 'core:TargetTemperatureState': if (value >= 16) { this.targetTemperature?.updateValue(value); } break; case 'core:HeatingOnOffState': case 'core:CoolingOnOffState': case 'io:PassAPCHeatingModeState': case 'io:PassAPCCoolingModeState': case 'io:PassAPCHeatingProfileState': case 'io:PassAPCCoolingProfileState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; let targetTemperature; const heatingCooling = this.getHeatingCooling(); if (this.device.get(`core:${heatingCooling}OnOffState`) === 'off') { targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } else { targetTemperature = targetTemperature = this.device.get(`core:${heatingCooling}TargetTemperatureState`) || this.device.get('core:TargetTemperatureState'); const currentTemperature = this.currentTemperature?.value || targetTemperature; if (heatingCooling === 'Heating') { if (currentTemperature >= (targetTemperature + 0.5)) { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); } } else { if (currentTemperature <= (targetTemperature - 0.5)) { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); } } targetState = Characteristics.TargetHeatingCoolingState.AUTO; } if (this.device.get(`io:PassAPC${heatingCooling}ModeState`) === 'internalScheduling') { this.prog?.updateValue(true); } else { this.prog?.updateValue(false); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } if (this.targetTemperature !== undefined && targetTemperature >= 16 && this.isIdle) { this.targetTemperature.updateValue(targetTemperature); } } /** * Helpers */ private getHeatingCooling() { const operatingMode = this.device.parent?.get('io:PassAPCOperatingModeState'); if (operatingMode === 'cooling') { return 'Cooling'; } else { return 'Heating'; } } private getProfile() { const heatingCooling = this.getHeatingCooling(); if (this.device.get(`core:Eco${heatingCooling}TargetTemperatureState`) === this.device.get('core:TargetTemperatureState')) { return 'Eco'; } else { return 'Comfort'; } } private launchRefreshStates() { clearTimeout(this.refreshStatesTimeout); this.refreshStatesTimeout = setTimeout(() => { const commands = [ new Command('refreshTargetTemperature'), new Command('refreshPassAPCHeatingProfile'), ]; this.executeCommands(commands); }, 30 * 1000); } private launchRefreshTemperature() { clearTimeout(this.refreshStatesTimeout); this.refreshStatesTimeout = setTimeout(() => { this.executeCommands(new Command('refreshTargetTemperature')); }, 30 * 1000); } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticPassAPCHeatingZone.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticPassAPCHeatingZone extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['eco', 'prog']; protected MIN_TEMP = 10; protected MAX_TEMP = 35; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; protected getTargetStateCommands(value): Command | Array { const commands: Array = []; switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: commands.push(new Command('setHeatingOnOffState', 'on')); if (this.prog?.value) { commands.push(new Command('setPassAPCHeatingMode', 'internalScheduling')); } else { commands.push(new Command('setDerogationOnOffState', 'off')); if (this.eco?.value) { commands.push(new Command('setPassAPCHeatingMode', 'eco')); } else { commands.push(new Command('setPassAPCHeatingMode', 'comfort')); } } break; case Characteristics.TargetHeatingCoolingState.OFF: commands.push(new Command('setHeatingOnOffState', 'off')); //commands.push(new Command('setHeatingOnOffState', 'on')); //commands.push(new Command('setPassAPCHeatingMode', 'absence')); break; } return commands; } protected getTargetTemperatureCommands(value): Command | Array { const duration = this.derogationDuration; const commands: Array = []; if (this.prog?.value) { commands.push(new Command('setDerogatedTargetTemperature', value)); commands.push(new Command('setDerogationTime', duration)); commands.push(new Command('setDerogationOnOffState', 'on')); } else { if (this.eco?.value) { commands.push(new Command('setEcoHeatingTargetTemperature', value)); } else { commands.push(new Command('setComfortHeatingTargetTemperature', value)); } } return commands; } protected onStateChanged(name, value) { switch (name) { case 'core:TemperatureState': this.onTemperatureUpdate(value); break; case 'core:TargetTemperatureState': case 'core:HeatingOnOffState': case 'io:PassAPCHeatingModeState': case 'io:PassAPCHeatingProfileState': case 'core:ComfortHeatingTargetTemperatureState': case 'core:EcoHeatingTargetTemperatureState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; if (this.device.get('core:HeatingOnOffState') === 'on') { targetState = Characteristics.TargetHeatingCoolingState.AUTO; switch (this.device.get('io:PassAPCHeatingModeState')) { case 'off': case 'absence': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetTemperature?.updateValue(this.device.get('core:TargetTemperatureState')); break; case 'auto': case 'internalScheduling': case 'externalScheduling': this.prog?.updateValue(true); if (this.device.get('io:PassAPCHeatingProfileState') === 'comfort') { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); } if (this.device.get('io:PassAPCHeatingProfileState') === 'derogation') { this.targetTemperature?.updateValue(this.device.get('core:DerogatedTargetTemperatureState')); } else if (this.device.get('io:PassAPCHeatingProfileState') === 'comfort') { this.targetTemperature?.updateValue(this.device.get('core:ComfortHeatingTargetTemperatureState')); } else if (this.device.get('io:PassAPCHeatingProfileState') === 'eco') { this.targetTemperature?.updateValue(this.device.get('core:EcoHeatingTargetTemperatureState')); } else { this.targetTemperature?.updateValue(this.device.get('core:TargetTemperatureState')); } break; case 'comfort': this.prog?.updateValue(false); this.eco?.updateValue(false); this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.targetTemperature?.updateValue(this.device.get('core:ComfortHeatingTargetTemperatureState')); break; case 'eco': this.prog?.updateValue(false); this.eco?.updateValue(true); this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.targetTemperature?.updateValue(this.device.get('core:EcoHeatingTargetTemperatureState')); break; } } else { targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetTemperature?.updateValue(this.device.get('core:TargetTemperatureState')); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/AtlanticPassAPCZoneControl.ts ================================================ import { Perms } from 'homebridge'; import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class AtlanticPassAPCZoneControl extends HeatingSystem { protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService() { const service = super.registerMainService(); this.targetTemperature?.setProps({ perms: [Perms.PAIRED_READ, Perms.EVENTS] }); return service; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return [ new Command('setPassAPCOperatingMode', 'heating'), new Command('setHeatingCoolingAutoSwitch', 'on'), ]; case Characteristics.TargetHeatingCoolingState.HEAT: return [ new Command('setPassAPCOperatingMode', 'heating'), new Command('setHeatingCoolingAutoSwitch', 'off'), ]; case Characteristics.TargetHeatingCoolingState.COOL: return [ new Command('setPassAPCOperatingMode', 'cooling'), new Command('setHeatingCoolingAutoSwitch', 'off'), ]; default: case Characteristics.TargetHeatingCoolingState.OFF: return [ new Command('setPassAPCOperatingMode', 'stop'), ]; } } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected getTargetTemperatureCommands(value): Command | Array { return []; } protected onStateChanged(name, value) { switch (name) { case 'io:PassAPCOperatingModeState': case 'core:HeatingCoolingAutoSwitchState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; switch (this.device.get('io:PassAPCOperatingModeState')) { case 'heating': if (this.device.get('core:HeatingCoolingAutoSwitchState') === 'on') { targetState = Characteristics.TargetHeatingCoolingState.AUTO; } else { targetState = Characteristics.TargetHeatingCoolingState.HEAT; } this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); break; case 'cooling': if (this.device.get('core:HeatingCoolingAutoSwitchState') === 'on') { targetState = Characteristics.TargetHeatingCoolingState.AUTO; } else { targetState = Characteristics.TargetHeatingCoolingState.COOL; } this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); break; case 'stop': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); break; } // eslint-disable-next-line eqeqeq if (this.targetState !== undefined && targetState != null && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/ProgrammableAndProtectableThermostatSetPoint.ts ================================================ import ThermostatSetPoint from './ThermostatSetPoint'; export default class ProgrammableAndProtectableThermostatSetPoint extends ThermostatSetPoint { } ================================================ FILE: src/mappers/HeatingSystem/SomfyHeatingTemperatureInterface.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class SomfyHeatingTemperatureInterface extends HeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['prog', 'eco']; protected MIN_TEMP = 0; protected MAX_TEMP = 26; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected getTargetStateCommands(value): Command | Array | undefined { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return [ new Command('setOnOff', 'on'), new Command('setOperatingMode', 'both'), ]; case Characteristics.TargetHeatingCoolingState.HEAT: return [ new Command('setOnOff', 'on'), new Command('setOperatingMode', 'heating'), ]; case Characteristics.TargetHeatingCoolingState.COOL: return [ new Command('setOnOff', 'on'), new Command('setOperatingMode', 'cooling'), ]; case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setOnOff', 'off'); } } protected getProgCommands(): Command | Array | undefined { if (this.prog?.value) { return new Command('setActiveMode', 'auto'); } else { if (this.eco?.value) { return new Command('setManuAndSetPointModes', 'eco'); } else { return new Command('setManuAndSetPointModes', 'comfort'); } } } protected getTargetTemperatureCommands(value): Command | Array | undefined { if (this.device.get('ovp:HeatingTemperatureInterfaceSetPointModeState') === 'comfort') { return new Command('setComfortTemperature', value); } else { return new Command('setEcoTemperature', value); } } protected onStateChanged(name, value) { switch (name) { case 'core:OnOffState': case 'ovp:HeatingTemperatureInterfaceOperatingModeState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; if (this.device.get('core:OnOffState') === 'on') { switch (this.device.get('ovp:HeatingTemperatureInterfaceOperatingModeState')) { case 'both': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); targetState = Characteristics.TargetHeatingCoolingState.AUTO; break; case 'heating': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); targetState = Characteristics.TargetHeatingCoolingState.HEAT; break; case 'cooling': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); targetState = Characteristics.TargetHeatingCoolingState.COOL; break; } } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); targetState = Characteristics.TargetHeatingCoolingState.OFF; } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/SomfyThermostat.ts ================================================ import { Characteristics } from '../../Platform'; import { Command, ExecutionState } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class SomfyThermostat extends HeatingSystem { protected MIN_TEMP = 0; protected MAX_TEMP = 26; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; private lastRefresh = Date.now(); protected registerMainService() { const service = super.registerMainService(); this.targetState?.onGet(this.refreshStates.bind(this)); return service; } protected async refreshStates() { if (this.lastRefresh < Date.now() - (60 * 1000)) { this.lastRefresh = Date.now(); await this.executeCommands(new Command('refreshState')); } return this.targetState?.value ?? Characteristics.TargetHeatingCoolingState.OFF; } protected getTargetStateCommands(value): Command | Array | undefined { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('exitDerogation'); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setDerogation', ['atHomeMode', 'further_notice']); case Characteristics.TargetHeatingCoolingState.COOL: return new Command('setDerogation', ['sleepingMode', 'further_notice']); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setDerogation', ['awayMode', 'further_notice']); } } protected getTargetTemperatureCommands(value): Command | Array | undefined { return new Command('setDerogation', [value, 'further_notice']); } protected onStateChanged(name, value) { switch (name) { case 'core:TargetTemperatureState': case 'core:DerogatedTargetTemperatureState': case 'core:DerogationActivationState': case 'somfythermostat:DerogationHeatingModeState': this.postpone(this.computeStates); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; let targetTemperature; const derog = this.device.get('core:DerogationActivationState') === 'active'; const mode = this.device.get(derog ? 'somfythermostat:DerogationHeatingModeState' : 'somfythermostat:HeatingModeState'); switch (mode) { case 'atHomeMode': case 'geofencingMode': case 'manualMode': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); targetState = derog ? Characteristics.TargetHeatingCoolingState.HEAT : Characteristics.TargetHeatingCoolingState.AUTO; break; case 'sleepingMode': case 'suddenDropMode': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); targetState = derog ? Characteristics.TargetHeatingCoolingState.COOL : Characteristics.TargetHeatingCoolingState.AUTO; break; case 'awayMode': case 'freezeMode': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); targetState = Characteristics.TargetHeatingCoolingState.OFF; break; } switch (mode) { case 'atHomeMode': targetTemperature = this.device.get('somfythermostat:AtHomeTargetTemperatureState'); break; case 'geofencingMode': targetTemperature = this.device.get('somfythermostat:GeofencingModeTargetTemperatureState'); break; case 'manualMode': targetTemperature = this.device.get('somfythermostat:ManualModeTargetTemperatureState'); break; case 'sleepingMode': targetTemperature = this.device.get('somfythermostat:SleepingModeTargetTemperatureState'); break; case 'suddenDropMode': targetTemperature = this.device.get('somfythermostat:SuddenDropModeTargetTemperatureState'); break; case 'awayMode': targetTemperature = this.device.get('somfythermostat:AwayModeTargetTemperatureState'); break; case 'freezeMode': targetTemperature = this.device.get('somfythermostat:FreezeModeTargetTemperatureState'); break; } if (targetTemperature === undefined || targetTemperature === null) { targetTemperature = this.device.get(derog ? 'core:DerogatedTargetTemperatureState' : 'core:TargetTemperatureState'); } if (this.targetTemperature !== undefined && targetTemperature !== undefined) { this.targetTemperature.updateValue(targetTemperature); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem/ThermostatSetPoint.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class ThermostatSetPoint extends HeatingSystem { protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, ]; protected registerMainService() { const service = super.registerMainService(); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); return service; } protected getTargetTemperatureCommands(value): Command | Array { return new Command('setHeatingTargetTemperature', value); } protected onStateChanged(name, value) { switch (name) { case 'zwave:SetPointHeatingValueState': case 'core:RoomTemperatureState': this.onTemperatureUpdate(value); break; case 'core:HeatingTargetTemperatureState': this.targetTemperature?.updateValue(value); break; default: super.onStateChanged(name, value); break; } } } ================================================ FILE: src/mappers/HeatingSystem/ValveHeatingTemperatureInterface.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class ValveHeatingTemperatureInterface extends HeatingSystem { protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected getTargetStateCommands(value): Command | Array | undefined { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('exitDerogation'); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setDerogation', ['comfort', 'further_notice']); case Characteristics.TargetHeatingCoolingState.COOL: return new Command('setDerogation', ['eco', 'further_notice']); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setDerogation', ['away', 'further_notice']); } } protected getTargetTemperatureCommands(value): Command | Array | undefined { return new Command('setDerogation', [value, 'further_notice']); } protected onStateChanged(name, value) { switch (name) { case 'core:OperatingModeState': case 'io:CurrentHeatingModeState': this.postpone(this.computeStates); break; case 'core:TargetRoomTemperatureState': this.targetTemperature?.updateValue(value); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; const auto = ['auto', 'prog', 'program'].includes(this.device.get('core:OperatingModeState') || ''); switch (this.device.get('io:CurrentHeatingModeState')) { case 'manual': case 'comfort': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); targetState = auto ? Characteristics.TargetHeatingCoolingState.AUTO : Characteristics.TargetHeatingCoolingState.HEAT; break; case 'eco': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); targetState = auto ? Characteristics.TargetHeatingCoolingState.AUTO : Characteristics.TargetHeatingCoolingState.COOL; break; case 'off': case 'awayMode': case 'frostprotection': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); targetState = Characteristics.TargetHeatingCoolingState.OFF; break; } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/HeatingSystem.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, Service } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; import { EcoCharacteristic, ProgCharacteristic, TotalConsumptionCharacteristic } from '../CustomCharacteristics'; export default class HeatingSystem extends Mapper { protected THERMOSTAT_CHARACTERISTICS: string[] = []; protected MIN_TEMP = 7; protected MAX_TEMP = 30; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; protected currentTemperature: Characteristic | undefined; protected targetTemperature: Characteristic | undefined; protected currentState: Characteristic | undefined; protected targetState: Characteristic | undefined; protected on: Characteristic | undefined; protected prog: Characteristic | undefined; protected eco: Characteristic | undefined; protected consumption: Characteristic | undefined; protected derogationDuration; protected comfortTemperature; protected ecoTemperature; protected applyConfig(config) { this.derogationDuration = config['derogationDuration'] || 1; this.comfortTemperature = config['comfort'] || 19; this.ecoTemperature = config['eco'] || 17; } protected registerMainService(): Service { const service = this.registerService(Services.Thermostat); service.setPrimaryService(true); service.addOptionalCharacteristic(ProgCharacteristic); service.addOptionalCharacteristic(EcoCharacteristic); this.currentTemperature = service.getCharacteristic(Characteristics.CurrentTemperature); this.targetTemperature = service.getCharacteristic(Characteristics.TargetTemperature); this.currentState = service.getCharacteristic(Characteristics.CurrentHeatingCoolingState); this.targetState = service.getCharacteristic(Characteristics.TargetHeatingCoolingState); this.currentTemperature.setProps({ minStep: 0.1 }); this.targetState?.setProps({ validValues: this.TARGET_MODES }); this.targetTemperature?.setProps({ minValue: this.MIN_TEMP, maxValue: this.MAX_TEMP, minStep: 0.5 }); const temp = Number(this.targetTemperature.value) if (this.targetTemperature && temp < this.targetTemperature.props.minValue!) { this.targetTemperature.value = this.targetTemperature.props.minValue!; } if (this.targetTemperature && temp > this.targetTemperature.props.maxValue!) { this.targetTemperature.value = this.targetTemperature.props.maxValue!; } if (this.THERMOSTAT_CHARACTERISTICS.includes('prog')) { this.prog = service.getCharacteristic(ProgCharacteristic); this.prog.onSet((value) => { this.prog?.updateValue(value); this.sendProgCommands(); }); } if (this.THERMOSTAT_CHARACTERISTICS.includes('eco')) { this.eco = service.getCharacteristic(EcoCharacteristic); this.eco.onSet((value) => { this.eco?.updateValue(value); this.sendProgCommands(); }); } if (this.device.hasSensor('CumulativeElectricPowerConsumptionSensor')) { service.addOptionalCharacteristic(TotalConsumptionCharacteristic); this.consumption = service.getCharacteristic(TotalConsumptionCharacteristic); } this.targetState?.onSet(this.setTargetState.bind(this)); this.targetTemperature?.onSet(this.debounce(this.setTargetTemperature)); return service; } protected registerSwitchService(subtype?: string): Service { const service = this.registerService(Services.Switch, subtype); this.on = service.getCharacteristic(Characteristics.On); this.on?.onSet(this.setOn.bind(this)); return service; } protected getTargetStateCommands(value): Command | Array | undefined { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('auto'); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('heat'); case Characteristics.TargetHeatingCoolingState.COOL: return new Command('cool'); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('off'); default: return new Command('auto'); } } protected async setTargetState(value) { if (value === this.targetState?.value) { return; } const action = await this.executeCommands(this.getTargetStateCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.COMPLETED: if (this.stateless) { this.currentState?.updateValue(value); } break; case ExecutionState.FAILED: if (this.currentState) { this.targetState?.updateValue(this.currentState.value); } break; } }); } protected getTargetTemperatureCommands(value): Command | Array | undefined { return new Command('setTargetTemperature', value); } protected async setTargetTemperature(value) { await this.executeCommands(this.getTargetTemperatureCommands(value)); } protected getOnCommands(value): Command | Array | undefined { return new Command('setOn', value); } protected async setOn(value) { const action = await this.executeCommands(this.getOnCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.FAILED: this.on?.updateValue(!value); break; } }); } protected getProgCommands(): Command | Array | undefined { return this.getTargetStateCommands(this.targetState?.value); } protected sendProgCommands() { if (this.targetState?.value !== Characteristics.TargetHeatingCoolingState.OFF) { this.executeCommands(this.getProgCommands()); } } protected onTemperatureUpdate(value) { this.currentTemperature?.updateValue(value > 273.15 ? (value - 273.15) : value); } protected onStateChanged(name: string, value) { switch (name) { case 'core:TemperatureState': this.onTemperatureUpdate(value); break; case 'core:TargetTemperatureState': this.targetTemperature?.updateValue(value); break; case 'core:ElectricEnergyConsumptionState': this.consumption?.updateValue(value / 1000); break; } } } ================================================ FILE: src/mappers/HitachiHeatingSystem/HitachiAirToAirHeatPump.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import HeatingSystem from '../HeatingSystem'; export default class HitachiAirToAirHeatPump extends HeatingSystem { protected MIN_TEMP = 16; protected MAX_TEMP = 30; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.COOL, Characteristics.TargetHeatingCoolingState.OFF, ]; protected getTargetStateCommands(value): Command | Array | undefined { return this.getCommands(value, this.targetTemperature?.value); } protected getTargetTemperatureCommands(value): Command | Array { return this.getCommands(this.targetState?.value, value); } protected onStateChanged(name: string, value) { switch (name) { case 'ovp:ModeChangeState': case 'ovp:MainOperationState': if (this.device.get('ovp:MainOperationState') === 'Off' || this.device.get('ovp:MainOperationState') === 'off') { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.OFF); } else { switch (this.device.get('ovp:ModeChangeState')?.toLowerCase()) { case 'auto cooling': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); break; case 'auto heating': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.AUTO); break; case 'cooling': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.COOL); break; case 'heating': this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.targetState?.updateValue(Characteristics.TargetHeatingCoolingState.HEAT); break; } } break; case 'ovp:RoomTemperatureState': this.onTemperatureUpdate(value); break; case 'core:TargetTemperatureState': this.targetTemperature?.updateValue(value); break; /* case 'ovp:TemperatureChangeState': if(value <= 5 && this.currentTemperature) { this.targetTemperature?.updateValue(this.currentTemperature.value + value); } else { this.targetTemperature?.updateValue(value); } break; */ } } private getCommands(state, temperature) { const currentState = this.currentState ? this.currentState.value : 0; const currentTemperature = this.currentTemperature && this.currentTemperature.value !== null ? this.currentTemperature.value : 0; let onOff = 'on'; const fanMode = 'auto'; const progMode = 'manu'; let heatMode = 'auto'; const autoTemp = Math.trunc(Math.max(Math.min(temperature - parseInt(currentTemperature.toString()), 5), -5)); switch (state) { case Characteristics.TargetHeatingCoolingState.OFF: onOff = 'off'; switch (currentState) { case Characteristics.CurrentHeatingCoolingState.HEAT: heatMode = 'heating'; break; case Characteristics.CurrentHeatingCoolingState.COOL: heatMode = 'cooling'; break; default: temperature = autoTemp; break; } break; case Characteristics.TargetHeatingCoolingState.HEAT: heatMode = 'heating'; break; case Characteristics.TargetHeatingCoolingState.COOL: heatMode = 'cooling'; break; case Characteristics.TargetHeatingCoolingState.AUTO: heatMode = 'auto'; temperature = autoTemp; break; default: temperature = autoTemp; break; } temperature = Math.round(temperature); this.debug('FROM ' + currentState + '/' + currentTemperature + ' TO ' + state + '/' + temperature); return new Command('globalControl', [onOff, temperature, fanMode, heatMode, progMode]); } } ================================================ FILE: src/mappers/HitachiHeatingSystem/HitachiAirToWaterHeatingZone.ts ================================================ import HeatingSystem from '../HeatingSystem'; export default class HitachiAirToWaterHeatingZone extends HeatingSystem { } ================================================ FILE: src/mappers/HitachiHeatingSystem/HitachiAirToWaterMainComponent.ts ================================================ import HeatingSystem from '../HeatingSystem'; export default class HitachiAirToWaterMainComponent extends HeatingSystem { protected MIN_TEMP = 0; } ================================================ FILE: src/mappers/HitachiHeatingSystem/HitachiDHW.ts ================================================ import WaterHeatingSystem from '../WaterHeatingSystem'; export default class HitachiDHW extends WaterHeatingSystem { } ================================================ FILE: src/mappers/HumiditySensor/WaterDetectionSensor.ts ================================================ import { Characteristics } from '../../Platform'; import ContactSensor from '../ContactSensor'; export default class WaterDetectionSensor extends ContactSensor { protected onStateChanged(name: string, value) { switch(name) { case 'core:WaterDetectionState ': this.state?.updateValue( value === 'detected' ? Characteristics.ContactSensorState.CONTACT_DETECTED : Characteristics.ContactSensorState.CONTACT_NOT_DETECTED ); break; } } } ================================================ FILE: src/mappers/HumiditySensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class HumiditySensor extends Mapper { protected humidity: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.HumiditySensor); this.humidity = service.getCharacteristic(Characteristics.CurrentRelativeHumidity); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:RelativeHumidityState': this.humidity?.updateValue(value); break; } } } ================================================ FILE: src/mappers/Light.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, CharacteristicSetCallback } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class Light extends Mapper { protected on: Characteristic | undefined; protected hue: Characteristic | undefined; protected brightness: Characteristic | undefined; protected saturation: Characteristic | undefined; protected registerMainService() { const service = this.registerService(this.device.hasCommand('setIntensity') ? Services.Lightbulb : Services.Switch); this.on = service.getCharacteristic(Characteristics.On); this.on.onSet(this.setOn.bind(this)); if (this.device.hasCommand('setIntensity')) { this.brightness = service.getCharacteristic(Characteristics.Brightness); this.brightness.onSet(this.setBrightness.bind(this)); if (this.device.hasCommand('setHueAndSaturation')) { this.hue = service.getCharacteristic(Characteristics.Hue); this.saturation = service.getCharacteristic(Characteristics.Saturation); this.saturation.onSet(this.setSaturation.bind(this)); } } return service; } protected getOnOffCommands(value): Command | Array { return new Command(value ? 'on' : 'off'); } protected async setOn(value) { const action = await this.executeCommands(this.getOnOffCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected getBrightnessCommands(value): Command | Array { return new Command('setIntensity', value); } protected async setBrightness(value) { const action = await this.executeCommands(this.getBrightnessCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected getSaturationCommands(value): Command | Array { return new Command('setHueAndSaturation', [this.hue?.value, value]); } protected async setSaturation(value) { const action = await this.executeCommands(this.getSaturationCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected onStateChanged(name: string, value): boolean { switch (name) { case 'core:OnOffState': this.on?.updateValue(value === 'on'); break; case 'core:IntensityState': case 'core:LightIntensityState': this.brightness?.updateValue(value); break; case 'core:ColorHueState': this.hue?.updateValue(value); break; case 'core:ColorSaturationState': this.saturation?.updateValue(value); break; } return false; } } ================================================ FILE: src/mappers/LightSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class LightSensor extends Mapper { protected lightLevel: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.LightSensor); this.lightLevel = service.getCharacteristic(Characteristics.CurrentAmbientLightLevel); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:LuminanceState': this.lightLevel?.updateValue(value); break; } } } ================================================ FILE: src/mappers/OccupancySensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class OccupancySensor extends Mapper { protected occupancy: Characteristic | undefined; protected fault: Characteristic | undefined; protected battery: Characteristic | undefined; protected registerMainService() { const motion = this.device.definition.widgetName.startsWith('Motion'); const service = this.registerService(motion ? Services.MotionSensor : Services.OccupancySensor); this.occupancy = service.getCharacteristic(motion ? Characteristics.MotionDetected : Characteristics.OccupancyDetected); if (this.device.hasState('core:SensorDefectState')) { this.fault = service.getCharacteristic(Characteristics.StatusFault); this.battery = service.getCharacteristic(Characteristics.StatusLowBattery); } return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:OccupancyState': this.occupancy?.updateValue(value === 'personInside'); break; case 'core:SensorDefectState': switch (value) { case 'lowBattery': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_LOW); break; case 'maintenanceRequired': case 'dead': this.fault?.updateValue(Characteristics.StatusFault.GENERAL_FAULT); break; case 'noDefect': this.fault?.updateValue(Characteristics.StatusFault.NO_FAULT); this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_NORMAL); break; } break; } } } ================================================ FILE: src/mappers/OnOff.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, CharacteristicSetCallback } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class OnOff extends Mapper { protected on: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.Switch); this.on = service.getCharacteristic(Characteristics.On); this.on.onSet(this.setOn.bind(this)); return service; } protected getOnOffCommands(value): Command | Array { return new Command(value ? 'on' : 'off'); } protected async setOn(value) { const action = await this.executeCommands(this.getOnOffCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected onStateChanged(name: string, value): boolean { switch (name) { case 'core:OnOffState': this.on?.updateValue(value === 'on'); break; } return false; } } ================================================ FILE: src/mappers/Pergola/BioclimaticPergola.ts ================================================ import { Command } from 'overkiz-client'; import Pergola from '../Pergola'; export default class BioclimaticPergola extends Pergola { protected getTargetCommands(value) { return new Command('setOrientation', this.reversedValue(value)); } protected onStateChanged(name, value) { switch(name) { case 'core:SlatsOrientationState': this.currentPosition?.updateValue(this.reversedValue(value)); if(this.isIdle) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; default: break; } } } ================================================ FILE: src/mappers/Pergola/PergolaHorizontalAwningUno.ts ================================================ import Pergola from '../Pergola'; export default class PergolaHorizontalAwningUno extends Pergola { protected onStateChanged(name: string, value) { // Fix (https://github.com/dubocr/homebridge-tahoma/issues/305) value = 100 - value; switch (name) { case 'core:ClosureState': this.currentPosition?.updateValue(this.reversedValue(value)); if (!this.device.hasState('core:TargetClosureState') && this.isIdle) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; case 'core:TargetClosureState': this.targetPosition?.updateValue(this.reversedValue(value)); if (!this.device.hasState('core:ClosureState')) { this.currentPosition?.updateValue(this.reversedValue(value)); } break; } } } ================================================ FILE: src/mappers/Pergola.ts ================================================ import RollerShutter from './RollerShutter'; export default class Pergola extends RollerShutter { } ================================================ FILE: src/mappers/RainSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class RainSensor extends Mapper { protected rain: Characteristic | undefined; protected fault: Characteristic | undefined; protected battery: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.ContactSensor); this.rain = service.getCharacteristic(Characteristics.ContactSensorState); if (this.device.hasState('core:SensorDefectState')) { this.fault = service.getCharacteristic(Characteristics.StatusFault); this.battery = service.getCharacteristic(Characteristics.StatusLowBattery); } return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:RainState': this.rain?.updateValue(value === 'detected'); break; case 'core:SensorDefectState': switch (value) { case 'lowBattery': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_LOW); break; case 'maintenanceRequired': case 'dead': this.fault?.updateValue(Characteristics.StatusFault.GENERAL_FAULT); break; case 'noDefect': this.fault?.updateValue(Characteristics.StatusFault.NO_FAULT); this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_NORMAL); break; } break; } } } ================================================ FILE: src/mappers/RemoteController.ts ================================================ import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; import { Characteristics, Services } from '../Platform'; export default class RemoteController extends Mapper { protected event: Characteristic | undefined; protected registerMainService() { throw new Error('Service RemoteController not implemented'); const service = this.registerService(Services.StatelessProgrammableSwitch); this.event = service.getCharacteristic(Characteristics.ProgrammableSwitchEvent); return service; } protected onStateChanged(name: string, value) { switch(name) { default: this.event?.updateValue(value); } } } ================================================ FILE: src/mappers/RollerShutter/PositionableRollerShutterUno.ts ================================================ import RollerShutter from '../RollerShutter'; export default class PositionableRollerShutterUno extends RollerShutter { protected onStateChanged(name: string, value) { switch(name) { case 'core:TargetClosureState': if(this.isIdle) { this.targetPosition?.updateValue(this.reversedValue(value)); } this.currentPosition?.updateValue(this.reversedValue(value)); break; } } } ================================================ FILE: src/mappers/RollerShutter/PositionableRollerShutterWithLowSpeedManagement.ts ================================================ import moment from 'moment'; import { Command } from 'overkiz-client'; import RollerShutter from '../RollerShutter'; export default class PositionableRollerShutterWithLowSpeedManagement extends RollerShutter { protected lowSpeed; protected applyConfig(config) { this.lowSpeed = config['lowSpeed'] || false; } protected getTargetCommands(value) { if (this.isLowSpeed) { return new Command('setClosureAndLinearSpeed', [this.reversedValue(value), 'lowspeed']); } else { return new Command('setClosure', this.reversedValue(value)); } } protected get isLowSpeed() { if (this.lowSpeed === true) { return true; } else if (typeof this.lowSpeed === 'string') { const parts = this.lowSpeed.split(new RegExp('[-:]')); const now = moment(); const start = moment(); const end = moment(); start.set({ 'hour': parseInt(parts[0]), 'minute': parseInt(parts[1]), 'second': 0, 'millisecond': 0 }); end.set({ 'hour': parseInt(parts[2]), 'minute': parseInt(parts[3]), 'second': 0, 'millisecond': 0 }); if (end.isBefore(start)) { return now.isAfter(start) || now.isBefore(end); } else { return now.isBetween(start, end); } } else { return false; } } } ================================================ FILE: src/mappers/RollerShutter.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, Service } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; import { MyPositionCharacteristic } from '../CustomCharacteristics'; export default class RollerShutter extends Mapper { protected expectedStates = ['core:ClosureState', 'core:TargetClosureState']; protected windowService: Service | undefined; protected currentPosition: Characteristic | undefined; protected targetPosition: Characteristic | undefined; protected positionState: Characteristic | undefined; protected obstructionDetected: Characteristic | undefined; protected my: Characteristic | undefined; protected reverse; protected initPosition; protected defaultPosition; protected blindsOnRollerShutter; protected movementDuration; protected offsetMovementDuration; protected cancelTimeout; protected applyConfig(config) { this.defaultPosition = config['defaultPosition'] || 0; this.initPosition = config['initPosition'] !== undefined ? config['initPosition'] : (config['defaultPosition'] || 50); this.reverse = config['reverse'] || false; this.movementDuration = config['movementDuration'] || 0; this.offsetMovementDuration = config['offsetMovementDuration'] || 0; this.blindsOnRollerShutter = config['blindsOnRollerShutter'] || false; } protected registerMainService() { const service = this.registerService(Services.WindowCovering); service.addOptionalCharacteristic(MyPositionCharacteristic); this.currentPosition = service.getCharacteristic(Characteristics.CurrentPosition); this.targetPosition = service.getCharacteristic(Characteristics.TargetPosition); this.positionState = service.getCharacteristic(Characteristics.PositionState); if (this.stateless) { //this.currentPosition.updateValue(this.initPosition); //this.targetPosition.updateValue(this.initPosition); if (this.device.hasCommand('my')) { this.my = service.getCharacteristic(MyPositionCharacteristic); this.my.onSet(this.setMyPosition.bind(this)); } } else { this.obstructionDetected = service.getCharacteristic(Characteristics.ObstructionDetected); } if (service.testCharacteristic(Characteristics.On)) { this.my = service.getCharacteristic(Characteristics.On); service.removeCharacteristic(this.my); } this.positionState.updateValue(Characteristics.PositionState.STOPPED); this.targetPosition.onSet(this.debounce(this.setTargetPosition, [0, 100])); return service; } /** * Triggered when Homekit try to modify the Characteristic.TargetPosition * HomeKit '0' (Close) => 0% Deployment * HomeKit '100' (Open) => 100% Deployment **/ protected getTargetCommands(value): Command | Command[] { if (this.stateless) { if (value === 100) { return new Command(this.reverse ? 'close' : 'open'); } else if (value === 0) { return new Command(this.reverse ? 'open' : 'close'); } else { if (this.movementDuration > 0) { const delta = value - Number(this.currentPosition!.value); if (this.reverse) { return new Command(delta > 0 ? 'close' : 'open'); } else { return new Command(delta > 0 ? 'open' : 'close'); } } else { return new Command('my'); } } } else { return new Command('setClosure', this.reversedValue(value)); } } /** * Triggered when Homekit try to modify the Characteristic.TargetPosition * HomeKit '0' (Close) => 100% Closure * HomeKit '100' (Open) => 0% Closure **/ async setTargetPosition(value) { if (this.cancelTimeout !== null) { clearTimeout(this.cancelTimeout); } const standalone = this.stateless && this.movementDuration > 0 && value !== 100 && value !== 0; const action = await this.executeCommands(this.getTargetCommands(value), standalone); action.on('update', (state, data) => { const positionState = (value === 100 || value > (this.currentPosition?.value || 0)) ? Characteristics.PositionState.INCREASING : Characteristics.PositionState.DECREASING; switch (state) { case ExecutionState.IN_PROGRESS: if (standalone) { const delta = value - Number(this.currentPosition!.value); const duration = this.offsetMovementDuration * 1000 + Math.round(this.movementDuration * Math.abs(delta) * 1000 / 100); this.info('Will stop movement in ' + duration + ' millisec'); this.cancelTimeout = setTimeout(() => { this.cancelTimeout = null; if (this.isIdle) { this.executeCommands(new Command('stop'), true); } else { this.cancelExecution().catch(this.error.bind(this)); } }, duration); } this.positionState?.updateValue(positionState); break; case ExecutionState.COMPLETED: this.positionState?.updateValue(Characteristics.PositionState.STOPPED); if (this.stateless) { if (this.defaultPosition) { this.currentPosition?.updateValue(this.defaultPosition); this.targetPosition?.updateValue(this.defaultPosition); } else { this.currentPosition?.updateValue(value); } } else { this.obstructionDetected?.updateValue(false); } if (this.blindsOnRollerShutter && value < 98) { this.executeCommands(new Command('setClosure', value + 2)); } break; case ExecutionState.FAILED: if (this.stateless && data.failureType === 'CMDCANCELLED' && this.movementDuration > 0) { if (this.defaultPosition) { this.currentPosition?.updateValue(this.defaultPosition); this.targetPosition?.updateValue(this.defaultPosition); } else { this.currentPosition?.updateValue(value); } } this.positionState?.updateValue(Characteristics.PositionState.STOPPED); this.obstructionDetected?.updateValue(data.failureType === 'WHILEEXEC_BLOCKED_BY_HAZARD'); if (!this.device.hasState('core:TargetClosureState') && this.currentPosition) { this.targetPosition?.updateValue(this.currentPosition.value); } break; } }); } /** * Set My position **/ async setMyPosition(value) { if (!value) { return; } const action = await this.executeCommands(new Command('my')); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: this.my?.updateValue(0); if (this.stateless) { if (this.defaultPosition) { this.currentPosition?.updateValue(this.defaultPosition); this.targetPosition?.updateValue(this.defaultPosition); } else { this.currentPosition?.updateValue(50); this.targetPosition?.updateValue(50); } } break; case ExecutionState.FAILED: this.my?.updateValue(0); this.obstructionDetected?.updateValue(data.failureType === 'WHILEEXEC_BLOCKED_BY_HAZARD'); if (!this.device.hasState('core:TargetClosureState') && this.currentPosition) { this.targetPosition?.updateValue(this.currentPosition.value); } break; } }); } protected reversedValue(value) { return this.reverse ? value : (100 - value); } protected onStateChanged(name: string, value) { switch (name) { case 'core:ClosureState': this.currentPosition?.updateValue(this.reversedValue(value)); if (!this.device.hasState('core:TargetClosureState') && this.isIdle) { this.targetPosition?.updateValue(this.reversedValue(value)); } break; case 'core:TargetClosureState': this.targetPosition?.updateValue(this.reversedValue(value)); if (!this.device.hasState('core:ClosureState')) { this.currentPosition?.updateValue(this.reversedValue(value)); } break; } } } ================================================ FILE: src/mappers/Screen.ts ================================================ import RollerShutter from './RollerShutter'; export default class Screen extends RollerShutter { } ================================================ FILE: src/mappers/Shutter.ts ================================================ import RollerShutter from './RollerShutter'; export default class Shutter extends RollerShutter { } ================================================ FILE: src/mappers/Siren.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic, CharacteristicSetCallback } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class Siren extends Mapper { protected mute: Characteristic | undefined; protected volume: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.Speaker); this.mute = service.getCharacteristic(Characteristics.Mute); this.volume = service.getCharacteristic(Characteristics.Volume); this.mute.onSet(this.setMute.bind(this)); this.volume.onSet(this.setVolume.bind(this)); this.mute.updateValue(true); return service; } protected getMuteCommands(value): Command | Array { return new Command(value ? 'off' : 'on'); } protected async setMute(value) { const action = await this.executeCommands(this.getMuteCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected getVolumeCommands(value): Command | Array { return new Command('setVolume', value); } protected async setVolume(value) { const action = await this.executeCommands(this.getVolumeCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.COMPLETED: break; case ExecutionState.FAILED: break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'core:OnOffState': this.mute?.updateValue(value === 'off'); break; } } } ================================================ FILE: src/mappers/SmokeSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class SmokeSensor extends Mapper { protected smoke: Characteristic | undefined; protected active: Characteristic | undefined; protected fault: Characteristic | undefined; protected battery: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.SmokeSensor); this.smoke = service.getCharacteristic(Characteristics.SmokeDetected); if ( this.device.hasState('core:SensorDefectState') || this.device.hasState('io:SensorDefMaintenanceSensorPartBatteryStateectState') || this.device.hasState('io:MaintenanceRadioPartBatteryState') || this.device.hasState('io:SensorRoomState') ) { this.fault = service.getCharacteristic(Characteristics.StatusFault); this.battery = service.getCharacteristic(Characteristics.StatusLowBattery); } if (this.device.hasState('core:StatusState')) { this.active = service.getCharacteristic(Characteristics.StatusActive); } return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:StatusState': this.active?.updateValue(value === 'available'); break; case 'core:SmokeState': this.smoke?.updateValue(value === 'detected'); break; case 'core:SensorDefectState': switch (value) { case 'lowBattery': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_LOW); break; case 'maintenanceRequired': case 'dead': this.fault?.updateValue(Characteristics.StatusFault.GENERAL_FAULT); break; case 'noDefect': this.fault?.updateValue(Characteristics.StatusFault.NO_FAULT); this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_NORMAL); break; } break; case 'io:MaintenanceRadioPartBatteryState': case 'io:MaintenanceSensorPartBatteryState': switch (value) { case 'absence': case 'normal': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_NORMAL); break; case 'low': this.battery?.updateValue(Characteristics.StatusLowBattery.BATTERY_LEVEL_LOW); break; } break; case 'io:SensorRoomState': switch (value) { case 'clean': this.fault?.updateValue(Characteristics.StatusFault.NO_FAULT); break; case 'dirty': this.fault?.updateValue(Characteristics.StatusFault.GENERAL_FAULT); break; } break; } } } ================================================ FILE: src/mappers/SwingingShutter.ts ================================================ import RollerShutter from './RollerShutter'; export default class SwingingShutter extends RollerShutter { } ================================================ FILE: src/mappers/TemperatureSensor.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import Mapper from '../Mapper'; export default class TemperatureSensor extends Mapper { protected temperature: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.TemperatureSensor); this.temperature = service.getCharacteristic(Characteristics.CurrentTemperature); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:TemperatureState': this.temperature?.updateValue(value > 200 ? (value - 273.15) : value); break; } } } ================================================ FILE: src/mappers/VenetianBlind.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import RollerShutter from './RollerShutter'; export default class VenetianBlind extends RollerShutter { protected currentAngle: Characteristic | undefined; protected targetAngle: Characteristic | undefined; protected blindMode; protected applyConfig(config) { super.applyConfig(config); this.blindMode = config['blindMode'] || false; } protected registerMainService() { const service = super.registerMainService(); if (!this.stateless) { this.currentAngle = service?.getCharacteristic(Characteristics.CurrentHorizontalTiltAngle); this.targetAngle = service?.getCharacteristic(Characteristics.TargetHorizontalTiltAngle); this.targetAngle?.setProps({ minStep: 10 }); this.targetAngle?.onSet(this.debounce(this.setTargetAnglePosition)); if (this.blindMode && this.currentAngle) { service?.removeCharacteristic(this.currentAngle); } if (this.blindMode && this.targetAngle) { service?.removeCharacteristic(this.targetAngle); } } return service; } protected orientationToAngle(value) { return Math.round((value * 1.8) - 90); } protected angleToOrientation(value) { return Math.round((value + 90) / 1.8); } protected getTargetCommands(value): Command | Command[] { if (this.stateless) { if (value === 100) { return new Command('open'); } else if (value === 0) { return new Command('close'); } else { if (this.movementDuration > 0) { const delta = value - Number(this.currentPosition!.value); return new Command(delta > 0 ? 'open' : 'close'); } else { return new Command('my'); } } } else if (this.blindMode) { if (value === 100) { return new Command('open'); } else if (this.device.hasCommand('setClosureAndOrientation')) { return new Command('setClosureAndOrientation', [100, this.reversedValue(value)]); } else { return [ new Command('setClosure', 100), new Command('setOrientation', this.reversedValue(value)), ]; } } else if (this.device.hasCommand('setClosureAndOrientation')) { return new Command('setClosureAndOrientation', [ this.reversedValue(value), this.angleToOrientation(this.targetAngle?.value), ]); } else { const commands = [ new Command('setClosure', this.reversedValue(value)), ]; if (this.device.hasCommand('setOrientation')) { commands.push(new Command('setOrientation', this.angleToOrientation(this.targetAngle?.value))); } return commands; } } protected getTargetAngleCommands(value) { if (this.stateless) { return []; } else if (this.device.hasCommand('setClosureAndOrientation')) { return new Command('setClosureAndOrientation', [ this.reversedValue(this.targetPosition?.value), this.angleToOrientation(value), ]); } else { const commands = [ new Command('setClosure', this.reversedValue(this.targetPosition?.value)), ]; if (this.device.hasCommand('setOrientation')) { commands.push(new Command('setOrientation', this.angleToOrientation(value))); } return commands; } } async setTargetAnglePosition(value) { const action = await this.executeCommands(this.getTargetAngleCommands(value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.FAILED: if (this.currentAngle) { this.targetAngle?.updateValue(this.currentAngle.value); } break; } }); } protected onStateChanged(name, value) { if (this.blindMode) { switch (name) { case 'core:OpenClosedState': case 'core:SlateOrientationState': if (this.device.get('core:OpenClosedState') === 'closed') { const position = this.reversedValue(this.device.get('core:SlateOrientationState')); const target = Number(this.targetPosition?.value); if (Number.isInteger(position)) { this.currentPosition?.updateValue(position); if (this.isIdle || Math.round(position / 5) === Math.round(target / 5)) { this.targetPosition?.updateValue(position); } } } else { this.currentPosition?.updateValue(100); if (this.isIdle) { this.targetPosition?.updateValue(100); } } break; default: break; } } else { super.onStateChanged(name, value); switch (name) { case 'core:SlateOrientationState': this.currentAngle?.updateValue(this.orientationToAngle(value)); if (this.isIdle) { this.targetAngle?.updateValue(this.orientationToAngle(value)); } break; default: break; } } } } ================================================ FILE: src/mappers/VentilationSystem/DimplexVentilationInletOutlet.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import VentilationSystem from '../VentilationSystem'; export default class DimplexVentilationInletOutlet extends VentilationSystem { protected getTargetStateCommands(value): Command | Array { switch(value) { case Characteristics.TargetAirPurifierState.AUTO: return new Command('auto'); case Characteristics.TargetAirPurifierState.MANUAL: default: return new Command('max'); } } } ================================================ FILE: src/mappers/VentilationSystem.ts ================================================ import { Characteristics, Services } from '../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import Mapper from '../Mapper'; export default class VentilationSystem extends Mapper { protected active: Characteristic | undefined; protected currentState: Characteristic | undefined; protected targetState: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.AirPurifier); this.active = service.getCharacteristic(Characteristics.Active); this.currentState = service.getCharacteristic(Characteristics.CurrentAirPurifierState); this.targetState = service.getCharacteristic(Characteristics.TargetAirPurifierState); this.targetState?.onSet(this.setTargetState.bind(this)); return service; } protected getTargetStateCommands(value): Command | Array { switch (value) { case Characteristics.TargetAirPurifierState.AUTO: return new Command('setAirDemandMode', 'auto'); case Characteristics.TargetAirPurifierState.MANUAL: default: return new Command('setAirDemandMode', 'boost'); } } protected async setTargetState(value) { const action = await this.executeCommands(this.getTargetStateCommands(value)); action.on('update', (state) => { switch (state) { case ExecutionState.COMPLETED: if (this.stateless) { this.currentState?.updateValue(value); } break; case ExecutionState.FAILED: if (this.currentState) { this.targetState?.updateValue(this.currentState.value); } break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'io:AirDemandModeState': switch (value) { case 'auto': this.targetState?.updateValue(Characteristics.TargetAirPurifierState.AUTO); break; default: this.targetState?.updateValue(Characteristics.TargetAirPurifierState.MANUAL); break; } break; } } } ================================================ FILE: src/mappers/WaterHeatingSystem/AtlanticPassAPCDHW.ts ================================================ import { Characteristics } from '../../Platform'; import { Command } from 'overkiz-client'; import WaterHeatingSystem from '../WaterHeatingSystem'; export default class AtlanticPassAPCDHW extends WaterHeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['eco', 'prog']; protected registerServices() { const services = super.registerServices(); if (this.device.hasCommand('setBoostOnOffState')) { const boost = this.registerSwitchService('boost'); services.push(boost); } return services; } protected getTargetStateCommands(value): Command | Array { const commands: Array = []; switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: commands.push(new Command('setDHWOnOffState', 'on')); if (this.prog?.value) { commands.push(new Command('setPassAPCDHWMode', 'internalScheduling')); } else { if (this.eco?.value) { commands.push(new Command('setPassAPCDHWMode', 'eco')); } else { commands.push(new Command('setPassAPCDHWMode', 'comfort')); } } break; case Characteristics.TargetHeatingCoolingState.OFF: commands.push(new Command('setDHWOnOffState', 'off')); break; } return commands; } protected getTargetTemperatureCommands(value): Command | Array { if (this.targetState?.value === Characteristics.TargetHeatingCoolingState.COOL) { return new Command('setEcoTargetDHWTemperature', value); } else { return new Command('setComfortTargetDHWTemperature', value); } } protected getOnCommands(value): Command | Array { return new Command('setBoostOnOffState', value ? 'on' : 'off'); } protected onStateChanged(name: string, value) { switch (name) { case 'core:TargetDHWTemperatureState': this.onTemperatureUpdate(value); //this.postpone(this.computeStates); break; case 'core:DHWOnOffState': case 'io:PassAPCDHWModeState': case 'io:PassAPCDHWProfileState': case 'core:ComfortTargetDHWTemperatureState': case 'core:EcoTargetDHWTemperatureState': this.postpone(this.computeStates); break; case 'core:BoostOnOffState': this.on?.updateValue(value === 'on'); break; default: super.onStateChanged(name, value); break; } } protected computeStates() { let targetState; if (this.device.get('core:DHWOnOffState') === 'on') { targetState = Characteristics.TargetHeatingCoolingState.AUTO; switch (this.device.get('io:PassAPCDHWModeState')) { case 'off': case 'stop': targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetTemperature?.updateValue(this.device.get('core:TargetDHWTemperatureState')); break; case 'internalScheduling': case 'externalScheduling': this.prog?.updateValue(true); if (this.device.get('io:PassAPCDHWProfileState') === 'comfort') { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.targetTemperature?.updateValue(this.device.get('core:ComfortTargetDHWTemperatureState')); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.targetTemperature?.updateValue(this.device.get('core:EcoTargetDHWTemperatureState')); } break; case 'comfort': this.prog?.updateValue(false); this.eco?.updateValue(false); this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); this.targetTemperature?.updateValue(this.device.get('core:ComfortTargetDHWTemperatureState')); break; case 'eco': this.prog?.updateValue(false); this.eco?.updateValue(true); this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); this.targetTemperature?.updateValue(this.device.get('core:EcoTargetDHWTemperatureState')); break; } } else { targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); this.targetTemperature?.updateValue(this.device.get('core:TargetDHWTemperatureState')); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/WaterHeatingSystem/DomesticHotWaterProduction/AtlanticDomesticHotWaterProductionV2_SPLIT_IOComponent.ts ================================================ import { Service } from 'homebridge'; import { Command } from 'overkiz-client'; import { Characteristics } from '../../../Platform'; import DomesticHotWaterProduction from '../DomesticHotWaterProduction'; export default class AtlanticDomesticHotWaterProductionV2_SPLIT_IOComponent extends DomesticHotWaterProduction { protected THERMOSTAT_CHARACTERISTICS = ['eco']; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService(): Service { const service = super.registerMainService(); this.targetTemperature?.setProps({ minValue: 50.0, maxValue: 54.5, validValues: [50, 52, 54, 54.5, 55], minStep: 2, }); return service; } protected getTargetTemperatureCommands(value): Command | Array { const safeValue = value === 54 ? 54.5 : value; return new Command('setTargetTemperature', safeValue); } protected getTargetStateCommands(value): Command | Array | undefined { const commands = Array(); if (this.targetState?.value === Characteristics.TargetHeatingCoolingState.OFF) { commands.push(new Command('setCurrentOperatingMode', { 'relaunch': 'off', 'absence': 'off' })); } switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: commands.push(new Command('setDHWMode', 'autoMode')); break; case Characteristics.TargetHeatingCoolingState.HEAT: if (this.eco?.value) { commands.push(new Command('setDHWMode', 'manualEcoActive')); } else { commands.push(new Command('setDHWMode', 'manualEcoInactive')); } break; case Characteristics.TargetHeatingCoolingState.OFF: commands.push(new Command('setCurrentOperatingMode', { 'relaunch': 'off', 'absence': 'on' })); break; } return commands; } protected getOnCommands(value): Command | Array { return new Command('setCurrentOperatingMode', { 'relaunch': value ? 'on' : 'off', 'absence': 'off' }); } protected onStateChanged(name: string, value) { switch (name) { case 'io:MiddleWaterTemperatureState': this.currentTemperature?.updateValue(value); break; case 'core:TargetTemperatureState': this.targetTemperature?.updateValue(value); break; case 'io:DHWModeState': case 'core:OperatingModeState': this.postpone(this.computeStates); break; } } protected computeStates() { let targetState; const operatingMode = this.device.get('core:OperatingModeState'); this.on?.updateValue(operatingMode.relaunch !== 'off'); if (operatingMode.absence === 'off') { switch (this.device.get('io:DHWModeState')) { case 'autoMode': targetState = Characteristics.TargetHeatingCoolingState.AUTO; break; case 'manualEcoInactive': this.eco?.updateValue(false); targetState = Characteristics.TargetHeatingCoolingState.HEAT; break; case 'manualEcoActive': this.eco?.updateValue(true); targetState = Characteristics.TargetHeatingCoolingState.HEAT; break; } const powerHeatPumpState = this.device.get('io:PowerHeatPumpState'); const powerHeatElectricalState = this.device.get('io:PowerHeatElectricalState'); if (powerHeatElectricalState > 100 || powerHeatPumpState > 100) { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); } else { this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } } else { targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/WaterHeatingSystem/DomesticHotWaterProduction.ts ================================================ import { Characteristics } from '../../Platform'; import { Characteristic } from 'homebridge'; import { Command, ExecutionState } from 'overkiz-client'; import WaterHeatingSystem from '../WaterHeatingSystem'; import { CurrentShowerCharacteristic, TargetShowerCharacteristic } from '../../CustomCharacteristics'; export default class DomesticHotWaterProduction extends WaterHeatingSystem { protected THERMOSTAT_CHARACTERISTICS = ['eco']; protected currentShower: Characteristic | undefined; protected targetShower: Characteristic | undefined; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.HEAT, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService() { const service = super.registerMainService(); service.addOptionalCharacteristic(TargetShowerCharacteristic); service.addOptionalCharacteristic(CurrentShowerCharacteristic); if (this.device.hasState('core:NumberOfShowerRemainingState')) { this.currentShower = service.getCharacteristic(CurrentShowerCharacteristic); this.targetShower = service.getCharacteristic(TargetShowerCharacteristic); this.targetShower.setProps({ minValue: this.device.getNumber('core:MinimalShowerManualModeState'), maxValue: this.device.getNumber('core:MaximalShowerManualModeState'), }); this.targetShower.onSet(this.setTargetShower.bind(this)); } return service; } protected registerServices() { const services = super.registerServices(); const boost = this.registerSwitchService('boost'); services.push(boost); return services; } protected getTargetStateCommands(value): Command | Array | undefined { const commands = Array(); if (this.device.hasCommand('setDHWMode')) { if (this.targetState?.value === Characteristics.TargetHeatingCoolingState.OFF) { commands.push(new Command('setAbsenceMode', 'off')); } switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: commands.push(new Command('setDHWMode', 'autoMode')); break; case Characteristics.TargetHeatingCoolingState.HEAT: if (this.eco?.value) { commands.push(new Command('setDHWMode', 'manualEcoActive')); } else { commands.push(new Command('setDHWMode', 'manualEcoInactive')); } break; case Characteristics.TargetHeatingCoolingState.OFF: commands.push(new Command('setAbsenceMode', 'on')); break; } return commands; } else if (this.device.hasCommand('setCurrentOperatingMode')) { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return new Command('setCurrentOperatingMode', { 'relaunch': 'off', 'absence': 'off' }); case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setCurrentOperatingMode', { 'relaunch': 'on', 'absence': 'off' }); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setCurrentOperatingMode', { 'relaunch': 'off', 'absence': 'on' }); } } else if (this.device.hasCommand('setBoostModeDuration')) { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return [ new Command('setBoostModeDuration', 0), new Command('setAwayModeDuration', 0), ]; case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setBoostModeDuration', 1); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setAwayModeDuration', 30); } } else if (this.device.hasCommand('setBoostMode')) { switch (value) { case Characteristics.TargetHeatingCoolingState.AUTO: return [ new Command('setBoostMode', 'off'), new Command('setAbsenceMode', 'off'), ]; case Characteristics.TargetHeatingCoolingState.HEAT: return new Command('setBoostMode', 'on'); case Characteristics.TargetHeatingCoolingState.OFF: return new Command('setAbsenceMode', 'on'); } } } protected getTargetTemperatureCommands(value): Command | Array { return new Command('setWaterTargetTemperature', value); } protected getOnCommands(value): Command | Array { return new Command('setBoostMode', value ? 'on' : 'off'); } async setTargetShower(value) { const previous = this.targetShower?.value; const action = await this.executeCommands(new Command('setExpectedNumberOfShower', value)); action.on('update', (state, data) => { switch (state) { case ExecutionState.FAILED: if (previous) { this.targetShower?.updateValue(previous); } break; } }); } protected onStateChanged(name: string, value) { switch (name) { case 'io:DHWBoostModeState': case 'modbuslink:DHWBoostModeState': this.on?.updateValue(value !== 'off'); break; case 'core:WaterTemperatureState': if (!this.device.hasState('io:MiddleWaterTemperatureState')) { this.currentTemperature?.updateValue(value); } break; case 'io:MiddleWaterTemperatureState': case 'modbuslink:MiddleWaterTemperatureState': this.currentTemperature?.updateValue(value); break; case 'core:TargetTemperatureState': case 'core:WaterTargetTemperatureState': this.targetTemperature?.updateValue(value); break; case 'io:DHWModeState': case 'io:DHWAbsenceModeState': this.postpone(this.computeStates, 'io'); break; case 'modbuslink:DHWModeState': case 'modbuslink:DHWAbsenceModeState': this.postpone(this.computeStates, 'modbuslink'); break; case 'core:NumberOfShowerRemainingState': this.currentShower?.updateValue(value); break; case 'core:ExpectedNumberOfShowerState': this.targetShower?.updateValue(value); break; } } protected computeStates(protcol) { let targetState; if (this.device.get(protcol+':DHWAbsenceModeState') === 'off') { switch (this.device.get(protcol+':DHWModeState')) { case 'autoMode': targetState = Characteristics.TargetHeatingCoolingState.AUTO; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); break; case 'manualEcoInactive': this.eco?.updateValue(false); targetState = Characteristics.TargetHeatingCoolingState.HEAT; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.HEAT); break; case 'manualEcoActive': this.eco?.updateValue(true); targetState = Characteristics.TargetHeatingCoolingState.HEAT; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.COOL); break; } } else { targetState = Characteristics.TargetHeatingCoolingState.OFF; this.currentState?.updateValue(Characteristics.CurrentHeatingCoolingState.OFF); } if (this.targetState !== undefined && targetState !== undefined && this.isIdle) { this.targetState.updateValue(targetState); } } } ================================================ FILE: src/mappers/WaterHeatingSystem/DomesticHotWaterTank.ts ================================================ import { Service } from 'homebridge'; import { Command } from 'overkiz-client'; import WaterHeatingSystem from '../WaterHeatingSystem'; export default class DomesticHotWaterTank extends WaterHeatingSystem { protected registerMainService(): Service { return this.registerSwitchService('boost'); } protected getOnCommands(value): Command | Array { return new Command('setForceHeating', value ? 'on' : 'off'); } protected onStateChanged(name: string, value) { switch (name) { case 'io:ForceHeatingState': this.on?.updateValue(value === 'on'); break; } } } ================================================ FILE: src/mappers/WaterHeatingSystem.ts ================================================ import { Characteristics } from '../Platform'; import { Service } from 'homebridge'; import HeatingSystem from './HeatingSystem'; export default class WaterHeatingSystem extends HeatingSystem { protected MIN_TEMP = 45; protected MAX_TEMP = 65; protected TARGET_MODES = [ Characteristics.TargetHeatingCoolingState.AUTO, Characteristics.TargetHeatingCoolingState.OFF, ]; protected registerMainService(): Service { const service = super.registerMainService(); service.setPrimaryService(true); this.targetTemperature?.setProps({ minStep: 1 }); return service; } } ================================================ FILE: src/mappers/WaterSensor.ts ================================================ import { Characteristic } from 'homebridge'; import { Characteristics, Services } from '../Platform'; import Mapper from '../Mapper'; export default class WaterSensor extends Mapper { protected leak: Characteristic | undefined; protected registerMainService() { const service = this.registerService(Services.LeakSensor); this.leak = service.getCharacteristic(Characteristics.LeakDetected); return service; } protected onStateChanged(name: string, value) { switch (name) { case 'core:WaterDetectionState': this.leak?.updateValue( value === 'detected' ? Characteristics.LeakDetected.LEAK_DETECTED : Characteristics.LeakDetected.LEAK_NOT_DETECTED ); break; } } } ================================================ FILE: src/mappers/Window.ts ================================================ import RollerShutter from './RollerShutter'; import { Characteristics, Services } from '../Platform'; export default class Window extends RollerShutter { protected registerMainService() { const service = this.registerService(Services.Window); this.currentPosition = service.getCharacteristic(Characteristics.CurrentPosition); this.targetPosition = service.getCharacteristic(Characteristics.TargetPosition); this.positionState = service.getCharacteristic(Characteristics.PositionState); if (this.stateless) { this.currentPosition.updateValue(this.initPosition); this.targetPosition.updateValue(this.initPosition); } else { this.obstructionDetected = service.getCharacteristic(Characteristics.ObstructionDetected); } this.positionState.updateValue(Characteristics.PositionState.STOPPED); this.targetPosition.onSet(this.debounce(this.setTargetPosition, [0, 100])); return service; } } ================================================ FILE: src/mappers/WindowHandle.ts ================================================ import { Characteristics } from '../Platform'; import ContactSensor from './ContactSensor'; export default class WindowHandle extends ContactSensor { protected onStateChanged(name: string, value) { switch (name) { case 'core:ThreeWayHandleDirectionState': switch (value) { case 'closed': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_DETECTED); break; case 'tilt': case 'open': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_NOT_DETECTED); break; } break; case 'core:OpenClosedState': switch (value) { case 'closed': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_DETECTED); break; case 'open': this.state?.updateValue(Characteristics.ContactSensorState.CONTACT_NOT_DETECTED); break; } break; } } } ================================================ FILE: src/settings.ts ================================================ /** * This is the name of the platform that users will use to register the plugin in the Homebridge config.json */ export const PLATFORM_NAME = 'Tahoma'; /** * This must match the name of your plugin as defined the package.json */ export const PLUGIN_NAME = 'homebridge-tahoma'; ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2018", // ~node10 "module": "commonjs", "lib": [ "es2015", "es2016", "es2017", "es2018" ], "declaration": true, "declarationMap": true, "sourceMap": true, "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "noImplicitAny": false, "resolveJsonModule": true }, "include": [ "src/", "src/lang/*.json" ], "exclude": [ "**/*.spec.ts" ] }