├── .github ├── FUNDING.yml └── workflows │ ├── hassfest.yaml │ ├── release.yaml │ └── validate.yaml ├── .readme_content ├── ecodevices_energy.jpg ├── ecodevices_entities.jpg ├── ecodevices_options.jpg └── ecodevices_params.jpg ├── .releaserc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── custom_components └── ecodevices │ ├── __init__.py │ ├── binary_sensor.py │ ├── config_flow.py │ ├── const.py │ ├── entity.py │ ├── manifest.json │ ├── sensor.py │ ├── strings.json │ └── translations │ ├── en.json │ └── fr.json ├── hacs.json └── scripts └── release.sh /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Aohzan] 2 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: home-assistant/actions/hassfest@master 15 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Semantic Release 15 | uses: cycjimmy/semantic-release-action@v4 16 | with: 17 | semantic_version: 24 18 | extra_plugins: | 19 | @semantic-release/commit-analyzer@13 20 | @semantic-release/release-notes-generator@14 21 | conventional-changelog-conventionalcommits@8 22 | @semantic-release/changelog@6 23 | @semantic-release/exec@6 24 | @semantic-release/git@10 25 | @semantic-release/github@11 26 | id: semantic 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/validate.yaml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - name: HACS validation 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" 18 | -------------------------------------------------------------------------------- /.readme_content/ecodevices_energy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aohzan/ecodevices/ee1b9dfcf1c0f83aaff2aa75ea64b897a540db18/.readme_content/ecodevices_energy.jpg -------------------------------------------------------------------------------- /.readme_content/ecodevices_entities.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aohzan/ecodevices/ee1b9dfcf1c0f83aaff2aa75ea64b897a540db18/.readme_content/ecodevices_entities.jpg -------------------------------------------------------------------------------- /.readme_content/ecodevices_options.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aohzan/ecodevices/ee1b9dfcf1c0f83aaff2aa75ea64b897a540db18/.readme_content/ecodevices_options.jpg -------------------------------------------------------------------------------- /.readme_content/ecodevices_params.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aohzan/ecodevices/ee1b9dfcf1c0f83aaff2aa75ea64b897a540db18/.readme_content/ecodevices_params.jpg -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "tagFormat": "${version}", 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | [ 6 | "@semantic-release/release-notes-generator", 7 | { 8 | "preset": "conventionalcommits" 9 | } 10 | ], 11 | [ 12 | "@semantic-release/changelog", 13 | { 14 | "changelogFile": "CHANGELOG.md", 15 | "changelogTitle": "# Changelog" 16 | } 17 | ], 18 | [ 19 | "@semantic-release/exec", 20 | { 21 | "prepareCmd": "scripts/release.sh ${nextRelease.version}" 22 | } 23 | ], 24 | [ 25 | "@semantic-release/git", 26 | { 27 | "assets": [ 28 | "custom_components/*/manifest.json", 29 | "CHANGELOG.md" 30 | ], 31 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 32 | } 33 | ], 34 | [ 35 | "@semantic-release/github", 36 | { 37 | "assets": [ 38 | "release.zip" 39 | ] 40 | } 41 | ] 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [5.4.0](https://github.com/Aohzan/ecodevices/compare/5.3.1...5.4.0) (2025-04-23) 4 | 5 | ### Features 6 | 7 | * add release ([ee5345c](https://github.com/Aohzan/ecodevices/commit/ee5345c3aa510857331082169ecd66885bd8e338)) 8 | 9 | ## 5.3.1 10 | 11 | * Fix error when last state was unavailable 12 | 13 | ## 5.3.0 14 | 15 | * Fix deprecations 16 | * Bump pyecodevices 17 | 18 | ## 5.2.0 19 | 20 | * Compare total sensors with previous value when possible to avoid wrong values 21 | 22 | ## 5.1.0 23 | 24 | * Add binary sensor in Tempo and HCHP modes to know if currently in HC or not 25 | * Add sensor in Tempo mode to know today and tomorrow colors 26 | * Teleinfo instant power is now in VA instead of W 27 | * Remove warning message for HCHP and Tempo total entity when equal to 0 28 | 29 | ## 5.0.0 30 | 31 | * Add Tempo support 32 | * Add Teleinfo type input: `base`, `hchp` or `tempo` (automatic migration during upgrade) 33 | 34 | ## 4.8.0 35 | 36 | * Add divider factor for both meters to allow user to choose a compatible unit of measurement (example, m³ for gas instead of dm³ with a divider factor of 1000) 37 | * Update translation 38 | 39 | ## 4.7.0 40 | 41 | * add some teleinfo attributes: `Avertissement depassement`, `Conso instant general`, `Puissance apparente` 42 | * code enhancement 43 | * bump dependencies 44 | 45 | ## 4.6.1 46 | 47 | * fix DeviceInfo for HA 2023.8 release 48 | 49 | ## 4.6.0 50 | 51 | * bump pyecodevices (force xml encoding) 52 | 53 | ## 4.5.0 54 | 55 | * fix T2 attributes 56 | * do not raise error when total value not greater than 0, just warning message 57 | 58 | ## 4.4.0 59 | 60 | * Add `type_heures_demain` info on teleinfo inputs 61 | 62 | ## 4.3.2 63 | 64 | * Fix coordinator data keys 65 | 66 | ## 4.3.1 67 | 68 | * Fix values assignments 69 | 70 | ## 4.3.0 71 | 72 | * Add support of teleinfo "jours bleu,blanc,rouge" 73 | * Fix deprecation types and various improvments 74 | 75 | ## 4.2.2 76 | 77 | * Remove wrong old code 78 | 79 | ## 4.2.1 80 | 81 | * Fix incrementation 82 | 83 | ## 4.2.0 84 | 85 | * Replace deprecated async_get_registry method 86 | 87 | ## 4.1.0 88 | 89 | * Add instant entity for counters 90 | 91 | ## 4.0.0 92 | 93 | * You have to delete the integration before upgrade, and make installation again 94 | * Handle multiple Eco-Device on the same hostname but different port 95 | * Change default teleinfo unit from `VA` to `W` 96 | * Change entity and device unique id 97 | 98 | ## 3.1.0 99 | 100 | * Add HC/HP maangements for teleinfo inputs 101 | * Set static units for teleinfo inputs 102 | * Code improvments 103 | 104 | ## 3.0.2 105 | 106 | * Fix empty dict in config flow 107 | 108 | ## 3.0.1 109 | 110 | * Fix optional keys error in config flow 111 | 112 | ## 3.0.0 113 | 114 | * /!\ Need Home-Assistant version 202111 at least 115 | * /!\ Breaking : the total value is now the same that display on Eco-Devices Web UI, and no more multiplied by 1000, watch the unit of measurement which can change (example: Wh => kWh, dm³ => m³) 116 | * Full configuration since options UI 117 | * Add device configuration link 118 | 119 | ## 2.5.0 120 | 121 | * Allow to set a different unit of measurement for counters total sensors [issue#12](https://github.com/Aohzan/ecodevices/issues/12) 122 | * Don't report 0 for total sensors [issue#13](https://github.com/Aohzan/ecodevices/issues/13) 123 | 124 | ## 2.4.0 125 | 126 | * Remove name parameters 127 | * Use new sensor properties 128 | 129 | ## 2.3.1 130 | 131 | * Fix total for c1 132 | 133 | ## 2.3.0 134 | 135 | * /!\ Need Home-Assistant version 202109 at least 136 | * Add entities for total 137 | * Add state class property and adjust default classes for Energy dashboard compatibility 138 | 139 | ## 2.2.0 140 | 141 | * Add HACS manifest 142 | 143 | ## 2.1.0 144 | 145 | * Add attributes for teleinfo inputs 146 | 147 | ## 2.0.0 148 | 149 | * Major rewrite: remove old entry and entities before upgrade 150 | * Use updated async api calls 151 | * Add attributes to sensor to get all informations from Eco Devices 152 | 153 | ## 1.0.0 154 | 155 | * Initial release 156 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eco-Devices integration for Home Assistant 2 | 3 | ![GitHub release (with filter)](https://img.shields.io/github/v/release/aohzan/ecodevices) ![GitHub](https://img.shields.io/github/license/aohzan/ecodevices) [![Donate](https://img.shields.io/badge/$-support-ff69b4.svg?style=flat)](https://github.com/sponsors/Aohzan) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) 4 | 5 | This a _custom component_ for [Home Assistant](https://www.home-assistant.io/). 6 | The `ecodevices` integration allows you to get information from [GCE Eco-Devices](http://gce-electronics.com/fr/carte-relais-ethernet-module-rail-din/409-teleinformation-ethernet-ecodevices.html) (/!\ not the RT2). 7 | 8 | You will get two sensors by input enabled with all attributes availables. 9 | 10 | ## Installation 11 | 12 | ### HACS 13 | 14 | HACS > Integrations > Explore & Add Repositories > GCE Eco-Devices > Install this repository 15 | 16 | ### Manually 17 | 18 | Copy the `custom_components/ecodevices` folder into the config folder. 19 | 20 | ## Configuration 21 | 22 | To add ecodevices to your installation, go to Configuration >> Integrations in the UI, click the button with + sign and from the list of integrations select GCE Eco-Devices. 23 | 24 | ## Example 25 | 26 | ![[Energy Dashboard]](.readme_content/ecodevices_energy.jpg) 27 | ![[Device page]](.readme_content/ecodevices_entities.jpg) 28 | ![[Options]](.readme_content/ecodevices_options.jpg) 29 | ![[Params]](.readme_content/ecodevices_params.jpg) 30 | 31 | ## More entities 32 | 33 | If you want individual entities for more informations, you can get it from main sensor attributes, for example: 34 | 35 | ```yaml 36 | template: 37 | - sensor: 38 | - name: "Intensité phase 1" 39 | unique_id: intensity_phase_1 40 | unit_of_measurement: "A" 41 | state: "{{ state_attr('sensor.compteur_linky', 'intensite_now_ph1') | int }}" 42 | ``` 43 | -------------------------------------------------------------------------------- /custom_components/ecodevices/__init__.py: -------------------------------------------------------------------------------- 1 | """GCE Eco-Devices integration.""" 2 | 3 | import asyncio 4 | from datetime import timedelta 5 | import logging 6 | 7 | from pyecodevices import ( 8 | EcoDevices, 9 | EcoDevicesCannotConnectError, 10 | EcoDevicesInvalidAuthError, 11 | ) 12 | 13 | from homeassistant.config_entries import ConfigEntry 14 | from homeassistant.const import ( 15 | CONF_HOST, 16 | CONF_PASSWORD, 17 | CONF_PORT, 18 | CONF_SCAN_INTERVAL, 19 | CONF_USERNAME, 20 | ) 21 | from homeassistant.core import HomeAssistant 22 | from homeassistant.exceptions import ConfigEntryNotReady 23 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 24 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 25 | 26 | from .const import ( 27 | CONF_T1_ENABLED, 28 | CONF_T1_TYPE, 29 | CONF_T2_ENABLED, 30 | CONF_T2_TYPE, 31 | CONF_TI_TYPE_BASE, 32 | CONF_TI_TYPE_HCHP, 33 | CONTROLLER, 34 | COORDINATOR, 35 | DOMAIN, 36 | PLATFORMS, 37 | UNDO_UPDATE_LISTENER, 38 | ) 39 | 40 | _LOGGER = logging.getLogger(__name__) 41 | 42 | 43 | async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 44 | """Migrate old config entry.""" 45 | 46 | if entry.version == 1: 47 | _LOGGER.debug("Entry version %s needs migration", entry.version) 48 | new_options = entry.options.copy() 49 | 50 | t1_enabled = new_options.get( 51 | CONF_T1_ENABLED, entry.data.get(CONF_T1_ENABLED, False) 52 | ) 53 | t2_enabled = new_options.get( 54 | CONF_T2_ENABLED, entry.data.get(CONF_T2_ENABLED, False) 55 | ) 56 | t1_hchp = new_options.get("t1_hchp", entry.data.get("t1_hchp", False)) 57 | t2_hchp = new_options.get("t2_hchp", entry.data.get("t2_hchp", False)) 58 | 59 | if t1_enabled: 60 | new_options[CONF_T1_TYPE] = ( 61 | CONF_TI_TYPE_HCHP if t1_hchp else CONF_TI_TYPE_BASE 62 | ) 63 | _LOGGER.debug("Set T1 type to %s", new_options[CONF_T1_TYPE]) 64 | if t2_enabled: 65 | new_options[CONF_T2_TYPE] = ( 66 | CONF_TI_TYPE_HCHP if t2_hchp else CONF_TI_TYPE_BASE 67 | ) 68 | _LOGGER.debug("Set T2 type to %s", new_options[CONF_T2_TYPE]) 69 | 70 | entry.version = 2 71 | 72 | hass.config_entries.async_update_entry( 73 | entry, data=entry.data, options=new_options 74 | ) 75 | _LOGGER.debug("Migration to version %s successful", entry.version) 76 | 77 | return True 78 | 79 | 80 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 81 | """Set up Eco-Devices from a config entry.""" 82 | hass.data.setdefault(DOMAIN, {}) 83 | config = entry.data 84 | options = entry.options 85 | 86 | scan_interval = options.get(CONF_SCAN_INTERVAL, config.get(CONF_SCAN_INTERVAL)) 87 | username = options.get(CONF_USERNAME, config.get(CONF_USERNAME)) 88 | password = options.get(CONF_PASSWORD, config.get(CONF_PASSWORD)) 89 | 90 | session = async_get_clientsession(hass, False) 91 | 92 | controller = EcoDevices( 93 | config[CONF_HOST], 94 | config[CONF_PORT], 95 | username, 96 | password, 97 | session=session, 98 | ) 99 | 100 | try: 101 | await controller.get_info() 102 | except EcoDevicesCannotConnectError as exception: 103 | raise ConfigEntryNotReady from exception 104 | 105 | async def async_update_data(): 106 | """Fetch data from API.""" 107 | try: 108 | return await controller.global_get() 109 | except EcoDevicesInvalidAuthError as err: 110 | raise UpdateFailed("Authentication error on Eco-Devices") from err 111 | except EcoDevicesCannotConnectError as err: 112 | raise UpdateFailed(f"Failed to communicating with API: {err}") from err 113 | 114 | coordinator = DataUpdateCoordinator( 115 | hass, 116 | _LOGGER, 117 | name=DOMAIN, 118 | update_method=async_update_data, 119 | update_interval=timedelta(seconds=scan_interval), 120 | ) 121 | 122 | await coordinator.async_refresh() 123 | 124 | if not coordinator.last_update_success: 125 | raise ConfigEntryNotReady 126 | 127 | undo_listener = entry.add_update_listener(_async_update_listener) 128 | 129 | hass.data[DOMAIN][entry.entry_id] = { 130 | CONTROLLER: controller, 131 | COORDINATOR: coordinator, 132 | UNDO_UPDATE_LISTENER: undo_listener, 133 | } 134 | 135 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 136 | 137 | return True 138 | 139 | 140 | async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): 141 | """Handle options update.""" 142 | await hass.config_entries.async_reload(entry.entry_id) 143 | 144 | 145 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 146 | """Unload a config entry.""" 147 | unload_ok = all( 148 | await asyncio.gather( 149 | *( 150 | hass.config_entries.async_forward_entry_unload(entry, component) 151 | for component in PLATFORMS 152 | ) 153 | ) 154 | ) 155 | 156 | hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() 157 | 158 | if unload_ok: 159 | hass.data[DOMAIN].pop(entry.entry_id) 160 | 161 | return unload_ok 162 | -------------------------------------------------------------------------------- /custom_components/ecodevices/binary_sensor.py: -------------------------------------------------------------------------------- 1 | """Support for the GCE Eco-Devices binary sensors.""" 2 | 3 | import logging 4 | 5 | from pyecodevices import EcoDevices 6 | 7 | from homeassistant.components.binary_sensor import BinarySensorEntity 8 | from homeassistant.config_entries import ConfigEntry 9 | from homeassistant.core import HomeAssistant 10 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 11 | from homeassistant.helpers.update_coordinator import ( 12 | CoordinatorEntity, 13 | DataUpdateCoordinator, 14 | ) 15 | from homeassistant.util import slugify 16 | 17 | from .const import ( 18 | CONF_T1_ENABLED, 19 | CONF_T1_TYPE, 20 | CONF_T2_ENABLED, 21 | CONF_T2_TYPE, 22 | CONF_TI_TYPE_HCHP, 23 | CONF_TI_TYPE_TEMPO, 24 | CONTROLLER, 25 | COORDINATOR, 26 | DEFAULT_T1_NAME, 27 | DEFAULT_T2_NAME, 28 | DOMAIN, 29 | ) 30 | from .entity import get_device_info 31 | 32 | _LOGGER = logging.getLogger(__name__) 33 | 34 | 35 | async def async_setup_entry( 36 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 37 | ) -> None: 38 | """Set up the GCE Eco-Devices platform.""" 39 | data = hass.data[DOMAIN][entry.entry_id] 40 | controller = data[CONTROLLER] 41 | coordinator = data[COORDINATOR] 42 | config = entry.data 43 | options = entry.options 44 | 45 | t1_enabled = options.get(CONF_T1_ENABLED, config.get(CONF_T1_ENABLED)) 46 | t1_type = options.get(CONF_T1_TYPE, config.get(CONF_T1_TYPE)) 47 | t2_enabled = options.get(CONF_T2_ENABLED, config.get(CONF_T2_ENABLED)) 48 | t2_type = options.get(CONF_T2_TYPE, config.get(CONF_T2_TYPE)) 49 | 50 | entities: list[BinarySensorEntity] = [] 51 | 52 | for input_number in (1, 2): 53 | if t1_enabled and input_number == 1 or t2_enabled and input_number == 2: 54 | _LOGGER.debug("Add the teleinfo %s binary_sensor entities", input_number) 55 | if t1_type in (CONF_TI_TYPE_HCHP, CONF_TI_TYPE_TEMPO) or t2_type in ( 56 | CONF_TI_TYPE_HCHP, 57 | CONF_TI_TYPE_TEMPO, 58 | ): 59 | prefix_name = DEFAULT_T1_NAME if input_number == 1 else DEFAULT_T2_NAME 60 | entities.append( 61 | TeleinfoInputHeuresCreuses( 62 | controller, 63 | coordinator, 64 | input_number=input_number, 65 | input_name="PTEC", 66 | name=f"{prefix_name} Heures Creuses", 67 | ) 68 | ) 69 | 70 | if entities: 71 | async_add_entities(entities) 72 | 73 | 74 | class TeleinfoInputHeuresCreuses(CoordinatorEntity, BinarySensorEntity): 75 | """Representation of a Eco-Devices HC binary sensor.""" 76 | 77 | _attr_icon = "mdi:cash-clock" 78 | 79 | def __init__( 80 | self, 81 | controller: EcoDevices, 82 | coordinator: DataUpdateCoordinator, 83 | input_number: int, 84 | input_name: str, 85 | name: str, 86 | ) -> None: 87 | """Initialize the binary sensor.""" 88 | super().__init__(coordinator) 89 | self.controller = controller 90 | self._input_number = input_number 91 | self._input_name = input_name 92 | self._attr_name = name 93 | self._attr_unique_id = slugify( 94 | f"{DOMAIN}_{self.controller.mac_address}_binary_sensor_{self._input_number}_{self._input_name}" 95 | ) 96 | self._attr_device_info = get_device_info(self.controller) 97 | 98 | @property 99 | def is_on(self) -> bool | None: 100 | """Return the state.""" 101 | if type_heure := self.coordinator.data.get( 102 | f"T{self._input_number}_{self._input_name}" 103 | ): 104 | if type_heure.startswith("HC"): 105 | return True 106 | return False 107 | return None 108 | -------------------------------------------------------------------------------- /custom_components/ecodevices/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow to configure the GCE Eco-Devices integration.""" 2 | 3 | from typing import Any 4 | 5 | from pyecodevices import ( 6 | EcoDevices, 7 | EcoDevicesCannotConnectError, 8 | EcoDevicesInvalidAuthError, 9 | ) 10 | import voluptuous as vol 11 | 12 | from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_DEVICE_CLASSES 13 | from homeassistant.config_entries import ( 14 | HANDLERS, 15 | ConfigEntry, 16 | ConfigFlow, 17 | ConfigFlowResult, 18 | OptionsFlow, 19 | ) 20 | from homeassistant.const import ( 21 | CONF_HOST, 22 | CONF_PASSWORD, 23 | CONF_PORT, 24 | CONF_SCAN_INTERVAL, 25 | CONF_USERNAME, 26 | ) 27 | from homeassistant.core import callback 28 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 29 | 30 | from .const import ( 31 | CONF_C1_DEVICE_CLASS, 32 | CONF_C1_DIVIDER_FACTOR, 33 | CONF_C1_ENABLED, 34 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT, 35 | CONF_C1_UNIT_OF_MEASUREMENT, 36 | CONF_C2_DEVICE_CLASS, 37 | CONF_C2_DIVIDER_FACTOR, 38 | CONF_C2_ENABLED, 39 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT, 40 | CONF_C2_UNIT_OF_MEASUREMENT, 41 | CONF_T1_ENABLED, 42 | CONF_T1_TYPE, 43 | CONF_T2_ENABLED, 44 | CONF_T2_TYPE, 45 | CONF_TI_TYPE_BASE, 46 | CONF_TI_TYPES, 47 | DEFAULT_SCAN_INTERVAL, 48 | DOMAIN, 49 | ) 50 | 51 | BASE_SCHEMA = vol.Schema( 52 | { 53 | vol.Required(CONF_HOST): str, 54 | vol.Required(CONF_PORT, default=80): int, 55 | vol.Optional(CONF_USERNAME): str, 56 | vol.Optional(CONF_PASSWORD): str, 57 | vol.Required(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): int, 58 | vol.Required(CONF_T1_ENABLED, default=False): bool, 59 | vol.Required(CONF_T2_ENABLED, default=False): bool, 60 | vol.Required(CONF_C1_ENABLED, default=False): bool, 61 | vol.Required(CONF_C2_ENABLED, default=False): bool, 62 | } 63 | ) 64 | 65 | 66 | @HANDLERS.register(DOMAIN) 67 | class EcoDevicesConfigFlow(ConfigFlow, domain=DOMAIN): 68 | """Handle a eco-devices config flow.""" 69 | 70 | VERSION = 2 71 | 72 | def __init__(self) -> None: 73 | """Initialize class variables.""" 74 | self.base_input: dict[str, Any] = {} 75 | 76 | async def async_step_user(self, user_input=None) -> ConfigFlowResult: 77 | """Handle a flow initialized by the user.""" 78 | errors: dict[str, str] = {} 79 | if user_input is None: 80 | return self.async_show_form( 81 | step_id="user", data_schema=BASE_SCHEMA, errors=errors 82 | ) 83 | 84 | entry = await self.async_set_unique_id( 85 | "_".join([DOMAIN, user_input[CONF_HOST], str(user_input[CONF_PORT])]) 86 | ) 87 | 88 | if entry: 89 | self._abort_if_unique_id_configured() 90 | 91 | session = async_get_clientsession(self.hass, False) 92 | 93 | errors = await _test_connection(session, user_input) 94 | 95 | if errors: 96 | return self.async_show_form( 97 | step_id="user", data_schema=BASE_SCHEMA, errors=errors 98 | ) 99 | 100 | self.base_input = user_input 101 | return await self.async_step_params() 102 | 103 | async def async_step_params(self, user_input=None) -> ConfigFlowResult: 104 | """Handle the param flow to customize the device accordly to enabled inputs.""" 105 | if user_input is not None: 106 | user_input.update(self.base_input) 107 | return self.async_create_entry( 108 | title=f"Eco-Devices {user_input[CONF_HOST]}:{user_input[CONF_PORT]}", 109 | data=user_input, 110 | ) 111 | 112 | return self.async_show_form( 113 | step_id="params", 114 | data_schema=vol.Schema(_get_params(self.base_input, {})), 115 | ) 116 | 117 | @staticmethod 118 | @callback 119 | def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: 120 | """Define the config flow to handle options.""" 121 | return EcoDevicesOptionsFlowHandler(config_entry) 122 | 123 | 124 | class EcoDevicesOptionsFlowHandler(OptionsFlow): 125 | """Handle a EcoDevices options flow.""" 126 | 127 | def __init__(self, config_entry: ConfigEntry) -> None: 128 | """Initialize.""" 129 | self.config_entry: ConfigEntry = config_entry 130 | self.base_input: dict[str, Any] = {} 131 | 132 | async def async_step_init(self, user_input) -> ConfigFlowResult: 133 | """Manage the options.""" 134 | errors = {} 135 | if user_input is not None: 136 | session = async_get_clientsession(self.hass, False) 137 | user_input[CONF_HOST] = self.config_entry.data[CONF_HOST] 138 | user_input[CONF_PORT] = self.config_entry.data[CONF_PORT] 139 | errors = await _test_connection(session, user_input) 140 | if not errors: 141 | self.base_input = user_input 142 | return await self.async_step_params() 143 | 144 | config = self.config_entry.data 145 | options = self.config_entry.options 146 | 147 | scan_interval = options.get(CONF_SCAN_INTERVAL, config.get(CONF_SCAN_INTERVAL)) 148 | username = options.get(CONF_USERNAME, config.get(CONF_USERNAME)) 149 | password = options.get(CONF_PASSWORD, config.get(CONF_PASSWORD)) 150 | t1_enabled = options.get(CONF_T1_ENABLED, config.get(CONF_T1_ENABLED)) 151 | t2_enabled = options.get(CONF_T2_ENABLED, config.get(CONF_T2_ENABLED)) 152 | c1_enabled = options.get(CONF_C1_ENABLED, config.get(CONF_C1_ENABLED)) 153 | c2_enabled = options.get(CONF_C2_ENABLED, config.get(CONF_C2_ENABLED)) 154 | 155 | options_schema = { 156 | vol.Optional(CONF_USERNAME, default=username): str, 157 | vol.Optional(CONF_PASSWORD, default=password): str, 158 | vol.Required(CONF_SCAN_INTERVAL, default=scan_interval): int, 159 | vol.Required(CONF_T1_ENABLED, default=t1_enabled): bool, 160 | vol.Required(CONF_T2_ENABLED, default=t2_enabled): bool, 161 | vol.Required(CONF_C1_ENABLED, default=c1_enabled): bool, 162 | vol.Required(CONF_C2_ENABLED, default=c2_enabled): bool, 163 | } 164 | 165 | return self.async_show_form( 166 | step_id="init", data_schema=vol.Schema(options_schema), errors=errors 167 | ) 168 | 169 | async def async_step_params(self, user_input=None) -> ConfigFlowResult: 170 | """Handle the param flow to customize the device accordly to enabled inputs.""" 171 | if user_input is not None: 172 | user_input.update(self.base_input) 173 | return self.async_create_entry( 174 | title=f"Eco-Devices {user_input[CONF_HOST]}:{user_input[CONF_PORT]}", 175 | data=user_input, 176 | ) 177 | 178 | config = self.config_entry.data 179 | options = self.config_entry.options 180 | 181 | base_params = { 182 | CONF_T1_TYPE: options.get( 183 | CONF_T1_TYPE, 184 | config.get(CONF_T1_TYPE, False), 185 | ), 186 | CONF_T2_TYPE: options.get( 187 | CONF_T2_TYPE, 188 | config.get(CONF_T2_TYPE, False), 189 | ), 190 | CONF_C1_DEVICE_CLASS: options.get( 191 | CONF_C1_DEVICE_CLASS, config.get(CONF_C1_DEVICE_CLASS) 192 | ), 193 | CONF_C1_UNIT_OF_MEASUREMENT: options.get( 194 | CONF_C1_UNIT_OF_MEASUREMENT, config.get(CONF_C1_UNIT_OF_MEASUREMENT) 195 | ), 196 | CONF_C1_DIVIDER_FACTOR: options.get( 197 | CONF_C1_DIVIDER_FACTOR, config.get(CONF_C1_DIVIDER_FACTOR, 1) 198 | ), 199 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT: options.get( 200 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT, 201 | config.get(CONF_C1_TOTAL_UNIT_OF_MEASUREMENT), 202 | ), 203 | CONF_C2_DEVICE_CLASS: options.get( 204 | CONF_C2_DEVICE_CLASS, config.get(CONF_C2_DEVICE_CLASS) 205 | ), 206 | CONF_C2_UNIT_OF_MEASUREMENT: options.get( 207 | CONF_C2_UNIT_OF_MEASUREMENT, config.get(CONF_C2_UNIT_OF_MEASUREMENT) 208 | ), 209 | CONF_C2_DIVIDER_FACTOR: options.get( 210 | CONF_C2_DIVIDER_FACTOR, config.get(CONF_C2_DIVIDER_FACTOR, 1) 211 | ), 212 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT: options.get( 213 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT, 214 | config.get(CONF_C2_TOTAL_UNIT_OF_MEASUREMENT), 215 | ), 216 | } 217 | 218 | return self.async_show_form( 219 | step_id="params", 220 | data_schema=vol.Schema(_get_params(self.base_input, base_params)), 221 | ) 222 | 223 | 224 | async def _test_connection(session, user_input): 225 | errors = {} 226 | 227 | controller = EcoDevices( 228 | user_input[CONF_HOST], 229 | user_input[CONF_PORT], 230 | user_input.get(CONF_USERNAME), 231 | user_input.get(CONF_PASSWORD), 232 | session=session, 233 | ) 234 | 235 | try: 236 | await controller.get_info() 237 | except EcoDevicesInvalidAuthError: 238 | errors["base"] = "invalid_auth" 239 | except EcoDevicesCannotConnectError: 240 | errors["base"] = "cannot_connect" 241 | 242 | return errors 243 | 244 | 245 | def _get_params(base_input, base_params): 246 | params_schema = {} 247 | if base_input[CONF_T1_ENABLED]: 248 | params_schema.update( 249 | { 250 | vol.Required( 251 | CONF_T1_TYPE, 252 | default=base_params.get(CONF_T1_TYPE, CONF_TI_TYPE_BASE), 253 | ): vol.All(str, vol.Lower, vol.In(CONF_TI_TYPES)), 254 | } 255 | ) 256 | 257 | if base_input[CONF_T2_ENABLED]: 258 | params_schema.update( 259 | { 260 | vol.Required( 261 | CONF_T2_TYPE, 262 | default=base_params.get(CONF_T2_TYPE, CONF_TI_TYPE_BASE), 263 | ): vol.All(str, vol.Lower, vol.In(CONF_TI_TYPES)), 264 | } 265 | ) 266 | 267 | if base_input[CONF_C1_ENABLED]: 268 | params_schema.update( 269 | { 270 | vol.Required( 271 | CONF_C1_DEVICE_CLASS, default=base_params.get(CONF_C1_DEVICE_CLASS) 272 | ): vol.All(str, vol.Lower, vol.In(SENSOR_DEVICE_CLASSES)), 273 | vol.Optional( 274 | CONF_C1_UNIT_OF_MEASUREMENT, 275 | default=base_params.get(CONF_C1_UNIT_OF_MEASUREMENT), 276 | ): str, 277 | vol.Required( 278 | CONF_C1_DIVIDER_FACTOR, 279 | default=base_params.get(CONF_C1_DIVIDER_FACTOR), 280 | ): int, 281 | vol.Optional( 282 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT, 283 | default=base_params.get(CONF_C1_TOTAL_UNIT_OF_MEASUREMENT), 284 | ): str, 285 | } 286 | ) 287 | 288 | if base_input[CONF_C2_ENABLED]: 289 | params_schema.update( 290 | { 291 | vol.Required( 292 | CONF_C2_DEVICE_CLASS, default=base_params.get(CONF_C2_DEVICE_CLASS) 293 | ): vol.All(str, vol.Lower, vol.In(SENSOR_DEVICE_CLASSES)), 294 | vol.Optional( 295 | CONF_C2_UNIT_OF_MEASUREMENT, 296 | default=base_params.get(CONF_C2_UNIT_OF_MEASUREMENT), 297 | ): str, 298 | vol.Required( 299 | CONF_C2_DIVIDER_FACTOR, 300 | default=base_params.get(CONF_C2_DIVIDER_FACTOR), 301 | ): int, 302 | vol.Optional( 303 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT, 304 | default=base_params.get(CONF_C2_TOTAL_UNIT_OF_MEASUREMENT), 305 | ): str, 306 | } 307 | ) 308 | 309 | return params_schema 310 | -------------------------------------------------------------------------------- /custom_components/ecodevices/const.py: -------------------------------------------------------------------------------- 1 | """Constant for the eco-devices integration.""" 2 | 3 | DOMAIN = "ecodevices" 4 | 5 | CONTROLLER = "controller" 6 | COORDINATOR = "coordinator" 7 | PLATFORMS = ["binary_sensor", "sensor"] 8 | UNDO_UPDATE_LISTENER = "undo_update_listener" 9 | 10 | CONF_T1_ENABLED = "t1_enabled" 11 | CONF_T1_TYPE = "t1_type" 12 | CONF_T2_ENABLED = "t2_enabled" 13 | CONF_T2_TYPE = "t2_type" 14 | CONF_C1_ENABLED = "c1_enabled" 15 | CONF_C1_UNIT_OF_MEASUREMENT = "c1_unit_of_measurement" 16 | CONF_C1_DIVIDER_FACTOR = "c1_divider_factor" 17 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT = "c1_total_unit_of_measurement" 18 | CONF_C1_DEVICE_CLASS = "c1_device_class" 19 | CONF_C2_ENABLED = "c2_enabled" 20 | CONF_C2_UNIT_OF_MEASUREMENT = "c2_unit_of_measurement" 21 | CONF_C2_DIVIDER_FACTOR = "c2_divider_factor" 22 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT = "c2_total_unit_of_measurement" 23 | CONF_C2_DEVICE_CLASS = "c2_device_class" 24 | 25 | DEFAULT_T1_NAME = "Teleinfo 1" 26 | DEFAULT_T2_NAME = "Teleinfo 2" 27 | DEFAULT_C1_NAME = "Meter 1" 28 | DEFAULT_C2_NAME = "Meter 2" 29 | DEFAULT_SCAN_INTERVAL = 5 30 | 31 | CONF_TI_TYPE_BASE = "base" 32 | CONF_TI_TYPE_HCHP = "hchp" 33 | CONF_TI_TYPE_TEMPO = "tempo" 34 | CONF_TI_TYPES = [ 35 | CONF_TI_TYPE_BASE, 36 | CONF_TI_TYPE_HCHP, 37 | CONF_TI_TYPE_TEMPO, 38 | ] 39 | 40 | TELEINFO_EXTRA_ATTR = { 41 | "type_heures": "PTEC", 42 | "souscription": "ISOUSC", 43 | "intensite_max": "IMAX", 44 | "intensite_max_ph1": "IMAX1", 45 | "intensite_max_ph2": "IMAX2", 46 | "intensite_max_ph3": "IMAX3", 47 | "intensite_now": "IINST", 48 | "intensite_now_ph1": "IINST1", 49 | "intensite_now_ph2": "IINST2", 50 | "intensite_now_ph3": "IINST3", 51 | "conso_instant_general": "PPAP", 52 | "puissance_apparente": "PAPP", 53 | "avertissement_depassement": "ADPS", 54 | "numero_compteur": "ADCO", 55 | "option_tarifaire": "OPTARIF", 56 | "index_base": "BASE", 57 | "etat": "MOTDETAT", 58 | "presence_potentiels": "PPOT", 59 | # HCHP 60 | "index_heures_creuses": "HCHC", 61 | "index_heures_pleines": "HCHP", 62 | "index_heures_normales": "EJPHN", 63 | "index_heures_pointes": "EJPHPM", 64 | "preavis_heures_pointes": "PEJP", 65 | "groupe_horaire": "HHPHC", 66 | # Tempo 67 | "index_heures_creuses_jour_bleu": "BBRHCJB", 68 | "index_heures_pleines_jour_bleu": "BBRHPJB", 69 | "index_heures_creuses_jour_blanc": "BBRHCJW", 70 | "index_heures_pleines_jour_blanc": "BBRHPJW", 71 | "index_heures_creuses_jour_rouge": "BBRHCJR", 72 | "index_heures_pleines_jour_rouge": "BBRHPJR", 73 | "type_heures_demain": "DEMAIN", 74 | } 75 | TELEINFO_TEMPO_ATTR = { 76 | "Jour Bleu HC": "BBRHCJB", 77 | "Jour Bleu HP": "BBRHPJB", 78 | "Jour Blanc HC": "BBRHCJW", 79 | "Jour Blanc HP": "BBRHPJW", 80 | "Jour Ro" + "uge HC": "BBRHCJR", # bypass codespell 81 | "Jour Ro" + "uge HP": "BBRHPJR", # bypass codespell 82 | } 83 | -------------------------------------------------------------------------------- /custom_components/ecodevices/entity.py: -------------------------------------------------------------------------------- 1 | """Generic entity for EcoDevices.""" 2 | 3 | from pyecodevices import EcoDevices 4 | 5 | from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo 6 | 7 | from .const import DOMAIN 8 | 9 | 10 | def get_device_info(controller: EcoDevices) -> DeviceInfo: 11 | """Get device info.""" 12 | return DeviceInfo( 13 | identifiers={(DOMAIN, controller.mac_address)}, 14 | manufacturer="GCE Electronics", 15 | model="Eco-Devices", 16 | name=f"Eco-Devices {controller.host}:{controller.port!s}", 17 | sw_version=controller.version, 18 | connections={(CONNECTION_NETWORK_MAC, controller.mac_address)}, 19 | configuration_url=f"http://{controller.host}:{controller.port}", 20 | ) 21 | -------------------------------------------------------------------------------- /custom_components/ecodevices/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "ecodevices", 3 | "name": "GCE Eco-Devices", 4 | "codeowners": [ 5 | "@Aohzan" 6 | ], 7 | "config_flow": true, 8 | "dependencies": [], 9 | "documentation": "https://github.com/Aohzan/ecodevices", 10 | "iot_class": "local_polling", 11 | "issue_tracker": "https://github.com/Aohzan/ecodevices/issues", 12 | "requirements": [ 13 | "xmltodict==0.13.0", 14 | "pyecodevices==1.7.0" 15 | ], 16 | "version": "5.4.0" 17 | } 18 | -------------------------------------------------------------------------------- /custom_components/ecodevices/sensor.py: -------------------------------------------------------------------------------- 1 | """Support for the GCE Eco-Devices.""" 2 | 3 | from collections.abc import Mapping 4 | import logging 5 | from typing import Any 6 | 7 | from pyecodevices import EcoDevices 8 | 9 | from homeassistant.components.sensor import ( 10 | DEVICE_CLASS_STATE_CLASSES, 11 | RestoreSensor, 12 | SensorDeviceClass, 13 | SensorStateClass, 14 | ) 15 | from homeassistant.config_entries import ConfigEntry 16 | from homeassistant.const import ( 17 | STATE_UNAVAILABLE, 18 | STATE_UNKNOWN, 19 | UnitOfApparentPower, 20 | UnitOfEnergy, 21 | ) 22 | from homeassistant.core import HomeAssistant 23 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 24 | from homeassistant.helpers.typing import StateType 25 | from homeassistant.helpers.update_coordinator import ( 26 | CoordinatorEntity, 27 | DataUpdateCoordinator, 28 | ) 29 | from homeassistant.util import slugify 30 | 31 | from .const import ( 32 | CONF_C1_DEVICE_CLASS, 33 | CONF_C1_DIVIDER_FACTOR, 34 | CONF_C1_ENABLED, 35 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT, 36 | CONF_C1_UNIT_OF_MEASUREMENT, 37 | CONF_C2_DEVICE_CLASS, 38 | CONF_C2_DIVIDER_FACTOR, 39 | CONF_C2_ENABLED, 40 | CONF_C2_TOTAL_UNIT_OF_MEASUREMENT, 41 | CONF_C2_UNIT_OF_MEASUREMENT, 42 | CONF_T1_ENABLED, 43 | CONF_T1_TYPE, 44 | CONF_T2_ENABLED, 45 | CONF_T2_TYPE, 46 | CONF_TI_TYPE_BASE, 47 | CONF_TI_TYPE_HCHP, 48 | CONF_TI_TYPE_TEMPO, 49 | CONTROLLER, 50 | COORDINATOR, 51 | DEFAULT_C1_NAME, 52 | DEFAULT_C2_NAME, 53 | DEFAULT_T1_NAME, 54 | DEFAULT_T2_NAME, 55 | DOMAIN, 56 | TELEINFO_EXTRA_ATTR, 57 | TELEINFO_TEMPO_ATTR, 58 | ) 59 | from .entity import get_device_info 60 | 61 | _LOGGER = logging.getLogger(__name__) 62 | 63 | 64 | async def async_setup_entry( 65 | hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback 66 | ) -> None: 67 | """Set up the GCE Eco-Devices platform.""" 68 | data = hass.data[DOMAIN][entry.entry_id] 69 | controller = data[CONTROLLER] 70 | coordinator = data[COORDINATOR] 71 | config = entry.data 72 | options = entry.options 73 | 74 | t1_enabled = options.get(CONF_T1_ENABLED, config.get(CONF_T1_ENABLED)) 75 | t1_type = options.get(CONF_T1_TYPE, config.get(CONF_T1_TYPE)) 76 | t2_enabled = options.get(CONF_T2_ENABLED, config.get(CONF_T2_ENABLED)) 77 | t2_type = options.get(CONF_T2_TYPE, config.get(CONF_T2_TYPE)) 78 | c1_enabled = options.get(CONF_C1_ENABLED, config.get(CONF_C1_ENABLED)) 79 | c2_enabled = options.get(CONF_C2_ENABLED, config.get(CONF_C2_ENABLED)) 80 | 81 | entities: list[EdSensorEntity] = [] 82 | 83 | for ti_input_number in (1, 2): 84 | if t1_enabled and ti_input_number == 1 or t2_enabled and ti_input_number == 2: 85 | _LOGGER.debug("Add the teleinfo %s sensor entities", ti_input_number) 86 | default_name = DEFAULT_T1_NAME if ti_input_number == 1 else DEFAULT_T2_NAME 87 | ti_type = t1_type if ti_input_number == 1 else t2_type 88 | 89 | entities.append( 90 | TeleinfoInputEdDevice( 91 | controller, 92 | coordinator, 93 | input_number=ti_input_number, 94 | input_name=f"T{ti_input_number}", 95 | name=default_name, 96 | unit=UnitOfApparentPower.VOLT_AMPERE, 97 | device_class=SensorDeviceClass.POWER, 98 | state_class=SensorStateClass.MEASUREMENT, 99 | icon="mdi:flash", 100 | ) 101 | ) 102 | if ti_type == CONF_TI_TYPE_BASE: 103 | entities.append( 104 | TeleinfoInputTotalEdDevice( 105 | controller, 106 | coordinator, 107 | input_number=ti_input_number, 108 | input_name=f"T{ti_input_number}_total", 109 | name=default_name + " Total", 110 | unit=UnitOfEnergy.WATT_HOUR, 111 | device_class=SensorDeviceClass.ENERGY, 112 | state_class=SensorStateClass.TOTAL_INCREASING, 113 | icon="mdi:meter-electric", 114 | ) 115 | ) 116 | elif ti_type == CONF_TI_TYPE_HCHP: 117 | entities.append( 118 | TeleinfoInputTotalHchpEdDevice( 119 | controller, 120 | coordinator, 121 | input_number=ti_input_number, 122 | input_name=f"T{ti_input_number}_total", 123 | name=default_name + " Total", 124 | unit=UnitOfEnergy.WATT_HOUR, 125 | device_class=SensorDeviceClass.ENERGY, 126 | state_class=SensorStateClass.TOTAL_INCREASING, 127 | icon="mdi:meter-electric", 128 | ) 129 | ) 130 | entities.append( 131 | TeleinfoInputTotalHcEdDevice( 132 | controller, 133 | coordinator, 134 | input_number=ti_input_number, 135 | input_name=f"T{ti_input_number}_total_hc", 136 | name=default_name + " HC Total", 137 | unit=UnitOfEnergy.WATT_HOUR, 138 | device_class=SensorDeviceClass.ENERGY, 139 | state_class=SensorStateClass.TOTAL_INCREASING, 140 | icon="mdi:meter-electric", 141 | ) 142 | ) 143 | entities.append( 144 | TeleinfoInputTotalHpEdDevice( 145 | controller, 146 | coordinator, 147 | input_number=ti_input_number, 148 | input_name=f"T{ti_input_number}_total_hp", 149 | name=default_name + " HP Total", 150 | unit=UnitOfEnergy.WATT_HOUR, 151 | device_class=SensorDeviceClass.ENERGY, 152 | state_class=SensorStateClass.TOTAL_INCREASING, 153 | icon="mdi:meter-electric", 154 | ) 155 | ) 156 | elif ti_type == CONF_TI_TYPE_TEMPO: 157 | entities.append( 158 | TeleinfoInputTotalTempoEdDevice( 159 | controller, 160 | coordinator, 161 | input_number=ti_input_number, 162 | input_name=f"T{ti_input_number}_total", 163 | name=default_name + " Total", 164 | unit=UnitOfEnergy.WATT_HOUR, 165 | device_class=SensorDeviceClass.ENERGY, 166 | state_class=SensorStateClass.TOTAL_INCREASING, 167 | icon="mdi:meter-electric", 168 | ) 169 | ) 170 | for desc, key in TELEINFO_TEMPO_ATTR.items(): 171 | entities.append( 172 | TeleinfoInputTempoEdDevice( 173 | controller, 174 | coordinator, 175 | input_number=ti_input_number, 176 | input_name=f"T{ti_input_number}_{key}", 177 | name=default_name + " " + desc + " Total", 178 | unit=UnitOfEnergy.WATT_HOUR, 179 | device_class=SensorDeviceClass.ENERGY, 180 | state_class=SensorStateClass.TOTAL_INCREASING, 181 | icon="mdi:meter-electric", 182 | ) 183 | ) 184 | entities.append( 185 | TeleinfoInputColor( 186 | controller, 187 | coordinator, 188 | input_number=ti_input_number, 189 | input_name=f"T{ti_input_number}_PTEC", 190 | name=default_name + " Tempo Couleur", 191 | icon="mdi:palette", 192 | device_class=SensorDeviceClass.ENUM, 193 | ) 194 | ) 195 | entities.append( 196 | TeleinfoInputColor( 197 | controller, 198 | coordinator, 199 | input_number=ti_input_number, 200 | input_name=f"T{ti_input_number}_DEMAIN", 201 | name=default_name + " Tempo Couleur Demain", 202 | icon="mdi:palette", 203 | device_class=SensorDeviceClass.ENUM, 204 | ) 205 | ) 206 | for ci_input_number in (1, 2): 207 | if c1_enabled and ci_input_number == 1 or c2_enabled and ci_input_number == 2: 208 | _LOGGER.debug("Add the meter %s sensor entities", ci_input_number) 209 | default_name = DEFAULT_C1_NAME if ci_input_number == 1 else DEFAULT_C2_NAME 210 | ci_unit = ( 211 | CONF_C1_UNIT_OF_MEASUREMENT 212 | if ci_input_number == 1 213 | else CONF_C2_UNIT_OF_MEASUREMENT 214 | ) 215 | ci_total_unit = ( 216 | CONF_C1_TOTAL_UNIT_OF_MEASUREMENT 217 | if ci_input_number == 1 218 | else CONF_C2_TOTAL_UNIT_OF_MEASUREMENT 219 | ) 220 | ci_device_class = ( 221 | CONF_C1_DEVICE_CLASS if ci_input_number == 1 else CONF_C2_DEVICE_CLASS 222 | ) 223 | ci_divider_factor = ( 224 | CONF_C1_DIVIDER_FACTOR 225 | if ci_input_number == 1 226 | else CONF_C2_DIVIDER_FACTOR 227 | ) 228 | 229 | entities.append( 230 | MeterInputEdDevice( 231 | controller, 232 | coordinator, 233 | input_number=ci_input_number, 234 | input_name=f"c{ci_input_number}", 235 | name=default_name, 236 | unit=options.get(ci_unit, config.get(ci_unit)), 237 | device_class=options.get( 238 | ci_device_class, config.get(ci_device_class) 239 | ), 240 | state_class=SensorStateClass.MEASUREMENT 241 | if ( 242 | SensorStateClass.MEASUREMENT 243 | in ( 244 | DEVICE_CLASS_STATE_CLASSES.get( 245 | options.get( 246 | ci_device_class, config.get(ci_device_class) 247 | ), 248 | {}, 249 | ) 250 | ) 251 | ) 252 | else None, 253 | icon="mdi:counter", 254 | divider_factor=options.get( 255 | ci_divider_factor, config.get(ci_divider_factor) 256 | ), 257 | ) 258 | ) 259 | entities.append( 260 | MeterInputDailyEdDevice( 261 | controller, 262 | coordinator, 263 | input_number=ci_input_number, 264 | input_name=f"c{ci_input_number}_daily", 265 | name=default_name + " Daily", 266 | unit=options.get(ci_unit, config.get(ci_unit)), 267 | device_class=options.get( 268 | ci_device_class, config.get(ci_device_class) 269 | ), 270 | state_class=SensorStateClass.TOTAL, 271 | icon="mdi:counter", 272 | divider_factor=options.get( 273 | ci_divider_factor, config.get(ci_divider_factor) 274 | ), 275 | ) 276 | ) 277 | entities.append( 278 | MeterInputTotalEdDevice( 279 | controller, 280 | coordinator, 281 | input_number=ci_input_number, 282 | input_name=f"c{ci_input_number}_total", 283 | name=default_name + " Total", 284 | unit=options.get( 285 | ci_total_unit, 286 | config.get( 287 | ci_total_unit, 288 | config.get(ci_unit), 289 | ), 290 | ), 291 | device_class=options.get( 292 | ci_device_class, config.get(ci_device_class) 293 | ), 294 | state_class=SensorStateClass.TOTAL_INCREASING, 295 | icon="mdi:counter", 296 | ) 297 | ) 298 | 299 | if entities: 300 | async_add_entities(entities) 301 | 302 | 303 | class EdSensorEntity(CoordinatorEntity, RestoreSensor): 304 | """Representation of a generic Eco-Devices sensor.""" 305 | 306 | def __init__( 307 | self, 308 | controller: EcoDevices, 309 | coordinator: DataUpdateCoordinator, 310 | input_number: int, 311 | input_name: str, 312 | name: str, 313 | unit: str | None = None, 314 | device_class: SensorDeviceClass | None = None, 315 | state_class: SensorStateClass | None = None, 316 | icon: str | None = None, 317 | divider_factor: float | None = None, 318 | ) -> None: 319 | """Initialize the sensor.""" 320 | super().__init__(coordinator) 321 | self.controller = controller 322 | self._input_name = input_name 323 | self._input_number = input_number 324 | self._divider_factor = divider_factor 325 | 326 | self._attr_name = name 327 | self._attr_native_unit_of_measurement = unit 328 | self._attr_device_class = device_class 329 | self._attr_state_class = state_class 330 | self._attr_icon = icon 331 | self._attr_unique_id = slugify( 332 | f"{DOMAIN}_{self.controller.mac_address}_sensor_{self._input_name}" 333 | ) 334 | self._attr_device_info = get_device_info(self.controller) 335 | 336 | self._last_state: StateType | None = None 337 | 338 | async def async_added_to_hass(self) -> None: 339 | """Restore state on startup.""" 340 | await super().async_added_to_hass() 341 | 342 | last_state = await self.async_get_last_state() 343 | 344 | if last_state is not None and last_state.state not in ( 345 | STATE_UNKNOWN, 346 | STATE_UNAVAILABLE, 347 | ): 348 | self._last_state = last_state.state 349 | 350 | 351 | class TeleinfoInputEdDevice(EdSensorEntity): 352 | """Initialize the Teleinfo Input sensor.""" 353 | 354 | @property 355 | def native_value(self) -> int: 356 | """Return the state.""" 357 | return self.coordinator.data[f"T{self._input_number}_PAPP"] 358 | 359 | @property 360 | def extra_state_attributes(self) -> Mapping[str, Any]: 361 | """Return the state attributes.""" 362 | if self.coordinator.data: 363 | return { 364 | k: self.coordinator.data.get(f"T{self._input_number}_{v}") 365 | for k, v in TELEINFO_EXTRA_ATTR.items() 366 | } 367 | raise EcoDevicesIncorrectValueError("Data not received.") 368 | 369 | 370 | class TeleinfoInputTotalEdDevice(EdSensorEntity): 371 | """Initialize the Teleinfo Input Total sensor.""" 372 | 373 | @property 374 | def native_value(self) -> float | None: 375 | """Return the total value if it's greater than 0.""" 376 | if (value := float(self.coordinator.data[f"T{self._input_number}_BASE"])) > 0: 377 | if self._last_state is None or value >= float(self._last_state): 378 | self._last_state = value 379 | return value 380 | _LOGGER.warning( 381 | "Value %s for %s not greater or equal to the previous one %s, ignore", 382 | value, 383 | self.entity_id, 384 | self._last_state, 385 | ) 386 | return None 387 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 388 | return None 389 | 390 | 391 | class TeleinfoInputTotalHchpEdDevice(EdSensorEntity): 392 | """Initialize the Teleinfo Input HCHP Total sensor.""" 393 | 394 | @property 395 | def native_value(self) -> float | None: 396 | """Return the total value if it's greater than 0.""" 397 | value_hc = float(self.coordinator.data[f"T{self._input_number}_HCHC"]) 398 | value_hp = float(self.coordinator.data[f"T{self._input_number}_HCHP"]) 399 | if (value := value_hc + value_hp) > 0: 400 | if self._last_state is None or value >= float(self._last_state): 401 | self._last_state = value 402 | return value 403 | _LOGGER.warning( 404 | "Value %s for %s not greater or equal to the previous one %s, ignore", 405 | value, 406 | self.entity_id, 407 | self._last_state, 408 | ) 409 | return None 410 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 411 | return None 412 | 413 | 414 | class TeleinfoInputTotalHcEdDevice(EdSensorEntity): 415 | """Initialize the Teleinfo Input HC Total sensor.""" 416 | 417 | @property 418 | def native_value(self) -> float | None: 419 | """Return the total value if it's greater than 0.""" 420 | if (value := float(self.coordinator.data[f"T{self._input_number}_HCHC"])) > 0: 421 | if self._last_state is None or value >= float(self._last_state): 422 | self._last_state = value 423 | return value 424 | _LOGGER.warning( 425 | "Value %s for %s not greater or equal to the previous one %s, ignore", 426 | value, 427 | self.entity_id, 428 | self._last_state, 429 | ) 430 | return None 431 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 432 | return None 433 | 434 | 435 | class TeleinfoInputTotalHpEdDevice(EdSensorEntity): 436 | """Initialize the Teleinfo Input HP Total sensor.""" 437 | 438 | @property 439 | def native_value(self) -> float | None: 440 | """Return the total value if it's greater than 0.""" 441 | if (value := float(self.coordinator.data[f"T{self._input_number}_HCHP"])) > 0: 442 | if self._last_state is None or value >= float(self._last_state): 443 | self._last_state = value 444 | return value 445 | _LOGGER.warning( 446 | "Value %s for %s not greater or equal to the previous one %s, ignore", 447 | value, 448 | self.entity_id, 449 | self._last_state, 450 | ) 451 | return None 452 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 453 | return None 454 | 455 | 456 | class TeleinfoInputTotalTempoEdDevice(EdSensorEntity): 457 | """Initialize the Teleinfo Input Tempo Total sensor.""" 458 | 459 | @property 460 | def native_value(self) -> float | None: 461 | """Return the total value if it's greater than 0.""" 462 | value = 0.0 463 | for key in TELEINFO_TEMPO_ATTR.values(): 464 | value += float(self.coordinator.data[f"T{self._input_number}_{key}"]) 465 | if value > 0: 466 | if self._last_state is None or value >= float(self._last_state): 467 | self._last_state = value 468 | return value 469 | _LOGGER.warning( 470 | "Value %s for %s not greater or equal to the previous one %s, ignore", 471 | value, 472 | self.entity_id, 473 | self._last_state, 474 | ) 475 | return None 476 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 477 | return None 478 | 479 | 480 | class TeleinfoInputTempoEdDevice(EdSensorEntity): 481 | """Initialize the Teleinfo Input Tempo sensor.""" 482 | 483 | @property 484 | def native_value(self) -> float | None: 485 | """Return the total value if it's greater than 0 or the previous value.""" 486 | if (value := float(self.coordinator.data[self._input_name.upper()])) > 0: 487 | if self._last_state is None or value >= float(self._last_state): 488 | self._last_state = value 489 | return value 490 | _LOGGER.warning( 491 | "Value %s for %s not greater or equal to the previous one %s, ignore", 492 | value, 493 | self.entity_id, 494 | self._last_state, 495 | ) 496 | return None 497 | _LOGGER.debug("Value for %s equal to 0, ignore", self.entity_id) 498 | return None 499 | 500 | 501 | class TeleinfoInputColor(EdSensorEntity): 502 | """Initialize the Teleinfo Input color sensor.""" 503 | 504 | _attr_options = [ 505 | "🔵", 506 | "⚪", 507 | "🔴", 508 | "❓", 509 | ] 510 | 511 | @property 512 | def native_value(self) -> str | None: 513 | """Return the state.""" 514 | if type_heure := self.coordinator.data.get(self._input_name): 515 | if type_heure.endswith("JB"): 516 | return "🔵" 517 | if type_heure.endswith("JW"): 518 | return "⚪" 519 | if type_heure.endswith("JR"): 520 | return "🔴" 521 | return "❓" 522 | 523 | @property 524 | def extra_state_attributes(self) -> Mapping[str, Any]: 525 | """Return the state attributes.""" 526 | color_name = "inconnu" 527 | if type_heure := self.coordinator.data.get(self._input_name): 528 | if type_heure.endswith("JB"): 529 | color_name = "Bleu" 530 | if type_heure.endswith("JW"): 531 | color_name = "Blanc" 532 | if type_heure.endswith("JR"): 533 | color_name = "Ro" + "uge" # bypass codespell 534 | return {"name": color_name} 535 | 536 | 537 | class MeterInputEdDevice(EdSensorEntity): 538 | """Initialize the meter input sensor.""" 539 | 540 | @property 541 | def native_value(self) -> float: 542 | """Return the state.""" 543 | value = int(self.coordinator.data[f"meter{self._input_number + 1}"]) 544 | if self._divider_factor: 545 | return value / self._divider_factor 546 | return value 547 | 548 | @property 549 | def extra_state_attributes(self) -> Mapping[str, Any]: 550 | """Return the state attributes.""" 551 | if self.coordinator.data: 552 | return { 553 | "total": self.coordinator.data[f"count{self._input_number - 1}"], 554 | "fuel": self.coordinator.data[f"c{self._input_number - 1}_fuel"], 555 | } 556 | raise EcoDevicesIncorrectValueError("Data not received.") 557 | 558 | 559 | class MeterInputDailyEdDevice(EdSensorEntity): 560 | """Initialize the meter input daily sensor.""" 561 | 562 | @property 563 | def native_value(self) -> float: 564 | """Return the state.""" 565 | value = int(self.coordinator.data[f"c{self._input_number - 1}day"]) 566 | if self._divider_factor: 567 | return value / self._divider_factor 568 | return value 569 | 570 | 571 | class MeterInputTotalEdDevice(EdSensorEntity): 572 | """Initialize the meter input total sensor.""" 573 | 574 | @property 575 | def native_value(self) -> float | None: 576 | """Return the total value if it's greater than 0.""" 577 | if ( 578 | value := float(self.coordinator.data[f"count{self._input_number - 1}"]) 579 | ) > 0: 580 | if self._last_state is None or value >= float(self._last_state): 581 | self._last_state = value / 1000 582 | return value / 1000 583 | _LOGGER.warning( 584 | "Total value for meter input %s not greater than 0, ignore", 585 | self._input_number, 586 | ) 587 | return None 588 | 589 | 590 | class EcoDevicesIncorrectValueError(Exception): 591 | """Exception to indicate that the Eco-Device return an incorrect value.""" 592 | -------------------------------------------------------------------------------- /custom_components/ecodevices/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Initial Eco-Devices configuration", 6 | "description": "Set connexion settings and select which input you use.", 7 | "data": { 8 | "host": "[%key:common::config_flow::data::host%]", 9 | "port": "[%key:common::config_flow::data::port%]", 10 | "scan_interval": "Seconds between updates", 11 | "username": "[%key:common::config_flow::data::username%]", 12 | "password": "[%key:common::config_flow::data::password%]", 13 | "t1_enabled": "Teleinfo 1", 14 | "t2_enabled": "Teleinfo 2", 15 | "c1_enabled": "Meter 1", 16 | "c2_enabled": "Meter 2" 17 | } 18 | }, 19 | "params": { 20 | "title": "Parameters of Eco-Devices entities", 21 | "data": { 22 | "t1_type": "Teleinfo 1 - Rate type (Base, HC/HP or Tempo)", 23 | "t2_type": "Teleinfo 2 - Rate type (Base, HC/HP or Tempo)", 24 | "c1_device_class": "Meter 1 - Device Class (sensor)", 25 | "c1_unit_of_measurement": "Meter 1 - Unit of measurement for current and daily values", 26 | "c1_total_unit_of_measurement": "Meter 1 - Unit of measurement for total value", 27 | "c1_divider_factor": "Meter 1 - Divider factor for the value of current and daily sensors", 28 | "c2_device_class": "Meter 2 - Device Class (sensor)", 29 | "c2_unit_of_measurement": "Meter 2 - Unit of measurement for current and daily values", 30 | "c2_total_unit_of_measurement": "Meter 2 - Unit of measurement for total value", 31 | "c2_divider_factor": "Meter 2 - Divider factor for the value of current and daily sensors" 32 | } 33 | } 34 | }, 35 | "error": { 36 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 37 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 38 | "unknown": "[%key:common::config_flow::error::unknown%]" 39 | }, 40 | "abort": { 41 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 42 | } 43 | }, 44 | "options": { 45 | "step": { 46 | "init": { 47 | "title": "Eco-Devices options", 48 | "description": "Update connexion settings and select which input you use.", 49 | "data": { 50 | "username": "[%key:common::config_flow::data::username%]", 51 | "password": "[%key:common::config_flow::data::password%]", 52 | "scan_interval": "Seconds between updates", 53 | "t1_enabled": "Teleinfo 1", 54 | "t2_enabled": "Teleinfo 2", 55 | "c1_enabled": "Meter 1", 56 | "c2_enabled": "Meter 2" 57 | } 58 | }, 59 | "params": { 60 | "title": "Parameters of Eco-Devices entities", 61 | "data": { 62 | "t1_type": "Teleinfo 1 - Rate type (Base, HC/HP or Tempo)", 63 | "t2_type": "Teleinfo 2 - Rate type (Base, HC/HP or Tempo)", 64 | "c1_device_class": "Meter 1 - Device Class (sensor)", 65 | "c1_unit_of_measurement": "Meter 1 - Unit of measurement for current and daily values", 66 | "c1_total_unit_of_measurement": "Meter 1 - Unit of measurement for total value", 67 | "c1_divider_factor": "Meter 1 - Divider factor for the value of current and daily sensors", 68 | "c2_device_class": "Meter 2 - Device Class (sensor)", 69 | "c2_unit_of_measurement": "Meter 2 - Unit of measurement for current and daily values", 70 | "c2_total_unit_of_measurement": "Meter 2 - Unit of measurement for total value", 71 | "c2_divider_factor": "Meter 2 - Divider factor for the value of current and daily sensors" 72 | } 73 | } 74 | }, 75 | "error": { 76 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 77 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 78 | "unknown": "[%key:common::config_flow::error::unknown%]" 79 | }, 80 | "abort": { 81 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /custom_components/ecodevices/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "abort": { 4 | "already_configured": "Device is already configured" 5 | }, 6 | "error": { 7 | "cannot_connect": "Failed to connect", 8 | "invalid_auth": "Invalid authentication", 9 | "unknown": "Unexpected error" 10 | }, 11 | "step": { 12 | "params": { 13 | "data": { 14 | "c1_device_class": "Meter 1 - Device Class (sensor)", 15 | "c1_divider_factor": "Meter 1 - Divider factor for the value of current and daily sensors", 16 | "c1_total_unit_of_measurement": "Meter 1 - Unit of measurement for total value", 17 | "c1_unit_of_measurement": "Meter 1 - Unit of measurement for current and daily values", 18 | "c2_device_class": "Meter 2 - Device Class (sensor)", 19 | "c2_divider_factor": "Meter 2 - Divider factor for the value of current and daily sensors", 20 | "c2_total_unit_of_measurement": "Meter 2 - Unit of measurement for total value", 21 | "c2_unit_of_measurement": "Meter 2 - Unit of measurement for current and daily values", 22 | "t1_type": "Teleinfo 1 - Rate type (Base, HC/HP or Tempo)", 23 | "t2_type": "Teleinfo 2 - Rate type (Base, HC/HP or Tempo)" 24 | }, 25 | "title": "Parameters of Eco-Devices entities" 26 | }, 27 | "user": { 28 | "data": { 29 | "c1_enabled": "Meter 1", 30 | "c2_enabled": "Meter 2", 31 | "host": "Host", 32 | "password": "Password", 33 | "port": "Port", 34 | "scan_interval": "Seconds between updates", 35 | "t1_enabled": "Teleinfo 1", 36 | "t2_enabled": "Teleinfo 2", 37 | "username": "Username" 38 | }, 39 | "description": "Set connexion settings and select which input you use.", 40 | "title": "Initial Eco-Devices configuration" 41 | } 42 | } 43 | }, 44 | "options": { 45 | "abort": { 46 | "already_configured": "Device is already configured" 47 | }, 48 | "error": { 49 | "cannot_connect": "Failed to connect", 50 | "invalid_auth": "Invalid authentication", 51 | "unknown": "Unexpected error" 52 | }, 53 | "step": { 54 | "init": { 55 | "data": { 56 | "c1_enabled": "Meter 1", 57 | "c2_enabled": "Meter 2", 58 | "password": "Password", 59 | "scan_interval": "Seconds between updates", 60 | "t1_enabled": "Teleinfo 1", 61 | "t2_enabled": "Teleinfo 2", 62 | "username": "Username" 63 | }, 64 | "description": "Update connexion settings and select which input you use.", 65 | "title": "Eco-Devices options" 66 | }, 67 | "params": { 68 | "data": { 69 | "c1_device_class": "Meter 1 - Device Class (sensor)", 70 | "c1_divider_factor": "Meter 1 - Divider factor for the value of current and daily sensors", 71 | "c1_total_unit_of_measurement": "Meter 1 - Unit of measurement for total value", 72 | "c1_unit_of_measurement": "Meter 1 - Unit of measurement for current and daily values", 73 | "c2_device_class": "Meter 2 - Device Class (sensor)", 74 | "c2_divider_factor": "Meter 2 - Divider factor for the value of current and daily sensors", 75 | "c2_total_unit_of_measurement": "Meter 2 - Unit of measurement for total value", 76 | "c2_unit_of_measurement": "Meter 2 - Unit of measurement for current and daily values", 77 | "t1_type": "Teleinfo 1 - Rate type (Base, HC/HP or Tempo)", 78 | "t2_type": "Teleinfo 2 - Rate type (Base, HC/HP or Tempo)" 79 | }, 80 | "title": "Parameters of Eco-Devices entities" 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /custom_components/ecodevices/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "title": "Configuration initiale de l'Eco-Device", 6 | "description": "D\u00e9finissez les param\u00e8tres de connexion ainsi que les entr\u00e9es utilis\u00e9es.", 7 | "data": { 8 | "host": "Adresse IP ou DNS", 9 | "port": "Port", 10 | "username": "Nom d'utilisateur", 11 | "password": "Mot de passe", 12 | "scan_interval": "Interval de mise à jour en secondes", 13 | "t1_enabled": "T\u00e9l\u00e9info 1", 14 | "t2_enabled": "T\u00e9l\u00e9info 2", 15 | "c1_enabled": "Compteur 1", 16 | "c2_enabled": "Compteur 2" 17 | } 18 | }, 19 | "params": { 20 | "title": "Paramètres des entités", 21 | "data": { 22 | "t1_type": "Teleinfo 1 - Type de tarifs (Base, HC/HP ou Tempo)", 23 | "t2_type": "Teleinfo 2 - Type de tarifs (Base, HC/HP ou Tempo)", 24 | "c1_divider_factor": "Compteur 1 - Facteur de division pour les valeurs courantes et journalière", 25 | "c1_device_class": "Compteur 1 - Type", 26 | "c1_unit_of_measurement": "Compteur 1 - Unit\u00e9 pour les valeurs courantes et journalière", 27 | "c1_total_unit_of_measurement": "Compteur 1 - Unit\u00e9 pour la valeur total", 28 | "c2_divider_factor": "Compteur 2 - Facteur de division pour les valeurs courantes et journalière", 29 | "c2_device_class": "Compteur 2 - Type", 30 | "c2_unit_of_measurement": "Compteur 2 - Unit\u00e9 pour les valeurs courantes et journalière", 31 | "c2_total_unit_of_measurement": "Compteur 2 - Unit\u00e9 pour la valeur total" 32 | } 33 | } 34 | }, 35 | "abort": { 36 | "already_configured": "D\u00e9j\u00e0 configur\u00e9" 37 | }, 38 | "error": { 39 | "cannot_connect": "Probl\u00e8me de connexion", 40 | "invalid_auth": "Probl\u00e8me d'authentification", 41 | "unknown": "Erreur non reconnue" 42 | } 43 | }, 44 | "options": { 45 | "step": { 46 | "init": { 47 | "title": "Options Eco-Devices", 48 | "description": "Mise à jour des param\u00e8tres de connexion ainsi que les entr\u00e9es utilis\u00e9es.", 49 | "data": { 50 | "username": "Nom d'utilisateur", 51 | "password": "Mot de passe", 52 | "scan_interval": "Interval de mise à jour en secondes", 53 | "t1_enabled": "T\u00e9l\u00e9info 1", 54 | "t2_enabled": "T\u00e9l\u00e9info 2", 55 | "c1_enabled": "Compteur 1", 56 | "c2_enabled": "Compteur 2" 57 | } 58 | }, 59 | "params": { 60 | "title": "Paramètres des entités", 61 | "data": { 62 | "t1_type": "Teleinfo 1 - Type de tarifs (Base, HC/HP ou Tempo)", 63 | "t2_type": "Teleinfo 2 - Type de tarifs (Base, HC/HP ou Tempo)", 64 | "c1_divider_factor": "Compteur 1 - Facteur de division pour les valeurs courantes et journalière", 65 | "c1_device_class": "Compteur 1 - Type", 66 | "c1_unit_of_measurement": "Compteur 1 - Unit\u00e9 pour les valeurs courantes et journalière", 67 | "c1_total_unit_of_measurement": "Compteur 1 - Unit\u00e9 pour la valeur total", 68 | "c2_divider_factor": "Compteur 2 - Facteur de division pour les valeurs courantes et journalière", 69 | "c2_device_class": "Compteur 2 - Type", 70 | "c2_unit_of_measurement": "Compteur 2 - Unit\u00e9 pour les valeurs courantes et journalière", 71 | "c2_total_unit_of_measurement": "Compteur 2 - Unit\u00e9 pour la valeur total" 72 | } 73 | } 74 | }, 75 | "abort": { 76 | "already_configured": "D\u00e9j\u00e0 configur\u00e9" 77 | }, 78 | "error": { 79 | "cannot_connect": "Probl\u00e8me de connexion", 80 | "invalid_auth": "Probl\u00e8me d'authentification", 81 | "unknown": "Erreur non reconnue" 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GCE Eco-Devices", 3 | "country": "FR", 4 | "render_readme": true 5 | } 6 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ $# -ne 1 ]; then 6 | echo "Missing version" 7 | echo "Usage: $0 version" 8 | exit 1 9 | fi 10 | 11 | ROOT=$(realpath "$(dirname "$0")/..") 12 | CUSTOM_COMPONENT="${ROOT}/custom_components/$(ls "${ROOT}/custom_components" | head -n 1)" 13 | MANIFEST=${CUSTOM_COMPONENT}/manifest.json 14 | 15 | echo "Setting version to ${1} in ${MANIFEST}" 16 | cat <<<$(jq ".version=\"${1}\"" "${MANIFEST}") >"${MANIFEST}" 17 | 18 | echo "Creating release zip" 19 | cd "${CUSTOM_COMPONENT}" && zip "${ROOT}/release.zip" -r ./ 20 | --------------------------------------------------------------------------------