Compare commits

..
28 Commits
Author SHA1 Message Date
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 5ac8d8d7ae only check pack_num once 2024-04-01 14:31:03 +02: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
20 changed files with 156 additions and 81 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
github: Patrick762
+7 -11
View File
@@ -20,26 +20,21 @@ body:
label: Version label: Version
description: What version of our software are you running? description: What version of our software are you running?
options: options:
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.11 - 0.0.11
- 0.0.10 - 0.0.10
- 0.0.9 - 0.0.9
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 +45,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
+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.
+11 -6
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, False)
if address is None: if address is None:
_LOGGER.error("Device has no address") _LOGGER.error("Device has no address")
@@ -51,9 +52,8 @@ async def async_setup_entry(
for field_key, field_config in all_fields.items(): for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key): if bluetti_device.has_field(field_key):
if field_config.type == FieldType.BOOL: if field_config.type == FieldType.BOOL:
category = None
if field_config.setter is True: if field_config.setter is True:
category = EntityCategory.DIAGNOSTIC continue
sensors_to_add.append( sensors_to_add.append(
BluettiBinarySensor( BluettiBinarySensor(
@@ -62,7 +62,6 @@ async def async_setup_entry(
address, address,
field_key, field_key,
field_config.name, field_config.name,
category=category,
) )
) )
@@ -79,7 +78,6 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
address, address,
response_key: str, response_key: str,
name: str, name: str,
category: EntityCategory | None = None,
): ):
"""Init battery entity.""" """Init battery entity."""
super().__init__(coordinator) super().__init__(coordinator)
@@ -93,7 +91,11 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
self._attr_name = name self._attr_name = name
self._attr_available = False self._attr_available = False
self._attr_unique_id = get_unique_id(e_name) 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
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
@@ -108,11 +110,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 +126,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
@@ -42,6 +42,11 @@ class BluettiDevice:
def writable_ranges(self) -> List[range]: def writable_ranges(self) -> List[range]:
"""The address ranges that are writable""" """The address ranges that are writable"""
return [] 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): def has_field(self, field: str):
return any(f.name == field for f in self.struct.fields) return any(f.name == field for f in self.struct.fields)
@@ -56,3 +56,9 @@ class ProtocolV1Device(BluettiDevice):
@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_num_field(self) -> List[ReadHoldingRegisters]:
return [
ReadHoldingRegisters(96, 1),
]
@@ -97,18 +97,20 @@ class DeviceReader:
_LOGGER.warning("Got a parse exception") _LOGGER.warning("Got a parse exception")
# Execute pack polling commands # 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") _LOGGER.debug("Polling battery packs")
for pack in range(1, self.bluetti_device.pack_num_max + 1): for pack in range(1, self.bluetti_device.pack_num_max + 1):
# Set current pack number _LOGGER.debug("Setting pack_num to %i", pack)
await self._async_send_command(
self.bluetti_device.build_setter_command(
"pack_num", pack
)
)
# We need to wait after switching packs for the data to be available # Set current pack number
await asyncio.sleep(5) 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)
# b'\x00\x02' b'\x00\x03' here (seems like result)
for command in pack_commands: for command in pack_commands:
# Request & parse result for each pack # Request & parse result for each pack
@@ -121,18 +123,6 @@ class DeviceReader:
) )
_LOGGER.debug("Parsed data: %s", parsed) _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(): for key, value in parsed.items():
parsed_data.update({key + str(pack): value}) parsed_data.update({key + str(pack): value})
@@ -140,7 +130,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 +139,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:
@@ -175,18 +173,16 @@ class DeviceReader:
return cast(bytes, res) return cast(bytes, res)
except TimeoutError: except TimeoutError:
_LOGGER.warning("Polling single command timed out") _LOGGER.debug("Polling single command timed out")
except ModbusError as err: except ModbusError as err:
_LOGGER.warning( _LOGGER.debug(
"Got an invalid request error for %s: %s", "Got an invalid request error for %s: %s",
command, command,
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()
@@ -24,7 +24,7 @@ class AC200L(ProtocolV1Device):
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -42,6 +42,7 @@ class AC200L(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 5), ReadHoldingRegisters(70, 5),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3060, 2), ReadHoldingRegisters(3060, 2),
] ]
@@ -24,7 +24,7 @@ class AC200M(ProtocolV1Device):
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -42,6 +42,7 @@ class AC200M(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 5), ReadHoldingRegisters(70, 5),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3060, 2), ReadHoldingRegisters(3060, 2),
] ]
@@ -15,22 +15,22 @@ class AC300(ProtocolV1Device):
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_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_int_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2) 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_int_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_int_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("pv_input_voltage1", 86, multiplier=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2) self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10)
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -57,6 +57,7 @@ class AC300(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 11), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1), ReadHoldingRegisters(3011, 1),
@@ -15,22 +15,22 @@ class AC500(ProtocolV1Device):
self.struct.add_enum_field("ac_output_mode", 70, OutputMode) 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_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_int_field("internal_power_one", 73)
self.struct.add_decimal_field("internal_ac_frequency", 74, 2) 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_int_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_int_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("pv_input_voltage1", 86, multiplier=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("pv_input_power1", 87, 1, multiplier=10)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2) self.struct.add_decimal_field("pv_input_current1", 88, 2, multiplier=10)
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -56,6 +56,7 @@ class AC500(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 11), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1), ReadHoldingRegisters(3011, 1),
@@ -30,7 +30,7 @@ class EP500(ProtocolV1Device):
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -52,6 +52,7 @@ class EP500(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 11), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1), ReadHoldingRegisters(3011, 1),
@@ -30,7 +30,7 @@ class EP500P(ProtocolV1Device):
# Battery packs # Battery packs
self.struct.add_uint_field("pack_num_max", 91) # internal self.struct.add_uint_field("pack_num_max", 91) # internal
self.struct.add_decimal_field("total_battery_voltage", 92, 1) 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_decimal_field("pack_voltage", 98, 2) # Full pack voltage
self.struct.add_uint_field("pack_battery_percent", 99) 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
@@ -52,6 +52,7 @@ class EP500P(ProtocolV1Device):
return super().polling_commands + [ return super().polling_commands + [
ReadHoldingRegisters(70, 11), ReadHoldingRegisters(70, 11),
ReadHoldingRegisters(86, 3), ReadHoldingRegisters(86, 3),
ReadHoldingRegisters(92, 1),
ReadHoldingRegisters(3001, 1), ReadHoldingRegisters(3001, 1),
ReadHoldingRegisters(3004, 2), ReadHoldingRegisters(3004, 2),
ReadHoldingRegisters(3011, 1), ReadHoldingRegisters(3011, 1),
@@ -53,6 +53,6 @@ def build_device(address: str, name: str):
def get_type_by_bt_name(bt_name: str): def get_type_by_bt_name(bt_name: str):
match = DEVICE_NAME_RE.match(bt_name) match = DEVICE_NAME_RE.match(bt_name)
if match is None or len(match) < 2: if match is None:
return "Unknown" return "Unknown"
return match[1] return match[1]
@@ -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]], multiplier: float):
self.range = range self.range = range
self.multiplier = multiplier
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.multiplier
def in_range(self, val: int) -> bool: def in_range(self, val: int) -> bool:
if self.range is None: if self.range is None:
@@ -79,15 +82,16 @@ class EnumField(DeviceField):
class DecimalField(DeviceField): class DecimalField(DeviceField):
def __init__( 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.scale = scale
self.range = range self.range = range
self.multiplier = multiplier
super().__init__(name, address, 1) super().__init__(name, address, 1)
def parse(self, data: bytes) -> Decimal: def parse(self, data: bytes) -> Decimal:
val = Decimal(struct.unpack("!H", data)[0]) 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: def in_range(self, val: Decimal) -> bool:
if self.range is None: if self.range is None:
@@ -144,8 +148,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, multiplier: float = 1):
self.fields.append(UintField(name, address, range)) self.fields.append(UintField(name, address, range, multiplier))
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))
@@ -157,9 +161,9 @@ class DeviceStruct:
self.fields.append(EnumField(name, address, enum)) self.fields.append(EnumField(name, address, enum))
def add_decimal_field( 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): def add_decimal_array_field(self, name: str, address: int, size: int, scale: int):
self.fields.append(DecimalArrayField(name, address, size, scale)) self.fields.append(DecimalArrayField(name, address, size, scale))
+11 -1
View File
@@ -14,7 +14,7 @@ from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak, BluetoothServiceInfoBleak,
async_discovered_service_info, 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.data_entry_flow import FlowResult
from homeassistant.helpers import selector 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) await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured() 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 # Get device type if needed
if isinstance(discovery_info.name, str) and discovery_info.name.startswith("PBOX"): if isinstance(discovery_info.name, str) and discovery_info.name.startswith("PBOX"):
bleak_device = BleakClient(discovery_info.device) bleak_device = BleakClient(discovery_info.device)
device_type = await recognize_device(bleak_device, self.hass.loop.create_future) device_type = await recognize_device(bleak_device, self.hass.loop.create_future)
_LOGGER.info("Device identified as %s", device_type) _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()) discovery_info.name = discovery_info.name.replace("PBOX", device_type.strip())
self._discovery_info = discovery_info self._discovery_info = discovery_info
@@ -76,6 +85,7 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={ data={
CONF_ADDRESS: discovery_info.address, CONF_ADDRESS: discovery_info.address,
CONF_NAME: name, CONF_NAME: name,
CONF_TYPE: discovery_info.manufacturer_data.get(CONF_TYPE, "Unknown"),
}, },
) )
@@ -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()
+1 -1
View File
@@ -23,5 +23,5 @@
"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.23" "version": "0.1.0"
} }
+14 -5
View File
@@ -57,7 +57,7 @@ async def async_setup_entry(
all_fields.update({name+str(pack): field}) all_fields.update({name+str(pack): field})
for field_key, field_config in all_fields.items(): 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 category = None
if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS: if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS:
category = EntityCategory.DIAGNOSTIC category = EntityCategory.DIAGNOSTIC
@@ -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.debug("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
+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