diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index ca6457a..0e09ee7 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -36,7 +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) + use_controls = entry.data.get(CONF_USE_CONTROLS, False) if address is None: _LOGGER.error("Device has no address") @@ -52,12 +52,7 @@ 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 - - # Ignore setters if controls enabled - if field_config.setter is True and use_controls is True: continue sensors_to_add.append( @@ -67,7 +62,6 @@ async def async_setup_entry( address, field_key, field_config.name, - category=category, ) ) @@ -84,7 +78,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): address, response_key: str, name: str, - category: EntityCategory | None = None, ): """Init battery entity.""" super().__init__(coordinator) @@ -98,7 +91,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity): 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: diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py index dcc5cd7..0d1edae 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/BluettiDevice.py @@ -42,6 +42,11 @@ class BluettiDevice: def writable_ranges(self) -> List[range]: """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) diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py index 7f1f585..8160342 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/base_devices/ProtocolV1Device.py @@ -56,3 +56,9 @@ class ProtocolV1Device(BluettiDevice): @property def writable_ranges(self) -> List[range]: return [range(3006, 3009)] + + @property + def pack_num_field(self) -> List[ReadHoldingRegisters]: + return [ + ReadHoldingRegisters(96, 1), + ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py index 0e04564..e721ded 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/bluetooth/device_reader.py @@ -97,20 +97,20 @@ 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( - "pack_num", pack - ) + command = self.bluetti_device.build_setter_command( + "pack_num", pack ) - - # We need to wait after switching packs for the data to be available - await asyncio.sleep(5) - - pack_ok = False + body = command.parse_response( + await self._async_send_command(command) + ) + _LOGGER.debug("Raw data set: %s", body) + # b'\x00\x02' b'\x00\x03' here (seems like result) for command in pack_commands: # Request & parse result for each pack @@ -123,20 +123,6 @@ class DeviceReader: ) _LOGGER.debug("Parsed data: %s", parsed) - if not pack_ok: # Only check this once - 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 - pack_ok = True - for key, value in parsed.items(): parsed_data.update({key + str(pack): value}) @@ -187,9 +173,9 @@ 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, diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py index ab80a10..f4c16b5 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200l.py @@ -24,7 +24,7 @@ class AC200L(ProtocolV1Device): # 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_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 @@ -42,6 +42,7 @@ class AC200L(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 5), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3060, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py index 79feaea..fb0d97c 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac200m.py @@ -24,7 +24,7 @@ class AC200M(ProtocolV1Device): # 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_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 @@ -42,6 +42,7 @@ class AC200M(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 5), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3060, 2), ] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py index 3f6b9e0..e7a9303 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac300.py @@ -15,22 +15,22 @@ class AC300(ProtocolV1Device): 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_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, scale=.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_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", 96) # internal + 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 @@ -57,6 +57,7 @@ class AC300(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py index 7d53243..3159dde 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ac500.py @@ -15,22 +15,22 @@ class AC500(ProtocolV1Device): 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_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, scale=.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_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", 96) # internal + 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 @@ -56,6 +56,7 @@ class AC500(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py index 59101d6..1554ce9 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500.py @@ -30,7 +30,7 @@ class EP500(ProtocolV1Device): # 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_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 @@ -52,6 +52,7 @@ class EP500(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py index 62b62de..ba19241 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/devices/ep500p.py @@ -30,7 +30,7 @@ class EP500P(ProtocolV1Device): # 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_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 @@ -52,6 +52,7 @@ class EP500P(ProtocolV1Device): return super().polling_commands + [ ReadHoldingRegisters(70, 11), ReadHoldingRegisters(86, 3), + ReadHoldingRegisters(92, 1), ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3011, 1), diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py index e0e56c7..ed912b2 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/device_builder.py @@ -53,6 +53,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] diff --git a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py index 10fd169..70ef49e 100644 --- a/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py +++ b/custom_components/bluetti_bt/bluetti_bt_lib/utils/struct.py @@ -30,15 +30,15 @@ class DeviceField: class UintField(DeviceField): - def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], scale: int): + def __init__(self, name: str, address: int, range: Optional[Tuple[int, int]], multiplier: float): self.range = range - self.scale = scale + self.multiplier = multiplier super().__init__(name, address, 1) def parse(self, data: bytes) -> int: val = struct.unpack("!H", data)[0] - return val * self.scale + return val * self.multiplier def in_range(self, val: int) -> bool: if self.range is None: @@ -82,15 +82,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) * self.multiplier def in_range(self, val: Decimal) -> bool: if self.range is None: @@ -147,8 +148,8 @@ class DeviceStruct: def __init__(self): self.fields = [] - def add_uint_field(self, name: str, address: int, range: Tuple[int, int] = None, scale: int = 1): - self.fields.append(UintField(name, address, range, scale)) + 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)) @@ -160,9 +161,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)) diff --git a/custom_components/bluetti_bt/config_flow.py b/custom_components/bluetti_bt/config_flow.py index 0d20d13..545ff4e 100644 --- a/custom_components/bluetti_bt/config_flow.py +++ b/custom_components/bluetti_bt/config_flow.py @@ -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"), }, ) diff --git a/custom_components/bluetti_bt/manifest.json b/custom_components/bluetti_bt/manifest.json index be3733e..0afaf91 100644 --- a/custom_components/bluetti_bt/manifest.json +++ b/custom_components/bluetti_bt/manifest.json @@ -23,5 +23,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/Patrick762/hassio-bluetti-bt/issues", "requirements": [], - "version": "0.0.24" + "version": "0.0.25" } diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index 4b621b0..ba61abe 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -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 @@ -156,7 +156,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity): 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) + _LOGGER.debug("No data for available for (%s)", self._response_key) self._attr_available = False self.async_write_ha_state() return