diff --git a/custom_components/bluetti_bt/__init__.py b/custom_components/bluetti_bt/__init__.py index 34f8497..4deeadc 100644 --- a/custom_components/bluetti_bt/__init__.py +++ b/custom_components/bluetti_bt/__init__.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +import logging from homeassistant.components import bluetooth from homeassistant.config_entries import ConfigEntry @@ -11,10 +12,11 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN, MANUFACTURER, CONF_USE_CONTROLS from .coordinator import PollingCoordinator PLATFORMS: [Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -22,6 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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: return False @@ -37,8 +40,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault(entry.entry_id, 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 - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await hass.config_entries.async_forward_entry_setups(entry, platforms) return True diff --git a/custom_components/bluetti_bt/const.py b/custom_components/bluetti_bt/const.py index 9848d21..2d8d3ad 100644 --- a/custom_components/bluetti_bt/const.py +++ b/custom_components/bluetti_bt/const.py @@ -8,6 +8,11 @@ MANUFACTURER = "Bluetti" CONF_OPTIONS = "options" CONF_USE_CONTROLS = "use_controls" +CONTROL_FIELDS = [ + "ac_output_on", + "dc_output_on", +] + DIAGNOSTIC_FIELDS = [ "max_ac_input_power", "max_ac_input_current", diff --git a/custom_components/bluetti_bt/switch.py b/custom_components/bluetti_bt/switch.py new file mode 100644 index 0000000..e215650 --- /dev/null +++ b/custom_components/bluetti_bt/switch.py @@ -0,0 +1,135 @@ +"""Bluetti BT switches.""" + +from __future__ import annotations + +import logging + +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.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, 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( + hass.data[DOMAIN][entry.entry_id], + device_info, + address, + field_key, + field_config.home_assistant_extra.get(CONF_NAME, ""), + ) + ) + + async_add_entities(sensors_to_add) + + +class BluettiSwitch(CoordinatorEntity, SwitchEntity): + """Bluetti universal switch.""" + + def __init__( + self, + coordinator: PollingCoordinator, + device_info: DeviceInfo, + address, + response_key: str, + name: str, + category: EntityCategory | None = None, + ): + """Init 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 + 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.warning("Turn on") + + async def async_turn_off(self, **kwargs): + """Turn the entity off.""" + _LOGGER.warning("Turn off")