Compare commits

..
87 Commits
Author SHA1 Message Date
Patrick762 edf5e87720 Merge pull request #128 from Patrick762/version-update
update integration manifest version
2024-08-04 12:13:29 +02:00
Patrick762 742964b78b update integration manifest version 2024-08-04 12:12:55 +02:00
Patrick762 21e89e738d Merge pull request #127 from Kathou99/main
Update config_flow.py
2024-08-04 12:08:16 +02:00
Patrick762 52fd1932fa Merge pull request #118 from AChep/main
Various imprvements for AC300 & AC500
2024-08-04 12:07:43 +02:00
KathouQC(GirlsPowers) 3e54283faf Update config_flow.py
fix this bug: OptionsFlowHandler.async_step_init: Config entry f3a445b566f0876e0d79f02059b7cb2a has not been changed
2024-08-03 18:15:49 -04:00
Artem Chepurnyi 9be8aa47ac Add dsp/arm_version to diagnostic fields 2024-08-03 21:36:27 +03:00
Artem ChepurnyiandPatrick762 8967f3379c Fix a typo in the Polling commands text
Co-authored-by: Patrick762 <[email protected]>
2024-08-03 21:32:28 +03:00
Patrick762 6f9afedfdb Merge pull request #119 from kvanonavk/main
AC180P Advanced Integration
2024-07-19 16:17:16 +02:00
Patrick762 fe23dd00cd Merge pull request #122 from Patrick762/ac70-fix
Added ac70 auto detection
2024-07-19 16:13:48 +02:00
Patrick762 566761dba7 update version 2024-07-19 16:13:19 +02:00
Patrick762 33cb6e6fbd added ac70 auto detection 2024-07-19 15:02:57 +02:00
kvanonavk 5823136280 Merge branch 'Patrick762:main' into main 2024-07-05 14:44:12 +03:00
kvanonavk 7b6f845edb fixes 2024-07-05 14:42:56 +03:00
kvanonavk 1fbcf83b1b simple set charging mode (no turbo) 2024-07-01 18:16:58 +03:00
Artem Chepurnyi da2017a24b chore: Mention that AC500 is tested 2024-06-30 17:14:30 +03:00
Artem Chepurnyi 83f0db267c Merge branch 'Patrick762:main' into main 2024-06-30 17:12:50 +03:00
Patrick762 149633f9c8 Increase version number 2024-06-30 15:04:23 +02:00
Patrick762 9ab7cefda7 Merge pull request #116 from matthewpucc/main
Support AC70 and AC70P Device
2024-06-30 15:03:35 +02:00
Patrick762 52853d6d3e Merge pull request #117 from rotx/fix-multiply
Fix multiplication of Decimal with float
2024-06-30 15:03:14 +02:00
kvanonavk c96f551024 fixes 2024-06-30 14:11:31 +03:00
kvanonavk 75209ee1aa readme fix 2 2024-06-30 13:59:45 +03:00
kvanonavk 7396616113 readme fix 2024-06-30 13:56:58 +03:00
kvanonavk a7f7ff1843 fixes 2024-06-30 13:55:10 +03:00
kvanonavk cc755b3003 test + fix 2024-06-30 13:25:39 +03:00
kvanonavk e5f690ef3b AC180P more data and functionality 2024-06-30 12:24:52 +03:00
Artem Chepurnyi fa9bcf63ca fix: Actually retry connecting to a device 2024-06-29 17:04:43 +03:00
Artem Chepurnyi 211f1971c5 chore: Remove .DS_Store from the repo 2024-06-29 16:48:09 +03:00
Artem Chepurnyi a1e9c180b3 improvement: Automatically build polling commands basing on the fields 2024-06-29 16:45:59 +03:00
Artem Chepurnyi 0e5c1a0b44 fix: Increase polling interval and timeout #109 2024-06-29 16:45:24 +03:00
Artem Chepurnyi 0f33a31ee5 fix: Correctly read battery packs' values - wait before setting the target pack number #109 2024-06-29 16:44:44 +03:00
Artem Chepurnyi 23551ae829 improvement: Add ARM and DSP versions 2024-06-29 16:42:32 +03:00
Artem Chepurnyi a54b36a90b fix: Initialize sensor. _unavailable_counter with a 5 2024-06-29 16:42:04 +03:00
Artem Chepurnyi f85661345c fix(AC500): Change max pack number to 6 2024-06-29 13:58:22 +03:00
Robert Muchsel 3d3401c942 Fix multiplication of Decimal with float 2024-06-21 20:52:48 -05:00
Matthew Puccerella f50232ee89 Merge pull request #1 from matthewpucc/support_ac70_devices
Add support AC70 and AC70P devices
2024-06-20 16:12:53 -07:00
Matthew Puccerella 5922362772 Add support AC70 and AC70P devices 2024-06-20 23:11:47 +00:00
Patrick762 6478825503 Merge pull request #107 from Patrick762/next
Fixed values and improved unavailable handling
2024-06-02 14:06:20 +02:00
Patrick762 3169ee182e increase version number 2024-06-02 14:03:02 +02:00
Patrick762 ece1fc4129 only set unavailable after 5 retries 2024-06-02 13:58:21 +02:00
Patrick762 4d54e048c5 ignore packs not connected 2024-06-02 13:49:18 +02:00
Patrick762 09a9112279 check pack_num after setting it 2024-06-02 13:48:45 +02:00
Patrick762 b37276ec52 updated ac200l sensors based on feedback 2024-06-02 13:12:43 +02:00
Patrick762 4f88792371 Update bug_report.yaml 2024-05-31 17:16:26 +02:00
Patrick762 7da392d85a Merge pull request #54 from Patrick762/dev
[WIP] Release
2024-05-31 17:05:16 +02:00
Patrick762 03ea349cbf set new version to 0.1 2024-05-31 17:04:41 +02:00
Patrick762 5a762266c0 Merge pull request #104 from Patrick762/values-fix-2
Values and pack polling fix
2024-05-31 17:02:07 +02:00
Patrick762 12a59346e9 fix device type info 2024-05-31 16:59:46 +02:00
Patrick762 206099ab64 increase version number 2024-05-31 14:23:43 +02:00
Patrick762 6be2abaaf2 fix pack polling 2024-05-31 14:23:07 +02:00
Patrick762 c3bfad0ea6 add pack sensors if available 2024-05-31 13:46:09 +02:00
Patrick762 b548528085 updated pack polling 2024-05-31 12:55:46 +02:00
Patrick762 6e433e3551 fix total_battery_voltage 2024-05-31 12:14:51 +02:00
Patrick762 b34d0a3039 fix ac300 and ac500 solar string values and renamed entity 2024-05-31 12:06:21 +02:00
Patrick762 2922c5cb90 add multiplier to decimal field 2024-05-31 12:05:09 +02:00
Patrick762 9af3d67ec6 removed binary sensors for disabled control entities to prevent duplicates 2024-05-31 11:34:25 +02:00
Patrick762 221da9a38d fix internal_power sensors for ac300 and ac500 2024-05-31 11:24:20 +02:00
Patrick762 f5430216a8 Merge pull request #103 from Patrick762/values-fix-1
Values fix AC300 and AC500
2024-05-30 20:25:02 +02:00
Patrick762 591c86baf5 fix internal_dc_input_voltage for ac300 and ac500 2024-05-30 20:22:43 +02:00
Patrick762 97154bb027 increase version number 2024-05-30 19:57:02 +02:00
Patrick762 b2672ec302 fix availability states 2024-05-30 19:56:23 +02:00
Patrick762 f5b8e09fb4 prevent binary_sensor if controls enabled 2024-05-30 15:03:47 +02:00
Patrick762 c8d75d1899 Merge pull request #102 from Patrick762/bug-report-template
updated bug report template
2024-05-30 12:30:47 +02:00
Patrick762 efaa9d0fdb updated bug report template 2024-05-30 12:29:53 +02:00
Patrick762 9315650ddc Merge pull request #88 from Patrick762/pack-polling
Check pack_num only once
2024-05-30 12:27:51 +02:00
Patrick762 9df2073585 Merge pull request #96 from KM011092/dev
Additional EP600 values
2024-04-28 17:33:31 +02:00
KM011092 2c6df77078 Deleted .idea directory 2024-04-28 17:31:03 +02:00
KM011092 e584d32037 Additional EP600 values
Added Consumption and Grid, Consumption and Solar in Totals
2024-04-19 11:41:43 +02:00
Patrick762 5ac8d8d7ae only check pack_num once 2024-04-01 14:31:03 +02:00
Patrick762 0cd5ae10fb Merge pull request #87 from Patrick762/pack-polling-fix
Fix writeable_ranges for pack_polling
2024-04-01 14:13:48 +02:00
Patrick762 a93b80796a increase version number 2024-04-01 14:11:24 +02:00
Patrick762 a7967e74dd fix writeable_ranges for ac500, ac300, ep500 and ep500p 2024-04-01 14:08:42 +02:00
Patrick762 5bd98d6de3 Merge pull request #86 from Patrick762/ac200l
Ac200l support and manifest fix
2024-04-01 12:41:23 +02:00
Patrick762 2a4c3e943c updated manifest version 2024-04-01 12:37:48 +02:00
Patrick762 dcfebb0f84 added untested AC200L support based on AC200M 2024-04-01 12:37:22 +02:00
Patrick762 44dbd2b965 Merge pull request #85 from Patrick762/manifest-fix
fix dependency for bt adapters
2024-04-01 12:29:54 +02:00
Patrick762 89928e2d9c fix dependency for bt adapters 2024-04-01 12:29:01 +02:00
Patrick762 dc3aaf92e1 Merge pull request #77 from Patrick762/register-readers-fix
Bugfixes
2024-03-16 17:53:19 +01:00
Patrick762 95b1dafeb1 increase version number 2024-03-16 17:51:46 +01:00
Patrick762 15a890fb7d fix enum entities 2024-03-16 17:49:17 +01:00
Patrick762 542f244a28 removed pack polling from base device 2024-03-16 17:27:28 +01:00
Patrick762 eb701de865 fixed some polling addresses 2024-03-16 16:57:25 +01:00
Patrick762 7f576df84b Merge pull request #58 from Patrick762/Patrick762-patch-1
Create LICENSE
2024-02-18 20:15:44 +01:00
Patrick762 8bc8ba4a4b Create LICENSE 2024-02-18 20:15:19 +01:00
Patrick762 213ee82e63 Merge pull request #56 from Patrick762/Patrick762-patch-1
Update versions in bug_report template
2024-02-18 17:22:21 +01:00
Patrick762 c08099ca4f Update versions in bug_report template 2024-02-18 17:21:36 +01:00
Patrick762 4008dc844e Merge pull request #53 from Patrick762/Patrick762-patch-1
Create FUNDING.yml
2024-02-18 12:43:17 +01:00
Patrick762 e27c28b5c8 Create FUNDING.yml 2024-02-18 12:41:25 +01:00
32 changed files with 547 additions and 214 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
github: Patrick762
+4 -19
View File
@@ -14,32 +14,16 @@ body:
value: "An exception appeared"
validations:
required: true
- type: dropdown
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
options:
- 0.0.11
- 0.0.10
- 0.0.9
default: 0
label: What version of our software are you running?
validations:
required: true
- type: dropdown
- type: textarea
id: device
attributes:
label: What device are you seeing the problem on?
multiple: true
options:
- AC60
- AC200M
- AC300
- AC500
- EB3A
- EP500
- EP500P
- EP900
validations:
required: true
- type: dropdown
@@ -50,6 +34,7 @@ body:
options:
- USB dongle
- ESPHome bluetooth proxy
- Internal bluetooth adapter
validations:
required: true
- type: dropdown
+1
View File
@@ -1,2 +1,3 @@
*/__pycache__/
*.pyc
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Patrick762
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5 -2
View File
@@ -17,11 +17,14 @@ After the installation, you can use this button to install the integration:
### Supported devices:
- AC60
- AC70 (basic data)
- AC70P (untested)
- AC180 (basic data)
- AC180P (basic data)
- AC180P (tested)
- AC200L (untested)
- AC200M
- AC300 (tested)
- AC500
- AC500 (tested)
- EB3A
- EP500
- EP500P
+2 -2
View File
@@ -38,9 +38,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
address = entry.data.get(CONF_ADDRESS)
device_name = entry.data.get(CONF_NAME)
use_controls = entry.data.get(CONF_USE_CONTROLS)
polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 20)
polling_interval = entry.data.get(CONF_POLLING_INTERVAL, 60)
persistent_conn = entry.data.get(CONF_PERSISTENT_CONN, False)
polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 45)
polling_timeout = entry.data.get(CONF_POLLING_TIMEOUT, 120)
max_retries = entry.data.get(CONF_MAX_RETRIES, 5)
if address is None:
+41 -10
View File
@@ -22,7 +22,7 @@ from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, FieldType
from .bluetti_bt_lib.utils.device_builder import build_device
from . import device_info as dev_info, get_unique_id
from .const import DATA_COORDINATOR, DOMAIN
from .const import DATA_COORDINATOR, DOMAIN, CONF_USE_CONTROLS
from .coordinator import PollingCoordinator
from .utils import unique_id_loggable
@@ -36,6 +36,7 @@ async def async_setup_entry(
device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS)
use_controls = entry.data.get(CONF_USE_CONTROLS, False)
if address is None:
_LOGGER.error("Device has no address")
@@ -51,9 +52,8 @@ async def async_setup_entry(
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key):
if field_config.type == FieldType.BOOL:
category = None
if field_config.setter is True:
category = EntityCategory.DIAGNOSTIC
continue
sensors_to_add.append(
BluettiBinarySensor(
@@ -62,7 +62,6 @@ async def async_setup_entry(
address,
field_key,
field_config.name,
category=category,
)
)
@@ -79,7 +78,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
address,
response_key: str,
name: str,
category: EntityCategory | None = None,
):
"""Init battery entity."""
super().__init__(coordinator)
@@ -88,12 +86,38 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
self._unavailable_counter = 5
self._attr_device_info = device_info
self._attr_name = name
self._attr_available = False
self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback
def _handle_coordinator_update(self) -> None:
@@ -102,17 +126,24 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected:
return
if self.coordinator.data is None:
_LOGGER.debug(
"Data from coordinator is None",
)
self._set_unavailable("Data is None")
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict):
_LOGGER.debug(
"Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id)
)
self._attr_available = False
self._set_unavailable("Invalid data")
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
self._set_unavailable("No data")
return
if not isinstance(response_data, bool):
@@ -121,9 +152,9 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
unique_id_loggable(self._attr_unique_id),
response_data,
)
self._attr_available = False
self._set_unavailable("Invalid data type")
return
self._attr_available = True
self._set_available()
self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state()
@@ -43,6 +43,11 @@ class BluettiDevice:
"""The address ranges that are writable"""
return []
@property
def pack_num_field(self) -> List[ReadHoldingRegisters]:
"""The address 'range' of the pack num result. Matches pack_num_result"""
return []
def has_field(self, field: str):
return any(f.name == field for f in self.struct.fields)
@@ -14,6 +14,8 @@ class ProtocolV1Device(BluettiDevice):
# Device info
self.struct.add_string_field("device_type", 10, 6)
self.struct.add_sn_field("serial_number", 17)
self.struct.add_version_field('arm_version', 23)
self.struct.add_version_field('dsp_version', 25)
# Power IO
self.struct.add_uint_field("dc_input_power", 36)
@@ -29,13 +31,6 @@ class ProtocolV1Device(BluettiDevice):
# Battery
self.struct.add_uint_field("total_battery_percent", 43)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
# Output state
self.struct.add_bool_field("ac_output_on", 48)
self.struct.add_bool_field("dc_output_on", 49)
@@ -56,10 +51,8 @@ class ProtocolV1Device(BluettiDevice):
ReadHoldingRegisters(36, 4),
ReadHoldingRegisters(41, 1),
ReadHoldingRegisters(43, 1),
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(48, 2),
ReadHoldingRegisters(3007, 2),
]
@property
@@ -67,9 +60,7 @@ class ProtocolV1Device(BluettiDevice):
return [range(3006, 3009)]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
def pack_num_field(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
]
@@ -53,6 +53,8 @@ class DeviceReader:
if filter_registers is not None:
polling_commands = filter_registers
pack_commands = []
_LOGGER.info("Polling commands: " + ",".join([f"{c.starting_address}-{c.starting_address + c.quantity - 1}" for c in polling_commands]))
_LOGGER.info("Pack comands: " + ",".join([f"{c.starting_address}-{c.starting_address + c.quantity - 1}" for c in pack_commands]))
parsed_data: dict = {}
@@ -66,8 +68,8 @@ class DeviceReader:
await self.client.connect()
break
except Exception as e:
if attempt == self.max_retries or attempt == 1:
raise # pass exception on max_retries attempt
if attempt == self.max_retries:
raise e # pass exception on max_retries attempt
else:
_LOGGER.warning(
f"Connect unsucessful (attempt {attempt}): {e}. Retrying..."
@@ -97,17 +99,29 @@ class DeviceReader:
_LOGGER.warning("Got a parse exception")
# Execute pack polling commands
if len(pack_commands) > 0:
if len(pack_commands) > 0 and len(self.bluetti_device.pack_num_field) == 1:
_LOGGER.debug("Polling battery packs")
for pack in range(1, self.bluetti_device.pack_num_max + 1):
_LOGGER.debug("Setting pack_num to %i", pack)
# Set current pack number
await self._async_send_command(
self.bluetti_device.build_setter_command(
command = self.bluetti_device.build_setter_command(
"pack_num", pack
)
body = command.parse_response(
await self._async_send_command(command)
)
_LOGGER.debug("Raw data set: %s", body)
# We need to wait after switching packs for the data to be available
# Check set pack_num
set_pack = int.from_bytes(body, byteorder='big')
if set_pack is not pack:
_LOGGER.warning("Pack polling failed (pack_num %i doesn't match expected %i)", set_pack, pack)
continue
if self.bluetti_device.pack_num_max > 1:
# We need to wait after switching packs
# for the data to be available
await asyncio.sleep(5)
for command in pack_commands:
@@ -121,26 +135,16 @@ class DeviceReader:
)
_LOGGER.debug("Parsed data: %s", parsed)
pack_number = parsed.get("pack_num")
if (
not isinstance(pack_number, int)
or pack_number != pack
):
_LOGGER.debug(
"Parsed pack_num(%s) does not match expected '%s'",
pack_number,
pack,
)
continue
for key, value in parsed.items():
# Ignore likely unavailable pack data
if value != 0:
parsed_data.update({key + str(pack): value})
except ParseError:
_LOGGER.warning("Got a parse exception...")
except TimeoutError:
_LOGGER.error("Polling timed out")
except TimeoutError as err:
_LOGGER.error(f"Polling timed out ({self.polling_timeout}s). Trying again later", exc_info=err)
return None
except BleakError as err:
_LOGGER.error("Bleak error: %s", err)
@@ -149,10 +153,18 @@ class DeviceReader:
# Disconnect if connection not persistant
if not self.persistent_conn:
if self.has_notifier:
try:
await self.client.stop_notify(NOTIFY_UUID)
except:
# Ignore errors here
pass
self.has_notifier = False
await self.client.disconnect()
# Check if dict is empty
if not parsed_data:
return None
return parsed_data
async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes:
@@ -175,18 +187,16 @@ class DeviceReader:
return cast(bytes, res)
except TimeoutError:
_LOGGER.warning("Polling single command timed out")
_LOGGER.debug("Polling single command timed out")
except ModbusError as err:
_LOGGER.warning(
_LOGGER.debug(
"Got an invalid request error for %s: %s",
command,
err,
)
except (BadConnectionError, BleakError) as err:
_LOGGER.warning(
"Needed to disconnect due to error: %s (This can also be the case if you used device controls)",
err,
)
# Ignore other errors
pass
# caught an exception, return empty bytes object
return bytes()
@@ -1,8 +1,52 @@
"""AC180P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
from typing import List
from ..field_enums import ChargingMode
from ..utils.commands import ReadHoldingRegisters
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC180P(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC180P", sn)
# Power IO
self.struct.add_uint_field('dc_output_power', 140)
self.struct.add_uint_field('ac_output_power', 142)
self.struct.add_uint_field('dc_input_power', 144)
self.struct.add_uint_field('ac_input_power', 146)
# Input Details (1100 - 1300)
self.struct.add_decimal_field('ac_input_voltage', 1314, 1)
# Controls (2000)
self.struct.add_bool_field('ac_output_on_switch', 2011)
self.struct.add_bool_field('dc_output_on_switch', 2012)
self.struct.add_bool_field('silent_charging_on', 2020) # to be able to choose between normal and silent charging modes
self.struct.add_enum_field('charging_mode', 2020, ChargingMode) # can't be set from HA, custom switch needed
self.struct.add_bool_field('power_lifting_on', 2021)
# Controls (2200)
self.struct.add_bool_field('grid_enhancement_mode_on', 2225)
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(140, 1),
ReadHoldingRegisters(142, 1),
ReadHoldingRegisters(144, 1),
ReadHoldingRegisters(146, 1),
ReadHoldingRegisters(1314, 1),
ReadHoldingRegisters(2011, 1),
ReadHoldingRegisters(2012, 1),
ReadHoldingRegisters(2020, 1),
ReadHoldingRegisters(2021, 1),
ReadHoldingRegisters(2225, 1),
]
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(2000, 2022),
range(2200, 2226)
]
@@ -0,0 +1,56 @@
"""AC200L fields."""
from typing import List
from ..utils.commands import ReadHoldingRegisters
from ..field_enums import AutoSleepMode, OutputMode
from ..base_devices.ProtocolV1Device import ProtocolV1Device
class AC200L(ProtocolV1Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC200L", sn)
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1, multiplier=10)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2, multiplier=10)
self.struct.add_uint_field("internal_dc_input_voltage", 86, multiplier=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1, multiplier=10)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1, multiplier=.1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls
self.struct.add_bool_field("power_off", 3060)
self.struct.add_enum_field("auto_sleep_mode", 3061, AutoSleepMode)
@property
def pack_num_max(self):
return 3
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [range(3060, 3062)]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -13,16 +13,22 @@ class AC200M(ProtocolV1Device):
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_uint_field("internal_ac_voltage", 71)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 1)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2)
self.struct.add_uint_field("internal_dc_input_voltage", 86)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls
self.struct.add_bool_field("power_off", 3060)
@@ -34,15 +40,17 @@ class AC200M(ProtocolV1Device):
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(70, 4),
ReadHoldingRegisters(86, 3),
]
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [range(3060, 3061)]
return super().writable_ranges + [range(3060, 3062)]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ReadHoldingRegisters(105, 16)]
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -11,27 +11,32 @@ class AC300(ProtocolV1Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC300", sn)
# Details (matches EP500 + AC500)
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_uint_field("internal_ac_voltage", 71)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 1)
self.struct.add_int_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2)
self.struct.add_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_int_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1)
self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100))
self.struct.add_uint_field("internal_power_three", 79)
self.struct.add_int_field("internal_power_three", 79)
self.struct.add_decimal_field("ac_input_frequency", 80, 2)
self.struct.add_uint_field("internal_dc_input_voltage", 86)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
self.struct.add_uint_field("pv_input_voltage1", 86, multiplier=.1)
self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10)
self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls (matches EP500 + AC500)
# Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field(
@@ -49,21 +54,20 @@ class AC300(ProtocolV1Device):
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(70, 10),
ReadHoldingRegisters(86, 3),
]
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(3001, 3016),
range(3061),
range(3001, 3017),
range(3061, 3062),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [
ReadHoldingRegisters(105, 16),
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -11,26 +11,32 @@ class AC500(ProtocolV1Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC500", sn)
# Details (matches EP500 + AC300)
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_uint_field("internal_ac_voltage", 71)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 1)
self.struct.add_int_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2)
self.struct.add_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_int_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1)
self.struct.add_decimal_field("internal_current_three", 78, 1, (0, 100))
self.struct.add_uint_field("internal_power_three", 79)
self.struct.add_int_field("internal_power_three", 79)
self.struct.add_decimal_field("ac_input_frequency", 80, 2)
self.struct.add_uint_field("internal_dc_input_voltage", 86)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
self.struct.add_uint_field("pv_input_voltage1", 86, multiplier=.1)
self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10)
self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10)
# Battery packs (matches EP500)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field('pack_bms_version', 201)
# Controls (matches EP500 + AC300)
# Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field(
@@ -44,24 +50,24 @@ class AC500(ProtocolV1Device):
@property
def pack_num_max(self):
return 4
return 6
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(70, 10),
ReadHoldingRegisters(86, 3),
]
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(3001, 3016),
range(3061),
range(3001, 3017),
range(3061, 3062),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [
ReadHoldingRegisters(105, 16),
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -0,0 +1,8 @@
"""AC70 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70", sn)
@@ -0,0 +1,8 @@
"""AC70P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70P(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70P", sn)
@@ -25,13 +25,7 @@ class EB3A(ProtocolV1Device):
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(77, 1),
ReadHoldingRegisters(86, 1),
ReadHoldingRegisters(3034, 1),
ReadHoldingRegisters(3060, 1),
ReadHoldingRegisters(3063, 5),
]
return self.struct.get_read_holding_registers()
@property
def writable_ranges(self) -> List[range]:
@@ -11,12 +11,12 @@ class EP500(ProtocolV1Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "EP500", sn)
# Details (matches AC500 + AC300)
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_uint_field("internal_ac_voltage", 71)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 1)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2)
self.struct.add_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1)
@@ -27,10 +27,16 @@ class EP500(ProtocolV1Device):
self.struct.add_decimal_field("internal_dc_input_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs (matches AC500)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls (matches AC500 + AC300)
# Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field(
@@ -44,20 +50,20 @@ class EP500(ProtocolV1Device):
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(70, 10),
ReadHoldingRegisters(86, 3),
]
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(3001, 3016),
range(3061),
range(3001, 3017),
range(3061, 3062),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [
ReadHoldingRegisters(105, 16),
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -11,12 +11,12 @@ class EP500P(ProtocolV1Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "EP500P", sn)
# Details (matches AC500 + AC300)
# Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode)
self.struct.add_uint_field("internal_ac_voltage", 71)
self.struct.add_decimal_field("internal_ac_voltage", 71, 1)
self.struct.add_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 1)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2)
self.struct.add_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1)
@@ -27,10 +27,16 @@ class EP500P(ProtocolV1Device):
self.struct.add_decimal_field("internal_dc_input_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs (matches AC500)
# Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1)
self.struct.add_uint_field("pack_num_result", 96) # internal
self.struct.add_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201)
# Controls (matches AC500 + AC300)
# Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field(
@@ -44,20 +50,20 @@ class EP500P(ProtocolV1Device):
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(70, 10),
ReadHoldingRegisters(86, 3),
]
return self.struct.get_read_holding_registers(
filter=lambda address: address <= 90 or address >= 256 # pack_polling_commands
)
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(3001, 3016),
range(3061),
range(3001, 3017),
range(3061, 3062),
]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [
ReadHoldingRegisters(105, 16),
return [
ReadHoldingRegisters(91, 37),
ReadHoldingRegisters(201, 2),
]
@@ -62,6 +62,22 @@ class EP600(ProtocolV2Device):
self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1)
self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1)
# KM - Consumption
self.struct.add_int_field("consumption_power_phase1", 1430)
self.struct.add_int_field("consumption_power_phase2", 1436)
self.struct.add_int_field("consumption_power_phase3", 1442)
self.struct.add_decimal_field("consumption_voltage_phase1", 1431, 1)
self.struct.add_decimal_field("consumption_voltage_phase2", 1437, 1)
self.struct.add_decimal_field("consumption_voltage_phase3", 1443, 1)
self.struct.add_decimal_field("consumption_current_phase1", 1432, 1)
self.struct.add_decimal_field("consumption_current_phase2", 1438, 1)
self.struct.add_decimal_field("consumption_current_phase3", 1444, 1)
# KM - Totals
self.struct.add_uint_field('pv_input_power_all', 144) # Total PV in
self.struct.add_uint_field('consumption_power_all', 142) # Total AC out
self.struct.add_uint_field('grid_power_all', 146) # Total Grid in - value only +/- unknown
# Statistics
self.struct.add_decimal_field(
"total_ac_consumption", 152, 1
@@ -79,36 +95,6 @@ class EP600(ProtocolV2Device):
self.struct.add_version_field("safety_module_version", 6181) # internal
self.struct.add_version_field("high_voltage_module_version", 6184) # internal
# @property
# def pack_num_max(self):
# return 16
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(2022, 2),
ReadHoldingRegisters(2213, 5),
ReadHoldingRegisters(1212, 3),
ReadHoldingRegisters(1220, 3),
ReadHoldingRegisters(1228, 3),
ReadHoldingRegisters(1236, 3),
ReadHoldingRegisters(1244, 3),
ReadHoldingRegisters(1300, 1),
ReadHoldingRegisters(1313, 3),
ReadHoldingRegisters(1319, 3),
ReadHoldingRegisters(1325, 3),
ReadHoldingRegisters(1500, 1),
ReadHoldingRegisters(1510, 3),
ReadHoldingRegisters(1517, 3),
ReadHoldingRegisters(1524, 3),
ReadHoldingRegisters(152, 1),
ReadHoldingRegisters(156, 1),
ReadHoldingRegisters(158, 1),
]
# @property
# def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
# return super().pack_polling_commands + [
# ReadHoldingRegisters(6101, 10),
# ReadHoldingRegisters(6175, 12),
# ]
return self.struct.get_read_holding_registers()
@@ -125,6 +125,14 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
"ac_input_power": PowerFieldAttributes("AC Input Power"),
"ac_output_power": PowerFieldAttributes("AC Output Power"),
"dc_output_power": PowerFieldAttributes("DC Output Power"),
"dsp_version": FieldAttributes(
type=FieldType.NUMERIC,
name=f"DSP Version",
),
"arm_version": FieldAttributes(
type=FieldType.NUMERIC,
name=f"ARM Version",
),
"power_generation": EnergyFieldAttributes("Total Power Generation"),
"total_battery_percent": FieldAttributes(
type=FieldType.NUMERIC,
@@ -217,6 +225,26 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
"total_ac_consumption": EnergyFieldAttributes("Total Load Consumption"),
"total_grid_consumption": EnergyFieldAttributes("Total Grid Consumption"),
"total_grid_feed": EnergyFieldAttributes("Total Grid Feed"),
# KM - Consumption
"consumption_power_phase1": PowerFieldAttributes("Consumption Power Phase 1"),
"consumption_power_phase2": PowerFieldAttributes("Consumption Power Phase 2"),
"consumption_power_phase3": PowerFieldAttributes("Consumption Power Phase 3"),
"consumption_voltage_phase1": VoltageFieldAttributes("Consumption Voltage Phase 1"),
"consumption_voltage_phase2": VoltageFieldAttributes("Consumption Voltage Phase 2"),
"consumption_voltage_phase3": VoltageFieldAttributes("Consumption Voltage Phase 3"),
"consumption_current_phase1": CurrentFieldAttributes("Consumption Current Phase 1"),
"consumption_current_phase2": CurrentFieldAttributes("Consumption Current Phase 2"),
"consumption_current_phase3": CurrentFieldAttributes("Consumption Current Phase 3"),
# KM - Consumption end
# KM - Totals
"pv_input_power_all": PowerFieldAttributes("PV Input Power All"),
"consumption_power_all": PowerFieldAttributes("Consumption Power All"),
"grid_power_all": PowerFieldAttributes("Grid Power All"),
# KM - Totals end
# Device specific controls
"power_off": FieldAttributes(
type=FieldType.BOOL,
@@ -296,6 +324,16 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
setter=True,
name="Power Lifting",
),
"grid_enhancement_mode_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Grid Enhancement Mode",
),
"silent_charging_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Silent Charging",
),
}
@@ -3,8 +3,11 @@
import re
from ..devices.ac60 import AC60
from ..devices.ac70 import AC70
from ..devices.ac70p import AC70P
from ..devices.ac180 import AC180
from ..devices.ac180p import AC180P
from ..devices.ac200l import AC200L
from ..devices.ac200m import AC200M
from ..devices.ac300 import AC300
from ..devices.ac500 import AC500
@@ -16,7 +19,7 @@ from ..devices.ep760 import EP760
from ..devices.ep800 import EP800
DEVICE_NAME_RE = re.compile(
r"^(AC60|AC180|AC180P|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$"
r"^(AC60|AC70|AC70P|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$"
)
@@ -24,10 +27,16 @@ def build_device(address: str, name: str):
match = DEVICE_NAME_RE.match(name)
if match[1] == "AC60":
return AC60(address, match[2])
if match[1] == "AC70":
return AC70(address, match[2])
if match[1] == "AC70P":
return AC70P(address, match[2])
if match[1] == "AC180":
return AC180(address, match[2])
if match[1] == "AC180P":
return AC180P(address, match[2])
if match[1] == "AC200L":
return AC200L(address, match[2])
if match[1] == "AC200M":
return AC200M(address, match[2])
if match[1] == "AC300":
@@ -50,6 +59,6 @@ def build_device(address: str, name: str):
def get_type_by_bt_name(bt_name: str):
match = DEVICE_NAME_RE.match(bt_name)
if match is None or len(match) < 2:
if match is None:
return "Unknown"
return match[1]
@@ -4,6 +4,7 @@
from decimal import Decimal
from enum import Enum
from .commands import ReadHoldingRegisters
import struct
from typing import Any, List, Optional, Tuple, Type
@@ -30,12 +31,15 @@ class DeviceField:
class UintField(DeviceField):
def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]]):
def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], multiplier: float):
self.range = range
self.multiplier = multiplier
super().__init__(name, address, 1)
def parse(self, data: bytes) -> int:
return struct.unpack("!H", data)[0]
val = struct.unpack("!H", data)[0]
return val * self.multiplier
def in_range(self, val: int) -> bool:
if self.range is None:
@@ -79,15 +83,16 @@ class EnumField(DeviceField):
class DecimalField(DeviceField):
def __init__(
self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]]
self, name: str, address: int, scale: int, range: Optional[Tuple[int, int]], multiplier: float
):
self.scale = scale
self.range = range
self.multiplier = multiplier
super().__init__(name, address, 1)
def parse(self, data: bytes) -> Decimal:
val = Decimal(struct.unpack("!H", data)[0])
return val / 10 ** self.scale
return (val / 10 ** self.scale) * Decimal(self.multiplier)
def in_range(self, val: Decimal) -> bool:
if self.range is None:
@@ -144,8 +149,8 @@ class DeviceStruct:
def __init__(self):
self.fields = []
def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None):
self.fields.append(UintField(name, address, range))
def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, multiplier: float = 1):
self.fields.append(UintField(name, address, range, multiplier))
def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None):
self.fields.append(IntField(name, address, range))
@@ -157,9 +162,9 @@ class DeviceStruct:
self.fields.append(EnumField(name, address, enum))
def add_decimal_field(
self, name: str, address: int, scale: int, range: Tuple[int, int] = None
self, name: str, address: int, scale: int, range: Tuple[int, int] = None, multiplier: float = 1
):
self.fields.append(DecimalField(name, address, scale, range))
self.fields.append(DecimalField(name, address, scale, range, multiplier))
def add_decimal_array_field(self, name: str, address: int, size: int, scale: int):
self.fields.append(DecimalArrayField(name, address, size, scale))
@@ -176,6 +181,35 @@ class DeviceStruct:
def add_sn_field(self, name: str, address: int):
self.fields.append(SerialNumberField(name, address))
def get_read_holding_registers(self, tolerance=20, filter=lambda address: True):
address_set = set()
for field in self.fields:
for address in range(field.address, field.address + field.size):
address_set.add(address)
# Filter out addresses that we specificly
# specified to exclude.
for address in set(address_set):
if not filter(address):
address_set.remove(address)
# Fast path: no addresses -> no commands.
if not address_set:
return []
commands = []
address_list = sorted(address_set)
start = address_list[0]
last = address_list[0]
for address in address_list[1:]:
# Create a new group if there's a gap
# in the addresses.
if address >= last + tolerance:
commands.append(ReadHoldingRegisters(start, last - start + 1))
start = address
last = address
commands.append(ReadHoldingRegisters(start, last - start + 1))
return commands
def parse(self, starting_address: int, data: bytes) -> dict:
# Offsets and size are counted in 2 byte chunks, so for the range we
# need to divide the byte size by 2
+12 -7
View File
@@ -14,7 +14,7 @@ from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_TYPE
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
@@ -48,11 +48,20 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
if isinstance(discovery_info.name, str):
name = re.sub("[^A-Z0-9]+", "", discovery_info.name)
discovery_info.manufacturer_data = {
CONF_TYPE: get_type_by_bt_name(name)
}
# Get device type if needed
if isinstance(discovery_info.name, str) and discovery_info.name.startswith("PBOX"):
bleak_device = BleakClient(discovery_info.device)
device_type = await recognize_device(bleak_device, self.hass.loop.create_future)
_LOGGER.info("Device identified as %s", device_type)
discovery_info.manufacturer_data = {
CONF_TYPE: device_type.strip(),
}
discovery_info.name = discovery_info.name.replace("PBOX", device_type.strip())
self._discovery_info = discovery_info
@@ -76,6 +85,7 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={
CONF_ADDRESS: discovery_info.address,
CONF_NAME: name,
CONF_TYPE: discovery_info.manufacturer_data.get(CONF_TYPE, "Unknown"),
},
)
@@ -140,7 +150,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
if user_input[CONF_MAX_RETRIES] < 1:
return self.async_abort(reason="invalid_retries")
changed = self.hass.config_entries.async_update_entry(
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
@@ -153,11 +163,6 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
},
},
)
if changed is False:
_LOGGER.error(
"Method OptionsFlowHandler.async_step_init: Config entry %s has not been changed",
self.config_entry.entry_id,
)
return self.async_create_entry(
title="",
data={
+6
View File
@@ -23,6 +23,7 @@ SUPPORTED_MODELS = [
"EP500P",
"EP600",
"EP760",
"AC180P",
]
CONTROL_FIELDS = [
@@ -31,6 +32,8 @@ CONTROL_FIELDS = [
"power_off",
"eco_on",
"power_lifting_on",
"grid_enhancement_mode_on",
"silent_charging_on",
]
DIAGNOSTIC_FIELDS = [
@@ -39,10 +42,13 @@ DIAGNOSTIC_FIELDS = [
"max_ac_output_power",
"max_ac_output_current",
"bcu_version",
"dsp_version",
"arm_version",
"safety_module_version",
"high_voltage_module_version",
"power_generation",
"total_ac_consumption",
"total_grid_consumption",
"total_grid_feed",
"ac_input_voltage",
]
@@ -42,6 +42,8 @@ class PollingCoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=polling_interval),
)
self.address = address
# Create client
self.logger.debug("Creating client")
device = bluetooth.async_ble_device_from_address(hass, address)
@@ -67,4 +69,10 @@ class PollingCoordinator(DataUpdateCoordinator):
so entities can quickly look up their data.
"""
# Check if device is connected
if bluetooth.async_address_present(self.hass, self.address, connectable=True) is False:
self.logger.warning("Device not connected")
self.last_update_success = False
return None
return await self.reader.read_data()
+4 -2
View File
@@ -3,7 +3,9 @@
"name": "Bluetti BT",
"bluetooth": [
{ "local_name": "AC60*" },
{ "local_name": "AC70*" },
{ "local_name": "AC180*" },
{ "local_name": "AC200L*" },
{ "local_name": "AC200M*" },
{ "local_name": "AC300*" },
{ "local_name": "AC500*" },
@@ -16,11 +18,11 @@
],
"codeowners": ["@Patrick762"],
"config_flow": true,
"dependencies": ["bluetooth"],
"dependencies": ["bluetooth_adapters"],
"documentation": "https://github.com/Patrick762/hassio-bluetti-bt",
"integration_type": "device",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues",
"requirements": [],
"version": "0.0.20"
"version": "0.1.4"
}
+37 -11
View File
@@ -57,7 +57,7 @@ async def async_setup_entry(
all_fields.update({name+str(pack): field})
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key):
if bluetti_device.has_field(field_key) or (len(bluetti_device.pack_polling_commands) > 0 and field_key.startswith("pack_")):
category = None
if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS:
category = EntityCategory.DIAGNOSTIC
@@ -114,6 +114,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
self._unavailable_counter = 0
self._attr_device_info = device_info
self._attr_name = name
@@ -125,6 +126,32 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
self._attr_entity_category = category
self._options = options
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
@@ -133,10 +160,10 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
return
if self.coordinator.data is None:
_LOGGER.warning(
"Data from coordinator is None. Skipping update for %s",
unique_id_loggable(self._attr_unique_id)
_LOGGER.debug(
"Data from coordinator is None",
)
self._set_unavailable("Data is None")
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
@@ -144,13 +171,13 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
_LOGGER.warning(
"Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id)
)
self._attr_available = False
self._set_unavailable("Invalid data")
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
_LOGGER.warning("No data for available for (%s)", self._response_key)
self._attr_available = False
_LOGGER.debug("No data for available for (%s)", self._response_key)
self._set_unavailable("No data")
return
if (
@@ -166,19 +193,18 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
response_data,
type(response_data),
)
self._attr_available = False
self._set_unavailable("Invalid data type")
return
self._attr_available = True
self._set_available()
# Different for enum and numeric
if (
self._options is not None
and isinstance(response_data, Enum)
and response_data.value < len(self._options)
):
# Enum
self._attr_native_value = self._options[response_data.value]
self._attr_native_value = response_data.name
else:
# Numeric
self._attr_native_value = response_data
+9 -1
View File
@@ -108,6 +108,11 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
self._attr_entity_category = category
self._attr_device_class = SwitchDeviceClass.OUTLET
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
@@ -117,15 +122,17 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict):
_LOGGER.error(
_LOGGER.debug(
"Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id)
)
self._attr_available = False
self.async_write_ha_state()
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
self.async_write_ha_state()
return
if not isinstance(response_data, bool):
@@ -135,6 +142,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
response_data,
)
self._attr_available = False
self.async_write_ha_state()
return
self._attr_available = True
+5
View File
@@ -0,0 +1,5 @@
homeassistant
bleak_retry_connector
pyserial
bluetooth_auto_recovery
crcmod
+18
View File
@@ -4,6 +4,8 @@ import unittest
from custom_components.bluetti_bt.bluetti_bt_lib.utils.device_builder import build_device
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac60 import AC60
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70 import AC70
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70p import AC70P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180 import AC180
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200m import AC200M
@@ -32,6 +34,22 @@ class TestDeviceBuilder(unittest.TestCase):
self.assertIsInstance(built, AC60)
self.assertEqual(built.address, bt_addr)
def test_build_ac70(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC7056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70)
self.assertEqual(built.address, bt_addr)
def test_build_ac70p(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC70P56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70P)
self.assertEqual(built.address, bt_addr)
def test_build_ac180(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC18056786746478"