Skip to content

Commit

Permalink
Add BSBLan Climate integration (home-assistant#32375)
Browse files Browse the repository at this point in the history
* 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
liudger and MartinHjelmare authored May 10, 2020
1 parent e2b622f commit cf30895
Show file tree
Hide file tree
Showing 15 changed files with 635 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ omit =
homeassistant/components/brottsplatskartan/sensor.py
homeassistant/components/browser/*
homeassistant/components/brunt/cover.py
homeassistant/components/bsblan/__init__.py
homeassistant/components/bsblan/climate.py
homeassistant/components/bsblan/const.py
homeassistant/components/bt_home_hub_5/device_tracker.py
homeassistant/components/bt_smarthub/device_tracker.py
homeassistant/components/buienradar/sensor.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ homeassistant/components/braviatv/* @robbiet480 @bieniu
homeassistant/components/broadlink/* @danielhiversen @felipediel
homeassistant/components/brother/* @bieniu
homeassistant/components/brunt/* @eavanvalkenburg
homeassistant/components/bsblan/* @liudger
homeassistant/components/bt_smarthub/* @jxwolstenholme
homeassistant/components/buienradar/* @mjj4791 @ties
homeassistant/components/cast/* @emontnemery
Expand Down
64 changes: 64 additions & 0 deletions homeassistant/components/bsblan/__init__.py
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
237 changes: 237 additions & 0 deletions homeassistant/components/bsblan/climate.py
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 == "&deg;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,
}
Loading

0 comments on commit cf30895

Please sign in to comment.