Compare commits

...
35 Commits
Author SHA1 Message Date
Patrick762 4c286324c7 Merge pull request #12 from Patrick762/dev
increased version number
2023-08-19 22:42:07 +02:00
Patrick762 6361e661c0 increased version number 2023-08-19 22:41:39 +02:00
Patrick762 0d237f2e45 Merge pull request #11 from Patrick762/dev
Improvements and bug fixes
2023-08-19 22:40:40 +02:00
Patrick762 95f2331cc3 fixed enum sensors 2023-08-19 22:38:30 +02:00
Patrick762 61bc379d3a updated readme 2023-08-19 21:20:32 +02:00
Patrick762 dd7665f069 added option to turn on and off outputs 2023-08-19 21:17:44 +02:00
Patrick762 8fce8270b2 added switches for ac and dc on off 2023-08-19 17:38:24 +02:00
Patrick762 886ab957bb added options flow 2023-08-19 17:06:58 +02:00
Patrick762 5a6dbb8d00 added "my" link to readme 2023-08-19 17:05:59 +02:00
Patrick762 d54bf7c526 Merge pull request #7 from Patrick762/dev
Added more EP600 fields
2023-08-04 19:49:33 +02:00
Patrick762 22c2f0c7d0 edited comment 2023-08-04 19:49:22 +02:00
Patrick762 23dc2b7f41 renamed some fields, added grid input power fields 2023-08-04 19:47:42 +02:00
Patrick762 84a742968a added more ep600 fields 2023-08-04 19:27:58 +02:00
Patrick762 ed818070c2 Merge pull request #6 from Patrick762/dev
Updated EP600 fields
2023-08-03 22:01:00 +02:00
Patrick762 beb4e6b8fb added input and output frequency for ep600 2023-08-03 21:58:59 +02:00
Patrick762 7444097c60 added more ep600 voltage entities 2023-08-03 21:44:51 +02:00
Patrick762 0912fcbc49 Merge pull request #5 from Patrick762/sensor-availability
Updated sensor availability detection
2023-08-03 16:16:21 +02:00
Patrick762 80b03fc1d9 Merge branch 'sensor-availability' of https://github.com/Patrick762/hassio-bluetti-bt into sensor-availability 2023-08-03 16:11:26 +02:00
Patrick762 92c130b2d6 updated sensor availability detection 2023-08-03 16:10:30 +02:00
Patrick762 f8e382347e Merge pull request #4 from Patrick762/device-support
More EP600 fields
2023-08-03 16:08:51 +02:00
Patrick762 bab008b89c set logging level of parsed data to debug 2023-08-03 16:06:43 +02:00
Patrick762 811501b382 added adl400 voltage fields 2023-08-03 16:04:23 +02:00
Patrick762 90f66c8548 added dummy device to entities 2023-08-03 14:58:46 +02:00
Patrick762 924eaa90b6 Merge pull request #3 from Patrick762/device-support
Added more fields for ep600
2023-08-02 21:23:00 +02:00
Patrick762 4f95fed85a added more fields for ep600 2023-08-02 21:07:24 +02:00
Patrick762 332cf6dc28 Merge pull request #2 from Patrick762/device-support
Support for more fields
2023-08-01 21:23:38 +02:00
Patrick762 cc7891ed6a added more fields 2023-08-01 21:20:02 +02:00
Patrick762 12cd2719d2 added dummy device class to append new fields 2023-08-01 20:37:52 +02:00
Patrick762 e1d4446fb8 updated sensor availability detection 2023-08-01 19:46:48 +02:00
Patrick762 b7c705f224 updated sensor data handling 2023-07-31 20:32:59 +02:00
Patrick762 a71f419676 separated sensors and binary_sensors 2023-07-31 19:24:56 +02:00
Patrick762 7af1fb02c6 separated coordinator and sensor 2023-07-31 19:01:56 +02:00
Patrick762 b8275a4c33 updated version number 2023-07-31 16:53:12 +02:00
Patrick762 e57abaa58a updated readme 2023-07-31 16:52:57 +02:00
Patrick762 1e38903dcf fix name for some devices 2023-07-31 16:51:23 +02:00
11 changed files with 1225 additions and 182 deletions

No files matched your search

+7 -1
View File
@@ -3,6 +3,7 @@
[![Validate with hassfest](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml) [![Validate with hassfest](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/hassfest_validation.yml)
[![HACS Action](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml) [![HACS Action](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml/badge.svg)](https://github.com/Patrick762/hassio-bluetti-bt/actions/workflows/HACS.yml)
[![Add repo to HACS](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?repository=hassio-bluetti-bt&owner=Patrick762)
Bluetti Integration for Home Assistant Bluetti Integration for Home Assistant
@@ -13,7 +14,7 @@ Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt).
- AC60 - AC60
- AC200M - AC200M
- AC300 - AC300 (tested)
- AC500 - AC500
- EB3A - EB3A
- EP500 - EP500
@@ -22,3 +23,8 @@ Based on [bluetti_mqtt](https://github.com/warhammerkid/bluetti_mqtt).
### Available sensors: ### Available sensors:
All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py)) All sensors which are available in bluetti_mqtt (Based on [this file](https://github.com/warhammerkid/bluetti_mqtt/blob/main/bluetti_mqtt/mqtt_client.py))
excluding battery pack fields.
### Available controls:
If enabled in the Integration options (you need to reload the integration if you change this option):
AC and DC outputs
+24 -4
View File
@@ -3,23 +3,28 @@
from __future__ import annotations from __future__ import annotations
import re import re
import logging
from homeassistant.components import bluetooth from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_TYPE, Platform from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, MANUFACTURER from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING
from .coordinator import PollingCoordinator
PLATFORMS: [Platform] = [Platform.SENSOR] PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bluetti Powerstation from a config entry.""" """Set up Bluetti Powerstation from a config entry."""
address = entry.data.get(CONF_ADDRESS) address = entry.data.get(CONF_ADDRESS)
device_name = entry.data.get(CONF_NAME)
use_controls = entry.data.get(CONF_USE_CONTROLS)
if address is None: if address is None:
return False return False
@@ -27,8 +32,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if not bluetooth.async_address_present(hass, address): if not bluetooth.async_address_present(hass, address):
raise ConfigEntryNotReady("Bluetti device not present") raise ConfigEntryNotReady("Bluetti device not present")
# Create data structure
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(entry.entry_id, {})
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_POLLING_RUNNING, False)
# Create coordinator for polling
coordinator = PollingCoordinator(hass, address, device_name)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator)
platforms: list = PLATFORMS
if use_controls is True:
_LOGGER.warning("You are using controls with this integration at your own risk!")
platforms.append(Platform.SWITCH)
# Setup platforms # Setup platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, platforms)
return True return True
@@ -0,0 +1,130 @@
"""Bluetti BT sensors."""
from __future__ import annotations
import logging
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
CONF_ADDRESS,
CONF_NAME,
EntityCategory,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from bluetti_mqtt.bluetooth import build_device
from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS,
MqttFieldType,
)
from . import device_info as dev_info, get_unique_id
from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS
from .coordinator import PollingCoordinator, DummyDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Setup sensor entities."""
device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS)
if address is None:
_LOGGER.error("Device has no address")
# Generate device info
_LOGGER.info("Creating sensors for device with address %s", address)
device_info = dev_info(entry)
# Add sensors according to device_info
bluetti_device = build_device(address, device_name)
bluetti_device = DummyDevice(bluetti_device)
sensors_to_add = []
all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS)
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key):
if field_config.type == MqttFieldType.BOOL:
category = None
if field_config.setter is True:
category = EntityCategory.DIAGNOSTIC
sensors_to_add.append(
BluettiBinarySensor(
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info,
address,
field_key,
field_config.home_assistant_extra.get(CONF_NAME, ""),
category=category,
)
)
async_add_entities(sensors_to_add)
class BluettiBinarySensor(CoordinatorEntity, BinarySensorEntity):
"""Bluetti universal binary sensor."""
def __init__(
self,
coordinator: PollingCoordinator,
device_info: DeviceInfo,
address,
response_key: str,
name: str,
category: EntityCategory | None = None,
):
"""Init battery entity."""
super().__init__(coordinator)
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
self._attr_device_info = device_info
self._attr_name = e_name
self._attr_available = False
self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
_LOGGER.debug("Updating state of %s", self._attr_unique_id)
if not isinstance(self.coordinator.data, dict):
_LOGGER.error(
"Invalid data from coordinator (sensor.%s)", self._attr_unique_id
)
self._attr_available = False
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
return
if not isinstance(response_data, bool):
_LOGGER.warning(
"Invalid response data type from coordinator (sensor.%s): %s",
self._attr_unique_id,
response_data,
)
self._attr_available = False
return
self._attr_available = True
self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state()
+57 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import re
import logging import logging
import voluptuous as vol import voluptuous as vol
@@ -13,9 +14,10 @@ from homeassistant.components.bluetooth import (
) )
from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME from homeassistant.const import CONF_ADDRESS, CONF_TYPE, CONF_NAME
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from . import get_type_by_bt_name from . import get_type_by_bt_name
from .const import DOMAIN from .const import DOMAIN, CONF_USE_CONTROLS
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -54,7 +56,7 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data={ data={
CONF_ADDRESS: discovery_info.address, CONF_ADDRESS: discovery_info.address,
CONF_TYPE: dev_type, CONF_TYPE: dev_type,
CONF_NAME: discovery_info.name, CONF_NAME: re.sub("[^A-Z0-9]+", "", discovery_info.name),
}, },
) )
@@ -85,3 +87,56 @@ class BluettiConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", step_id="user",
data_schema=data_schema, data_schema=data_schema,
) )
@staticmethod
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a option flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
changed = self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
**{
CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS],
},
},
)
if changed is False:
_LOGGER.error(
"Method OptionsFlowHandler.async_step_init: Config entry %s has not been changed",
self.config_entry.entry_id,
)
return self.async_create_entry(
title="",
data={
CONF_USE_CONTROLS: user_input[CONF_USE_CONTROLS],
},
)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(
CONF_USE_CONTROLS,
default=self.config_entry.data.get(CONF_USE_CONTROLS, False),
): selector.BooleanSelector(),
}
),
)
+455 -2
View File
@@ -1,7 +1,460 @@
"""Constants for the Bluetti BT integration.""" """Constants for the Bluetti BT integration."""
from bluetti_mqtt.mqtt_client import MqttFieldConfig, MqttFieldType
DOMAIN = "bluetti_bt" DOMAIN = "bluetti_bt"
MANUFACTURER = "Bluetti" MANUFACTURER = "Bluetti"
API_RESPONSE_BATTERY_RANGE_START = "battery_range_start" CONF_OPTIONS = "options"
API_RESPONSE_BATTERY_RANGE_END = "battery_range_end" CONF_USE_CONTROLS = "use_controls"
DATA_COORDINATOR = "coordinator"
DATA_POLLING_RUNNING = "polling_running"
CONTROL_FIELDS = [
"ac_output_on",
"dc_output_on",
]
DIAGNOSTIC_FIELDS = [
"max_ac_input_power",
"max_ac_input_current",
"max_ac_output_power",
"max_ac_output_current",
"bcu_version",
"safety_module_version",
"high_voltage_module_version",
]
ADDITIONAL_DEVICE_FIELDS = {
"max_ac_input_power": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Max AC Input Power per Phase",
},
),
"max_ac_input_current": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Max AC Input Current per Phase",
},
),
"max_ac_output_power": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Max AC Output Power per Phase",
},
),
"max_ac_output_current": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Max AC Output Current per Phase",
},
),
"bcu_version": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={"name": "BCU Version"},
),
"safety_module_version": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={"name": "Safety Module Version"},
),
"high_voltage_module_version": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={"name": "High Voltage Safety Module Version"},
),
"adl400_ac_input_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Voltage Phase 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_voltage_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Voltage Phase 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"adl400_ac_input_voltage_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "ADL400 AC Input Voltage Phase 3",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_voltage_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_voltage_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Voltage Phase 3",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 1",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 2",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_current_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Current Phase 3",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"grid_input_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Grid Input Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_frequency": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Frequency",
"unit_of_measurement": "Hz",
"device_class": "frequency",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 1",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 2",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_voltage_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Voltage Phase 3",
"unit_of_measurement": "V",
"device_class": "voltage",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 1",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 2",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_power_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Power Phase 3",
"unit_of_measurement": "W",
"device_class": "power",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 1",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 2",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"ac_output_current_phase3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "AC Output Current Phase 3",
"unit_of_measurement": "A",
"device_class": "current",
"state_class": "measurement",
"force_update": True,
},
),
"testing0": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 0",
},
),
"testing1": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 1",
},
),
"testing2": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 2",
},
),
"testing3": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 3",
},
),
"testing4": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 4",
},
),
"testing5": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 5",
},
),
"testing6": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 6",
},
),
"testing7": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 7",
},
),
"testing8": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 8",
},
),
"testing9": MqttFieldConfig(
type=MqttFieldType.NUMERIC,
setter=False,
advanced=False,
home_assistant_extra={
"name": "Register Testing 9",
},
),
}
+263
View File
@@ -0,0 +1,263 @@
"""Coordinator for Bluetti integration."""
from __future__ import annotations
from typing import List, cast
import asyncio
from datetime import timedelta
import logging
import async_timeout
from bleak import BleakClient, BleakError
from homeassistant.components import bluetooth
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from bluetti_mqtt.bluetooth.client import BluetoothClient
from bluetti_mqtt.bluetooth import (
BadConnectionError,
ModbusError,
ParseError,
build_device,
)
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.core.commands import ReadHoldingRegisters
from .const import DATA_POLLING_RUNNING, DOMAIN
_LOGGER = logging.getLogger(__name__)
class DummyDevice(BluettiDevice):
"""Dummy device used to add more fields to existing devices.
Changes made here are only temporary and should also be
contributed to https://github.com/warhammerkid/bluetti_mqtt
"""
def __init__(self, device: BluettiDevice):
"""Init dummy device with real device."""
self.struct = device.struct
if device.type == "EP600":
# ADL400 Smart Meter for AC Solar
self.struct.add_uint_field("adl400_ac_input_power_phase1", 1228)
self.struct.add_uint_field("adl400_ac_input_power_phase2", 1236)
self.struct.add_uint_field("adl400_ac_input_power_phase3", 1244)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase1", 1229, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase2", 1237, 1)
self.struct.add_decimal_field("adl400_ac_input_voltage_phase3", 1245, 1)
# Grid Input
self.struct.add_decimal_field("grid_input_frequency", 1300, 1)
self.struct.add_uint_field("grid_input_power_phase1", 1313)
self.struct.add_uint_field("grid_input_power_phase2", 1319)
self.struct.add_uint_field("grid_input_power_phase3", 1325)
self.struct.add_decimal_field("grid_input_voltage_phase1", 1314, 1)
self.struct.add_decimal_field("grid_input_voltage_phase2", 1320, 1)
self.struct.add_decimal_field("grid_input_voltage_phase3", 1326, 1)
self.struct.add_decimal_field("grid_input_current_phase1", 1315, 1)
self.struct.add_decimal_field("grid_input_current_phase2", 1321, 1)
self.struct.add_decimal_field("grid_input_current_phase3", 1327, 1)
# EP600 AC Output
self.struct.add_decimal_field("ac_output_frequency", 1500, 1)
self.struct.add_uint_field("ac_output_power_phase1", 1510)
self.struct.add_uint_field("ac_output_power_phase2", 1517)
self.struct.add_uint_field("ac_output_power_phase3", 1524)
self.struct.add_decimal_field("ac_output_voltage_phase1", 1511, 1)
self.struct.add_decimal_field("ac_output_voltage_phase2", 1518, 1)
self.struct.add_decimal_field("ac_output_voltage_phase3", 1525, 1)
self.struct.add_decimal_field("ac_output_current_phase1", 1512, 1)
self.struct.add_decimal_field("ac_output_current_phase2", 1519, 1)
self.struct.add_decimal_field("ac_output_current_phase3", 1526, 1)
# Testing
# for i in range(0, 10):
# self.struct.add_decimal_field("testing"+str(i), 1520+i, 1)
super().__init__(device.address, device.type, device.sn)
self._parent = device
@property
def polling_commands(self) -> List[ReadHoldingRegisters]:
if self.type == "EP600":
return [
ReadHoldingRegisters(100, 62),
ReadHoldingRegisters(1228, 19),
ReadHoldingRegisters(1300, 28),
ReadHoldingRegisters(1500, 27),
ReadHoldingRegisters(2022, 6),
ReadHoldingRegisters(2213, 4),
# Battery
ReadHoldingRegisters(6101, 7),
ReadHoldingRegisters(6175, 11),
# Testing
# ReadHoldingRegisters(100, 62),
# ReadHoldingRegisters(1100, 51),
# ReadHoldingRegisters(1200, 80),
# ReadHoldingRegisters(1300, 31),
# ReadHoldingRegisters(1400, 48),
# ReadHoldingRegisters(1500, 30),
# ReadHoldingRegisters(2000, 89),
# ReadHoldingRegisters(2200, 41),
# ReadHoldingRegisters(2300, 36),
# ReadHoldingRegisters(6000, 32),
# ReadHoldingRegisters(6100, 100),
# ReadHoldingRegisters(6300, 100),
]
return self._parent.polling_commands
@property
def pack_polling_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.pack_logging_commands
@property
def logging_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.logging_commands
@property
def pack_logging_commands(self) -> List[ReadHoldingRegisters]:
return self._parent.pack_logging_commands
@property
def writable_ranges(self) -> List[range]:
return self._parent.writable_ranges
class PollingCoordinator(DataUpdateCoordinator):
"""Polling coordinator."""
def __init__(self, hass: HomeAssistant, address, device_name: str):
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
name="Bluetti polling coordinator",
update_interval=timedelta(seconds=20),
)
self._address = address
self.notify_future = None
self.current_command = None
self.notify_response = bytearray()
bluetti_device = build_device(address, device_name)
# Add or modify device fields
self.bluetti_device = DummyDevice(bluetti_device)
async def _async_update_data(self):
"""Fetch data from API endpoint.
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = True
self.logger.debug("Polling data")
device = bluetooth.async_ble_device_from_address(self.hass, self._address)
if device is None:
self.logger.error("Device %s not available", self._address)
return None
if self.bluetti_device is None:
self.logger.error("Device type for %s not found", self._address)
return None
# Polling
client = BleakClient(device)
parsed_data: dict = {}
try:
async with async_timeout.timeout(15):
await client.connect()
await client.start_notify(
BluetoothClient.NOTIFY_UUID, self._notification_handler
)
for command in self.bluetti_device.polling_commands:
try:
# Prepare to make request
self.current_command = command
self.notify_future = self.hass.loop.create_future()
self.notify_response = bytearray()
# Make request
self.logger.debug("Requesting %s", command)
await client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
# Wait for response
res = await asyncio.wait_for(
self.notify_future, timeout=BluetoothClient.RESPONSE_TIMEOUT
)
# Process data
self.logger.debug("Got %s bytes", len(res))
response = cast(bytes, res)
body = command.parse_response(response)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.debug("Parsed data: %s", parsed)
parsed_data.update(parsed)
except TimeoutError:
self.logger.warning(
"Polling timed out (address: %s)", self._address
)
except ParseError:
self.logger.warning("Got a parse exception...")
except ModbusError as err:
self.logger.warning(
"Got an invalid request error for %s: %s",
command,
err,
)
except (BadConnectionError, BleakError) as err:
self.logger.warning(
"Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err
)
except TimeoutError:
self.logger.warning("Polling timed out for device %s", self._address)
return None
except BleakError as err:
self.logger.warning("Bleak error: %s", err)
return None
finally:
await client.disconnect()
self.hass.data[DOMAIN][self.config_entry.entry_id][DATA_POLLING_RUNNING] = False
# Pass data back to sensors
return parsed_data
def _notification_handler(self, _sender: int, data: bytearray):
"""Handle bt data."""
# Ignore notifications we don't expect
if not self.notify_future or self.notify_future.done():
return
# If something went wrong, we might get weird data.
if data == b"AT+NAME?\r" or data == b"AT+ADV?\r":
err = BadConnectionError("Got AT+ notification")
self.notify_future.set_exception(err)
return
# Save data
self.notify_response.extend(data)
if len(self.notify_response) == self.current_command.response_size():
if self.current_command.is_valid_response(self.notify_response):
self.notify_future.set_result(self.notify_response)
else:
self.notify_future.set_exception(ParseError("Failed checksum"))
elif self.current_command.is_exception_response(self.notify_response):
# We got a MODBUS command exception
msg = f"MODBUS Exception {self.current_command}: {self.notify_response[2]}"
self.notify_future.set_exception(ModbusError(msg))
+1 -1
View File
@@ -18,5 +18,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": ["bluetti_mqtt==0.15.0"], "requirements": ["bluetti_mqtt==0.15.0"],
"version": "0.0.1" "version": "0.0.8"
} }
+78 -172
View File
@@ -1,16 +1,11 @@
"""Bluetti BT sensors.""" """Bluetti BT sensors."""
from __future__ import annotations from __future__ import annotations
from enum import Enum
import asyncio
from datetime import timedelta
import logging import logging
from typing import cast from decimal import Decimal
import async_timeout
from bleak import BleakClient, BleakError
from homeassistant.components import bluetooth
from homeassistant.components.sensor import SensorEntity, CONF_STATE_CLASS from homeassistant.components.sensor import SensorEntity, CONF_STATE_CLASS
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@@ -25,23 +20,18 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator,
) )
from bluetti_mqtt.bluetooth.client import BluetoothClient from bluetti_mqtt.bluetooth import build_device
from bluetti_mqtt.bluetooth import ( from bluetti_mqtt.mqtt_client import (
BadConnectionError, NORMAL_DEVICE_FIELDS,
ModbusError, DC_INPUT_FIELDS,
ParseError, MqttFieldType,
build_device,
) )
from bluetti_mqtt.mqtt_client import NORMAL_DEVICE_FIELDS
from . import device_info as dev_info, get_unique_id from . import device_info as dev_info, get_unique_id
from .const import ( from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS
API_RESPONSE_BATTERY_RANGE_START, from .coordinator import PollingCoordinator, DummyDevice
API_RESPONSE_BATTERY_RANGE_END,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -56,171 +46,53 @@ async def async_setup_entry(
if address is None: if address is None:
_LOGGER.error("Device has no address") _LOGGER.error("Device has no address")
# Create coordinator for polling
coordinator = PollingCoordinator(hass, address, device_name)
await coordinator.async_config_entry_first_refresh()
# Generate device info # Generate device info
_LOGGER.info("Creating sensors for device with address %s", address) _LOGGER.info("Creating sensors for device with address %s", address)
device_info = dev_info(entry) device_info = dev_info(entry)
# Add sensors according to device_info # Add sensors according to device_info
bluetti_device = build_device(address, device_name) bluetti_device = build_device(address, device_name)
bluetti_device = DummyDevice(bluetti_device)
sensors_to_add = [] sensors_to_add = []
for field_key, field_config in NORMAL_DEVICE_FIELDS.items(): all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS)
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key): if bluetti_device.has_field(field_key):
category = None category = None
if field_config.setter is True or field_key in DIAGNOSTIC_FIELDS:
if field_key in (
API_RESPONSE_BATTERY_RANGE_START,
API_RESPONSE_BATTERY_RANGE_END,
):
category = EntityCategory.DIAGNOSTIC category = EntityCategory.DIAGNOSTIC
if field_config.type == MqttFieldType.NUMERIC:
sensors_to_add.append( sensors_to_add.append(
BluettiSensor( BluettiSensor(
coordinator, hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info, device_info,
address, address,
field_key, field_key,
field_config.home_assistant_extra.get(CONF_NAME, ""), field_config.home_assistant_extra.get(CONF_NAME, ""),
field_config.home_assistant_extra.get(CONF_UNIT_OF_MEASUREMENT, ""), field_config.home_assistant_extra.get(CONF_UNIT_OF_MEASUREMENT),
field_config.home_assistant_extra.get(CONF_DEVICE_CLASS, ""), field_config.home_assistant_extra.get(CONF_DEVICE_CLASS),
field_config.home_assistant_extra.get(CONF_STATE_CLASS, ""), field_config.home_assistant_extra.get(CONF_STATE_CLASS),
category, category=category,
)
)
elif field_config.type == MqttFieldType.ENUM:
sensors_to_add.append(
BluettiSensor(
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info,
address,
field_key,
field_config.home_assistant_extra.get(CONF_NAME, ""),
options=field_config.home_assistant_extra.get(CONF_OPTIONS),
category=category,
) )
) )
async_add_entities(sensors_to_add) async_add_entities(sensors_to_add)
class PollingCoordinator(DataUpdateCoordinator):
"""Polling coordinator."""
def __init__(self, hass: HomeAssistant, address, device_name: str):
"""Initialize coordinator."""
super().__init__(
hass,
_LOGGER,
name="Bluetti polling coordinator",
update_interval=timedelta(seconds=20),
)
self._address = address
self.notify_future = None
self.current_command = None
self.notify_response = bytearray()
self.bluetti_device = build_device(address, device_name)
async def _async_update_data(self):
"""Fetch data from API endpoint.
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
self.logger.debug("Polling data")
device = bluetooth.async_ble_device_from_address(self.hass, self._address)
if device is None:
self.logger.error("Device not available")
return None
if self.bluetti_device is None:
self.logger.error("Device type not found")
return None
# Polling
client = BleakClient(device)
parsed_data: dict = {}
try:
async with async_timeout.timeout(15):
await client.connect()
await client.start_notify(
BluetoothClient.NOTIFY_UUID, self._notification_handler
)
for command in self.bluetti_device.polling_commands:
try:
# Prepare to make request
self.current_command = command
self.notify_future = self.hass.loop.create_future()
self.notify_response = bytearray()
# Make request
self.logger.debug("Requesting %s", command)
await client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
# Wait for response
res = await asyncio.wait_for(
self.notify_future, timeout=BluetoothClient.RESPONSE_TIMEOUT
)
# Process data
self.logger.debug("Got %s bytes", len(res))
response = cast(bytes, res)
body = command.parse_response(response)
parsed = self.bluetti_device.parse(
command.starting_address, body
)
self.logger.warning("Parsed data: %s", parsed)
parsed_data.update(parsed)
except TimeoutError:
self.logger.error("Polling timed out")
except ParseError:
self.logger.debug("Got a parse exception...")
except ModbusError as err:
self.logger.debug(
"Got an invalid request error for %s: %s",
command,
err,
)
except (BadConnectionError, BleakError) as err:
self.logger.debug("Needed to disconnect due to error: %s", err)
except TimeoutError:
self.logger.error("Polling timed out")
return None
except BleakError as err:
self.logger.error("Bleak error: %s", err)
return None
finally:
await client.disconnect()
# Pass data back to sensors
return parsed_data
def _notification_handler(self, _sender: int, data: bytearray):
"""Handle bt data."""
# Ignore notifications we don't expect
if not self.notify_future or self.notify_future.done():
return
# If something went wrong, we might get weird data.
if data == b"AT+NAME?\r" or data == b"AT+ADV?\r":
err = BadConnectionError("Got AT+ notification")
self.notify_future.set_exception(err)
return
# Save data
self.notify_response.extend(data)
if len(self.notify_response) == self.current_command.response_size():
if self.current_command.is_valid_response(self.notify_response):
self.notify_future.set_result(self.notify_response)
else:
self.notify_future.set_exception(ParseError("Failed checksum"))
elif self.current_command.is_exception_response(self.notify_response):
# We got a MODBUS command exception
msg = f"MODBUS Exception {self.current_command}: {self.notify_response[2]}"
self.notify_future.set_exception(ModbusError(msg))
class BluettiSensor(CoordinatorEntity, SensorEntity): class BluettiSensor(CoordinatorEntity, SensorEntity):
"""Bluetti universal sensor.""" """Bluetti universal sensor."""
@@ -231,10 +103,11 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
address, address,
response_key: str, response_key: str,
name: str, name: str,
unit_of_measurement: str, unit_of_measurement: str | None = None,
device_class: str, device_class: str | None = None,
state_class: str, state_class: str | None = None,
category: EntityCategory | None = None, category: EntityCategory | None = None,
options: list[str] | None = None,
): ):
"""Init battery entity.""" """Init battery entity."""
super().__init__(coordinator) super().__init__(coordinator)
@@ -245,24 +118,57 @@ class BluettiSensor(CoordinatorEntity, SensorEntity):
self._attr_device_info = device_info self._attr_device_info = device_info
self._attr_name = e_name self._attr_name = e_name
self._attr_available = False
self._attr_unique_id = get_unique_id(e_name) self._attr_unique_id = get_unique_id(e_name)
self._attr_native_unit_of_measurement = unit_of_measurement self._attr_native_unit_of_measurement = unit_of_measurement
self._attr_device_class = device_class self._attr_device_class = device_class
self._attr_state_class = state_class self._attr_state_class = state_class
self._attr_entity_category = category self._attr_entity_category = category
self._options = options
@property
def available(self) -> bool:
if self._address is None:
return False
return bluetooth.async_address_present(self.hass, self._address)
@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."""
_LOGGER.debug("Updating state of %s", self._attr_unique_id) _LOGGER.debug("Updating state of %s", self._attr_unique_id)
if not isinstance(self.coordinator.data, dict): if not isinstance(self.coordinator.data, dict):
_LOGGER.error("Invalid data from coordinator") _LOGGER.error(
"Invalid data from coordinator (sensor.%s)", self._attr_unique_id
)
self._attr_available = False
return return
self._attr_native_value = self.coordinator.data[self._response_key]
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
return
if (
not isinstance(response_data, int)
and not isinstance(response_data, float)
and not isinstance(response_data, complex)
and not isinstance(response_data, Decimal)
and not isinstance(response_data, Enum)
):
_LOGGER.warning(
"Invalid response data type from coordinator (sensor.%s): %s has type %s",
self._attr_unique_id,
response_data,
type(response_data),
)
self._attr_available = False
return
self._attr_available = True
# Different for enum and numeric
if (
self._options is not None
and isinstance(response_data, Enum)
and response_data.value < len(self._options)
):
# Enum
self._attr_native_value = self._options[response_data.value]
else:
# Numeric
self._attr_native_value = response_data
self.async_write_ha_state() self.async_write_ha_state()
+188
View File
@@ -0,0 +1,188 @@
"""Bluetti BT switches."""
from __future__ import annotations
import asyncio
import logging
import async_timeout
from bleak import BleakClient, BleakError
from homeassistant.components import bluetooth
from homeassistant.components.switch import SwitchEntity, SwitchDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import (
CONF_ADDRESS,
CONF_NAME,
EntityCategory,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
)
from bluetti_mqtt.bluetooth import build_device
from bluetti_mqtt.bluetooth.client import BluetoothClient
from bluetti_mqtt.core.devices.bluetti_device import BluettiDevice
from bluetti_mqtt.mqtt_client import (
NORMAL_DEVICE_FIELDS,
DC_INPUT_FIELDS,
MqttFieldType,
)
from . import device_info as dev_info, get_unique_id
from .const import CONTROL_FIELDS, DATA_COORDINATOR, DATA_POLLING_RUNNING, DOMAIN, ADDITIONAL_DEVICE_FIELDS
from .coordinator import PollingCoordinator, DummyDevice
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Setup switch entities."""
device_name = entry.data.get(CONF_NAME)
address = entry.data.get(CONF_ADDRESS)
if address is None:
_LOGGER.error("Device has no address")
# Generate device info
_LOGGER.info("Creating sensors for device with address %s", address)
device_info = dev_info(entry)
# Add sensors according to device_info
bluetti_device = build_device(address, device_name)
bluetti_device = DummyDevice(bluetti_device)
sensors_to_add = []
all_fields = NORMAL_DEVICE_FIELDS
all_fields.update(DC_INPUT_FIELDS)
all_fields.update(ADDITIONAL_DEVICE_FIELDS)
for field_key, field_config in all_fields.items():
if bluetti_device.has_field(field_key):
if field_config.type == MqttFieldType.BOOL:
if field_config.setter is True and field_key in CONTROL_FIELDS:
sensors_to_add.append(
BluettiSwitch(
bluetti_device,
hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR],
device_info,
address,
field_key,
field_config.home_assistant_extra.get(CONF_NAME, ""),
entry.entry_id
)
)
async_add_entities(sensors_to_add)
class BluettiSwitch(CoordinatorEntity, SwitchEntity):
"""Bluetti universal switch."""
def __init__(
self,
bluetti_device: BluettiDevice,
coordinator: PollingCoordinator,
device_info: DeviceInfo,
address,
response_key: str,
name: str,
entry_id: str,
category: EntityCategory | None = None,
):
"""Init entity."""
super().__init__(coordinator)
self._bluetti_device = bluetti_device
e_name = f"{device_info.get('name')} {name}"
self._address = address
self._response_key = response_key
self._entry_id = entry_id
self._attr_device_info = device_info
self._attr_name = e_name
self._attr_available = False
self._attr_unique_id = get_unique_id(e_name)
self._attr_entity_category = category
self._attr_device_class = SwitchDeviceClass.OUTLET
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
_LOGGER.debug("Updating state of %s", self._attr_unique_id)
if not isinstance(self.coordinator.data, dict):
_LOGGER.error(
"Invalid data from coordinator (sensor.%s)", self._attr_unique_id
)
self._attr_available = False
return
response_data = self.coordinator.data.get(self._response_key)
if response_data is None:
self._attr_available = False
return
if not isinstance(response_data, bool):
_LOGGER.warning(
"Invalid response data type from coordinator (sensor.%s): %s",
self._attr_unique_id,
response_data,
)
self._attr_available = False
return
self._attr_available = True
self._attr_is_on = self.coordinator.data[self._response_key] is True
self.async_write_ha_state()
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
_LOGGER.debug("Turn on %s on %s", self._response_key, self._address)
await self.write_to_device(True)
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
_LOGGER.debug("Turn off %s on %s", self._response_key, self._address)
await self.write_to_device(False)
async def write_to_device(self, state: bool):
"""Write to device."""
device = bluetooth.async_ble_device_from_address(self.hass, self._address)
if device is None:
_LOGGER.error("Device %s not available", self._address)
return
command = self._bluetti_device.build_setter_command(self._response_key, state)
# Wait until polling is done
while self.hass.data[DOMAIN][self._entry_id][DATA_POLLING_RUNNING] is True:
_LOGGER.debug("Waiting for polling to finish...")
asyncio.sleep(1)
client = BleakClient(device)
try:
async with async_timeout.timeout(15):
await client.connect()
# Send command
await client.write_gatt_char(
BluetoothClient.WRITE_UUID, bytes(command)
)
# Wait until device has changed value, otherwise reading register might reset it
await asyncio.sleep(5)
except TimeoutError:
_LOGGER.error("Timed out for device %s", self._address)
return None
except BleakError as err:
_LOGGER.error("Bleak error: %s", err)
return None
finally:
await client.disconnect()
await self.coordinator.async_request_refresh()
@@ -0,0 +1,11 @@
{
"options": {
"step": {
"init": {
"data": {
"use_controls": "Steuerung aktivieren (auf eigenes Risiko)"
}
}
}
}
}
@@ -0,0 +1,11 @@
{
"options": {
"step": {
"init": {
"data": {
"use_controls": "Use controls (use at own risk)"
}
}
}
}
}