Skip to content

Commit

Permalink
fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
jafar-atili committed Sep 28, 2022
1 parent f36d217 commit b60d0bf
Show file tree
Hide file tree
Showing 2 changed files with 218 additions and 45 deletions.
215 changes: 215 additions & 0 deletions homeassistant/components/switchbee/climate.py
@@ -0,0 +1,215 @@
"""Support for SwitchBee thermostat button."""
from __future__ import annotations

from typing import Any, cast

from switchbee import SWITCHBEE_BRAND
from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError
from switchbee.const import (
ApiAttribute,
ThermostatFanSpeed,
ThermostatMode,
ThermostatTemperatureUnit,
)
from switchbee.device import ApiStateCommand, SwitchBeeThermostat

from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import SwitchBeeCoordinator
from .const import DOMAIN
from .entity import SwitchBeeDeviceEntity

FAN_SB_TO_HASS = {
ThermostatFanSpeed.AUTO: FAN_AUTO,
ThermostatFanSpeed.LOW: FAN_LOW,
ThermostatFanSpeed.MEDIUM: FAN_MEDIUM,
ThermostatFanSpeed.HIGH: FAN_HIGH,
}

FAN_HASS_TO_SB = {
FAN_AUTO: ThermostatFanSpeed.AUTO,
FAN_LOW: ThermostatFanSpeed.LOW,
FAN_MEDIUM: ThermostatFanSpeed.MEDIUM,
FAN_HIGH: ThermostatFanSpeed.HIGH,
}

HVAC_MODE_SB_TO_HASS = {
ThermostatMode.COOL: HVACMode.COOL,
ThermostatMode.HEAT: HVACMode.HEAT,
ThermostatMode.FAN: HVACMode.FAN_ONLY,
}

HVAC_MODE_HASS_TO_SB = {
HVACMode.COOL: ThermostatMode.COOL,
HVACMode.HEAT: ThermostatMode.HEAT,
HVACMode.FAN_ONLY: ThermostatMode.FAN,
}

HVAC_ACTION_SB_TO_HASS = {
ThermostatMode.COOL: HVACAction.COOLING,
ThermostatMode.HEAT: HVACAction.HEATING,
ThermostatMode.FAN: HVACAction.FAN,
}

HVAC_UNIT_SB_TO_HASS = {
ThermostatTemperatureUnit.CELSIUS: TEMP_CELSIUS,
ThermostatTemperatureUnit.FAHRENHEIT: TEMP_FAHRENHEIT,
}

SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW]


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Switchbee thermostat."""
coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SwitchBeeClimate(switchbee_device, coordinator)
for switchbee_device in coordinator.data.values()
if isinstance(switchbee_device, SwitchBeeThermostat)
)


class SwitchBeeClimate(SwitchBeeDeviceEntity[SwitchBeeThermostat], ClimateEntity):
"""Representation of an Switchbee button."""

def __init__(
self,
device: SwitchBeeThermostat,
coordinator: SwitchBeeCoordinator,
) -> None:
"""Initialize the Switchbee switch."""
super().__init__(device, coordinator)
self._attr_name = f"{device.zone} {device.name}"
self._device_id = device.id
self._attr_unique_id = f"{coordinator.mac_formatted}-{device.id}"
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)

# set HVAC capabilities
self._attr_target_temperature_step = 1
self._attr_max_temp = device.max_temperature
self._attr_min_temp = device.min_temperature
self._attr_fan_modes = SUPPORTED_FAN_MODES
self._attr_temperature_unit = HVAC_UNIT_SB_TO_HASS[device.unit]
self._attr_hvac_modes = [HVAC_MODE_SB_TO_HASS[mode] for mode in device.modes]
self._attr_hvac_modes.append(HVACMode.OFF)
self._device: SwitchBeeThermostat = device
self._attr_device_info = DeviceInfo(
name=f"SwitchBee_{str(self._device.id)}",
identifiers={
(
DOMAIN,
f"{str(self._device.id)}-{coordinator.mac_formatted}",
)
},
manufacturer=SWITCHBEE_BRAND,
model=coordinator.api.module_display(device.unit_id),
suggested_area=device.zone,
via_device=(
DOMAIN,
f"{coordinator.api.name} ({coordinator.api.mac})",
),
)
self._update_device_attrs(device)

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._update_device_attrs(
cast(SwitchBeeThermostat, self.coordinator.data[self._device_id])
)
super()._handle_coordinator_update()

def _update_device_attrs(self, device: SwitchBeeThermostat) -> None:
if device.state == ApiStateCommand.OFF:
self._attr_hvac_mode: HVACMode = HVACMode.OFF
else:
self._attr_hvac_mode = HVAC_MODE_SB_TO_HASS[device.mode]
self._attr_fan_mode: str = FAN_SB_TO_HASS[device.fan]
self._attr_current_temperature: float = device.temperature
self._attr_target_temperature: int = device.target_temperature

def _create_switchbee_request(
self,
power: str = "",
mode: str = "",
fan: str = "",
target_temperature: int = 0,
) -> dict[str, Any]:
"""Create SwitchBee thermostat state object."""

new_power = power
if not new_power:
if self._attr_hvac_mode == HVACMode.OFF:
new_power = ApiStateCommand.OFF
else:
new_power = ApiStateCommand.ON

data = {
ApiAttribute.POWER: new_power,
ApiAttribute.MODE: mode
if mode
else HVAC_MODE_HASS_TO_SB[self._attr_hvac_mode],
ApiAttribute.FAN: fan if fan else FAN_HASS_TO_SB[self._attr_fan_mode],
ApiAttribute.CONFIGURED_TEMPERATURE: target_temperature
if target_temperature
else self._attr_target_temperature,
}

return data

async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode."""

if hvac_mode == HVACMode.OFF:
state = self._create_switchbee_request(power=ApiStateCommand.OFF)
else:
state = self._create_switchbee_request(
power=ApiStateCommand.ON, mode=HVAC_MODE_HASS_TO_SB[hvac_mode]
)

await self.operate(state)

async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
await self.operate(
self._create_switchbee_request(target_temperature=kwargs[ATTR_TEMPERATURE])
)

async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set AC fan mode."""
await self.operate(self._create_switchbee_request(fan=FAN_HASS_TO_SB[fan_mode]))

async def operate(self, state: dict[str, str | int]) -> None:
"""Send request to central unit."""
try:
await self.coordinator.api.set_state(self._device_id, state) # type: ignore[arg-type]
except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp:
raise HomeAssistantError(
f"Failed to set {self._attr_name} state {state}, error: {str(exp)}"
) from exp
else:
await self.coordinator.async_refresh()
48 changes: 3 additions & 45 deletions homeassistant/components/switchbee/cover.py
Expand Up @@ -5,11 +5,7 @@
import logging
from typing import Any, TypeVar, Union, cast

from switchbee.api import (
SwitchBeeDeviceOfflineError,
SwitchBeeError,
SwitchBeeTokenError,
)
from switchbee.api import SwitchBeeError, SwitchBeeTokenError
from switchbee.const import SomfyCommand
from switchbee.device import DeviceType, SwitchBeeShutter, SwitchBeeSomfy

Expand Down Expand Up @@ -84,11 +80,6 @@ def __init__(
| CoverEntityFeature.STOP
)

@property
def available(self) -> bool:
"""Return True if entity is available."""
return self._is_online and super().available

@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
Expand All @@ -98,26 +89,6 @@ def _handle_coordinator_update(self) -> None:
def _update_from_coordinator(self) -> None:
"""Update the entity attributes from the coordinator data."""

async def async_refresh_state() -> None:
"""Refresh the device state in the Central Unit.
This function addresses issue of a device that came online back but still report
unavailable state (-1).
Such device (offline device) will keep reporting unavailable state (-1)
until it has been actuated by the user (state changed to on/off).
With this code we keep trying setting dummy state for the device
in order for it to start reporting its real state back (assuming it came back online)
"""

try:
await self.coordinator.api.set_state(self._device.id, "dummy")
except SwitchBeeDeviceOfflineError:
return
except SwitchBeeError:
return

# Somfy devices within SwitchBee does not have any state, they can only be controlled
if self._device.type == DeviceType.Somfy:
return
Expand All @@ -127,24 +98,11 @@ async def async_refresh_state() -> None:
)

if coordinator_device.position == -1:
self.hass.async_create_task(async_refresh_state())
# if the device was online (now offline), log message and mark it as Unavailable
if self._is_online:
_LOGGER.warning(
"%s shutter is not responding, check the status in the SwitchBee mobile app",
self.name,
)
self._is_online = False

self._check_if_became_offline()
return

# check if the device was offline (now online) and bring it back
if not self._is_online:
_LOGGER.info(
"%s shutter is now responding",
self.name,
)
self._is_online = True
self._check_if_became_online()

self._attr_current_cover_position = coordinator_device.position

Expand Down

0 comments on commit b60d0bf

Please sign in to comment.