Compare commits

..
54 Commits
Author SHA1 Message Date
Patrick762 6f9afedfdb Merge pull request #119 from kvanonavk/main
AC180P Advanced Integration
2024-07-19 16:17:16 +02:00
Patrick762 fe23dd00cd Merge pull request #122 from Patrick762/ac70-fix
Added ac70 auto detection
2024-07-19 16:13:48 +02:00
Patrick762 566761dba7 update version 2024-07-19 16:13:19 +02:00
Patrick762 33cb6e6fbd added ac70 auto detection 2024-07-19 15:02:57 +02:00
kvanonavk 5823136280 Merge branch 'Patrick762:main' into main 2024-07-05 14:44:12 +03:00
kvanonavk 7b6f845edb fixes 2024-07-05 14:42:56 +03:00
kvanonavk 1fbcf83b1b simple set charging mode (no turbo) 2024-07-01 18:16:58 +03:00
Patrick762 149633f9c8 Increase version number 2024-06-30 15:04:23 +02:00
Patrick762 9ab7cefda7 Merge pull request #116 from matthewpucc/main
Support AC70 and AC70P Device
2024-06-30 15:03:35 +02:00
Patrick762 52853d6d3e Merge pull request #117 from rotx/fix-multiply
Fix multiplication of Decimal with float
2024-06-30 15:03:14 +02:00
kvanonavk c96f551024 fixes 2024-06-30 14:11:31 +03:00
kvanonavk 75209ee1aa readme fix 2 2024-06-30 13:59:45 +03:00
kvanonavk 7396616113 readme fix 2024-06-30 13:56:58 +03:00
kvanonavk a7f7ff1843 fixes 2024-06-30 13:55:10 +03:00
kvanonavk cc755b3003 test + fix 2024-06-30 13:25:39 +03:00
kvanonavk e5f690ef3b AC180P more data and functionality 2024-06-30 12:24:52 +03:00
Robert Muchsel 3d3401c942 Fix multiplication of Decimal with float 2024-06-21 20:52:48 -05:00
Matthew Puccerella f50232ee89 Merge pull request #1 from matthewpucc/support_ac70_devices
Add support AC70 and AC70P devices
2024-06-20 16:12:53 -07:00
Matthew Puccerella 5922362772 Add support AC70 and AC70P devices 2024-06-20 23:11:47 +00:00
Patrick762 6478825503 Merge pull request #107 from Patrick762/next
Fixed values and improved unavailable handling
2024-06-02 14:06:20 +02:00
Patrick762 3169ee182e increase version number 2024-06-02 14:03:02 +02:00
Patrick762 ece1fc4129 only set unavailable after 5 retries 2024-06-02 13:58:21 +02:00
Patrick762 4d54e048c5 ignore packs not connected 2024-06-02 13:49:18 +02:00
Patrick762 09a9112279 check pack_num after setting it 2024-06-02 13:48:45 +02:00
Patrick762 b37276ec52 updated ac200l sensors based on feedback 2024-06-02 13:12:43 +02:00
Patrick762 4f88792371 Update bug_report.yaml 2024-05-31 17:16:26 +02:00
Patrick762 7da392d85a Merge pull request #54 from Patrick762/dev
[WIP] Release
2024-05-31 17:05:16 +02:00
Patrick762 03ea349cbf set new version to 0.1 2024-05-31 17:04:41 +02:00
Patrick762 5a762266c0 Merge pull request #104 from Patrick762/values-fix-2
Values and pack polling fix
2024-05-31 17:02:07 +02:00
Patrick762 12a59346e9 fix device type info 2024-05-31 16:59:46 +02:00
Patrick762 206099ab64 increase version number 2024-05-31 14:23:43 +02:00
Patrick762 6be2abaaf2 fix pack polling 2024-05-31 14:23:07 +02:00
Patrick762 c3bfad0ea6 add pack sensors if available 2024-05-31 13:46:09 +02:00
Patrick762 b548528085 updated pack polling 2024-05-31 12:55:46 +02:00
Patrick762 6e433e3551 fix total_battery_voltage 2024-05-31 12:14:51 +02:00
Patrick762 b34d0a3039 fix ac300 and ac500 solar string values and renamed entity 2024-05-31 12:06:21 +02:00
Patrick762 2922c5cb90 add multiplier to decimal field 2024-05-31 12:05:09 +02:00
Patrick762 9af3d67ec6 removed binary sensors for disabled control entities to prevent duplicates 2024-05-31 11:34:25 +02:00
Patrick762 221da9a38d fix internal_power sensors for ac300 and ac500 2024-05-31 11:24:20 +02:00
Patrick762 f5430216a8 Merge pull request #103 from Patrick762/values-fix-1
Values fix AC300 and AC500
2024-05-30 20:25:02 +02:00
Patrick762 591c86baf5 fix internal_dc_input_voltage for ac300 and ac500 2024-05-30 20:22:43 +02:00
Patrick762 97154bb027 increase version number 2024-05-30 19:57:02 +02:00
Patrick762 b2672ec302 fix availability states 2024-05-30 19:56:23 +02:00
Patrick762 f5b8e09fb4 prevent binary_sensor if controls enabled 2024-05-30 15:03:47 +02:00
Patrick762 c8d75d1899 Merge pull request #102 from Patrick762/bug-report-template
updated bug report template
2024-05-30 12:30:47 +02:00
Patrick762 efaa9d0fdb updated bug report template 2024-05-30 12:29:53 +02:00
Patrick762 9315650ddc Merge pull request #88 from Patrick762/pack-polling
Check pack_num only once
2024-05-30 12:27:51 +02:00
Patrick762 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
28 changed files with 320 additions and 100 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
github: Patrick762
+4 -19
View File
@@ -14,32 +14,16 @@ body:
value: "An exception appeared" value: "An exception appeared"
validations: validations:
required: true required: true
- type: dropdown - type: textarea
id: version id: version
attributes: attributes:
label: Version label: What version of our software are you running?
description: What version of our software are you running?
options:
- 0.0.11
- 0.0.10
- 0.0.9
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 +34,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.
+3 -1
View File
@@ -17,8 +17,10 @@ After the installation, you can use this button to install the integration:
### Supported devices: ### Supported devices:
- AC60 - AC60
- AC70 (basic data)
- AC70P (untested)
- AC180 (basic data) - AC180 (basic data)
- AC180P (basic data) - AC180P (tested)
- AC200L (untested) - AC200L (untested)
- AC200M - AC200M
- AC300 (tested) - AC300 (tested)
+40 -10
View File
@@ -22,7 +22,7 @@ from .bluetti_bt_lib.field_attributes import FIELD_ATTRIBUTES, FieldType
from .bluetti_bt_lib.utils.device_builder import build_device from .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,32 @@ 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
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback @callback
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
@@ -102,17 +125,24 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected: if self.coordinator.reader.persistent_conn and not self.coordinator.reader.client.is_connected:
return return
if self.coordinator.data is None:
_LOGGER.debug(
"Data from coordinator is None",
)
self._set_unavailable("Data is None")
return
_LOGGER.debug("Updating state of %s", unique_id_loggable(self._attr_unique_id)) _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.debug( _LOGGER.debug(
"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._set_unavailable("Invalid data")
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._set_unavailable("No data")
return return
if not isinstance(response_data, bool): if not isinstance(response_data, bool):
@@ -121,9 +151,9 @@ class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
unique_id_loggable(self._attr_unique_id), unique_id_loggable(self._attr_unique_id),
response_data, response_data,
) )
self._attr_available = False self._set_unavailable("Invalid data type")
return return
self._attr_available = True self._set_available()
self._attr_is_on = self.coordinator.data[self._response_key] is True self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state() self.async_write_ha_state()
@@ -43,6 +43,11 @@ class BluettiDevice:
"""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,25 @@ 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):
_LOGGER.debug("Setting pack_num to %i", pack)
# Set current pack number # Set current pack number
await self._async_send_command( command = self.bluetti_device.build_setter_command(
self.bluetti_device.build_setter_command(
"pack_num", pack "pack_num", pack
) )
body = command.parse_response(
await self._async_send_command(command)
) )
_LOGGER.debug("Raw data set: %s", body)
# We need to wait after switching packs for the data to be available # Check set pack_num
await asyncio.sleep(5) set_pack = int.from_bytes(body, byteorder='big')
if set_pack is not pack:
_LOGGER.warning("Pack polling failed (pack_num %i doesn't match expected %i)", set_pack, pack)
continue
for command in pack_commands: for command in pack_commands:
# Request & parse result for each pack # Request & parse result for each pack
@@ -121,26 +128,16 @@ 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():
# Ignore likely unavailable pack data
if value != 0:
parsed_data.update({key + str(pack): value}) parsed_data.update({key + str(pack): value})
except ParseError: except ParseError:
_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 +146,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:
try:
await self.client.stop_notify(NOTIFY_UUID) 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 +180,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()
@@ -1,8 +1,52 @@
"""AC180P fields.""" """AC180P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device from typing import List
from ..field_enums import ChargingMode
from ..utils.commands import ReadHoldingRegisters
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC180P(ProtocolV2Device): class AC180P(ProtocolV2Device):
def __init__(self, address: str, sn: str): def __init__(self, address: str, sn: str):
super().__init__(address, "AC180P", sn) super().__init__(address, "AC180P", sn)
# Power IO
self.struct.add_uint_field('dc_output_power', 140)
self.struct.add_uint_field('ac_output_power', 142)
self.struct.add_uint_field('dc_input_power', 144)
self.struct.add_uint_field('ac_input_power', 146)
# Input Details (1100 - 1300)
self.struct.add_decimal_field('ac_input_voltage', 1314, 1)
# Controls (2000)
self.struct.add_bool_field('ac_output_on_switch', 2011)
self.struct.add_bool_field('dc_output_on_switch', 2012)
self.struct.add_bool_field('silent_charging_on', 2020) # to be able to choose between normal and silent charging modes
self.struct.add_enum_field('charging_mode', 2020, ChargingMode) # can't be set from HA, custom switch needed
self.struct.add_bool_field('power_lifting_on', 2021)
# Controls (2200)
self.struct.add_bool_field('grid_enhancement_mode_on', 2225)
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
return super().polling_commands + [
ReadHoldingRegisters(140, 1),
ReadHoldingRegisters(142, 1),
ReadHoldingRegisters(144, 1),
ReadHoldingRegisters(146, 1),
ReadHoldingRegisters(1314, 1),
ReadHoldingRegisters(2011, 1),
ReadHoldingRegisters(2012, 1),
ReadHoldingRegisters(2020, 1),
ReadHoldingRegisters(2021, 1),
ReadHoldingRegisters(2225, 1),
]
@property
def writable_ranges(self) -> List[range]:
return super().writable_ranges + [
range(2000, 2022),
range(2200, 2226)
]
@@ -13,18 +13,18 @@ class AC200L(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_decimal_field("internal_ac_voltage", 71, 1) self.struct.add_decimal_field("internal_ac_voltage", 71, 1, multiplier=10)
self.struct.add_decimal_field("internal_current_one", 72, 1) self.struct.add_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, 2) self.struct.add_decimal_field("internal_ac_frequency", 74, 2, multiplier=10)
self.struct.add_uint_field("internal_dc_input_voltage", 86) self.struct.add_uint_field("internal_dc_input_voltage", 86, multiplier=.1)
self.struct.add_decimal_field("internal_dc_input_power", 87, 1) self.struct.add_decimal_field("internal_dc_input_power", 87, 1, multiplier=10)
self.struct.add_decimal_field("internal_dc_input_current", 88, 2) 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_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, multiplier=.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),
@@ -0,0 +1,8 @@
"""AC70 fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70", sn)
@@ -0,0 +1,8 @@
"""AC70P fields."""
from ..base_devices.ProtocolV2Device import ProtocolV2Device
class AC70P(ProtocolV2Device):
def __init__(self, address: str, sn: str):
super().__init__(address, "AC70P", sn)
@@ -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),
@@ -316,6 +316,16 @@ FIELD_ATTRIBUTES: dict[str, FieldAttributes] = {
setter=True, setter=True,
name="Power Lifting", name="Power Lifting",
), ),
"grid_enhancement_mode_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Grid Enhancement Mode",
),
"silent_charging_on": FieldAttributes(
type=FieldType.BOOL,
setter=True,
name="Silent Charging",
),
} }
@@ -3,6 +3,8 @@
import re import re
from ..devices.ac60 import AC60 from ..devices.ac60 import AC60
from ..devices.ac70 import AC70
from ..devices.ac70p import AC70P
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.ac200l import AC200L
@@ -17,7 +19,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|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$" r"^(AC60|AC70|AC70P|AC180|AC180P|AC200L|AC200M|AC300|AC500|EB3A|EP500|EP500P|EP600|EP760|EP800)(\d+)$"
) )
@@ -25,6 +27,10 @@ def build_device(address: str, name: str):
match = DEVICE_NAME_RE.match(name) match = DEVICE_NAME_RE.match(name)
if match[1] == "AC60": if match[1] == "AC60":
return AC60(address, match[2]) return AC60(address, match[2])
if match[1] == "AC70":
return AC70(address, match[2])
if match[1] == "AC70P":
return AC70P(address, match[2])
if match[1] == "AC180": if match[1] == "AC180":
return AC180(address, match[2]) return AC180(address, match[2])
if match[1] == "AC180P": if match[1] == "AC180P":
@@ -53,6 +59,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) * Decimal(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"),
}, },
) )
+4
View File
@@ -23,6 +23,7 @@ SUPPORTED_MODELS = [
"EP500P", "EP500P",
"EP600", "EP600",
"EP760", "EP760",
"AC180P",
] ]
CONTROL_FIELDS = [ CONTROL_FIELDS = [
@@ -31,6 +32,8 @@ CONTROL_FIELDS = [
"power_off", "power_off",
"eco_on", "eco_on",
"power_lifting_on", "power_lifting_on",
"grid_enhancement_mode_on",
"silent_charging_on",
] ]
DIAGNOSTIC_FIELDS = [ DIAGNOSTIC_FIELDS = [
@@ -45,4 +48,5 @@ DIAGNOSTIC_FIELDS = [
"total_ac_consumption", "total_ac_consumption",
"total_grid_consumption", "total_grid_consumption",
"total_grid_feed", "total_grid_feed",
"ac_input_voltage",
] ]
@@ -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()
+2 -1
View File
@@ -3,6 +3,7 @@
"name": "Bluetti BT", "name": "Bluetti BT",
"bluetooth": [ "bluetooth": [
{ "local_name": "AC60*" }, { "local_name": "AC60*" },
{ "local_name": "AC70*" },
{ "local_name": "AC180*" }, { "local_name": "AC180*" },
{ "local_name": "AC200L*" }, { "local_name": "AC200L*" },
{ "local_name": "AC200M*" }, { "local_name": "AC200M*" },
@@ -23,5 +24,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.3"
} }
+36 -9
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
@@ -114,6 +114,7 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
e_name = f"{device_info.get('name')} {name}" e_name = f"{device_info.get('name')} {name}"
self._address = address self._address = address
self._response_key = response_key self._response_key = response_key
self._unavailable_counter = 0
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = name self._attr_name = name
@@ -125,6 +126,32 @@ 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
def _set_available(self):
"""Set sensor as available."""
self._attr_available = True
self._unavailable_counter = 0
self._attr_extra_state_attributes = {}
self.async_write_ha_state()
def _set_unavailable(self, cause: str = "Unknown"):
"""Set sensor as unavailable."""
self._unavailable_counter += 1
self._attr_extra_state_attributes = {
"unavailable_counter": self._unavailable_counter,
"unavailable_cause": cause,
}
if self._unavailable_counter >= 5:
self._attr_available = False
self.async_write_ha_state()
@callback @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 +160,10 @@ 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._set_unavailable("Data is None")
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))
@@ -144,13 +171,13 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
_LOGGER.warning( _LOGGER.warning(
"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._set_unavailable("Invalid data")
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._set_unavailable("No data")
return return
if ( if (
@@ -166,10 +193,10 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
response_data, response_data,
type(response_data), type(response_data),
) )
self._attr_available = False self._set_unavailable("Invalid data type")
return return
self._attr_available = True self._set_available()
# Different for enum and numeric # Different for enum and numeric
if ( if (
+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
+5
View File
@@ -0,0 +1,5 @@
homeassistant
bleak_retry_connector
pyserial
bluetooth_auto_recovery
crcmod
+18
View File
@@ -4,6 +4,8 @@ import unittest
from custom_components.bluetti_bt.bluetti_bt_lib.utils.device_builder import build_device from custom_components.bluetti_bt.bluetti_bt_lib.utils.device_builder import build_device
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac60 import AC60 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac60 import AC60
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70 import AC70
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac70p import AC70P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180 import AC180 from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180 import AC180
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac180p import AC180P
from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200m import AC200M from custom_components.bluetti_bt.bluetti_bt_lib.devices.ac200m import AC200M
@@ -32,6 +34,22 @@ class TestDeviceBuilder(unittest.TestCase):
self.assertIsInstance(built, AC60) self.assertIsInstance(built, AC60)
self.assertEqual(built.address, bt_addr) self.assertEqual(built.address, bt_addr)
def test_build_ac70(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC7056786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70)
self.assertEqual(built.address, bt_addr)
def test_build_ac70p(self):
bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC70P56786746478"
built = build_device(bt_addr, bt_name)
self.assertIsInstance(built, AC70P)
self.assertEqual(built.address, bt_addr)
def test_build_ac180(self): def test_build_ac180(self):
bt_addr = "aa:bb:cc:dd:ee:ff" bt_addr = "aa:bb:cc:dd:ee:ff"
bt_name = "AC18056786746478" bt_name = "AC18056786746478"