diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 4deeadc..6c5a740 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN, MANUFACTURER, CONF_USE_CONTROLS +from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER, CONF_USE_CONTROLS, DATA_POLLING_RUNNING from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -32,13 +32,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not bluetooth.async_address_present(hass, address): 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() - - # Create data structure - hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN].setdefault(entry.entry_id, coordinator) + hass.data[DOMAIN][entry.entry_id].setdefault(DATA_COORDINATOR, coordinator) platforms: list = PLATFORMS if use_controls is True: diff --git a/custom_components/bluetti_bt/binary_sensor.py b/custom_components/bluetti_bt/binary_sensor.py index f2e53d0..67cfe38 100644 --- a/custom_components/bluetti_bt/binary_sensor.py +++ b/custom_components/bluetti_bt/binary_sensor.py @@ -26,7 +26,7 @@ from bluetti_mqtt.mqtt_client import ( ) from . import device_info as dev_info, get_unique_id -from .const import DOMAIN, ADDITIONAL_DEVICE_FIELDS +from .const import DATA_COORDINATOR, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice _LOGGER = logging.getLogger(__name__) @@ -63,7 +63,7 @@ async def async_setup_entry( sensors_to_add.append( BluettiBinarySensor( - hass.data[DOMAIN][entry.entry_id], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], device_info, address, field_key, diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 2d8d3ad..2fdacb7 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -8,6 +8,9 @@ MANUFACTURER = "Bluetti" CONF_OPTIONS = "options" CONF_USE_CONTROLS = "use_controls" +DATA_COORDINATOR = "coordinator" +DATA_POLLING_RUNNING = "polling_running" + CONTROL_FIELDS = [ "ac_output_on", "dc_output_on", diff --git a/custom_components/bluetti_bt/coordinator.py b/custom_components/bluetti_bt/coordinator.py index 85102bb..0a62d41 100644 --- a/custom_components/bluetti_bt/coordinator.py +++ b/custom_components/bluetti_bt/coordinator.py @@ -26,6 +26,8 @@ from bluetti_mqtt.bluetooth import ( 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__) @@ -150,6 +152,8 @@ class PollingCoordinator(DataUpdateCoordinator): 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) @@ -203,7 +207,7 @@ class PollingCoordinator(DataUpdateCoordinator): parsed_data.update(parsed) except TimeoutError: - self.logger.error( + self.logger.warning( "Polling timed out (address: %s)", self._address ) except ParseError: @@ -216,17 +220,19 @@ class PollingCoordinator(DataUpdateCoordinator): ) except (BadConnectionError, BleakError) as err: self.logger.warning( - "Needed to disconnect due to error: %s", err + "Needed to disconnect due to error: %s (This can also be the case if you used device controls)", err ) except TimeoutError: - self.logger.error("Polling timed out for device %s", self._address) + self.logger.warning("Polling timed out for device %s", self._address) return None except BleakError as err: - self.logger.error("Bleak error: %s", 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 diff --git a/custom_components/bluetti_bt/sensor.py b/custom_components/bluetti_bt/sensor.py index fa32848..902145f 100644 --- a/custom_components/bluetti_bt/sensor.py +++ b/custom_components/bluetti_bt/sensor.py @@ -29,7 +29,7 @@ from bluetti_mqtt.mqtt_client import ( ) from . import device_info as dev_info, get_unique_id -from .const import DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS +from .const import DATA_COORDINATOR, DOMAIN, CONF_OPTIONS, DIAGNOSTIC_FIELDS, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice _LOGGER = logging.getLogger(__name__) @@ -65,7 +65,7 @@ async def async_setup_entry( if field_config.type == MqttFieldType.NUMERIC: sensors_to_add.append( BluettiSensor( - hass.data[DOMAIN][entry.entry_id], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], device_info, address, field_key, @@ -79,7 +79,7 @@ async def async_setup_entry( elif field_config.type == MqttFieldType.ENUM: sensors_to_add.append( BluettiSensor( - hass.data[DOMAIN][entry.entry_id], + hass.data[DOMAIN][entry.entry_id][DATA_COORDINATOR], device_info, address, field_key, diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py index e215650..0d6a83e 100644 --- a/custom_components/bluetti_bt/switch.py +++ b/custom_components/bluetti_bt/switch.py @@ -2,8 +2,13 @@ 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 @@ -19,6 +24,8 @@ from homeassistant.helpers.update_coordinator import ( ) 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, @@ -26,7 +33,7 @@ from bluetti_mqtt.mqtt_client import ( ) from . import device_info as dev_info, get_unique_id -from .const import CONTROL_FIELDS, DOMAIN, ADDITIONAL_DEVICE_FIELDS +from .const import CONTROL_FIELDS, DATA_COORDINATOR, DATA_POLLING_RUNNING, DOMAIN, ADDITIONAL_DEVICE_FIELDS from .coordinator import PollingCoordinator, DummyDevice _LOGGER = logging.getLogger(__name__) @@ -60,11 +67,13 @@ async def async_setup_entry( if field_config.setter is True and field_key in CONTROL_FIELDS: sensors_to_add.append( BluettiSwitch( - hass.data[DOMAIN][entry.entry_id], + 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 ) ) @@ -76,19 +85,23 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): 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 @@ -128,8 +141,48 @@ class BluettiSwitch(CoordinatorEntity, SwitchEntity): async def async_turn_on(self, **kwargs): """Turn the entity on.""" - _LOGGER.warning("Turn 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.warning("Turn 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()