Switch velbus from python-velbus to velbusaio (#54032)

* initial commit

* use new release

* Update for sensors

* big update

* pylint fixes, bump dependancy to 2021.8.2

* New version to try to fix the tests

* Fix a lot of errors, bump version

* more work

* Bump version

* Adde dimmer support

* Make sure the counters are useable in the energy dashboard

* bump version

* Fix testcases

* Update after review

* Bump version to be able to have some decent exception catches, add the temperature device class

* Readd the import of the platform from config file, but add a deprecation warning

* More comments updated

* Fix lefover index

* Fix unique id to be backwards compatible

* Fix small bug in covers

* Fix testcases

* Changes for theenery dashboard

* Fixed services

* Fix memo text

* Make the interface for a service the port string instead of the device selector

* Fix set_memo_text

* added an async scan task, more comments

* Accidently disabled some paltforms

* More comments, bump version

* Bump version, add extra attributes, enable mypy

* Removed new features

* More comments

* Bump version

* Update homeassistant/components/velbus/__init__.py

Co-authored-by: brefra <frank_van_breugel@hotmail.com>

* Readd the import step

Co-authored-by: brefra <frank_van_breugel@hotmail.com>
This commit is contained in:
Maikel Punie 2021-09-13 08:22:46 +02:00 committed by GitHub
parent 1f997fcd58
commit 7472fb2049
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 252 additions and 220 deletions

View file

@ -1,22 +1,28 @@
"""Support for Velbus devices.""" """Support for Velbus devices."""
from __future__ import annotations
import logging import logging
import velbus from velbusaio.controller import Velbus
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import CONF_MEMO_TEXT, DOMAIN, SERVICE_SET_MEMO_TEXT from .const import (
CONF_INTERFACE,
CONF_MEMO_TEXT,
DOMAIN,
SERVICE_SCAN,
SERVICE_SET_MEMO_TEXT,
SERVICE_SYNC,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VELBUS_MESSAGE = "velbus.message"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA {DOMAIN: vol.Schema({vol.Required(CONF_PORT): cv.string})}, extra=vol.ALLOW_EXTRA
) )
@ -29,6 +35,9 @@ async def async_setup(hass, config):
# Import from the configuration file if needed # Import from the configuration file if needed
if DOMAIN not in config: if DOMAIN not in config:
return True return True
_LOGGER.warning("Loading VELBUS via configuration.yaml is deprecated")
port = config[DOMAIN].get(CONF_PORT) port = config[DOMAIN].get(CONF_PORT)
data = {} data = {}
@ -39,57 +48,67 @@ async def async_setup(hass, config):
DOMAIN, context={"source": SOURCE_IMPORT}, data=data DOMAIN, context={"source": SOURCE_IMPORT}, data=data
) )
) )
return True return True
async def velbus_connect_task(
controller: Velbus, hass: HomeAssistant, entry_id: str
) -> None:
"""Task to offload the long running connect."""
await controller.connect()
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Establish connection with velbus.""" """Establish connection with velbus."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
def callback(): controller = Velbus(entry.data[CONF_PORT])
modules = controller.get_modules() hass.data[DOMAIN][entry.entry_id] = {}
discovery_info = {"cntrl": controller} hass.data[DOMAIN][entry.entry_id]["cntrl"] = controller
for platform in PLATFORMS: hass.data[DOMAIN][entry.entry_id]["tsk"] = hass.async_create_task(
discovery_info[platform] = [] velbus_connect_task(controller, hass, entry.entry_id)
for module in modules: )
for channel in range(1, module.number_of_channels() + 1):
for platform in PLATFORMS:
if platform in module.get_categories(channel):
discovery_info[platform].append(
(module.get_module_address(), channel)
)
hass.data[DOMAIN][entry.entry_id] = discovery_info
for platform in PLATFORMS: hass.config_entries.async_setup_platforms(entry, PLATFORMS)
hass.add_job(hass.config_entries.async_forward_entry_setup(entry, platform))
try: if hass.services.has_service(DOMAIN, SERVICE_SCAN):
controller = velbus.Controller(entry.data[CONF_PORT]) return True
controller.scan(callback)
except velbus.util.VelbusException as err:
_LOGGER.error("An error occurred: %s", err)
raise ConfigEntryNotReady from err
def syn_clock(self, service=None): def check_entry_id(interface: str):
try: for entry in hass.config_entries.async_entries(DOMAIN):
controller.sync_clock() if "port" in entry.data and entry.data["port"] == interface:
except velbus.util.VelbusException as err: return entry.entry_id
_LOGGER.error("An error occurred: %s", err) raise vol.Invalid(
"The interface provided is not defined as a port in a Velbus integration"
)
hass.services.async_register(DOMAIN, "sync_clock", syn_clock, schema=vol.Schema({})) async def scan(call):
await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].scan()
def set_memo_text(service): hass.services.async_register(
DOMAIN,
SERVICE_SCAN,
scan,
vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
)
async def syn_clock(call):
await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].sync_clock()
hass.services.async_register(
DOMAIN,
SERVICE_SYNC,
syn_clock,
vol.Schema({vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id)}),
)
async def set_memo_text(call):
"""Handle Memo Text service call.""" """Handle Memo Text service call."""
module_address = service.data[CONF_ADDRESS] memo_text = call.data[CONF_MEMO_TEXT]
memo_text = service.data[CONF_MEMO_TEXT]
memo_text.hass = hass memo_text.hass = hass
try: await hass.data[DOMAIN][call.data[CONF_INTERFACE]]["cntrl"].get_module(
controller.get_module(module_address).set_memo_text( call.data[CONF_ADDRESS]
memo_text.async_render() ).set_memo_text(memo_text.async_render())
)
except velbus.util.VelbusException as err:
_LOGGER.error("An error occurred while setting memo text: %s", err)
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
@ -97,6 +116,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
set_memo_text, set_memo_text,
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_INTERFACE): vol.All(cv.string, check_entry_id),
vol.Required(CONF_ADDRESS): vol.All( vol.Required(CONF_ADDRESS): vol.All(
vol.Coerce(int), vol.Range(min=0, max=255) vol.Coerce(int), vol.Range(min=0, max=255)
), ),
@ -111,35 +131,34 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Remove the velbus connection.""" """Remove the velbus connection."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN][entry.entry_id]["cntrl"].stop() await hass.data[DOMAIN][entry.entry_id]["cntrl"].stop()
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]: if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
hass.services.async_remove(DOMAIN, SERVICE_SCAN)
hass.services.async_remove(DOMAIN, SERVICE_SYNC)
hass.services.async_remove(DOMAIN, SERVICE_SET_MEMO_TEXT)
return unload_ok return unload_ok
class VelbusEntity(Entity): class VelbusEntity(Entity):
"""Representation of a Velbus entity.""" """Representation of a Velbus entity."""
def __init__(self, module, channel): def __init__(self, channel):
"""Initialize a Velbus entity.""" """Initialize a Velbus entity."""
self._module = module
self._channel = channel self._channel = channel
@property @property
def unique_id(self): def unique_id(self):
"""Get unique ID.""" """Get unique ID."""
serial = 0 if (serial := self._channel.get_module_serial()) == 0:
if self._module.serial == 0: serial = self._channel.get_module_address()
serial = self._module.get_module_address() return f"{serial}-{self._channel.get_channel_number()}"
else:
serial = self._module.serial
return f"{serial}-{self._channel}"
@property @property
def name(self): def name(self):
"""Return the display name of this entity.""" """Return the display name of this entity."""
return self._module.get_name(self._channel) return self._channel.get_name()
@property @property
def should_poll(self): def should_poll(self):
@ -148,26 +167,24 @@ class VelbusEntity(Entity):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Add listener for state changes.""" """Add listener for state changes."""
self._module.on_status_update(self._channel, self._on_update) self._channel.on_status_update(self._on_update)
def _on_update(self, state): async def _on_update(self):
self.schedule_update_ha_state() self.async_write_ha_state()
@property @property
def device_info(self): def device_info(self):
"""Return the device info.""" """Return the device info."""
return { return {
"identifiers": { "identifiers": {
(DOMAIN, self._module.get_module_address(), self._module.serial) (
DOMAIN,
self._channel.get_module_address(),
self._channel.get_module_serial(),
)
}, },
"name": "{} ({})".format( "name": self._channel.get_full_name(),
self._module.get_module_name(), self._module.get_module_address()
),
"manufacturer": "Velleman", "manufacturer": "Velleman",
"model": self._module.get_module_type_name(), "model": self._channel.get_module_type_name(),
"sw_version": "{}.{}-{}".format( "sw_version": self._channel.get_module_sw_version(),
self._module.memory_map_version,
self._module.build_year,
self._module.build_week,
),
} }

View file

@ -6,13 +6,12 @@ from .const import DOMAIN
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus binary sensor based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["binary_sensor"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("binary_sensor"):
module = cntrl.get_module(address) entities.append(VelbusBinarySensor(channel))
entities.append(VelbusBinarySensor(module, channel))
async_add_entities(entities) async_add_entities(entities)
@ -20,6 +19,6 @@ class VelbusBinarySensor(VelbusEntity, BinarySensorEntity):
"""Representation of a Velbus Binary Sensor.""" """Representation of a Velbus Binary Sensor."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if the sensor is on.""" """Return true if the sensor is on."""
return self._module.is_closed(self._channel) return self._channel.is_closed()

View file

@ -1,14 +1,12 @@
"""Support for Velbus thermostat.""" """Support for Velbus thermostat."""
import logging import logging
from velbus.util import VelbusException
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
HVAC_MODE_HEAT, HVAC_MODE_HEAT,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
from . import VelbusEntity from . import VelbusEntity
from .const import DOMAIN from .const import DOMAIN
@ -17,13 +15,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus binary sensor based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["climate"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("climate"):
module = cntrl.get_module(address) entities.append(VelbusClimate(channel))
entities.append(VelbusClimate(module, channel))
async_add_entities(entities) async_add_entities(entities)
@ -37,15 +34,13 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
@property @property
def temperature_unit(self): def temperature_unit(self):
"""Return the unit this state is expressed in.""" """Return the unit."""
if self._module.get_unit(self._channel) == TEMP_CELSIUS: return TEMP_CELSIUS
return TEMP_CELSIUS
return TEMP_FAHRENHEIT
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self._module.get_state(self._channel) return self._channel.get_state()
@property @property
def hvac_mode(self): def hvac_mode(self):
@ -66,18 +61,14 @@ class VelbusClimate(VelbusEntity, ClimateEntity):
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._module.get_climate_target() return self._channel.get_climate_target()
def set_temperature(self, **kwargs): def set_temperature(self, **kwargs):
"""Set new target temperatures.""" """Set new target temperatures."""
temp = kwargs.get(ATTR_TEMPERATURE) temp = kwargs.get(ATTR_TEMPERATURE)
if temp is None: if temp is None:
return return
try: self._channel.set_temp(temp)
self._module.set_temp(temp)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
return
self.schedule_update_ha_state() self.schedule_update_ha_state()
def set_hvac_mode(self, hvac_mode): def set_hvac_mode(self, hvac_mode):

View file

@ -1,7 +1,8 @@
"""Config flow for the Velbus platform.""" """Config flow for the Velbus platform."""
from __future__ import annotations from __future__ import annotations
import velbus import velbusaio
from velbusaio.exceptions import VelbusConnectionFailed
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -33,14 +34,15 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Create an entry async.""" """Create an entry async."""
return self.async_create_entry(title=name, data={CONF_PORT: prt}) return self.async_create_entry(title=name, data={CONF_PORT: prt})
def _test_connection(self, prt): async def _test_connection(self, prt):
"""Try to connect to the velbus with the port specified.""" """Try to connect to the velbus with the port specified."""
try: try:
controller = velbus.Controller(prt) controller = velbusaio.controller.Velbus(prt)
except Exception: # pylint: disable=broad-except await controller.connect(True)
await controller.stop()
except VelbusConnectionFailed:
self._errors[CONF_PORT] = "cannot_connect" self._errors[CONF_PORT] = "cannot_connect"
return False return False
controller.stop()
return True return True
def _prt_in_configuration_exists(self, prt: str) -> bool: def _prt_in_configuration_exists(self, prt: str) -> bool:
@ -56,7 +58,7 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
name = slugify(user_input[CONF_NAME]) name = slugify(user_input[CONF_NAME])
prt = user_input[CONF_PORT] prt = user_input[CONF_PORT]
if not self._prt_in_configuration_exists(prt): if not self._prt_in_configuration_exists(prt):
if self._test_connection(prt): if await self._test_connection(prt):
return self._create_device(name, prt) return self._create_device(name, prt)
else: else:
self._errors[CONF_PORT] = "already_configured" self._errors[CONF_PORT] = "already_configured"

View file

@ -2,6 +2,9 @@
DOMAIN = "velbus" DOMAIN = "velbus"
CONF_INTERFACE = "interface"
CONF_MEMO_TEXT = "memo_text" CONF_MEMO_TEXT = "memo_text"
SERVICE_SCAN = "scan"
SERVICE_SYNC = "sync_clock"
SERVICE_SET_MEMO_TEXT = "set_memo_text" SERVICE_SET_MEMO_TEXT = "set_memo_text"

View file

@ -1,8 +1,6 @@
"""Support for Velbus covers.""" """Support for Velbus covers."""
import logging import logging
from velbus.util import VelbusException
from homeassistant.components.cover import ( from homeassistant.components.cover import (
ATTR_POSITION, ATTR_POSITION,
SUPPORT_CLOSE, SUPPORT_CLOSE,
@ -19,13 +17,12 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus cover based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["cover"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("cover"):
module = cntrl.get_module(address) entities.append(VelbusCover(channel))
entities.append(VelbusCover(module, channel))
async_add_entities(entities) async_add_entities(entities)
@ -35,16 +32,14 @@ class VelbusCover(VelbusEntity, CoverEntity):
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
if self._module.support_position(): if self._channel.support_position():
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP | SUPPORT_SET_POSITION
return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP
@property @property
def is_closed(self): def is_closed(self):
"""Return if the cover is closed.""" """Return if the cover is closed."""
if self._module.get_position(self._channel) == 100: return self._channel.is_closed()
return True
return False
@property @property
def current_cover_position(self): def current_cover_position(self):
@ -53,33 +48,21 @@ class VelbusCover(VelbusEntity, CoverEntity):
None is unknown, 0 is closed, 100 is fully open None is unknown, 0 is closed, 100 is fully open
Velbus: 100 = closed, 0 = open Velbus: 100 = closed, 0 = open
""" """
pos = self._module.get_position(self._channel) pos = self._channel.get_position()
return 100 - pos return 100 - pos
def open_cover(self, **kwargs): async def async_open_cover(self, **kwargs):
"""Open the cover.""" """Open the cover."""
try: await self._channel.open()
self._module.open(self._channel)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
def close_cover(self, **kwargs): async def async_close_cover(self, **kwargs):
"""Close the cover.""" """Close the cover."""
try: await self._channel.close()
self._module.close(self._channel)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
def stop_cover(self, **kwargs): async def async_stop_cover(self, **kwargs):
"""Stop the cover.""" """Stop the cover."""
try: await self._channel.stop()
self._module.stop(self._channel)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
def set_cover_position(self, **kwargs): async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position.""" """Move the cover to a specific position."""
try: self._channel.set_position(100 - kwargs[ATTR_POSITION])
self._module.set(self._channel, (100 - kwargs[ATTR_POSITION]))
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)

View file

@ -1,8 +1,6 @@
"""Support for Velbus light.""" """Support for Velbus light."""
import logging import logging
from velbus.util import VelbusException
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
ATTR_FLASH, ATTR_FLASH,
@ -22,62 +20,61 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus light based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["light"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("light"):
module = cntrl.get_module(address) entities.append(VelbusLight(channel, False))
entities.append(VelbusLight(module, channel)) for channel in cntrl.get_all("led"):
entities.append(VelbusLight(channel, True))
async_add_entities(entities) async_add_entities(entities)
class VelbusLight(VelbusEntity, LightEntity): class VelbusLight(VelbusEntity, LightEntity):
"""Representation of a Velbus light.""" """Representation of a Velbus light."""
def __init__(self, channel, led):
"""Initialize a light Velbus entity."""
super().__init__(channel)
self._is_led = led
@property @property
def name(self): def name(self):
"""Return the display name of this entity.""" """Return the display name of this entity."""
if self._module.light_is_buttonled(self._channel): if self._is_led:
return f"LED {self._module.get_name(self._channel)}" return f"LED {self._channel.get_name()}"
return self._module.get_name(self._channel) return self._channel.get_name()
@property @property
def supported_features(self): def supported_features(self):
"""Flag supported features.""" """Flag supported features."""
if self._module.light_is_buttonled(self._channel): if self._is_led:
return SUPPORT_FLASH return SUPPORT_FLASH
return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION
@property
def entity_registry_enabled_default(self):
"""Disable Button LEDs by default."""
if self._module.light_is_buttonled(self._channel):
return False
return True
@property @property
def is_on(self): def is_on(self):
"""Return true if the light is on.""" """Return true if the light is on."""
return self._module.is_on(self._channel) return self._channel.is_on()
@property @property
def brightness(self): def brightness(self):
"""Return the brightness of the light.""" """Return the brightness of the light."""
return int((self._module.get_dimmer_state(self._channel) * 255) / 100) return int((self._channel.get_dimmer_state() * 255) / 100)
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Instruct the Velbus light to turn on.""" """Instruct the Velbus light to turn on."""
if self._module.light_is_buttonled(self._channel): if self._is_led:
if ATTR_FLASH in kwargs: if ATTR_FLASH in kwargs:
if kwargs[ATTR_FLASH] == FLASH_LONG: if kwargs[ATTR_FLASH] == FLASH_LONG:
attr, *args = "set_led_state", self._channel, "slow" attr, *args = "set_led_state", "slow"
elif kwargs[ATTR_FLASH] == FLASH_SHORT: elif kwargs[ATTR_FLASH] == FLASH_SHORT:
attr, *args = "set_led_state", self._channel, "fast" attr, *args = "set_led_state", "fast"
else: else:
attr, *args = "set_led_state", self._channel, "on" attr, *args = "set_led_state", "on"
else: else:
attr, *args = "set_led_state", self._channel, "on" attr, *args = "set_led_state", "on"
else: else:
if ATTR_BRIGHTNESS in kwargs: if ATTR_BRIGHTNESS in kwargs:
# Make sure a low but non-zero value is not rounded down to zero # Make sure a low but non-zero value is not rounded down to zero
@ -87,33 +84,24 @@ class VelbusLight(VelbusEntity, LightEntity):
brightness = max(int((kwargs[ATTR_BRIGHTNESS] * 100) / 255), 1) brightness = max(int((kwargs[ATTR_BRIGHTNESS] * 100) / 255), 1)
attr, *args = ( attr, *args = (
"set_dimmer_state", "set_dimmer_state",
self._channel,
brightness, brightness,
kwargs.get(ATTR_TRANSITION, 0), kwargs.get(ATTR_TRANSITION, 0),
) )
else: else:
attr, *args = ( attr, *args = (
"restore_dimmer_state", "restore_dimmer_state",
self._channel,
kwargs.get(ATTR_TRANSITION, 0), kwargs.get(ATTR_TRANSITION, 0),
) )
try: await getattr(self._channel, attr)(*args)
getattr(self._module, attr)(*args)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Instruct the velbus light to turn off.""" """Instruct the velbus light to turn off."""
if self._module.light_is_buttonled(self._channel): if self._is_led:
attr, *args = "set_led_state", self._channel, "off" attr, *args = "set_led_state", "off"
else: else:
attr, *args = ( attr, *args = (
"set_dimmer_state", "set_dimmer_state",
self._channel,
0, 0,
kwargs.get(ATTR_TRANSITION, 0), kwargs.get(ATTR_TRANSITION, 0),
) )
try: await getattr(self._channel, attr)(*args)
getattr(self._module, attr)(*args)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)

View file

@ -2,7 +2,7 @@
"domain": "velbus", "domain": "velbus",
"name": "Velbus", "name": "Velbus",
"documentation": "https://www.home-assistant.io/integrations/velbus", "documentation": "https://www.home-assistant.io/integrations/velbus",
"requirements": ["python-velbus==2.1.2"], "requirements": ["velbus-aio==2021.9.1"],
"config_flow": true, "config_flow": true,
"codeowners": ["@Cereal2nd", "@brefra"], "codeowners": ["@Cereal2nd", "@brefra"],
"iot_class": "local_push" "iot_class": "local_push"

View file

@ -1,30 +1,39 @@
"""Support for Velbus sensors.""" """Support for Velbus sensors."""
from homeassistant.components.sensor import SensorEntity from __future__ import annotations
from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR
from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
SensorEntity,
)
from homeassistant.const import (
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
)
from . import VelbusEntity from . import VelbusEntity
from .const import DOMAIN from .const import DOMAIN
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus sensor based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["sensor"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("sensor"):
module = cntrl.get_module(address) entities.append(VelbusSensor(channel))
entities.append(VelbusSensor(module, channel)) if channel.is_counter_channel():
if module.get_class(channel) == "counter": entities.append(VelbusSensor(channel, True))
entities.append(VelbusSensor(module, channel, True))
async_add_entities(entities) async_add_entities(entities)
class VelbusSensor(VelbusEntity, SensorEntity): class VelbusSensor(VelbusEntity, SensorEntity):
"""Representation of a sensor.""" """Representation of a sensor."""
def __init__(self, module, channel, counter=False): def __init__(self, channel, counter=False):
"""Initialize a sensor Velbus entity.""" """Initialize a sensor Velbus entity."""
super().__init__(module, channel) super().__init__(channel)
self._is_counter = counter self._is_counter = counter
@property @property
@ -35,28 +44,38 @@ class VelbusSensor(VelbusEntity, SensorEntity):
unique_id = f"{unique_id}-counter" unique_id = f"{unique_id}-counter"
return unique_id return unique_id
@property
def name(self):
"""Return the name for the sensor."""
name = super().name
if self._is_counter:
name = f"{name}-counter"
return name
@property @property
def device_class(self): def device_class(self):
"""Return the device class of the sensor.""" """Return the device class of the sensor."""
if self._module.get_class(self._channel) == "counter" and not self._is_counter: if self._is_counter:
if self._module.get_counter_unit(self._channel) == ENERGY_KILO_WATT_HOUR: return DEVICE_CLASS_ENERGY
return DEVICE_CLASS_POWER if self._channel.is_counter_channel():
return None return DEVICE_CLASS_POWER
return self._module.get_class(self._channel) if self._channel.is_temperature():
return DEVICE_CLASS_TEMPERATURE
return None
@property @property
def native_value(self): def native_value(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self._is_counter: if self._is_counter:
return self._module.get_counter_state(self._channel) return self._channel.get_counter_state()
return self._module.get_state(self._channel) return self._channel.get_state()
@property @property
def native_unit_of_measurement(self): def native_unit_of_measurement(self):
"""Return the unit this state is expressed in.""" """Return the unit this state is expressed in."""
if self._is_counter: if self._is_counter:
return self._module.get_counter_unit(self._channel) return self._channel.get_counter_unit()
return self._module.get_unit(self._channel) return self._channel.get_unit()
@property @property
def icon(self): def icon(self):
@ -64,3 +83,10 @@ class VelbusSensor(VelbusEntity, SensorEntity):
if self._is_counter: if self._is_counter:
return "mdi:counter" return "mdi:counter"
return None return None
@property
def state_class(self):
"""Return the state class of this device."""
if self._is_counter:
return STATE_CLASS_TOTAL_INCREASING
return STATE_CLASS_MEASUREMENT

View file

@ -1,6 +1,28 @@
sync_clock: sync_clock:
name: Sync clock name: Sync clock
description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink
fields:
interface:
name: Interface
description: The velbus interface to send the command to, this will be the same value as used during configuration
required: true
example: "192.168.1.5:27015"
default: ''
selector:
text:
scan:
name: Scan
description: Scan the velbus modules, this will be need if you see unknown module warnings in the logs, or when you added new modules
fields:
interface:
name: Interface
description: The velbus interface to send the command to, this will be the same value as used during configuration
required: true
example: "192.168.1.5:27015"
default: ''
selector:
text:
set_memo_text: set_memo_text:
name: Set memo text name: Set memo text
@ -8,6 +30,14 @@ set_memo_text:
Set the memo text to the display of modules like VMBGPO, VMBGPOD Set the memo text to the display of modules like VMBGPO, VMBGPOD
Be sure the page(s) of the module is configured to display the memo text. Be sure the page(s) of the module is configured to display the memo text.
fields: fields:
interface:
name: Interface
description: The velbus interface to send the command to, this will be the same value as used during configuration
required: true
example: "192.168.1.5:27015"
default: ''
selector:
text:
address: address:
name: Address name: Address
description: > description: >
@ -16,8 +46,8 @@ set_memo_text:
required: true required: true
selector: selector:
number: number:
min: 0 min: 1
max: 255 max: 254
memo_text: memo_text:
name: Memo text name: Memo text
description: > description: >

View file

@ -1,7 +1,6 @@
"""Support for Velbus switches.""" """Support for Velbus switches."""
import logging import logging
from typing import Any
from velbus.util import VelbusException
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
@ -13,12 +12,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, entry, async_add_entities): async def async_setup_entry(hass, entry, async_add_entities):
"""Set up Velbus switch based on config_entry.""" """Set up Velbus switch based on config_entry."""
await hass.data[DOMAIN][entry.entry_id]["tsk"]
cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"] cntrl = hass.data[DOMAIN][entry.entry_id]["cntrl"]
modules_data = hass.data[DOMAIN][entry.entry_id]["switch"]
entities = [] entities = []
for address, channel in modules_data: for channel in cntrl.get_all("switch"):
module = cntrl.get_module(address) entities.append(VelbusSwitch(channel))
entities.append(VelbusSwitch(module, channel))
async_add_entities(entities) async_add_entities(entities)
@ -26,20 +24,14 @@ class VelbusSwitch(VelbusEntity, SwitchEntity):
"""Representation of a switch.""" """Representation of a switch."""
@property @property
def is_on(self): def is_on(self) -> bool:
"""Return true if the switch is on.""" """Return true if the switch is on."""
return self._module.is_on(self._channel) return self._channel.is_on()
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs: Any) -> None:
"""Instruct the switch to turn on.""" """Instruct the switch to turn on."""
try: await self._channel.turn_on()
self._module.turn_on(self._channel)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs: Any) -> None:
"""Instruct the switch to turn off.""" """Instruct the switch to turn off."""
try: await self._channel.turn_off()
self._module.turn_off(self._channel)
except VelbusException as err:
_LOGGER.error("A Velbus error occurred: %s", err)

View file

@ -1924,9 +1924,6 @@ python-telnet-vlc==2.0.1
# homeassistant.components.twitch # homeassistant.components.twitch
python-twitch-client==0.6.0 python-twitch-client==0.6.0
# homeassistant.components.velbus
python-velbus==2.1.2
# homeassistant.components.vlc # homeassistant.components.vlc
python-vlc==1.1.2 python-vlc==1.1.2
@ -2350,6 +2347,9 @@ uvcclient==0.11.0
# homeassistant.components.vallox # homeassistant.components.vallox
vallox-websocket-api==2.8.1 vallox-websocket-api==2.8.1
# homeassistant.components.velbus
velbus-aio==2021.9.1
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.14 venstarcolortouch==0.14

View file

@ -1090,9 +1090,6 @@ python-tado==0.10.0
# homeassistant.components.twitch # homeassistant.components.twitch
python-twitch-client==0.6.0 python-twitch-client==0.6.0
# homeassistant.components.velbus
python-velbus==2.1.2
# homeassistant.components.awair # homeassistant.components.awair
python_awair==0.2.1 python_awair==0.2.1
@ -1312,6 +1309,9 @@ url-normalize==1.4.1
# homeassistant.components.uvc # homeassistant.components.uvc
uvcclient==0.11.0 uvcclient==0.11.0
# homeassistant.components.velbus
velbus-aio==2021.9.1
# homeassistant.components.venstar # homeassistant.components.venstar
venstarcolortouch==0.14 venstarcolortouch==0.14

View file

@ -1,7 +1,8 @@
"""Tests for the Velbus config flow.""" """Tests for the Velbus config flow."""
from unittest.mock import Mock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
from velbusaio.exceptions import VelbusConnectionFailed
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.velbus import config_flow from homeassistant.components.velbus import config_flow
@ -16,15 +17,15 @@ PORT_TCP = "127.0.1.0.1:3788"
@pytest.fixture(name="controller_assert") @pytest.fixture(name="controller_assert")
def mock_controller_assert(): def mock_controller_assert():
"""Mock the velbus controller with an assert.""" """Mock the velbus controller with an assert."""
with patch("velbus.Controller", side_effect=Exception()): with patch("velbusaio.controller.Velbus", side_effect=VelbusConnectionFailed()):
yield yield
@pytest.fixture(name="controller") @pytest.fixture(name="controller")
def mock_controller(): def mock_controller():
"""Mock a successful velbus controller.""" """Mock a successful velbus controller."""
controller = Mock() controller = AsyncMock()
with patch("velbus.Controller", return_value=controller): with patch("velbusaio.controller.Velbus", return_value=controller):
yield controller yield controller