├── hacs.json ├── docs ├── dtsu666_series.pdf ├── dtsu666-user-manual-en.pdf ├── 651ccaf12599fdfe0bd0bfc40e1d2513.pdf └── chintthreephase-instructionmanual-dt(s)su666-y0.464.1002v1.5190617.pdf ├── custom_components └── chint_pm │ ├── ha_core.code-workspace │ ├── manifest.json │ ├── const.py │ ├── strings.json │ ├── config_flow.py │ ├── __init__.py │ └── sensor.py ├── README.md ├── .github └── workflows │ ├── hassfest.yml │ └── hacs.yml ├── .gitignore └── LICENSE /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Chint DTSU-666-H Modbus", 3 | "render_readme": true 4 | } -------------------------------------------------------------------------------- /docs/dtsu666_series.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatula/ha_chint_pm/HEAD/docs/dtsu666_series.pdf -------------------------------------------------------------------------------- /docs/dtsu666-user-manual-en.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatula/ha_chint_pm/HEAD/docs/dtsu666-user-manual-en.pdf -------------------------------------------------------------------------------- /docs/651ccaf12599fdfe0bd0bfc40e1d2513.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatula/ha_chint_pm/HEAD/docs/651ccaf12599fdfe0bd0bfc40e1d2513.pdf -------------------------------------------------------------------------------- /custom_components/chint_pm/ha_core.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "../../.." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /docs/chintthreephase-instructionmanual-dt(s)su666-y0.464.1002v1.5190617.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lmatula/ha_chint_pm/HEAD/docs/chintthreephase-instructionmanual-dt(s)su666-y0.464.1002v1.5190617.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chint_pm 2 | homeassistant Chint power meter integration 3 | 4 | Changes note: 5 | - add suport multiple type of DTSU666 (will upload used specifications) 6 | - fix serial connection init issue 7 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yml: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /.github/workflows/hacs.yml: -------------------------------------------------------------------------------- 1 | name: HACS Action 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | hacs: 11 | name: HACS Action 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - name: HACS Action 15 | uses: "hacs/action@main" 16 | with: 17 | category: "integration" -------------------------------------------------------------------------------- /custom_components/chint_pm/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "chint_pm", 3 | "name": "Chint powermeter DTSU-666-h", 4 | "version": "0.0.9", 5 | "documentation": "https://www.github.com/lmatula", 6 | "issue_tracker": "", 7 | "requirements": [ 8 | "pymodbus>=3.11.0" 9 | ], 10 | "dependencies": [], 11 | "codeowners": [ 12 | "@lmatula" 13 | ], 14 | "config_flow": true, 15 | "iot_class": "local_polling" 16 | } 17 | -------------------------------------------------------------------------------- /custom_components/chint_pm/const.py: -------------------------------------------------------------------------------- 1 | """Constants for the Chint pm integration.""" 2 | from homeassistant.components.sensor import ( 3 | SensorDeviceClass, 4 | SensorStateClass, 5 | ) 6 | from homeassistant.const import ( 7 | UnitOfElectricCurrent, 8 | UnitOfElectricPotential, 9 | UnitOfFrequency, 10 | ) 11 | from homeassistant.helpers.entity import EntityCategory 12 | 13 | from datetime import timedelta 14 | 15 | DOMAIN = "chint_pm" 16 | DEFAULT_PORT = 502 17 | DEFAULT_SLAVE_ID = 11 18 | DEFAULT_SERIAL_SLAVE_ID = 11 19 | DEFAULT_USERNAME = "" 20 | DEFAULT_PASSWORD = "" 21 | 22 | CONF_SLAVE_IDS = "slave_ids" 23 | CONF_PHASE_MODE = "phase_mode" 24 | CONF_METER_TYPE = "meter_type" 25 | 26 | DATA_UPDATE_COORDINATORS = "update_coordinators" 27 | 28 | UPDATE_INTERVAL = timedelta(seconds=15) 29 | 30 | PHMODE_3P4W = "3P4W" 31 | PHMODE_3P3W = "3P3W" 32 | 33 | 34 | class MeterTypes: 35 | METER_TYPE_H_3P = "1" 36 | METER_TYPE_CT_3P = "2" 37 | -------------------------------------------------------------------------------- /custom_components/chint_pm/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "step": { 4 | "user": { 5 | "data": { 6 | "type": "Connection type" 7 | }, 8 | "title": "Select connection type" 9 | }, 10 | "setup_serial": { 11 | "data": { 12 | "port": "Select device", 13 | "slave_ids": "Slave IDs (Comma separated)" 14 | }, 15 | "title": "Device" 16 | }, 17 | "setup_serial_manual_path": { 18 | "data": { 19 | "port": "[%key:common::config_flow::data::usb_path%]" 20 | }, 21 | "title": "Path" 22 | }, 23 | "setup_network": { 24 | "data": { 25 | "host": "[%key:common::config_flow::data::host%]", 26 | "port": "[%key:common::config_flow::data::port%]", 27 | "slave_ids": "Slave IDs (Comma separated)" 28 | } 29 | }, 30 | "network_login": { 31 | "description": "Please enter the credentials", 32 | "data": { 33 | "username": "[%key:common::config_flow::data::username%]", 34 | "password": "[%key:common::config_flow::data::password%]" 35 | } 36 | } 37 | }, 38 | "error": { 39 | "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", 40 | "slave_cannot_connect": "Failed to connect to additional slave", 41 | "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", 42 | "unknown": "[%key:common::config_flow::error::unknown%]", 43 | "read_error": "Reading from the inverter failed.", 44 | "invalid_slave_ids": "Slave IDs must be comma-separated list of ints" 45 | }, 46 | "abort": { 47 | "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /custom_components/chint_pm/config_flow.py: -------------------------------------------------------------------------------- 1 | """Config flow for Chint pm integration.""" 2 | 3 | from __future__ import annotations 4 | 5 | import logging 6 | from typing import Any 7 | 8 | from pymodbus.client import ModbusSerialClient, ModbusTcpClient 9 | import serial.tools.list_ports 10 | import voluptuous as vol 11 | 12 | from homeassistant import config_entries 13 | from homeassistant.components import usb 14 | from homeassistant.const import ( 15 | CONF_HOST, 16 | CONF_PASSWORD, 17 | CONF_PORT, 18 | CONF_TYPE, 19 | CONF_USERNAME, 20 | ) 21 | from homeassistant.data_entry_flow import FlowResult 22 | import homeassistant.helpers.config_validation as cv 23 | 24 | from .const import ( 25 | CONF_METER_TYPE, 26 | CONF_PHASE_MODE, 27 | CONF_SLAVE_IDS, 28 | DEFAULT_PORT, 29 | DEFAULT_SERIAL_SLAVE_ID, 30 | DEFAULT_SLAVE_ID, 31 | DEFAULT_USERNAME, 32 | DOMAIN, 33 | PHMODE_3P3W, 34 | PHMODE_3P4W, 35 | MeterTypes, 36 | ) 37 | 38 | _LOGGER = logging.getLogger(__name__) 39 | 40 | STEP_SETUP_NETWORK_DATA_SCHEMA = vol.Schema( 41 | { 42 | vol.Required(CONF_HOST): str, 43 | vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port, 44 | vol.Required(CONF_SLAVE_IDS, default=str(DEFAULT_SLAVE_ID)): str, 45 | } 46 | ) 47 | 48 | STEP_LOGIN_DATA_SCHEMA = vol.Schema( 49 | { 50 | vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, 51 | vol.Required(CONF_PASSWORD): str, 52 | } 53 | ) 54 | 55 | STEP_PM_CONFIG_DATA_SCHEMA = vol.Schema( 56 | {vol.Required(CONF_PHASE_MODE): vol.In(["3P4W", "3P3W"])} 57 | ) 58 | 59 | STEP_METER_TYPE_CONFIG_DATA_SCHEMA = vol.Schema( 60 | { 61 | vol.Required(CONF_METER_TYPE): vol.In( 62 | { 63 | MeterTypes.METER_TYPE_H_3P: "DTSU666-H (Huawei)", 64 | MeterTypes.METER_TYPE_CT_3P: "DTSU666 (Normal)", 65 | } 66 | ) 67 | } 68 | ) 69 | 70 | CONF_MANUAL_PATH = "Enter Manually" 71 | 72 | 73 | def _resolve_ph_mode(net: int) -> str: 74 | if net == 0: 75 | return PHMODE_3P4W 76 | else: 77 | return PHMODE_3P3W 78 | 79 | 80 | async def validate_serial_setup(data: dict[str, Any]) -> dict[str, Any]: 81 | """Validate the serial device that was passed by the user.""" 82 | 83 | client = None 84 | try: 85 | client = ModbusSerialClient( 86 | port=data[CONF_PORT], baudrate=9600, bytesize=8, stopbits=1, parity="N" 87 | ) 88 | client.connect() 89 | 90 | rr = client.read_holding_registers( 91 | address=0x0, count=4, device_id=data[CONF_SLAVE_IDS][0] 92 | ) 93 | decoder = client.convert_from_registers( 94 | rr.registers, data_type=client.DATATYPE.UINT16 95 | ) 96 | rev = decoder[0] 97 | ucode = decoder[1] 98 | clre = decoder[2] 99 | net = decoder[3] 100 | 101 | rr = client.read_holding_registers( 102 | address=0xB, count=1, device_id=data[CONF_SLAVE_IDS][0] 103 | ) 104 | decoder = client.convert_from_registers( 105 | rr.registers, data_type=client.DATATYPE.UINT16 106 | ) 107 | # device_type = decoder[0] 108 | 109 | _LOGGER.info( 110 | "Successfully connected to pm phase mode %s", 111 | net, 112 | ) 113 | 114 | match data[CONF_METER_TYPE]: 115 | case MeterTypes.METER_TYPE_CT_3P: 116 | meter_type_name = "DTSU-666" 117 | case _: 118 | meter_type_name = "DTSU-666-H" 119 | 120 | result = { 121 | "model_name": f"{meter_type_name} ({data[CONF_PORT]}@{data[CONF_SLAVE_IDS][0]})", 122 | "rev": rev, 123 | CONF_PHASE_MODE: _resolve_ph_mode(net), 124 | } 125 | 126 | # Return info that you want to store in the config entry. 127 | return result 128 | 129 | finally: 130 | if client is not None: 131 | # Cleanup this inverter object explicitly to prevent it from trying to maintain a modbus connection 132 | client.close() 133 | 134 | 135 | async def validate_network_setup(data: dict[str, Any]) -> dict[str, Any]: 136 | """Validate the user input allows us to connect. 137 | Data has the keys from STEP_SETUP_NETWORK_DATA_SCHEMA with values provided by the user. 138 | """ 139 | 140 | client = None 141 | try: 142 | client = ModbusTcpClient(host=data[CONF_HOST], port=data[CONF_PORT], timeout=5) 143 | client.connect() 144 | 145 | rr = client.read_holding_registers( 146 | address=0x0, count=4, device_id=data[CONF_SLAVE_IDS][0] 147 | ) 148 | decoder = client.convert_from_registers( 149 | rr.registers, data_type=client.DATATYPE.UINT16 150 | ) 151 | rev = decoder[0] 152 | ucode = decoder[1] 153 | clre = decoder[2] 154 | net = decoder[3] 155 | 156 | rr = client.read_holding_registers( 157 | address=0xB, count=1, device_id=data[CONF_SLAVE_IDS][0] 158 | ) 159 | decoder = client.convert_from_registers( 160 | rr.registers, data_type=client.DATATYPE.UINT16 161 | ) 162 | # device_type = decoder[0] 163 | 164 | _LOGGER.info( 165 | "Successfully connected to pm phase mode %s", 166 | net, 167 | ) 168 | 169 | match data[CONF_METER_TYPE]: 170 | case MeterTypes.METER_TYPE_CT_3P: 171 | meter_type_name = "DTSU-666" 172 | case _: 173 | meter_type_name = "DTSU-666-H" 174 | 175 | result = { 176 | "model_name": f"{meter_type_name} ({data[CONF_HOST]}:{data[CONF_PORT]}@{data[CONF_SLAVE_IDS][0]})", 177 | "rev": rev, 178 | CONF_PHASE_MODE: _resolve_ph_mode(net), 179 | } 180 | 181 | # Return info that you want to store in the config entry. 182 | return result 183 | 184 | finally: 185 | if client is not None: 186 | # Cleanup this inverter object explicitly to prevent it from trying to maintain a modbus connection 187 | client.close() 188 | 189 | 190 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): 191 | """Handle a config flow for chint pm.""" 192 | 193 | VERSION = 3 194 | 195 | def __init__(self) -> None: 196 | """Initialize flow.""" 197 | 198 | self._host: str | None = None 199 | self._port: str | None = None 200 | self._slave_ids: list[int] | None = None 201 | self._info: dict | None = None 202 | self._username: str | None = None 203 | self._password: str | None = None 204 | self._pm_phase_mode: str | None = None 205 | self._meter_type: str | None = None 206 | 207 | # Only used in reauth flows: 208 | self._reauth_entry: config_entries.ConfigEntry | None = None 209 | 210 | async def async_step_user( 211 | self, user_input: dict[str, Any] | None = None 212 | ) -> FlowResult: 213 | """Step when user initializes a integration.""" 214 | return await self.async_step_setup_meter_type() 215 | 216 | async def async_step_connection_type( 217 | self, user_input: dict[str, Any] | None = None 218 | ) -> FlowResult: 219 | """Step when user initializes a integration.""" 220 | if user_input is not None: 221 | user_selection = user_input[CONF_TYPE] 222 | if user_selection == "Serial": 223 | return await self.async_step_setup_serial() 224 | 225 | return await self.async_step_setup_network() 226 | 227 | list_of_types = ["Serial", "Network"] 228 | 229 | schema = vol.Schema({vol.Required(CONF_TYPE): vol.In(list_of_types)}) 230 | return self.async_show_form(step_id="connection_type", data_schema=schema) 231 | 232 | async def async_step_setup_serial( 233 | self, user_input: dict[str, Any] | None = None 234 | ) -> FlowResult: 235 | """Handles connection parameters when using ModbusRTU.""" 236 | 237 | # Parameter configuration is always possible over serial connection 238 | 239 | errors = {} 240 | 241 | if user_input is not None: 242 | user_selection = user_input[CONF_PORT] 243 | if user_selection == CONF_MANUAL_PATH: 244 | self._slave_ids = user_input[CONF_SLAVE_IDS] 245 | return await self.async_step_setup_serial_manual_path() 246 | 247 | user_input[CONF_PORT] = await self.hass.async_add_executor_job( 248 | usb.get_serial_by_id, user_input[CONF_PORT] 249 | ) 250 | 251 | try: 252 | user_input[CONF_SLAVE_IDS] = list( 253 | map(int, user_input[CONF_SLAVE_IDS].split(",")) 254 | ) 255 | except ValueError: 256 | errors["base"] = "invalid_slave_ids" 257 | else: 258 | try: 259 | info = await validate_serial_setup( 260 | { 261 | CONF_PORT: user_input[CONF_PORT], 262 | CONF_SLAVE_IDS: user_input[CONF_SLAVE_IDS], 263 | CONF_METER_TYPE: self._meter_type, 264 | } 265 | ) 266 | 267 | except SlaveException: 268 | errors["base"] = "slave_cannot_connect" 269 | except Exception as exception: # pylint: disable=broad-except 270 | _LOGGER.exception(exception) 271 | errors["base"] = "unknown" 272 | else: 273 | await self.async_set_unique_id() 274 | self._abort_if_unique_id_configured( 275 | updates={ 276 | CONF_HOST: None, 277 | CONF_PORT: user_input[CONF_PORT], 278 | CONF_SLAVE_IDS: user_input[CONF_SLAVE_IDS], 279 | } 280 | ) 281 | 282 | self._port = user_input[CONF_PORT] 283 | self._slave_ids = user_input[CONF_SLAVE_IDS] 284 | 285 | self._info = info 286 | 287 | self.context["title_placeholders"] = {"name": info["model_name"]} 288 | 289 | # We can directly make the new entry 290 | return await self.async_step_pm_settings() 291 | # return await self._create_entry() 292 | 293 | ports = await self.hass.async_add_executor_job(serial.tools.list_ports.comports) 294 | list_of_ports = { 295 | port.device: usb.human_readable_device_name( 296 | port.device, 297 | port.serial_number, 298 | port.manufacturer, 299 | port.description, 300 | port.vid, 301 | port.pid, 302 | ) 303 | for port in ports 304 | } 305 | 306 | list_of_ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH 307 | 308 | schema = vol.Schema( 309 | { 310 | vol.Required(CONF_PORT): vol.In(list_of_ports), 311 | vol.Required(CONF_SLAVE_IDS, default=str(DEFAULT_SERIAL_SLAVE_ID)): str, 312 | } 313 | ) 314 | return self.async_show_form( 315 | step_id="setup_serial", 316 | data_schema=schema, 317 | errors=errors, 318 | ) 319 | 320 | async def async_step_setup_serial_manual_path( 321 | self, user_input: dict[str, Any] | None = None 322 | ) -> FlowResult: 323 | """Select path manually.""" 324 | errors = {} 325 | 326 | if user_input is not None: 327 | try: 328 | user_input[CONF_SLAVE_IDS] = list( 329 | map(int, user_input[CONF_SLAVE_IDS].split(",")) 330 | ) 331 | except ValueError: 332 | errors["base"] = "invalid_slave_ids" 333 | else: 334 | try: 335 | info = await validate_serial_setup( 336 | { 337 | CONF_PORT: user_input[CONF_PORT], 338 | CONF_SLAVE_IDS: user_input[CONF_SLAVE_IDS], 339 | CONF_METER_TYPE: self._meter_type, 340 | } 341 | ) 342 | 343 | except SlaveException: 344 | errors["base"] = "slave_cannot_connect" 345 | except Exception as exception: # pylint: disable=broad-except 346 | _LOGGER.exception(exception) 347 | errors["base"] = "unknown" 348 | else: 349 | await self.async_set_unique_id() 350 | self._abort_if_unique_id_configured( 351 | updates={ 352 | CONF_HOST: None, 353 | CONF_PORT: user_input[CONF_PORT], 354 | CONF_SLAVE_IDS: user_input[CONF_SLAVE_IDS], 355 | } 356 | ) 357 | 358 | self._port = user_input[CONF_PORT] 359 | self._slave_ids = user_input[CONF_SLAVE_IDS] 360 | 361 | self._info = info 362 | self.context["title_placeholders"] = {"name": info["model_name"]} 363 | 364 | # We can directly make the new entry 365 | return await self.async_step_pm_settings() 366 | # return await self._create_entry() 367 | 368 | schema = vol.Schema( 369 | { 370 | vol.Required(CONF_PORT): str, 371 | vol.Required(CONF_SLAVE_IDS, default=self._slave_ids): str, 372 | } 373 | ) 374 | return self.async_show_form( 375 | step_id="setup_serial_manual_path", data_schema=schema, errors=errors 376 | ) 377 | 378 | async def async_step_setup_network( 379 | self, user_input: dict[str, Any] | None = None 380 | ) -> FlowResult: 381 | """Handles connection parameters when using ModbusTCP.""" 382 | 383 | errors = {} 384 | 385 | if user_input is not None: 386 | try: 387 | user_input[CONF_SLAVE_IDS] = list( 388 | map(int, user_input[CONF_SLAVE_IDS].split(",")) 389 | ) 390 | except ValueError: 391 | errors["base"] = "invalid_slave_ids" 392 | else: 393 | try: 394 | info = await validate_network_setup( 395 | { 396 | CONF_HOST: user_input[CONF_HOST], 397 | CONF_PORT: user_input[CONF_PORT], 398 | CONF_SLAVE_IDS: user_input[CONF_SLAVE_IDS], 399 | CONF_METER_TYPE: self._meter_type, 400 | } 401 | ) 402 | 403 | except SlaveException: 404 | errors["base"] = "slave_cannot_connect" 405 | 406 | errors["base"] = "read_error" 407 | except Exception as exception: # pylint: disable=broad-except 408 | _LOGGER.exception(exception) 409 | errors["base"] = "unknown" 410 | else: 411 | await self.async_set_unique_id() 412 | self._abort_if_unique_id_configured() 413 | 414 | self._host = user_input[CONF_HOST] 415 | self._port = user_input[CONF_PORT] 416 | self._slave_ids = user_input[CONF_SLAVE_IDS] 417 | 418 | self._info = info 419 | 420 | self.context["title_placeholders"] = {"name": info["model_name"]} 421 | 422 | # Otherwise, we can directly create the device entry! 423 | return await self.async_step_pm_settings() 424 | # return await self._create_entry() 425 | 426 | return self.async_show_form( 427 | step_id="setup_network", 428 | data_schema=STEP_SETUP_NETWORK_DATA_SCHEMA, 429 | errors=errors, 430 | ) 431 | 432 | async def async_step_setup_meter_type( 433 | self, user_input: dict[str, Any] | None = None 434 | ) -> FlowResult: 435 | """enter pm configs""" 436 | errors = {} 437 | if user_input is not None: 438 | try: 439 | self._meter_type = user_input[CONF_METER_TYPE] 440 | return await self.async_step_connection_type() 441 | 442 | except Exception as exception: # pylint: disable=broad-except 443 | _LOGGER.exception(exception) 444 | errors["base"] = "unknown" 445 | return self.async_show_form( 446 | step_id="setup_meter_type", 447 | data_schema=STEP_METER_TYPE_CONFIG_DATA_SCHEMA, 448 | errors=errors, 449 | ) 450 | 451 | async def async_step_pm_settings( 452 | self, user_input: dict[str, Any] | None = None 453 | ) -> FlowResult: 454 | """enter pm configs""" 455 | errors = {} 456 | if user_input is not None: 457 | try: 458 | self._pm_phase_mode = user_input[CONF_PHASE_MODE] 459 | return await self._create_entry() 460 | 461 | except Exception as exception: # pylint: disable=broad-except 462 | _LOGGER.exception(exception) 463 | errors["base"] = "unknown" 464 | return self.async_show_form( 465 | step_id="pm_settings", data_schema=STEP_PM_CONFIG_DATA_SCHEMA, errors=errors 466 | ) 467 | 468 | async def _create_entry(self): 469 | """Create the entry.""" 470 | assert self._port is not None 471 | assert self._slave_ids is not None 472 | 473 | data = { 474 | CONF_HOST: self._host, 475 | CONF_PORT: self._port, 476 | CONF_SLAVE_IDS: self._slave_ids, 477 | CONF_USERNAME: self._username, 478 | CONF_PASSWORD: self._password, 479 | CONF_PHASE_MODE: self._pm_phase_mode, 480 | CONF_METER_TYPE: self._meter_type, 481 | } 482 | 483 | if self._reauth_entry: 484 | self.hass.config_entries.async_update_entry(self._reauth_entry, data=data) 485 | await self.hass.config_entries.async_reload(self._reauth_entry.entry_id) 486 | return self.async_abort(reason="reauth_successful") 487 | 488 | return self.async_create_entry(title=self._info["model_name"], data=data) 489 | 490 | 491 | class SlaveException(Exception): 492 | """Error while testing communication with a slave.""" 493 | -------------------------------------------------------------------------------- /custom_components/chint_pm/__init__.py: -------------------------------------------------------------------------------- 1 | """The Chint pm Integration.""" 2 | 3 | import asyncio 4 | from collections.abc import Awaitable, Callable 5 | from datetime import timedelta 6 | import logging 7 | import threading 8 | from typing import TypeVar 9 | 10 | # Use asyncio.timeout instead of async_timeout 11 | from pymodbus.client import AsyncModbusSerialClient, AsyncModbusTcpClient 12 | from pymodbus.exceptions import ModbusIOException 13 | 14 | from homeassistant.config_entries import ConfigEntry 15 | from homeassistant.const import CONF_HOST, CONF_PORT, Platform 16 | from homeassistant.core import HomeAssistant 17 | from homeassistant.helpers.debounce import Debouncer 18 | from homeassistant.helpers.entity import DeviceInfo 19 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 20 | 21 | from .const import ( 22 | CONF_METER_TYPE, 23 | CONF_SLAVE_IDS, 24 | DATA_UPDATE_COORDINATORS, 25 | DOMAIN, 26 | UPDATE_INTERVAL, 27 | MeterTypes, 28 | ) 29 | 30 | _LOGGER = logging.getLogger(__name__) 31 | 32 | PLATFORMS: list[Platform] = [ 33 | Platform.SENSOR, 34 | ] 35 | 36 | T = TypeVar("T") 37 | 38 | 39 | class ChintDxsuDevice: 40 | """Chint pm device object""" 41 | 42 | def __init__(self, hass, entry, scan_interval) -> None: 43 | """Initialize the Modbus hub.""" 44 | self._hass = hass 45 | self._entry = entry 46 | self._lock = threading.Lock() 47 | self._scan_interval = scan_interval 48 | self._unsub_interval_method = None 49 | self._sensors = [] 50 | self.data = {} 51 | 52 | async def update(self, client, unit_id): 53 | """update sensors""" 54 | match self._entry.data[CONF_METER_TYPE]: 55 | case MeterTypes.METER_TYPE_CT_3P: 56 | await self.read_values_type_normal(client, unit_id) 57 | case _: 58 | await self.read_values(client, unit_id) 59 | 60 | async def read_values(self, client, unit_id): 61 | """read modbus value groups""" 62 | 63 | async def read_header(registers): 64 | decoder = client.convert_from_registers( 65 | registers, data_type=client.DATATYPE.UINT16 66 | ) 67 | # REV verison 68 | self.data["rev"] = decoder[0] 69 | # UCode Programming password codE 70 | self.data["ucode"] = decoder[1] 71 | # ClrE Electric energy zero clearing CLr.E(1:zero clearing) 72 | self.data["clre"] = decoder[2] 73 | # net Selecting of the connection mode net(0:3P4W,1:3P3W) 74 | self.data["net"] = decoder[3] 75 | # decoder.skip_bytes(2 * 2) 76 | # IrAt Current Transformer Ratio 77 | self.data["irat"] = decoder[6] 78 | # UrAt Potential Transformer Ratio(*) 79 | self.data["urat"] = decoder[7] 80 | # decoder.skip_bytes(3 * 2) 81 | # Meter type 82 | self.data["meter_type"] = decoder[11] 83 | 84 | async def read_header_proto(registers): 85 | decoder = client.convert_from_registers( 86 | registers, data_type=client.DATATYPE.UINT16 87 | ) 88 | # Protocol Protocol changing-over 89 | self.data["protocol"] = decoder[0] 90 | # Addr Communication address Addr 91 | self.data["addr"] = decoder[1] 92 | # bAud Communication baud rate bAud 93 | self.data["baud"] = decoder[2] 94 | 95 | # Secound 96 | self.data["secound"] = decoder[3] 97 | # Minutes 98 | self.data["minutes"] = decoder[4] 99 | # Hour 100 | self.data["hour"] = decoder[5] 101 | # Day 102 | self.data["day"] = decoder[6] 103 | # Month 104 | self.data["month"] = decoder[7] 105 | # Year 106 | self.data["year"] = decoder[8] 107 | 108 | async def read_elecricity_power(registers): 109 | decoder = client.convert_from_registers( 110 | registers, data_type=client.DATATYPE.FLOAT32 111 | ) 112 | 113 | # Uab Line -line voltage, the unit is V 114 | self.data["uab"] = decoder[0] 115 | # Ubc Line -line voltage, the unit is V 116 | self.data["ubc"] = decoder[1] 117 | # Uca Line -line voltage, the unit is V 118 | self.data["uca"] = decoder[2] 119 | 120 | # Ua Phase-phase voltage, the unit is V 121 | self.data["ua"] = decoder[3] 122 | # Ub Phase-phase voltaget, he unit is V 123 | self.data["ub"] = decoder[4] 124 | # Uc Phase-phase voltage, the unit is V 125 | self.data["uc"] = decoder[5] 126 | 127 | # Ia The data of three phase current,the unit is A 128 | self.data["ia"] = decoder[6] 129 | # Ib The data of three phase current,the unit is A 130 | self.data["ib"] = decoder[7] 131 | # Ic The data of three phase current,the unit is A 132 | self.data["ic"] = decoder[8] 133 | 134 | # Pt Conjunction active power,the unit is W 135 | self.data["pt"] = decoder[9] 136 | # Pa A phase active power,the unit is W 137 | self.data["pa"] = decoder[10] 138 | # Pb B phase active power,the unit is W (invalid when three phase three wire) 139 | self.data["pb"] = decoder[11] 140 | # Pc C phase active power,the unit is W 141 | self.data["pc"] = decoder[12] 142 | 143 | # Qt Conjunction reactive power,the unit is var 144 | self.data["qt"] = decoder[13] 145 | # Qa A phase reactive power, the unit is var 146 | self.data["qa"] = decoder[14] 147 | # Qb B phase reactive power, the unit is var (invalid when three phase three wire) 148 | self.data["qb"] = decoder[15] 149 | # Qc C phase reactive power, the unit is var 150 | self.data["qc"] = decoder[16] 151 | 152 | async def read_elecricity_factor(registers): 153 | decoder = client.convert_from_registers( 154 | registers, data_type=client.DATATYPE.FLOAT32 155 | ) 156 | # PFt Conjunction power factor 157 | self.data["pft"] = decoder[0] 158 | # Pfa A phase power factor (invalid when three phase three wire) 159 | self.data["pfa"] = decoder[1] 160 | # PFb B phase power factor (invalid when three phase three wire) 161 | self.data["pfb"] = decoder[2] 162 | # PFc C phase power factor (invalid when three phase three wire) 163 | self.data["pfc"] = decoder[3] 164 | 165 | async def read_elecricity_other(registers): 166 | decoder = client.convert_from_registers( 167 | registers, data_type=client.DATATYPE.FLOAT32 168 | ) 169 | # Freq Frequency 170 | self.data["freq"] = decoder[0] 171 | # decoder.skip_bytes(2 * 4) 172 | # DmPt Total active power demand 173 | self.data["dmpt"] = decoder[3] 174 | 175 | async def read_total(registers): 176 | decoder = client.convert_from_registers( 177 | registers, data_type=client.DATATYPE.FLOAT32 178 | ) 179 | # ImpEp (current)positive active total energy 180 | self.data["impep"] = decoder[0] 181 | # decoder.skip_bytes(2 * 8) # Skip to negative energy position 182 | # ExpEp (current)negative active total energy 183 | self.data["expep"] = decoder[5] 184 | 185 | async def read_quadrant_i(registers): 186 | decoder = client.convert_from_registers( 187 | registers, data_type=client.DATATYPE.FLOAT32 188 | ) 189 | # (current) quadrant I reactive total energy 190 | self.data["q1eq"] = decoder[0] 191 | 192 | async def read_quadrant_ii(registers): 193 | decoder = client.convert_from_registers( 194 | registers, data_type=client.DATATYPE.FLOAT32 195 | ) 196 | # (current) quadrant II reactive total energy 197 | self.data["q2eq"] = decoder[0] 198 | 199 | async def read_quadrant_iii(registers): 200 | decoder = client.convert_from_registers( 201 | registers, data_type=client.DATATYPE.FLOAT32 202 | ) 203 | # (current) quadrant III reactive total energy 204 | self.data["q3eq"] = decoder[0] 205 | 206 | async def read_quadrant_iv(registers): 207 | decoder = client.convert_from_registers( 208 | registers, data_type=client.DATATYPE.FLOAT32 209 | ) 210 | # (current) quadrant IV reactive total energy 211 | self.data["q4eq"] = decoder[0] 212 | 213 | if client.connected: 214 | header = await client.read_holding_registers( 215 | address=0x0, count=12, device_id=unit_id 216 | ) 217 | header_proto = await client.read_holding_registers( 218 | address=0x2C, count=9, device_id=unit_id 219 | ) 220 | elecricity_power = await client.read_holding_registers( 221 | address=0x2000, count=0x22, device_id=unit_id 222 | ) 223 | elecricity_factor = await client.read_holding_registers( 224 | address=0x202A, count=8, device_id=unit_id 225 | ) 226 | elecricity_other = await client.read_holding_registers( 227 | address=0x2044, count=8, device_id=unit_id 228 | ) 229 | # documentation say address is 0x401e but this register contain invalid data, maybe only -H version? 230 | total = await client.read_holding_registers( 231 | address=0x4026, count=12, device_id=unit_id 232 | ) 233 | # (current) quadrant I reactive total energy 234 | quadrant_i = await client.read_holding_registers( 235 | address=0x4032, count=2, device_id=unit_id 236 | ) 237 | # (current) quadrant II reactive total energy 238 | quadrant_ii = await client.read_holding_registers( 239 | address=0x403C, count=2, device_id=unit_id 240 | ) 241 | # (current) quadrant III reactive total energy 242 | quadrant_iii = await client.read_holding_registers( 243 | address=0x4046, count=2, device_id=unit_id 244 | ) 245 | # (current) quadrant IV reactive total energy 246 | quadrant_iv = await client.read_holding_registers( 247 | address=0x4050, count=2, device_id=unit_id 248 | ) 249 | 250 | out = await asyncio.gather( 251 | read_header(header.registers), 252 | read_header_proto(header_proto.registers), 253 | read_elecricity_power(elecricity_power.registers), 254 | read_elecricity_factor(elecricity_factor.registers), 255 | read_elecricity_other(elecricity_other.registers), 256 | read_total(total.registers), 257 | read_quadrant_i(quadrant_i.registers), 258 | read_quadrant_ii(quadrant_ii.registers), 259 | read_quadrant_iii(quadrant_iii.registers), 260 | read_quadrant_iv(quadrant_iv.registers), 261 | return_exceptions=True, 262 | ) 263 | # print(out) 264 | 265 | async def read_values_type_normal(self, client, unit_id): 266 | """read modbus value groups""" 267 | 268 | async def read_header(registers): 269 | decoder = client.convert_from_registers( 270 | registers, data_type=client.DATATYPE.FLOAT32 271 | ) 272 | # REV verison 273 | self.data["rev"] = decoder[0] 274 | # UCode Programming password codE 275 | self.data["ucode"] = decoder[1] 276 | # ClrE Electric energy zero clearing CLr.E(1:zero clearing) 277 | self.data["clre"] = decoder[2] 278 | # net Selecting of the connection mode net(0:3P4W,13P3W) 279 | self.data["net"] = decoder[3] 280 | decoder.skip_bytes(2 * 2) 281 | # IrAt Current Transformer Ratio 282 | self.data["irat"] = decoder[6] 283 | # UrAt Potential Transformer Ratio(*) 284 | self.data["urat"] = decoder[7] 285 | 286 | async def read_header_proto(registers): 287 | decoder = client.convert_from_registers( 288 | registers, data_type=client.DATATYPE.FLOAT32 289 | ) 290 | # Protocol Protocol changing-over 291 | self.data["protocol"] = decoder[0] 292 | # Addr Communication address Addr 293 | self.data["baud"] = decoder[1] 294 | # bAud Communication baud rate bAud 295 | self.data["addr"] = decoder[2] 296 | 297 | async def read_elecricity_power(registers): 298 | decoder = client.convert_from_registers( 299 | registers, data_type=client.DATATYPE.FLOAT32 300 | ) 301 | 302 | # Uab Line -line voltage, the unit is V 303 | self.data["uab"] = decoder[0] 304 | # Ubc Line -line voltage, the unit is V 305 | self.data["ubc"] = decoder[1] 306 | # Uca Line -line voltage, the unit is V 307 | self.data["uca"] = decoder[2] 308 | 309 | # Ua Phase-phase voltage, the unit is V 310 | self.data["ua"] = decoder[3] 311 | # Ub Phase-phase voltaget, he unit is V 312 | self.data["ub"] = decoder[4] 313 | # Uc Phase-phase voltage, the unit is V 314 | self.data["uc"] = decoder[5] 315 | 316 | # Ia The data of three phase current,the unit is A 317 | self.data["ia"] = decoder[6] 318 | # Ib The data of three phase current,the unit is A 319 | self.data["ib"] = decoder[7] 320 | # Ic The data of three phase current,the unit is A 321 | self.data["ic"] = decoder[8] 322 | 323 | # Pt Conjunction active power,the unit is W 324 | self.data["pt"] = decoder[9] 325 | # Pa A phase active power,the unit is W 326 | self.data["pa"] = decoder[10] 327 | # Pb B phase active power,the unit is W (invalid when three phase three wire) 328 | self.data["pb"] = decoder[11] 329 | # Pc C phase active power,the unit is W 330 | self.data["pc"] = decoder[12] 331 | 332 | # Qt Conjunction reactive power,the unit is var 333 | self.data["qt"] = decoder[13] 334 | # Qa A phase reactive power, the unit is var 335 | self.data["qa"] = decoder[14] 336 | # Qb B phase reactive power, the unit is var (invalid when three phase three wire) 337 | self.data["qb"] = decoder[15] 338 | # Qc C phase reactive power, the unit is var 339 | self.data["qc"] = decoder[16] 340 | 341 | async def read_elecricity_factor(registers): 342 | decoder = client.convert_from_registers( 343 | registers, data_type=client.DATATYPE.FLOAT32 344 | ) 345 | # PFt Conjunction power factor 346 | self.data["pft"] = decoder[0] 347 | # Pfa A phase power factor (invalid when three phase three wire) 348 | self.data["pfa"] = decoder[1] 349 | # PFb B phase power factor (invalid when three phase three wire) 350 | self.data["pfb"] = decoder[2] 351 | # PFc C phase power factor (invalid when three phase three wire) 352 | self.data["pfc"] = decoder[3] 353 | 354 | async def read_elecricity_other(registers): 355 | decoder = client.convert_from_registers( 356 | registers, data_type=client.DATATYPE.FLOAT32 357 | ) 358 | # Freq Frequency 359 | self.data["freq"] = decoder[0] 360 | 361 | async def read_total(registers): 362 | decoder = client.convert_from_registers( 363 | registers, data_type=client.DATATYPE.FLOAT32 364 | ) 365 | # ImpEp (current)positive active total energy 366 | self.data["impep"] = decoder[0] 367 | decoder.skip_bytes(2 * 8) # Skip to negative energy position 368 | # ExpEp (current)negative active total energy 369 | self.data["expep"] = decoder[5] 370 | 371 | async def read_quadrant_i(registers): 372 | decoder = client.convert_from_registers( 373 | registers, data_type=client.DATATYPE.FLOAT32 374 | ) 375 | # (current) quadrant I reactive total energy 376 | self.data["q1eq"] = decoder[0] 377 | 378 | async def read_quadrant_ii(registers): 379 | # (current) quadrant II reactive total energy 380 | decoder = client.convert_from_registers( 381 | registers, data_type=client.DATATYPE.FLOAT32 382 | ) 383 | self.data["q2eq"] = decoder[0] 384 | 385 | async def read_quadrant_iii(registers): 386 | # (current) quadrant III reactive total energy 387 | decoder = client.convert_from_registers( 388 | registers, data_type=client.DATATYPE.FLOAT32 389 | ) 390 | self.data["q3eq"] = decoder[0] 391 | 392 | async def read_quadrant_iv(registers): 393 | decoder = client.convert_from_registers( 394 | registers, data_type=client.DATATYPE.FLOAT32 395 | ) 396 | # (current) quadrant IV reactive total energy 397 | self.data["q4eq"] = decoder[0] 398 | 399 | if client.connected: 400 | header = await client.read_holding_registers( 401 | address=0x0, count=12, device_id=unit_id 402 | ) 403 | header_proto = await client.read_holding_registers( 404 | address=0x2C, count=9, device_id=unit_id 405 | ) 406 | elecricity_power = await client.read_holding_registers( 407 | address=0x2000, count=0x22, device_id=unit_id 408 | ) 409 | elecricity_factor = await client.read_holding_registers( 410 | address=0x202A, count=8, device_id=unit_id 411 | ) 412 | elecricity_other = await client.read_holding_registers( 413 | address=0x2044, count=8, device_id=unit_id 414 | ) 415 | # documentation say address is 0x401e but this register contain invalid data, maybe only -H version? 416 | total = await client.read_holding_registers( 417 | address=0x101E, count=2, device_id=unit_id 418 | ) 419 | # (current) quadrant I reactive total energy 420 | quadrant_i = await client.read_holding_registers( 421 | address=0x1032, count=2, device_id=unit_id 422 | ) 423 | # (current) quadrant II reactive total energy 424 | quadrant_ii = await client.read_holding_registers( 425 | address=0x103C, count=2, device_id=unit_id 426 | ) 427 | # (current) quadrant III reactive total energy 428 | quadrant_iii = await client.read_holding_registers( 429 | address=0x1046, count=2, device_id=unit_id 430 | ) 431 | # (current) quadrant IV reactive total energy 432 | quadrant_iv = await client.read_holding_registers( 433 | address=0x1050, count=2, device_id=unit_id 434 | ) 435 | 436 | await asyncio.gather( 437 | read_header(header.registers), 438 | read_header_proto(header_proto.registers), 439 | read_elecricity_power(elecricity_power.registers), 440 | read_elecricity_factor(elecricity_factor.registers), 441 | read_elecricity_other(elecricity_other.registers), 442 | read_total(total.registers), 443 | read_quadrant_i(quadrant_i.registers), 444 | read_quadrant_ii(quadrant_ii.registers), 445 | read_quadrant_iii(quadrant_iii.registers), 446 | read_quadrant_iv(quadrant_iv.registers), 447 | return_exceptions=True, 448 | ) 449 | 450 | 451 | class ChintUpdateCoordinator(DataUpdateCoordinator): 452 | """A specialised DataUpdateCoordinator for chint smart meter.""" 453 | 454 | def __init__( 455 | self, 456 | hass: HomeAssistant, 457 | logger: logging.Logger, 458 | device: ChintDxsuDevice, 459 | entry: ConfigEntry, 460 | update_interval: timedelta | None = None, 461 | update_method: Callable[[], Awaitable[T]] | None = None, 462 | request_refresh_debouncer: Debouncer | None = None, 463 | ) -> None: 464 | """Create a ChintUpdateCoordinator.""" 465 | if entry.data[CONF_HOST] is None: 466 | port_host = "" 467 | port_name = str(entry.data[CONF_PORT]) 468 | else: 469 | port_host = "".join(filter(str.isalnum, entry.data[CONF_HOST])) 470 | port_name = "".join(filter(str.isalnum, str(entry.data[CONF_PORT]))) 471 | 472 | super().__init__( 473 | hass, 474 | logger, 475 | name=f"{port_host}_{port_name}_{entry.data[CONF_SLAVE_IDS][0]}_data_update_coordinator", 476 | update_interval=update_interval, 477 | update_method=update_method, 478 | request_refresh_debouncer=request_refresh_debouncer, 479 | ) 480 | self.device = device 481 | self._client: AsyncModbusSerialClient | AsyncModbusTcpClient 482 | self._unit_id = entry.data[CONF_SLAVE_IDS][0] 483 | self._entry = entry 484 | 485 | async def push_sensor_read(self, address, count, data_type): 486 | # TODO: push device addresses to read 487 | await self._client.read_holding_registers( 488 | address=address, count=count, device_id=self._unit_id 489 | ) 490 | self.device._sensors.append(1) 491 | 492 | async def create_client(self, port, host): 493 | """create one clinet object whole update cordinator""" 494 | try: 495 | if host is None: 496 | self._client = AsyncModbusSerialClient( 497 | port=port, 498 | baudrate=9600, 499 | bytesize=8, 500 | stopbits=1, 501 | parity="N", 502 | ) 503 | else: 504 | self._client = AsyncModbusTcpClient(host=host, port=port, timeout=5) 505 | 506 | # self._client.connect() 507 | except Exception as err: 508 | # always try to stop the bridge, as it will keep retrying 509 | # in the background otherwise! 510 | if self._client is not None: 511 | self._client.close() 512 | 513 | raise err 514 | 515 | async def _async_update_data(self): 516 | try: 517 | if not self._client.connected: 518 | await self._client.connect() 519 | else: 520 | # check alive 521 | try: 522 | await self._client.read_holding_registers( 523 | address=0x0, count=1, device_id=self._unit_id 524 | ) 525 | except ModbusIOException as merr: 526 | # merr.isError() 527 | await self._client.connect() 528 | 529 | async with asyncio.timeout(30): 530 | return await self.device.update(self._client, self._unit_id) 531 | except Exception as err: 532 | raise UpdateFailed(f"Could not update values: {err}") from err 533 | 534 | @property 535 | def device_info(self) -> DeviceInfo: 536 | """Return device information about this pm device.""" 537 | # _LOGGER.debug(self.coordinator.config_entry.data) 538 | match self._entry.data[CONF_METER_TYPE]: 539 | case MeterTypes.METER_TYPE_CT_3P: 540 | meter_type_name = "DTSU-666" 541 | case _: 542 | meter_type_name = "DTSU-666-H" 543 | return DeviceInfo( 544 | identifiers={(DOMAIN, self._entry.entry_id)}, 545 | name=self._entry.title, 546 | manufacturer="Chint", 547 | model=meter_type_name, 548 | ) 549 | 550 | async def stop(self): 551 | """Close the modbus connection""" 552 | await self._client.close() 553 | 554 | 555 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 556 | """Unload a config entry.""" 557 | if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): 558 | update_coordinators = hass.data[DOMAIN][entry.entry_id][ 559 | DATA_UPDATE_COORDINATORS 560 | ] 561 | for update_coordinator in update_coordinators: 562 | await update_coordinator.stop() 563 | 564 | hass.data[DOMAIN].pop(entry.entry_id) 565 | 566 | return unload_ok 567 | 568 | 569 | async def async_setup(hass: HomeAssistant, config): 570 | """Set up the chint modbus component.""" 571 | hass.data[DOMAIN] = {} 572 | return True 573 | 574 | 575 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 576 | """Set up a chint mobus.""" 577 | device = ChintDxsuDevice( 578 | hass, 579 | entry, 580 | UPDATE_INTERVAL, 581 | ) 582 | 583 | update_coordinators = [] 584 | 585 | update_coordinators.append( 586 | await _create_update_coordinator(hass, device, entry, UPDATE_INTERVAL) 587 | ) 588 | hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { 589 | DATA_UPDATE_COORDINATORS: update_coordinators, 590 | } 591 | 592 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) 593 | # await async_setup_services(hass, entry, device) 594 | return True 595 | 596 | 597 | async def _create_update_coordinator( 598 | hass: HomeAssistant, 599 | device: ChintDxsuDevice, 600 | entry: ConfigEntry, 601 | update_interval, 602 | ): 603 | coordinator = ChintUpdateCoordinator( 604 | hass, 605 | _LOGGER, 606 | device=device, 607 | entry=entry, 608 | update_interval=update_interval, 609 | ) 610 | 611 | await coordinator.create_client(entry.data[CONF_PORT], entry.data[CONF_HOST]) 612 | 613 | await coordinator.async_config_entry_first_refresh() 614 | 615 | return coordinator 616 | 617 | 618 | async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 619 | """Migrate old entry.""" 620 | _LOGGER.debug("Migrating from version %s", config_entry.version) 621 | 622 | if config_entry.version < 2: 623 | data = {**config_entry.data} 624 | 625 | data[CONF_METER_TYPE] = MeterTypes.METER_TYPE_H_3P 626 | 627 | config_entry.version = 2 628 | hass.config_entries.async_update_entry(config_entry, data=data) 629 | 630 | _LOGGER.info("Migration to version %s successful", config_entry.version) 631 | 632 | return True 633 | -------------------------------------------------------------------------------- /custom_components/chint_pm/sensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Callable 4 | from dataclasses import dataclass 5 | from itertools import zip_longest 6 | from typing import Any 7 | 8 | from homeassistant.components.sensor import ( 9 | SensorDeviceClass, 10 | SensorEntity, 11 | SensorEntityDescription, 12 | SensorStateClass, 13 | ) 14 | from homeassistant.const import ( 15 | UnitOfElectricCurrent, 16 | UnitOfElectricPotential, 17 | UnitOfEnergy, 18 | UnitOfFrequency, 19 | UnitOfPower, 20 | UnitOfReactivePower, 21 | ) 22 | from homeassistant.core import callback 23 | from homeassistant.helpers.entity import EntityCategory 24 | from homeassistant.helpers.update_coordinator import CoordinatorEntity 25 | 26 | from . import ChintDxsuDevice, ChintUpdateCoordinator 27 | from .const import ( 28 | CONF_METER_TYPE, 29 | CONF_PHASE_MODE, 30 | DATA_UPDATE_COORDINATORS, 31 | DOMAIN, 32 | PHMODE_3P3W, 33 | PHMODE_3P4W, 34 | MeterTypes, 35 | ) 36 | 37 | 38 | @dataclass 39 | class ChintPmSensorEntityDescription(SensorEntityDescription): 40 | """Chint PM Sensor Entity.""" 41 | 42 | phase_mode_relevant: str | None = None 43 | address: int | None = None 44 | count: int | None = None 45 | data_type: str | None = None 46 | value_conversion_function: Callable[[Any], str] | None = None 47 | 48 | 49 | SENSOR_DESCRIPTIONS: tuple[ChintPmSensorEntityDescription, ...] = ( 50 | ChintPmSensorEntityDescription( 51 | key="rev", 52 | name="Version", 53 | icon="mdi:package-variant", 54 | entity_category=EntityCategory.DIAGNOSTIC, 55 | entity_registry_enabled_default=False, 56 | ), 57 | ChintPmSensorEntityDescription( 58 | key="ucode", 59 | name="Programming password codE", 60 | icon="mdi:form-textbox-password", 61 | entity_category=EntityCategory.DIAGNOSTIC, 62 | entity_registry_enabled_default=False, 63 | ), 64 | ChintPmSensorEntityDescription( 65 | key="clre", 66 | name="Electric energy zero clearing CLr.E(1:zero clearing)", 67 | icon="mdi:tune-vertical-variant", 68 | entity_category=EntityCategory.DIAGNOSTIC, 69 | entity_registry_enabled_default=False, 70 | ), 71 | ChintPmSensorEntityDescription( 72 | key="net", 73 | name="Connection mode net", 74 | icon="mdi:tune-vertical-variant", 75 | entity_category=EntityCategory.DIAGNOSTIC, 76 | entity_registry_enabled_default=False, 77 | ), 78 | ChintPmSensorEntityDescription( 79 | key="irat", 80 | name="Current Transformer Ratio", 81 | icon="mdi:information-outline", 82 | entity_category=EntityCategory.DIAGNOSTIC, 83 | entity_registry_enabled_default=False, 84 | ), 85 | ChintPmSensorEntityDescription( 86 | key="urat", 87 | name="Potential Transformer Ratio(*)", 88 | icon="mdi:information-outline", 89 | entity_category=EntityCategory.DIAGNOSTIC, 90 | entity_registry_enabled_default=False, 91 | value_conversion_function=lambda value: value * 0.1, 92 | ), 93 | ChintPmSensorEntityDescription( 94 | key="meter_type", 95 | name="Meter type", 96 | icon="mdi:format-list-bulleted-type", 97 | entity_category=EntityCategory.DIAGNOSTIC, 98 | entity_registry_enabled_default=False, 99 | ), 100 | ChintPmSensorEntityDescription( 101 | key="protocol", 102 | name="Protocol changing-over", 103 | icon="mdi:electric-switch-closed", 104 | entity_category=EntityCategory.DIAGNOSTIC, 105 | entity_registry_enabled_default=False, 106 | ), 107 | ChintPmSensorEntityDescription( 108 | key="addr", 109 | name="Communication address Addr", 110 | icon="mdi:map-marker-outline", 111 | entity_category=EntityCategory.DIAGNOSTIC, 112 | entity_registry_enabled_default=False, 113 | ), 114 | ChintPmSensorEntityDescription( 115 | key="baud", 116 | name="Communication baud rate bAud", 117 | icon="mdi:speedometer", 118 | entity_category=EntityCategory.DIAGNOSTIC, 119 | entity_registry_enabled_default=False, 120 | ), 121 | ChintPmSensorEntityDescription( 122 | key="secound", 123 | name="Second", 124 | icon="mdi:clock-outline", 125 | entity_category=EntityCategory.DIAGNOSTIC, 126 | entity_registry_enabled_default=False, 127 | ), 128 | ChintPmSensorEntityDescription( 129 | key="minutes", 130 | name="Minute", 131 | icon="mdi:clock-outline", 132 | entity_category=EntityCategory.DIAGNOSTIC, 133 | entity_registry_enabled_default=False, 134 | ), 135 | ChintPmSensorEntityDescription( 136 | key="hour", 137 | name="Hour", 138 | icon="mdi:clock-outline", 139 | entity_category=EntityCategory.DIAGNOSTIC, 140 | entity_registry_enabled_default=False, 141 | ), 142 | ChintPmSensorEntityDescription( 143 | key="day", 144 | name="Day", 145 | icon="mdi:calendar-month-outline", 146 | entity_category=EntityCategory.DIAGNOSTIC, 147 | entity_registry_enabled_default=False, 148 | ), 149 | ChintPmSensorEntityDescription( 150 | key="month", 151 | name="Month", 152 | icon="mdi:calendar-month-outline", 153 | entity_category=EntityCategory.DIAGNOSTIC, 154 | entity_registry_enabled_default=False, 155 | ), 156 | ChintPmSensorEntityDescription( 157 | key="year", 158 | name="Year", 159 | icon="mdi:calendar-month-outline", 160 | entity_category=EntityCategory.DIAGNOSTIC, 161 | entity_registry_enabled_default=False, 162 | ), 163 | # electricity measurements 164 | ChintPmSensorEntityDescription( 165 | key="uab", 166 | name="Line AB-line voltage", 167 | phase_mode_relevant=PHMODE_3P3W, 168 | icon="mdi:sine-wave", 169 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 170 | device_class=SensorDeviceClass.VOLTAGE, 171 | state_class=SensorStateClass.MEASUREMENT, 172 | entity_registry_enabled_default=False, 173 | value_conversion_function=lambda value: round(value, 2), 174 | ), 175 | ChintPmSensorEntityDescription( 176 | key="ubc", 177 | name="Line BC-line voltage", 178 | phase_mode_relevant=PHMODE_3P3W, 179 | icon="mdi:sine-wave", 180 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 181 | device_class=SensorDeviceClass.VOLTAGE, 182 | state_class=SensorStateClass.MEASUREMENT, 183 | entity_registry_enabled_default=False, 184 | value_conversion_function=lambda value: round(value, 2), 185 | ), 186 | ChintPmSensorEntityDescription( 187 | key="uca", 188 | name="Line CA-line voltage", 189 | phase_mode_relevant=PHMODE_3P3W, 190 | icon="mdi:sine-wave", 191 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 192 | device_class=SensorDeviceClass.VOLTAGE, 193 | state_class=SensorStateClass.MEASUREMENT, 194 | entity_registry_enabled_default=False, 195 | value_conversion_function=lambda value: round(value, 2), 196 | ), 197 | ChintPmSensorEntityDescription( 198 | key="ua", 199 | name="A-phase voltage", 200 | phase_mode_relevant=PHMODE_3P4W, 201 | icon="mdi:sine-wave", 202 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 203 | device_class=SensorDeviceClass.VOLTAGE, 204 | state_class=SensorStateClass.MEASUREMENT, 205 | entity_registry_enabled_default=True, 206 | value_conversion_function=lambda value: round(value, 2), 207 | ), 208 | ChintPmSensorEntityDescription( 209 | key="ub", 210 | name="B-phase voltage", 211 | phase_mode_relevant=PHMODE_3P4W, 212 | icon="mdi:sine-wave", 213 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 214 | device_class=SensorDeviceClass.VOLTAGE, 215 | state_class=SensorStateClass.MEASUREMENT, 216 | entity_registry_enabled_default=True, 217 | value_conversion_function=lambda value: round(value, 2), 218 | ), 219 | ChintPmSensorEntityDescription( 220 | key="uc", 221 | name="C-phase voltage", 222 | phase_mode_relevant=PHMODE_3P4W, 223 | icon="mdi:sine-wave", 224 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 225 | device_class=SensorDeviceClass.VOLTAGE, 226 | state_class=SensorStateClass.MEASUREMENT, 227 | entity_registry_enabled_default=True, 228 | value_conversion_function=lambda value: round(value, 2), 229 | ), 230 | ChintPmSensorEntityDescription( 231 | key="ia", 232 | name="A phase current", 233 | icon="mdi:current-ac", 234 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 235 | device_class=SensorDeviceClass.CURRENT, 236 | state_class=SensorStateClass.MEASUREMENT, 237 | entity_registry_enabled_default=True, 238 | value_conversion_function=lambda value: round(value, 2), 239 | ), 240 | ChintPmSensorEntityDescription( 241 | key="ib", 242 | name="B phase current", 243 | phase_mode_relevant=PHMODE_3P4W, 244 | icon="mdi:current-ac", 245 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 246 | device_class=SensorDeviceClass.CURRENT, 247 | state_class=SensorStateClass.MEASUREMENT, 248 | entity_registry_enabled_default=True, 249 | value_conversion_function=lambda value: round(value, 2), 250 | ), 251 | ChintPmSensorEntityDescription( 252 | key="ic", 253 | name="C phase current", 254 | icon="mdi:current-ac", 255 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 256 | device_class=SensorDeviceClass.CURRENT, 257 | state_class=SensorStateClass.MEASUREMENT, 258 | entity_registry_enabled_default=True, 259 | value_conversion_function=lambda value: round(value, 2), 260 | ), 261 | ChintPmSensorEntityDescription( 262 | key="pt", 263 | name="Conjunction active power", 264 | icon="mdi:flash", 265 | native_unit_of_measurement=UnitOfPower.WATT, 266 | device_class=SensorDeviceClass.POWER, 267 | state_class=SensorStateClass.MEASUREMENT, 268 | entity_registry_enabled_default=True, 269 | value_conversion_function=lambda value: round(value, 2), 270 | ), 271 | ChintPmSensorEntityDescription( 272 | key="pa", 273 | name="A phase active power", 274 | icon="mdi:flash", 275 | native_unit_of_measurement=UnitOfPower.WATT, 276 | device_class=SensorDeviceClass.POWER, 277 | state_class=SensorStateClass.MEASUREMENT, 278 | entity_registry_enabled_default=True, 279 | value_conversion_function=lambda value: round(value, 2), 280 | ), 281 | ChintPmSensorEntityDescription( 282 | key="pb", 283 | name="B phase active power", 284 | phase_mode_relevant=PHMODE_3P4W, 285 | icon="mdi:flash", 286 | native_unit_of_measurement=UnitOfPower.WATT, 287 | device_class=SensorDeviceClass.POWER, 288 | state_class=SensorStateClass.MEASUREMENT, 289 | entity_registry_enabled_default=True, 290 | value_conversion_function=lambda value: round(value, 2), 291 | ), 292 | ChintPmSensorEntityDescription( 293 | key="pc", 294 | name="C phase active power", 295 | icon="mdi:flash", 296 | native_unit_of_measurement=UnitOfPower.WATT, 297 | device_class=SensorDeviceClass.POWER, 298 | state_class=SensorStateClass.MEASUREMENT, 299 | entity_registry_enabled_default=True, 300 | value_conversion_function=lambda value: round(value, 2), 301 | ), 302 | ChintPmSensorEntityDescription( 303 | key="qt", 304 | name="Conjunction reactive power", 305 | icon="mdi:lightning-bolt-circle", 306 | native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 307 | device_class=SensorDeviceClass.REACTIVE_POWER, 308 | state_class=SensorStateClass.MEASUREMENT, 309 | entity_registry_enabled_default=True, 310 | value_conversion_function=lambda value: round(value, 2), 311 | ), 312 | ChintPmSensorEntityDescription( 313 | key="qa", 314 | name="A phase reactive power", 315 | icon="mdi:lightning-bolt-circle", 316 | native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 317 | device_class=SensorDeviceClass.REACTIVE_POWER, 318 | state_class=SensorStateClass.MEASUREMENT, 319 | entity_registry_enabled_default=True, 320 | value_conversion_function=lambda value: round(value, 2), 321 | ), 322 | ChintPmSensorEntityDescription( 323 | key="qb", 324 | name="B phase reactive power", 325 | phase_mode_relevant=PHMODE_3P4W, 326 | icon="mdi:lightning-bolt-circle", 327 | native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 328 | device_class=SensorDeviceClass.REACTIVE_POWER, 329 | state_class=SensorStateClass.MEASUREMENT, 330 | entity_registry_enabled_default=True, 331 | value_conversion_function=lambda value: round(value, 2), 332 | ), 333 | ChintPmSensorEntityDescription( 334 | key="qc", 335 | name="C phase reactive power", 336 | icon="mdi:lightning-bolt-circle", 337 | native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE, 338 | device_class=SensorDeviceClass.REACTIVE_POWER, 339 | state_class=SensorStateClass.MEASUREMENT, 340 | entity_registry_enabled_default=True, 341 | value_conversion_function=lambda value: round(value, 2), 342 | ), 343 | ChintPmSensorEntityDescription( 344 | key="pft", 345 | name="Conjunction power factor", 346 | icon="mdi:math-cos", 347 | device_class=SensorDeviceClass.POWER_FACTOR, 348 | state_class=SensorStateClass.MEASUREMENT, 349 | entity_registry_enabled_default=False, 350 | value_conversion_function=lambda value: round(value, 2), 351 | ), 352 | ChintPmSensorEntityDescription( 353 | key="pfa", 354 | name="A phase power factor", 355 | phase_mode_relevant=PHMODE_3P4W, 356 | icon="mdi:math-cos", 357 | device_class=SensorDeviceClass.POWER_FACTOR, 358 | state_class=SensorStateClass.MEASUREMENT, 359 | entity_registry_enabled_default=False, 360 | value_conversion_function=lambda value: round(value, 2), 361 | ), 362 | ChintPmSensorEntityDescription( 363 | key="pfb", 364 | name="B phase power factor", 365 | phase_mode_relevant=PHMODE_3P4W, 366 | icon="mdi:math-cos", 367 | device_class=SensorDeviceClass.POWER_FACTOR, 368 | state_class=SensorStateClass.MEASUREMENT, 369 | entity_registry_enabled_default=False, 370 | value_conversion_function=lambda value: round(value, 2), 371 | ), 372 | ChintPmSensorEntityDescription( 373 | key="pfc", 374 | name="C phase power factor", 375 | phase_mode_relevant=PHMODE_3P4W, 376 | icon="mdi:math-cos", 377 | device_class=SensorDeviceClass.POWER_FACTOR, 378 | state_class=SensorStateClass.MEASUREMENT, 379 | entity_registry_enabled_default=False, 380 | value_conversion_function=lambda value: round(value, 2), 381 | ), 382 | ChintPmSensorEntityDescription( 383 | key="freq", 384 | name="Frequency", 385 | icon="mdi:wave", 386 | native_unit_of_measurement=UnitOfFrequency.HERTZ, 387 | device_class=SensorDeviceClass.FREQUENCY, 388 | state_class=SensorStateClass.MEASUREMENT, 389 | entity_registry_enabled_default=True, 390 | value_conversion_function=lambda value: round(value, 2), 391 | ), 392 | ChintPmSensorEntityDescription( 393 | key="dmpt", 394 | name="Total active power demand", 395 | icon="mdi:home-lightning-bolt-outline", 396 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 397 | device_class=SensorDeviceClass.CURRENT, 398 | state_class=SensorStateClass.MEASUREMENT, 399 | entity_registry_enabled_default=True, 400 | value_conversion_function=lambda value: round(value, 2), 401 | ), 402 | ChintPmSensorEntityDescription( 403 | key="impep", 404 | name="Positive active total energy", 405 | icon="mdi:transmission-tower-export", 406 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 407 | device_class=SensorDeviceClass.ENERGY, 408 | state_class=SensorStateClass.TOTAL_INCREASING, 409 | entity_registry_enabled_default=True, 410 | value_conversion_function=lambda value: round(value, 2), 411 | ), 412 | ChintPmSensorEntityDescription( 413 | key="expep", 414 | name="Negative active total energy", 415 | icon="mdi:transmission-tower-import", 416 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 417 | device_class=SensorDeviceClass.ENERGY, 418 | state_class=SensorStateClass.TOTAL_INCREASING, 419 | entity_registry_enabled_default=True, 420 | value_conversion_function=lambda value: round(value, 2), 421 | ), 422 | ChintPmSensorEntityDescription( 423 | key="q1eq", 424 | name="Quadrant I reactive total energy", 425 | icon="mdi:", 426 | native_unit_of_measurement="kVarh", 427 | device_class=None, 428 | state_class=SensorStateClass.TOTAL_INCREASING, 429 | entity_registry_enabled_default=False, 430 | value_conversion_function=lambda value: round(value, 2), 431 | ), 432 | ChintPmSensorEntityDescription( 433 | key="q2eq", 434 | name="Quadrant II reactive total energy", 435 | icon="mdi:", 436 | native_unit_of_measurement="kVarh", 437 | device_class=None, 438 | state_class=SensorStateClass.TOTAL_INCREASING, 439 | entity_registry_enabled_default=False, 440 | value_conversion_function=lambda value: round(value, 2), 441 | ), 442 | ChintPmSensorEntityDescription( 443 | key="q3eq", 444 | name="Quadrant III reactive total energy", 445 | icon="mdi:", 446 | native_unit_of_measurement="kVarh", 447 | device_class=None, 448 | state_class=SensorStateClass.TOTAL_INCREASING, 449 | entity_registry_enabled_default=False, 450 | value_conversion_function=lambda value: round(value, 2), 451 | ), 452 | ChintPmSensorEntityDescription( 453 | key="q4eq", 454 | name="Quadrant IV reactive total energy", 455 | icon="mdi:", 456 | native_unit_of_measurement="kVarh", 457 | device_class=None, 458 | state_class=SensorStateClass.TOTAL_INCREASING, 459 | entity_registry_enabled_default=False, 460 | value_conversion_function=lambda value: round(value, 2), 461 | ), 462 | ) 463 | 464 | SENSOR_DESCRIPTIONS_TYPE_NORMAL: tuple[ChintPmSensorEntityDescription, ...] = ( 465 | ChintPmSensorEntityDescription( 466 | key="rev", 467 | name="Version", 468 | icon="mdi:package-variant", 469 | entity_category=EntityCategory.DIAGNOSTIC, 470 | entity_registry_enabled_default=False, 471 | ), 472 | ChintPmSensorEntityDescription( 473 | key="ucode", 474 | name="Programming password codE", 475 | icon="mdi:form-textbox-password", 476 | entity_category=EntityCategory.DIAGNOSTIC, 477 | entity_registry_enabled_default=False, 478 | ), 479 | ChintPmSensorEntityDescription( 480 | key="clre", 481 | name="Electric energy zero clearing CLr.E(1:zero clearing)", 482 | icon="mdi:tune-vertical-variant", 483 | entity_category=EntityCategory.DIAGNOSTIC, 484 | entity_registry_enabled_default=False, 485 | ), 486 | ChintPmSensorEntityDescription( 487 | key="net", 488 | name="Connection mode net", 489 | icon="mdi:tune-vertical-variant", 490 | entity_category=EntityCategory.DIAGNOSTIC, 491 | entity_registry_enabled_default=False, 492 | ), 493 | ChintPmSensorEntityDescription( 494 | key="irat", 495 | name="Current Transformer Ratio", 496 | icon="mdi:information-outline", 497 | entity_category=EntityCategory.DIAGNOSTIC, 498 | entity_registry_enabled_default=False, 499 | ), 500 | ChintPmSensorEntityDescription( 501 | key="urat", 502 | name="Potential Transformer Ratio(*)", 503 | icon="mdi:information-outline", 504 | entity_category=EntityCategory.DIAGNOSTIC, 505 | entity_registry_enabled_default=False, 506 | value_conversion_function=lambda value: value * 0.1, 507 | ), 508 | ChintPmSensorEntityDescription( 509 | key="protocol", 510 | name="Protocol changing-over", 511 | icon="mdi:electric-switch-closed", 512 | entity_category=EntityCategory.DIAGNOSTIC, 513 | entity_registry_enabled_default=False, 514 | ), 515 | ChintPmSensorEntityDescription( 516 | key="addr", 517 | name="Communication address Addr", 518 | icon="mdi:map-marker-outline", 519 | entity_category=EntityCategory.DIAGNOSTIC, 520 | entity_registry_enabled_default=False, 521 | ), 522 | ChintPmSensorEntityDescription( 523 | key="baud", 524 | name="Communication baud rate bAud", 525 | icon="mdi:speedometer", 526 | entity_category=EntityCategory.DIAGNOSTIC, 527 | entity_registry_enabled_default=False, 528 | ), 529 | # electricity measurements 530 | ChintPmSensorEntityDescription( 531 | key="uab", 532 | name="Line AB-line voltage", 533 | phase_mode_relevant=PHMODE_3P3W, 534 | icon="mdi:sine-wave", 535 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 536 | device_class=SensorDeviceClass.VOLTAGE, 537 | state_class=SensorStateClass.MEASUREMENT, 538 | entity_registry_enabled_default=False, 539 | value_conversion_function=lambda value: round(value * 0.1, 2), 540 | ), 541 | ChintPmSensorEntityDescription( 542 | key="ubc", 543 | name="Line BC-line voltage", 544 | phase_mode_relevant=PHMODE_3P3W, 545 | icon="mdi:sine-wave", 546 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 547 | device_class=SensorDeviceClass.VOLTAGE, 548 | state_class=SensorStateClass.MEASUREMENT, 549 | entity_registry_enabled_default=False, 550 | value_conversion_function=lambda value: round(value * 0.1, 2), 551 | ), 552 | ChintPmSensorEntityDescription( 553 | key="uca", 554 | name="Line CA-line voltage", 555 | phase_mode_relevant=PHMODE_3P3W, 556 | icon="mdi:sine-wave", 557 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 558 | device_class=SensorDeviceClass.VOLTAGE, 559 | state_class=SensorStateClass.MEASUREMENT, 560 | entity_registry_enabled_default=False, 561 | value_conversion_function=lambda value: round(value * 0.1, 2), 562 | ), 563 | ChintPmSensorEntityDescription( 564 | key="ua", 565 | name="A-phase voltage", 566 | phase_mode_relevant=PHMODE_3P4W, 567 | icon="mdi:sine-wave", 568 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 569 | device_class=SensorDeviceClass.VOLTAGE, 570 | state_class=SensorStateClass.MEASUREMENT, 571 | entity_registry_enabled_default=True, 572 | value_conversion_function=lambda value: round(value * 0.1, 2), 573 | ), 574 | ChintPmSensorEntityDescription( 575 | key="ub", 576 | name="B-phase voltage", 577 | phase_mode_relevant=PHMODE_3P4W, 578 | icon="mdi:sine-wave", 579 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 580 | device_class=SensorDeviceClass.VOLTAGE, 581 | state_class=SensorStateClass.MEASUREMENT, 582 | entity_registry_enabled_default=True, 583 | value_conversion_function=lambda value: round(value * 0.1, 2), 584 | ), 585 | ChintPmSensorEntityDescription( 586 | key="uc", 587 | name="C-phase voltage", 588 | phase_mode_relevant=PHMODE_3P4W, 589 | icon="mdi:sine-wave", 590 | native_unit_of_measurement=UnitOfElectricPotential.VOLT, 591 | device_class=SensorDeviceClass.VOLTAGE, 592 | state_class=SensorStateClass.MEASUREMENT, 593 | entity_registry_enabled_default=True, 594 | value_conversion_function=lambda value: round(value * 0.1, 2), 595 | ), 596 | ChintPmSensorEntityDescription( 597 | key="ia", 598 | name="A phase current", 599 | icon="mdi:current-ac", 600 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 601 | device_class=SensorDeviceClass.CURRENT, 602 | state_class=SensorStateClass.MEASUREMENT, 603 | entity_registry_enabled_default=True, 604 | value_conversion_function=lambda value: round(value * 0.001, 2), 605 | ), 606 | ChintPmSensorEntityDescription( 607 | key="ib", 608 | name="B phase current", 609 | phase_mode_relevant=PHMODE_3P4W, 610 | icon="mdi:current-ac", 611 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 612 | device_class=SensorDeviceClass.CURRENT, 613 | state_class=SensorStateClass.MEASUREMENT, 614 | entity_registry_enabled_default=True, 615 | value_conversion_function=lambda value: round(value * 0.001, 2), 616 | ), 617 | ChintPmSensorEntityDescription( 618 | key="ic", 619 | name="C phase current", 620 | icon="mdi:current-ac", 621 | native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, 622 | device_class=SensorDeviceClass.CURRENT, 623 | state_class=SensorStateClass.MEASUREMENT, 624 | entity_registry_enabled_default=True, 625 | value_conversion_function=lambda value: round(value * 0.001, 2), 626 | ), 627 | ChintPmSensorEntityDescription( 628 | key="pt", 629 | name="Conjunction active power", 630 | icon="mdi:flash", 631 | native_unit_of_measurement=UnitOfPower.WATT, 632 | device_class=SensorDeviceClass.POWER, 633 | state_class=SensorStateClass.MEASUREMENT, 634 | entity_registry_enabled_default=True, 635 | value_conversion_function=lambda value: round(value * 0.1, 2), 636 | ), 637 | ChintPmSensorEntityDescription( 638 | key="pa", 639 | name="A phase active power", 640 | icon="mdi:flash", 641 | native_unit_of_measurement=UnitOfPower.WATT, 642 | device_class=SensorDeviceClass.POWER, 643 | state_class=SensorStateClass.MEASUREMENT, 644 | entity_registry_enabled_default=True, 645 | value_conversion_function=lambda value: round(value * 0.1, 2), 646 | ), 647 | ChintPmSensorEntityDescription( 648 | key="pb", 649 | name="B phase active power", 650 | phase_mode_relevant=PHMODE_3P4W, 651 | icon="mdi:flash", 652 | native_unit_of_measurement=UnitOfPower.WATT, 653 | device_class=SensorDeviceClass.POWER, 654 | state_class=SensorStateClass.MEASUREMENT, 655 | entity_registry_enabled_default=True, 656 | value_conversion_function=lambda value: round(value * 0.1, 2), 657 | ), 658 | ChintPmSensorEntityDescription( 659 | key="pc", 660 | name="C phase active power", 661 | icon="mdi:flash", 662 | native_unit_of_measurement=UnitOfPower.WATT, 663 | device_class=SensorDeviceClass.POWER, 664 | state_class=SensorStateClass.MEASUREMENT, 665 | entity_registry_enabled_default=True, 666 | value_conversion_function=lambda value: round(value * 0.1, 2), 667 | ), 668 | ChintPmSensorEntityDescription( 669 | key="qt", 670 | name="Conjunction reactive power", 671 | icon="mdi:lightning-bolt-circle", 672 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 673 | device_class=SensorDeviceClass.REACTIVE_POWER, 674 | state_class=SensorStateClass.MEASUREMENT, 675 | entity_registry_enabled_default=True, 676 | value_conversion_function=lambda value: round(value * 0.1, 2), 677 | ), 678 | ChintPmSensorEntityDescription( 679 | key="qa", 680 | name="A phase reactive power", 681 | icon="mdi:lightning-bolt-circle", 682 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 683 | device_class=SensorDeviceClass.REACTIVE_POWER, 684 | state_class=SensorStateClass.MEASUREMENT, 685 | entity_registry_enabled_default=True, 686 | value_conversion_function=lambda value: round(value * 0.1, 2), 687 | ), 688 | ChintPmSensorEntityDescription( 689 | key="qb", 690 | name="B phase reactive power", 691 | phase_mode_relevant=PHMODE_3P4W, 692 | icon="mdi:lightning-bolt-circle", 693 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 694 | device_class=SensorDeviceClass.REACTIVE_POWER, 695 | state_class=SensorStateClass.MEASUREMENT, 696 | entity_registry_enabled_default=True, 697 | value_conversion_function=lambda value: round(value * 0.1, 2), 698 | ), 699 | ChintPmSensorEntityDescription( 700 | key="qc", 701 | name="C phase reactive power", 702 | icon="mdi:lightning-bolt-circle", 703 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 704 | device_class=SensorDeviceClass.REACTIVE_POWER, 705 | state_class=SensorStateClass.MEASUREMENT, 706 | entity_registry_enabled_default=True, 707 | value_conversion_function=lambda value: round(value * 0.1, 2), 708 | ), 709 | ChintPmSensorEntityDescription( 710 | key="pft", 711 | name="Conjunction power factor", 712 | icon="mdi:math-cos", 713 | device_class=SensorDeviceClass.POWER_FACTOR, 714 | state_class=SensorStateClass.MEASUREMENT, 715 | entity_registry_enabled_default=False, 716 | value_conversion_function=lambda value: round(value * 0.001, 2), 717 | ), 718 | ChintPmSensorEntityDescription( 719 | key="pfa", 720 | name="A phase power factor", 721 | phase_mode_relevant=PHMODE_3P4W, 722 | icon="mdi:math-cos", 723 | device_class=SensorDeviceClass.POWER_FACTOR, 724 | state_class=SensorStateClass.MEASUREMENT, 725 | entity_registry_enabled_default=False, 726 | value_conversion_function=lambda value: round(value * 0.001, 2), 727 | ), 728 | ChintPmSensorEntityDescription( 729 | key="pfb", 730 | name="B phase power factor", 731 | phase_mode_relevant=PHMODE_3P4W, 732 | icon="mdi:math-cos", 733 | device_class=SensorDeviceClass.POWER_FACTOR, 734 | state_class=SensorStateClass.MEASUREMENT, 735 | entity_registry_enabled_default=False, 736 | value_conversion_function=lambda value: round(value * 0.001, 2), 737 | ), 738 | ChintPmSensorEntityDescription( 739 | key="pfc", 740 | name="C phase power factor", 741 | phase_mode_relevant=PHMODE_3P4W, 742 | icon="mdi:math-cos", 743 | device_class=SensorDeviceClass.POWER_FACTOR, 744 | state_class=SensorStateClass.MEASUREMENT, 745 | entity_registry_enabled_default=False, 746 | value_conversion_function=lambda value: round(value * 0.001, 2), 747 | ), 748 | ChintPmSensorEntityDescription( 749 | key="freq", 750 | name="Frequency", 751 | icon="mdi:wave", 752 | native_unit_of_measurement=UnitOfFrequency.HERTZ, 753 | device_class=SensorDeviceClass.FREQUENCY, 754 | state_class=SensorStateClass.MEASUREMENT, 755 | entity_registry_enabled_default=True, 756 | value_conversion_function=lambda value: round(value * 0.01, 2), 757 | ), 758 | ChintPmSensorEntityDescription( 759 | key="impep", 760 | name="Positive active total energy", 761 | icon="mdi:transmission-tower-export", 762 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 763 | device_class=SensorDeviceClass.ENERGY, 764 | state_class=SensorStateClass.TOTAL_INCREASING, 765 | entity_registry_enabled_default=True, 766 | value_conversion_function=lambda value: round(value, 2), 767 | ), 768 | ChintPmSensorEntityDescription( 769 | key="expep", 770 | name="Negative active total energy", 771 | icon="mdi:transmission-tower-import", 772 | native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, 773 | device_class=SensorDeviceClass.ENERGY, 774 | state_class=SensorStateClass.TOTAL_INCREASING, 775 | entity_registry_enabled_default=True, 776 | value_conversion_function=lambda value: round(value, 2), 777 | ), 778 | ChintPmSensorEntityDescription( 779 | key="q1eq", 780 | name="Quadrant I reactive total energy", 781 | icon="mdi:", 782 | native_unit_of_measurement="kVarh", 783 | device_class=None, 784 | state_class=SensorStateClass.TOTAL_INCREASING, 785 | entity_registry_enabled_default=False, 786 | value_conversion_function=lambda value: round(value, 2), 787 | ), 788 | ChintPmSensorEntityDescription( 789 | key="q2eq", 790 | name="Quadrant II reactive total energy", 791 | icon="mdi:", 792 | native_unit_of_measurement="kVarh", 793 | device_class=None, 794 | state_class=SensorStateClass.TOTAL_INCREASING, 795 | entity_registry_enabled_default=False, 796 | value_conversion_function=lambda value: round(value, 2), 797 | ), 798 | ChintPmSensorEntityDescription( 799 | key="q3eq", 800 | name="Quadrant III reactive total energy", 801 | icon="mdi:", 802 | native_unit_of_measurement="kVarh", 803 | device_class=None, 804 | state_class=SensorStateClass.TOTAL_INCREASING, 805 | entity_registry_enabled_default=False, 806 | value_conversion_function=lambda value: round(value, 2), 807 | ), 808 | ChintPmSensorEntityDescription( 809 | key="q4eq", 810 | name="Quadrant IV reactive total energy", 811 | icon="mdi:", 812 | native_unit_of_measurement="kVarh", 813 | device_class=None, 814 | state_class=SensorStateClass.TOTAL_INCREASING, 815 | entity_registry_enabled_default=False, 816 | value_conversion_function=lambda value: round(value, 2), 817 | ), 818 | ) 819 | 820 | 821 | async def async_setup_entry(hass, entry, async_add_entities): 822 | """Add pm entry.""" 823 | 824 | update_coordinators: list[ChintUpdateCoordinator] = hass.data[DOMAIN][ 825 | entry.entry_id 826 | ][DATA_UPDATE_COORDINATORS] 827 | 828 | entities_to_add: list[SensorEntity] = [] 829 | for idx, (update_coordinator) in enumerate(zip_longest(update_coordinators)): 830 | # device = update_coordinators[idx].device 831 | device_info = update_coordinator[idx].device_info 832 | 833 | match entry.data[CONF_METER_TYPE]: 834 | case MeterTypes.METER_TYPE_CT_3P: 835 | used_sensor_description = SENSOR_DESCRIPTIONS_TYPE_NORMAL 836 | case _: 837 | used_sensor_description = SENSOR_DESCRIPTIONS 838 | 839 | for entity_description in used_sensor_description: 840 | if entity_description.phase_mode_relevant == entry.data[CONF_PHASE_MODE]: 841 | entity_description.entity_registry_enabled_default = True 842 | 843 | sensor = ChintPMModbusSensor( 844 | update_coordinator[idx], entity_description, device_info 845 | ) 846 | # TODO: itt kell hozzá adnom a modbus cimeket 847 | # await update_coordinator[idx].push_sensor_read( 848 | # entity_description.address, 849 | # entity_description.count, 850 | # entity_description.data_type, 851 | # ) 852 | entities_to_add.append(sensor) 853 | 854 | async_add_entities(entities_to_add, True) 855 | 856 | 857 | class ChintPMModbusSensor(CoordinatorEntity, ChintDxsuDevice, SensorEntity): 858 | """power meter sensor""" 859 | 860 | def __init__( 861 | self, 862 | coordinator: ChintUpdateCoordinator, 863 | description: ChintPmSensorEntityDescription, 864 | device_info, 865 | ): 866 | """Batched Huawei Solar Sensor Entity constructor.""" 867 | super().__init__(coordinator) 868 | 869 | self.coordinator = coordinator 870 | self.entity_description = description 871 | 872 | self._attr_device_info = device_info 873 | self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" 874 | 875 | @callback 876 | def _handle_coordinator_update(self) -> None: 877 | """Handle updated data from the coordinator.""" 878 | if self.entity_description.key in self.coordinator.device.data: 879 | value = self.coordinator.device.data[self.entity_description.key] 880 | if self.entity_description.value_conversion_function: 881 | value = self.entity_description.value_conversion_function(value) 882 | 883 | self._attr_native_value = value 884 | self.async_write_ha_state() 885 | --------------------------------------------------------------------------------