forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add BSBLan Climate integration (home-assistant#32375)
* Initial commit for BSBLan Climate component The most basic climate functions work. * Delete manifest 2.json wrongly added to commit * fix incorrect name current_hvac_mode * update coverage to exclude bsblan * sorted and add configflow * removed unused code, etc * fix hvac, preset mix up now it sets hvac mode to none and preset to eco * fix naming * removed commented code and cleaned code that isn't needed * Add test for the configflow * Update requirements fixing some issues in bsblan Lib * Update coverage file to include configflow bsblan * Fix hvac preset is not in hvac mode rewrote how to handle presets. * Add passkey option My device had a passkey so I needed to push this functionality to do testing * Update constants include passkey and added some more for device indentification * add passkey for configflow * Fix use discovery_info instead of user_input also added passkey * Fix name * Fix for discovery_info[CONF_PORT] is None * Fix get value CONF_PORT * Fix move translation to new location * Fix get the right info * Fix remove zeroconf and fix the code * Add init for mockConfigEntry * Fix removed zeroconfig and fix code * Fix changed ClimateDevice to ClimatEntity * Fix log error message * Removed debug code * Change name of device. * Remove check This is done in the configflow * Remove period from logging message * Update homeassistant/components/bsblan/strings.json Co-authored-by: Martin Hjelmare <[email protected]> * Add passkey Co-authored-by: Martin Hjelmare <[email protected]>
- Loading branch information
1 parent
e2b622f
commit cf30895
Showing
15 changed files
with
635 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
"""The BSB-Lan integration.""" | ||
from datetime import timedelta | ||
import logging | ||
|
||
from bsblan import BSBLan, BSBLanConnectionError | ||
|
||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_HOST, CONF_PORT | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.typing import ConfigType | ||
|
||
from .const import CONF_PASSKEY, DATA_BSBLAN_CLIENT, DOMAIN | ||
|
||
SCAN_INTERVAL = timedelta(seconds=30) | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: | ||
"""Set up the BSB-Lan component.""" | ||
return True | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up BSB-Lan from a config entry.""" | ||
|
||
session = async_get_clientsession(hass) | ||
bsblan = BSBLan( | ||
entry.data[CONF_HOST], | ||
passkey=entry.data[CONF_PASSKEY], | ||
loop=hass.loop, | ||
port=entry.data[CONF_PORT], | ||
session=session, | ||
) | ||
|
||
try: | ||
await bsblan.info() | ||
except BSBLanConnectionError as exception: | ||
raise ConfigEntryNotReady from exception | ||
|
||
hass.data.setdefault(DOMAIN, {}) | ||
hass.data[DOMAIN][entry.entry_id] = {DATA_BSBLAN_CLIENT: bsblan} | ||
|
||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Unload BSBLan config entry.""" | ||
|
||
await hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN) | ||
|
||
# Cleanup | ||
del hass.data[DOMAIN][entry.entry_id] | ||
if not hass.data[DOMAIN]: | ||
del hass.data[DOMAIN] | ||
|
||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
"""BSBLAN platform to control a compatible Climate Device.""" | ||
from datetime import timedelta | ||
import logging | ||
from typing import Any, Callable, Dict, List, Optional | ||
|
||
from bsblan import BSBLan, BSBLanError, Info, State | ||
|
||
from homeassistant.components.climate import ClimateEntity | ||
from homeassistant.components.climate.const import ( | ||
ATTR_HVAC_MODE, | ||
ATTR_PRESET_MODE, | ||
HVAC_MODE_AUTO, | ||
HVAC_MODE_HEAT, | ||
HVAC_MODE_OFF, | ||
PRESET_ECO, | ||
PRESET_NONE, | ||
SUPPORT_PRESET_MODE, | ||
SUPPORT_TARGET_TEMPERATURE, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import ( | ||
ATTR_NAME, | ||
ATTR_TEMPERATURE, | ||
TEMP_CELSIUS, | ||
TEMP_FAHRENHEIT, | ||
) | ||
from homeassistant.helpers.entity import Entity | ||
from homeassistant.helpers.typing import HomeAssistantType | ||
|
||
from .const import ( | ||
ATTR_IDENTIFIERS, | ||
ATTR_MANUFACTURER, | ||
ATTR_MODEL, | ||
ATTR_TARGET_TEMPERATURE, | ||
DATA_BSBLAN_CLIENT, | ||
DOMAIN, | ||
) | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
PARALLEL_UPDATES = 1 | ||
SCAN_INTERVAL = timedelta(seconds=20) | ||
|
||
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | ||
|
||
HVAC_MODES = [ | ||
HVAC_MODE_AUTO, | ||
HVAC_MODE_HEAT, | ||
HVAC_MODE_OFF, | ||
] | ||
|
||
PRESET_MODES = [ | ||
PRESET_ECO, | ||
PRESET_NONE, | ||
] | ||
|
||
HA_STATE_TO_BSBLAN = { | ||
HVAC_MODE_AUTO: "1", | ||
HVAC_MODE_HEAT: "3", | ||
HVAC_MODE_OFF: "0", | ||
} | ||
|
||
BSBLAN_TO_HA_STATE = {value: key for key, value in HA_STATE_TO_BSBLAN.items()} | ||
|
||
HA_PRESET_TO_BSBLAN = { | ||
PRESET_ECO: "2", | ||
} | ||
|
||
BSBLAN_TO_HA_PRESET = { | ||
2: PRESET_ECO, | ||
} | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistantType, | ||
entry: ConfigEntry, | ||
async_add_entities: Callable[[List[Entity], bool], None], | ||
) -> None: | ||
"""Set up BSBLan device based on a config entry.""" | ||
bsblan: BSBLan = hass.data[DOMAIN][entry.entry_id][DATA_BSBLAN_CLIENT] | ||
info = await bsblan.info() | ||
async_add_entities([BSBLanClimate(entry.entry_id, bsblan, info)], True) | ||
|
||
|
||
class BSBLanClimate(ClimateEntity): | ||
"""Defines a BSBLan climate device.""" | ||
|
||
def __init__( | ||
self, entry_id: str, bsblan: BSBLan, info: Info, | ||
): | ||
"""Initialize BSBLan climate device.""" | ||
self._current_temperature: Optional[float] = None | ||
self._available = True | ||
self._current_hvac_mode: Optional[int] = None | ||
self._target_temperature: Optional[float] = None | ||
self._info: Info = info | ||
self.bsblan = bsblan | ||
self._temperature_unit = None | ||
self._hvac_mode = None | ||
self._preset_mode = None | ||
self._store_hvac_mode = None | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Return the name of the entity.""" | ||
return self._info.device_identification | ||
|
||
@property | ||
def available(self) -> bool: | ||
"""Return True if entity is available.""" | ||
return self._available | ||
|
||
@property | ||
def unique_id(self) -> str: | ||
"""Return the unique ID for this sensor.""" | ||
return self._info.device_identification | ||
|
||
@property | ||
def temperature_unit(self) -> str: | ||
"""Return the unit of measurement which this thermostat uses.""" | ||
if self._temperature_unit == "°C": | ||
return TEMP_CELSIUS | ||
return TEMP_FAHRENHEIT | ||
|
||
@property | ||
def supported_features(self) -> int: | ||
"""Flag supported features.""" | ||
return SUPPORT_FLAGS | ||
|
||
@property | ||
def current_temperature(self): | ||
"""Return the current temperature.""" | ||
return self._current_temperature | ||
|
||
@property | ||
def hvac_mode(self): | ||
"""Return the current operation mode.""" | ||
return self._current_hvac_mode | ||
|
||
@property | ||
def hvac_modes(self): | ||
"""Return the list of available operation modes.""" | ||
return HVAC_MODES | ||
|
||
@property | ||
def target_temperature(self): | ||
"""Return the temperature we try to reach.""" | ||
return self._target_temperature | ||
|
||
@property | ||
def preset_modes(self): | ||
"""List of available preset modes.""" | ||
return PRESET_MODES | ||
|
||
@property | ||
def preset_mode(self): | ||
"""Return the preset_mode.""" | ||
return self._preset_mode | ||
|
||
async def async_set_preset_mode(self, preset_mode): | ||
"""Set preset mode.""" | ||
_LOGGER.debug("Setting preset mode to: %s", preset_mode) | ||
if preset_mode == PRESET_NONE: | ||
# restore previous hvac mode | ||
self._current_hvac_mode = self._store_hvac_mode | ||
else: | ||
# Store hvac mode. | ||
self._store_hvac_mode = self._current_hvac_mode | ||
await self.async_set_data(preset_mode=preset_mode) | ||
|
||
async def async_set_hvac_mode(self, hvac_mode): | ||
"""Set HVAC mode.""" | ||
_LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) | ||
# preset should be none when hvac mode is set | ||
self._preset_mode = PRESET_NONE | ||
await self.async_set_data(hvac_mode=hvac_mode) | ||
|
||
async def async_set_temperature(self, **kwargs): | ||
"""Set new target temperatures.""" | ||
await self.async_set_data(**kwargs) | ||
|
||
async def async_set_data(self, **kwargs: Any) -> None: | ||
"""Set device settings using BSBLan.""" | ||
data = {} | ||
|
||
if ATTR_TEMPERATURE in kwargs: | ||
data[ATTR_TARGET_TEMPERATURE] = kwargs[ATTR_TEMPERATURE] | ||
_LOGGER.debug("Set temperature data = %s", data) | ||
|
||
if ATTR_HVAC_MODE in kwargs: | ||
data[ATTR_HVAC_MODE] = HA_STATE_TO_BSBLAN[kwargs[ATTR_HVAC_MODE]] | ||
_LOGGER.debug("Set hvac mode data = %s", data) | ||
|
||
if ATTR_PRESET_MODE in kwargs: | ||
# for now we set the preset as hvac_mode as the api expect this | ||
data[ATTR_HVAC_MODE] = HA_PRESET_TO_BSBLAN[kwargs[ATTR_PRESET_MODE]] | ||
|
||
try: | ||
await self.bsblan.thermostat(**data) | ||
except BSBLanError: | ||
_LOGGER.error("An error occurred while updating the BSBLan device") | ||
self._available = False | ||
|
||
async def async_update(self) -> None: | ||
"""Update BSBlan entity.""" | ||
try: | ||
state: State = await self.bsblan.state() | ||
except BSBLanError: | ||
if self._available: | ||
_LOGGER.error("An error occurred while updating the BSBLan device") | ||
self._available = False | ||
return | ||
|
||
self._available = True | ||
|
||
self._current_temperature = float(state.current_temperature) | ||
self._target_temperature = float(state.target_temperature) | ||
|
||
# check if preset is active else get hvac mode | ||
_LOGGER.debug("state hvac/preset mode: %s", state.current_hvac_mode) | ||
if state.current_hvac_mode == "2": | ||
self._preset_mode = PRESET_ECO | ||
else: | ||
self._current_hvac_mode = BSBLAN_TO_HA_STATE[state.current_hvac_mode] | ||
self._preset_mode = PRESET_NONE | ||
|
||
self._temperature_unit = state.temperature_unit | ||
|
||
@property | ||
def device_info(self) -> Dict[str, Any]: | ||
"""Return device information about this BSBLan device.""" | ||
return { | ||
ATTR_IDENTIFIERS: {(DOMAIN, self._info.device_identification)}, | ||
ATTR_NAME: "BSBLan Device", | ||
ATTR_MANUFACTURER: "BSBLan", | ||
ATTR_MODEL: self._info.controller_variant, | ||
} |
Oops, something went wrong.