Compare commits

..
25 Commits
Author SHA1 Message Date
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
19 changed files with 298 additions and 113 deletions

No files matched your search

+2 -11
View File
@@ -26,20 +26,10 @@ body:
default: 0 default: 0
validations: validations:
required: true required: true
- type: dropdown - type: textarea
id: device id: device
attributes: attributes:
label: What device are you seeing the problem on? label: What device are you seeing the problem on?
multiple: true
options:
- AC60
- AC200M
- AC300
- AC500
- EB3A
- EP500
- EP500P
- EP900
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
@@ -50,6 +40,7 @@ body:
options: options:
- USB dongle - USB dongle
- ESPHome bluetooth proxy - ESPHome bluetooth proxy
- Internal bluetooth adapter
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
+1
View File
@@ -19,6 +19,7 @@ After the installation, you can use this button to install the integration:
- AC60 - AC60
- AC180 (basic data) - AC180 (basic data)
- AC180P (basic data) - AC180P (basic data)
- AC200L (untested)
- AC200M - AC200M
- AC300 (tested) - AC300 (tested)
- AC500 - AC500
+14 -1
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 .bluetti_bt_lib.utils.device_builder import build_device
from . import device_info as dev_info, get_unique_id 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 .coordinator import PollingCoordinator
from .utils import unique_id_loggable from .utils import unique_id_loggable
@@ -36,6 +36,7 @@ async def async_setup_entry(
device_name = entry.data.get(CONF_NAME) device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS) address = entry.data.get(CONF_ADDRESS)
use_controls = entry.data.get(CONF_USE_CONTROLS)
if address is None: if address is None:
_LOGGER.error("Device has no address") _LOGGER.error("Device has no address")
@@ -55,6 +56,10 @@ async def async_setup_entry(
if field_config.setter is True: if field_config.setter is True:
category = EntityCategory.DIAGNOSTIC category = EntityCategory.DIAGNOSTIC
# Ignore setters if controls enabled
if field_config.setter is True and use_controls is True:
continue
sensors_to_add.append( sensors_to_add.append(
BluettiBinarySensor( BluettiBinarySensor(
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
@@ -95,6 +100,11 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
self._attr_unique_id = get_unique_id(e_name) self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category self._attr_entity_category = category
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
@@ -108,11 +118,13 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
"Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id) "Invalid data from coordinator (binary_sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
if not isinstance(response_data, bool): if not isinstance(response_data, bool):
@@ -122,6 +134,7 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
response_data, response_data,
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
self._attr_available = True self._attr_available = True
@@ -29,13 +29,6 @@ class ProtocolV1Device(BluettiDevice):
# Battery # Battery
self.struct.add_uint_field("total_battery_percent", 43) 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 # Output state
self.struct.add_bool_field("ac_output_on", 48) self.struct.add_bool_field("ac_output_on", 48)
self.struct.add_bool_field("dc_output_on", 49) self.struct.add_bool_field("dc_output_on", 49)
@@ -56,20 +49,10 @@ class ProtocolV1Device(BluettiDevice):
ReadHoldingRegisters(36, 4), ReadHoldingRegisters(36, 4),
ReadHoldingRegisters(41, 1), ReadHoldingRegisters(41, 1),
ReadHoldingRegisters(43, 1), ReadHoldingRegisters(43, 1),
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(48, 2), ReadHoldingRegisters(48, 2),
ReadHoldingRegisters(3007, 2),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return [range(3006, 3009)] return [range(3006, 3009)]
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
]
@@ -110,6 +110,8 @@ class DeviceReader:
# We need to wait after switching packs for the data to be available # We need to wait after switching packs for the data to be available
await asyncio.sleep(5) await asyncio.sleep(5)
pack_ok = False
for command in pack_commands: for command in pack_commands:
# Request & parse result for each pack # Request & parse result for each pack
try: try:
@@ -121,17 +123,19 @@ class DeviceReader:
) )
_LOGGER.debug("Parsed data: %s", parsed) _LOGGER.debug("Parsed data: %s", parsed)
pack_number = parsed.get("pack_num") if not pack_ok: # Only check this once
if ( pack_number = parsed.get("pack_num")
not isinstance(pack_number, int) if (
or pack_number != pack not isinstance(pack_number, int)
): or pack_number != pack
_LOGGER.debug( ):
"Parsed pack_num(%s) does not match expected '%s'", _LOGGER.debug(
pack_number, "Parsed pack_num(%s) does not match expected '%s'",
pack, pack_number,
) pack,
continue )
continue
pack_ok = True
for key, value in parsed.items(): for key, value in parsed.items():
parsed_data.update({key + str(pack): value}) parsed_data.update({key + str(pack): value})
@@ -140,7 +144,7 @@ class DeviceReader:
_LOGGER.warning("Got a parse exception...") _LOGGER.warning("Got a parse exception...")
except TimeoutError: except TimeoutError:
_LOGGER.error("Polling timed out") _LOGGER.warning("Polling timed out. Trying again later")
return None return None
except BleakError as err: except BleakError as err:
_LOGGER.error("Bleak error: %s", err) _LOGGER.error("Bleak error: %s", err)
@@ -149,10 +153,18 @@ class DeviceReader:
# Disconnect if connection not persistant # Disconnect if connection not persistant
if not self.persistent_conn: if not self.persistent_conn:
if self.has_notifier: if self.has_notifier:
await self.client.stop_notify(NOTIFY_UUID) try:
await self.client.stop_notify(NOTIFY_UUID)
except:
# Ignore errors here
pass
self.has_notifier = False self.has_notifier = False
await self.client.disconnect() await self.client.disconnect()
# Check if dict is empty
if not parsed_data:
return None
return parsed_data return parsed_data
async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes: async def _async_send_command(self, command: ReadHoldingRegisters) -> bytes:
@@ -183,10 +195,8 @@ class DeviceReader:
err, err,
) )
except (BadConnectionError, BleakError) as err: except (BadConnectionError, BleakError) as err:
_LOGGER.warning( # Ignore other errors
"Needed to disconnect due to error: %s (This can also be the case if you used device controls)", pass
err,
)
# caught an exception, return empty bytes object # caught an exception, return empty bytes object
return bytes() return bytes()
@@ -0,0 +1,59 @@
"""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)
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)
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", 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
# 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 super().polling_commands + [
ReadHoldingRegisters(70, 5),
ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3060, 2),
]
@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, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16)
]
@@ -13,15 +13,20 @@ class AC200M(ProtocolV1Device):
# Details # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) 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_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_power", 87, 1)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2) self.struct.add_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs # 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)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
# Controls # Controls
@@ -35,14 +40,20 @@ class AC200M(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 4), ReadHoldingRegisters(70, 5),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3060, 2),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [range(3060, 3061)] return super().writable_ranges + [range(3060, 3062)]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ReadHoldingRegisters(105, 16)] return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16)
]
@@ -11,27 +11,32 @@ class AC300(ProtocolV1Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "AC300", sn) super().__init__(address, "AC300", sn)
# Details (matches EP500 + AC500) # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) 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_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76) self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1) 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_decimal_field("internal_current_three", 78, 1, (0, 100))
self.struct.add_uint_field("internal_power_three", 79) self.struct.add_uint_field("internal_power_three", 79)
self.struct.add_decimal_field("ac_input_frequency", 80, 2) self.struct.add_decimal_field("ac_input_frequency", 80, 2)
self.struct.add_uint_field("internal_dc_input_voltage", 86) self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) 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_decimal_field("internal_dc_input_current", 88, 2)
# Battery packs # 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)
self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal self.struct.add_decimal_array_field("cell_voltages", 105, 16, 2) # internal
self.struct.add_version_field("pack_bms_version", 201) 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_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field( self.struct.add_enum_field(
@@ -50,20 +55,29 @@ class AC300(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 10), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3017),
range(3061), range(3061, 3062),
] ]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16), ReadHoldingRegisters(105, 16),
ReadHoldingRegisters(201, 2), ReadHoldingRegisters(201, 2),
] ]
@@ -11,26 +11,31 @@ class AC500(ProtocolV1Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "AC500", sn) super().__init__(address, "AC500", sn)
# Details (matches EP500 + AC300) # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) 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_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76) self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1) 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_decimal_field("internal_current_three", 78, 1, (0, 100))
self.struct.add_uint_field("internal_power_three", 79) self.struct.add_uint_field("internal_power_three", 79)
self.struct.add_decimal_field("ac_input_frequency", 80, 2) self.struct.add_decimal_field("ac_input_frequency", 80, 2)
self.struct.add_uint_field("internal_dc_input_voltage", 86) self.struct.add_uint_field("internal_dc_input_voltage", 86, scale=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) 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_decimal_field("internal_dc_input_current", 88, 2)
# 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", 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_decimal_array_field("cell_voltages", 105, 16, 2) # internal
# Controls (matches EP500 + AC300) # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field( self.struct.add_enum_field(
@@ -49,19 +54,28 @@ class AC500(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 10), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3017),
range(3061), range(3061, 3062),
] ]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16), ReadHoldingRegisters(105, 16),
] ]
@@ -11,12 +11,12 @@ class EP500(ProtocolV1Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "EP500", sn) super().__init__(address, "EP500", sn)
# Details (matches AC500 + AC300) # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) 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_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76) self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1) self.struct.add_decimal_field("ac_input_voltage", 77, 1)
@@ -27,10 +27,15 @@ class EP500(ProtocolV1Device):
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) 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_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", 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_decimal_array_field("cell_voltages", 105, 16, 2) # internal
# Controls (matches AC500 + AC300) # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field( self.struct.add_enum_field(
@@ -45,19 +50,28 @@ class EP500(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 10), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3017),
range(3061), range(3061, 3062),
] ]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16), ReadHoldingRegisters(105, 16),
] ]
@@ -11,12 +11,12 @@ class EP500P(ProtocolV1Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "EP500P", sn) super().__init__(address, "EP500P", sn)
# Details (matches AC500 + AC300) # Details
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_decimal_field("internal_current_one", 72, 1)
self.struct.add_uint_field("internal_power_one", 73) 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_decimal_field("internal_current_two", 75, 1)
self.struct.add_uint_field("internal_power_two", 76) self.struct.add_uint_field("internal_power_two", 76)
self.struct.add_decimal_field("ac_input_voltage", 77, 1) self.struct.add_decimal_field("ac_input_voltage", 77, 1)
@@ -27,10 +27,15 @@ class EP500P(ProtocolV1Device):
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) 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_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", 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_decimal_array_field("cell_voltages", 105, 16, 2) # internal
# Controls (matches AC500 + AC300) # Controls
self.struct.add_enum_field("ups_mode", 3001, UpsMode) self.struct.add_enum_field("ups_mode", 3001, UpsMode)
self.struct.add_bool_field("split_phase_on", 3004) self.struct.add_bool_field("split_phase_on", 3004)
self.struct.add_enum_field( self.struct.add_enum_field(
@@ -45,19 +50,28 @@ class EP500P(ProtocolV1Device):
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 10), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1),
ReadHoldingRegisters(3013, 1),
ReadHoldingRegisters(3015, 2),
ReadHoldingRegisters(3061, 1),
] ]
@property @property
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
return super().writable_ranges + [ return super().writable_ranges + [
range(3001, 3016), range(3001, 3017),
range(3061), range(3061, 3062),
] ]
@property @property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]: def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return super().pack_polling_commands + [ return [
ReadHoldingRegisters(91, 2),
ReadHoldingRegisters(96, 1),
ReadHoldingRegisters(98, 2),
ReadHoldingRegisters(105, 16), ReadHoldingRegisters(105, 16),
] ]
@@ -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_phase2", 1519, 1)
self.struct.add_decimal_field("ac_output_current_phase3", 1526, 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 # Statistics
self.struct.add_decimal_field( self.struct.add_decimal_field(
"total_ac_consumption", 152, 1 "total_ac_consumption", 152, 1
@@ -79,10 +95,6 @@ class EP600(ProtocolV2Device):
self.struct.add_version_field("safety_module_version", 6181) # internal self.struct.add_version_field("safety_module_version", 6181) # internal
self.struct.add_version_field("high_voltage_module_version", 6184) # internal self.struct.add_version_field("high_voltage_module_version", 6184) # internal
# @property
# def pack_num_max(self):
# return 16
@property @property
def polling_commands(self) -> List[ReadHoldingRegisters]: def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [ return super().polling_commands + [
@@ -104,11 +116,9 @@ class EP600(ProtocolV2Device):
ReadHoldingRegisters(152, 1), ReadHoldingRegisters(152, 1),
ReadHoldingRegisters(156, 1), ReadHoldingRegisters(156, 1),
ReadHoldingRegisters(158, 1), ReadHoldingRegisters(158, 1),
] # KM - Consumption
ReadHoldingRegisters(1430, 15),
# KM - Totals
ReadHoldingRegisters(142, 5),
# @property ]
# def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
# return super().pack_polling_commands + [
# ReadHoldingRegisters(6101, 10),
# ReadHoldingRegisters(6175, 12),
# ]
@@ -217,6 +217,26 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
"total_ac_consumption": EnergyFieldAttributes("Total Load Consumption"), "total_ac_consumption": EnergyFieldAttributes("Total Load Consumption"),
"total_grid_consumption": EnergyFieldAttributes("Total Grid Consumption"), "total_grid_consumption": EnergyFieldAttributes("Total Grid Consumption"),
"total_grid_feed": EnergyFieldAttributes("Total Grid Feed"), "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 # Device specific controls
"power_off": FieldAttributes( "power_off": FieldAttributes(
type=FieldType.BOOL, type=FieldType.BOOL,
@@ -5,6 +5,7 @@ import re
from ..devices.ac60 import AC60 from ..devices.ac60 import AC60
from ..devices.ac180 import AC180 from ..devices.ac180 import AC180
from ..devices.ac180p import AC180P from ..devices.ac180p import AC180P
from ..devices.ac200l import AC200L
from ..devices.ac200m import AC200M from ..devices.ac200m import AC200M
from ..devices.ac300 import AC300 from ..devices.ac300 import AC300
from ..devices.ac500 import AC500 from ..devices.ac500 import AC500
@@ -16,7 +17,7 @@ from ..devices.ep760 import EP760
from ..devices.ep800 import EP800 from ..devices.ep800 import EP800
DEVICE_NAME_RE = re.compile( DEVICE_NAME_RE = re.compile(
r"^(AC60|AC180|AC180P|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" r"^(AC60|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$"
) )
@@ -28,6 +29,8 @@ def build_device(address: str, name: str):
return AC180(address, match[2]) return AC180(address, match[2])
if match[1] == "AC180P": if match[1] == "AC180P":
return AC180P(address, match[2]) return AC180P(address, match[2])
if match[1] == "AC200L":
return AC200L(address, match[2])
if match[1] == "AC200M": if match[1] == "AC200M":
return AC200M(address, match[2]) return AC200M(address, match[2])
if match[1] == "AC300": if match[1] == "AC300":
@@ -30,12 +30,15 @@ class DeviceField:
class UintField(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]], scale: int):
self.range = range self.range = range
self.scale = scale
super().__init__(name, address, 1) super().__init__(name, address, 1)
def parse(self, data: bytes) -> int: def parse(self, data: bytes) -> int:
return struct.unpack("!H", data)[0] val = struct.unpack("!H", data)[0]
return val * self.scale
def in_range(self, val: int) -> bool: def in_range(self, val: int) -> bool:
if self.range is None: if self.range is None:
@@ -144,8 +147,8 @@ class DeviceStruct:
def __init__(self): def __init__(self):
self.fields = [] self.fields = []
def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None): def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: int = 1):
self.fields.append(UintField(name, address, range)) self.fields.append(UintField(name, address, range, scale))
def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None): def add_int_field(self, name: str, address: int, range: Tuple[int, int] = None):
self.fields.append(IntField(name, address, range)) self.fields.append(IntField(name, address, range))
@@ -42,6 +42,8 @@ class PollingCoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=polling_interval), update_interval=timedelta(seconds=polling_interval),
) )
self.address = address
# Create client # Create client
self.logger.debug("Creating client") self.logger.debug("Creating client")
device = bluetooth.async_ble_device_from_address(hass, address) device = bluetooth.async_ble_device_from_address(hass, address)
@@ -67,4 +69,10 @@ class PollingCoordinator(DataUpdateCoordinator):
so entities can quickly look up their data. 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() return await self.reader.read_data()
+3 -2
View File
@@ -4,6 +4,7 @@
"bluetooth": [ "bluetooth": [
{ "local_name": "AC60*" }, { "local_name": "AC60*" },
{ "local_name": "AC180*" }, { "local_name": "AC180*" },
{ "local_name": "AC200L*" },
{ "local_name": "AC200M*" }, { "local_name": "AC200M*" },
{ "local_name": "AC300*" }, { "local_name": "AC300*" },
{ "local_name": "AC500*" }, { "local_name": "AC500*" },
@@ -16,11 +17,11 @@
], ],
"codeowners": ["@Patrick762"], "codeowners": ["@Patrick762"],
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://github.com/Patrick762/hassio-bluetti-bt", "documentation": "https://github.com/Patrick762/hassio-bluetti-bt",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues",
"requirements": [], "requirements": [],
"version": "0.0.20" "version": "0.0.24"
} }
+13 -5
View File
@@ -125,6 +125,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
self._attr_entity_category = category self._attr_entity_category = category
self._options = options self._options = options
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
@@ -133,10 +138,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
return return
if self.coordinator.data is None: if self.coordinator.data is None:
_LOGGER.warning( _LOGGER.debug(
"Data from coordinator is None. Skipping update for %s", "Data from coordinator is None",
unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False
self.async_write_ha_state()
return return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
@@ -145,12 +151,14 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
"Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id) "Invalid data from coordinator (sensor.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
_LOGGER.warning("No data for available for (%s)", self._response_key) _LOGGER.warning("No data for available for (%s)", self._response_key)
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
if ( if (
@@ -167,6 +175,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
type(response_data), type(response_data),
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
self._attr_available = True self._attr_available = True
@@ -175,10 +184,9 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
if ( if (
self._options is not None self._options is not None
and isinstance(response_data, Enum) and isinstance(response_data, Enum)
and response_data.value < len(self._options)
): ):
# Enum # Enum
self._attr_native_value = self._options[response_data.value] self._attr_native_value = response_data.name
else: else:
# Numeric # Numeric
self._attr_native_value = response_data 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_entity_category = category
self._attr_device_class = SwitchDeviceClass.OUTLET self._attr_device_class = SwitchDeviceClass.OUTLET
@property
def available(self) -> bool:
"""Return if entity is available."""
return self._attr_available
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """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)) _LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id))
if not isinstance(self.coordinator.data, dict): if not isinstance(self.coordinator.data, dict):
_LOGGER.error( _LOGGER.debug(
"Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id) "Invalid data from coordinator (switch.%s)", unique_id_loggable(self._attr_unique_id)
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
response_data = self.coordinator.data.get(self._response_key) response_data = self.coordinator.data.get(self._response_key)
if response_data is None: if response_data is None:
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
if not isinstance(response_data, bool): if not isinstance(response_data, bool):
@@ -135,6 +142,7 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity):
response_data, response_data,
) )
self._attr_available = False self._attr_available = False
self.async_write_ha_state()
return return
self._attr_available = True self._attr_available = True