Add Z-Wave.Me integration (#65473)

* Add support of Z-Wave.Me Z-Way and RaZberry server (#61182)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: LawfulChaos <kerbalspacema@gmail.com>

* Add switch platform to Z-Wave.Me integration (#64957)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>

* Add button platform to Z-Wave.Me integration (#65109)

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix button controller access (#65117)

* Add lock platform to Z-Wave.Me integration #65109 (#65114)

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add sensor platform to Z-Wave.Me integration (#65132)

* Sensor Entity

* Sensor fixes

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Inline descriotion according to review proposal

* State Classes for sensor

* Generic sensor

* Generic sensor

Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add binary sensor platform to Z-Wave.Me integration (#65306)

* Binary Sensor Entity

* Update docstring

Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add Light Entity platform to Z-Wave.Me integration (#65331)

* Light Entity

* mypy fix

* Fixes, ZWaveMePlatforms enum

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fixes

* Fixes

* Fixes

Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Add Thermostat platform to Z-Wave.Me integration #65331 (#65371)

* Climate entity

* Climate entity

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Climate entity fix

* Clean up

* cleanup

* Import order fix

* Correct naming

Co-authored-by: Dmitry Vlasov <kerbalspacema@gmail.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Correct zwave_me .coveragerc (#65491)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: LawfulChaos <kerbalspacema@gmail.com>
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
pull/65958/head
Poltorak Serguei 2022-02-07 18:27:11 +03:00 committed by GitHub
parent b1015296d9
commit 3c5a667d97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1114 additions and 1 deletions

View File

@ -1410,6 +1410,16 @@ omit =
homeassistant/components/zwave/util.py
homeassistant/components/zwave_js/discovery.py
homeassistant/components/zwave_js/sensor.py
homeassistant/components/zwave_me/__init__.py
homeassistant/components/zwave_me/binary_sensor.py
homeassistant/components/zwave_me/button.py
homeassistant/components/zwave_me/climate.py
homeassistant/components/zwave_me/helpers.py
homeassistant/components/zwave_me/light.py
homeassistant/components/zwave_me/lock.py
homeassistant/components/zwave_me/number.py
homeassistant/components/zwave_me/sensor.py
homeassistant/components/zwave_me/switch.py
[report]
# Regexes for lines to exclude from consideration

View File

@ -1111,6 +1111,8 @@ homeassistant/components/zwave/* @home-assistant/z-wave
tests/components/zwave/* @home-assistant/z-wave
homeassistant/components/zwave_js/* @home-assistant/z-wave
tests/components/zwave_js/* @home-assistant/z-wave
homeassistant/components/zwave_me/* @lawfulchaos @Z-Wave-Me
tests/components/zwave_me/* @lawfulchaos @Z-Wave-Me
# Individual files
homeassistant/components/demo/weather @fabaff

View File

@ -0,0 +1,123 @@
"""The Z-Wave-Me WS integration."""
import asyncio
import logging
from zwave_me_ws import ZWaveMe, ZWaveMeData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, CONF_URL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, PLATFORMS, ZWaveMePlatform
_LOGGER = logging.getLogger(__name__)
ZWAVE_ME_PLATFORMS = [platform.value for platform in ZWaveMePlatform]
async def async_setup_entry(hass, entry):
"""Set up Z-Wave-Me from a config entry."""
hass.data.setdefault(DOMAIN, {})
controller = hass.data[DOMAIN][entry.entry_id] = ZWaveMeController(hass, entry)
if await controller.async_establish_connection():
hass.async_create_task(async_setup_platforms(hass, entry, controller))
return True
raise ConfigEntryNotReady()
async def async_unload_entry(hass, entry):
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
controller = hass.data[DOMAIN].pop(entry.entry_id)
await controller.zwave_api.close_ws()
return unload_ok
class ZWaveMeController:
"""Main ZWave-Me API class."""
def __init__(self, hass: HomeAssistant, config: ConfigEntry) -> None:
"""Create the API instance."""
self.device_ids: set = set()
self._hass = hass
self.config = config
self.zwave_api = ZWaveMe(
on_device_create=self.on_device_create,
on_device_update=self.on_device_update,
on_new_device=self.add_device,
token=self.config.data[CONF_TOKEN],
url=self.config.data[CONF_URL],
platforms=ZWAVE_ME_PLATFORMS,
)
self.platforms_inited = False
async def async_establish_connection(self):
"""Get connection status."""
is_connected = await self.zwave_api.get_connection()
return is_connected
def add_device(self, device: ZWaveMeData) -> None:
"""Send signal to create device."""
if device.deviceType in ZWAVE_ME_PLATFORMS and self.platforms_inited:
if device.id in self.device_ids:
dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{device.id}", device)
else:
dispatcher_send(
self._hass, f"ZWAVE_ME_NEW_{device.deviceType.upper()}", device
)
self.device_ids.add(device.id)
def on_device_create(self, devices: list) -> None:
"""Create multiple devices."""
for device in devices:
self.add_device(device)
def on_device_update(self, new_info: ZWaveMeData) -> None:
"""Send signal to update device."""
dispatcher_send(self._hass, f"ZWAVE_ME_INFO_{new_info.id}", new_info)
async def async_setup_platforms(
hass: HomeAssistant, entry: ConfigEntry, controller: ZWaveMeController
) -> None:
"""Set up platforms."""
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_setup(entry, platform)
for platform in PLATFORMS
]
)
controller.platforms_inited = True
await hass.async_add_executor_job(controller.zwave_api.get_devices)
class ZWaveMeEntity(Entity):
"""Representation of a ZWaveMe device."""
def __init__(self, controller, device):
"""Initialize the device."""
self.controller = controller
self.device = device
self._attr_name = device.title
self._attr_unique_id = f"{self.controller.config.unique_id}-{self.device.id}"
self._attr_should_poll = False
async def async_added_to_hass(self) -> None:
"""Connect to an updater."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, f"ZWAVE_ME_INFO_{self.device.id}", self.get_new_data
)
)
@callback
def get_new_data(self, new_data):
"""Update info in the HAss."""
self.device = new_data
self._attr_available = not new_data.isFailed
self.async_write_ha_state()

View File

@ -0,0 +1,75 @@
"""Representation of a sensorBinary."""
from __future__ import annotations
from zwave_me_ws import ZWaveMeData
from homeassistant.components.binary_sensor import (
DEVICE_CLASS_MOTION,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ZWaveMeController, ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
BINARY_SENSORS_MAP: dict[str, BinarySensorEntityDescription] = {
"generic": BinarySensorEntityDescription(
key="generic",
),
"motion": BinarySensorEntityDescription(
key="motion",
device_class=DEVICE_CLASS_MOTION,
),
}
DEVICE_NAME = ZWaveMePlatform.BINARY_SENSOR
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the binary sensor platform."""
@callback
def add_new_device(new_device: ZWaveMeData) -> None:
controller: ZWaveMeController = hass.data[DOMAIN][config_entry.entry_id]
description = BINARY_SENSORS_MAP.get(
new_device.probeType, BINARY_SENSORS_MAP["generic"]
)
sensor = ZWaveMeBinarySensor(controller, new_device, description)
async_add_entities(
[
sensor,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeBinarySensor(ZWaveMeEntity, BinarySensorEntity):
"""Representation of a ZWaveMe binary sensor."""
def __init__(
self,
controller: ZWaveMeController,
device: ZWaveMeData,
description: BinarySensorEntityDescription,
) -> None:
"""Initialize the device."""
super().__init__(controller=controller, device=device)
self.entity_description = description
@property
def is_on(self) -> bool:
"""Return the state of the sensor."""
return self.device.level == "on"

View File

@ -0,0 +1,40 @@
"""Representation of a toggleButton."""
from typing import Any
from homeassistant.components.button import ButtonEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
DEVICE_NAME = ZWaveMePlatform.BUTTON
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the number platform."""
@callback
def add_new_device(new_device):
controller = hass.data[DOMAIN][config_entry.entry_id]
button = ZWaveMeButton(controller, new_device)
async_add_entities(
[
button,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeButton(ZWaveMeEntity, ButtonEntity):
"""Representation of a ZWaveMe button."""
def press(self, **kwargs: Any) -> None:
"""Turn the entity on."""
self.controller.zwave_api.send_command(self.device.id, "on")

View File

@ -0,0 +1,101 @@
"""Representation of a thermostat."""
from __future__ import annotations
from zwave_me_ws import ZWaveMeData
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
HVAC_MODE_HEAT,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
TEMPERATURE_DEFAULT_STEP = 0.5
DEVICE_NAME = ZWaveMePlatform.CLIMATE
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the climate platform."""
@callback
def add_new_device(new_device: ZWaveMeData) -> None:
"""Add a new device."""
controller = hass.data[DOMAIN][config_entry.entry_id]
climate = ZWaveMeClimate(controller, new_device)
async_add_entities(
[
climate,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeClimate(ZWaveMeEntity, ClimateEntity):
"""Representation of a ZWaveMe sensor."""
def set_temperature(self, **kwargs) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return
self.controller.zwave_api.send_command(
self.device.id, f"exact?level={temperature}"
)
@property
def temperature_unit(self) -> str:
"""Return the temperature_unit."""
return self.device.scaleTitle
@property
def target_temperature(self) -> float:
"""Return the state of the sensor."""
return self.device.level
@property
def max_temp(self) -> float:
"""Return min temperature for the device."""
return self.device.max
@property
def min_temp(self) -> float:
"""Return max temperature for the device."""
return self.device.min
@property
def hvac_modes(self) -> list[str]:
"""Return the list of available operation modes."""
return [HVAC_MODE_HEAT]
@property
def hvac_mode(self) -> str:
"""Return the current mode."""
return HVAC_MODE_HEAT
@property
def supported_features(self) -> int:
"""Return the supported features."""
return SUPPORT_TARGET_TEMPERATURE
@property
def target_temperature_step(self) -> float:
"""Return the supported step of target temperature."""
return TEMPERATURE_DEFAULT_STEP

View File

@ -0,0 +1,84 @@
"""Config flow to configure ZWaveMe integration."""
import logging
from url_normalize import url_normalize
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_TOKEN, CONF_URL
from . import helpers
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
class ZWaveMeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""ZWaveMe integration config flow."""
def __init__(self):
"""Initialize flow."""
self.url = None
self.token = None
self.uuid = None
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user or started with zeroconf."""
errors = {}
if self.url is None:
schema = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_TOKEN): str,
}
)
else:
schema = vol.Schema(
{
vol.Required(CONF_TOKEN): str,
}
)
if user_input is not None:
if self.url is None:
self.url = user_input[CONF_URL]
self.token = user_input[CONF_TOKEN]
if not self.url.startswith(("ws://", "wss://")):
self.url = f"ws://{self.url}"
self.url = url_normalize(self.url, default_scheme="ws")
if self.uuid is None:
self.uuid = await helpers.get_uuid(self.url, self.token)
if self.uuid is not None:
await self.async_set_unique_id(self.uuid, raise_on_progress=False)
self._abort_if_unique_id_configured()
else:
errors["base"] = "no_valid_uuid_set"
if not errors:
return self.async_create_entry(
title=self.url,
data={CONF_URL: self.url, CONF_TOKEN: self.token},
)
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors,
)
async def async_step_zeroconf(self, discovery_info):
"""
Handle a discovered Z-Wave accessory - get url to pass into user step.
This flow is triggered by the discovery component.
"""
self.url = discovery_info.host
self.uuid = await helpers.get_uuid(self.url)
if self.uuid is None:
return self.async_abort(reason="no_valid_uuid_set")
await self.async_set_unique_id(self.uuid)
self._abort_if_unique_id_configured()
return await self.async_step_user()

View File

@ -0,0 +1,32 @@
"""Constants for ZWaveMe."""
from homeassistant.backports.enum import StrEnum
from homeassistant.const import Platform
# Base component constants
DOMAIN = "zwave_me"
class ZWaveMePlatform(StrEnum):
"""Included ZWaveMe platforms."""
BINARY_SENSOR = "sensorBinary"
BUTTON = "toggleButton"
CLIMATE = "thermostat"
LOCK = "doorlock"
NUMBER = "switchMultilevel"
SWITCH = "switchBinary"
SENSOR = "sensorMultilevel"
RGBW_LIGHT = "switchRGBW"
RGB_LIGHT = "switchRGB"
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.LIGHT,
Platform.LOCK,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]

View File

@ -0,0 +1,14 @@
"""Helpers for zwave_me config flow."""
from __future__ import annotations
from zwave_me_ws import ZWaveMe
async def get_uuid(url: str, token: str | None = None) -> str | None:
"""Get an uuid from Z-Wave-Me."""
conn = ZWaveMe(url=url, token=token)
uuid = None
if await conn.get_connection():
uuid = await conn.get_uuid()
await conn.close_ws()
return uuid

View File

@ -0,0 +1,85 @@
"""Representation of an RGB light."""
from __future__ import annotations
from typing import Any
from zwave_me_ws import ZWaveMeData
from homeassistant.components.light import ATTR_RGB_COLOR, COLOR_MODE_RGB, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the rgb platform."""
@callback
def add_new_device(new_device: ZWaveMeData) -> None:
"""Add a new device."""
controller = hass.data[DOMAIN][config_entry.entry_id]
rgb = ZWaveMeRGB(controller, new_device)
async_add_entities(
[
rgb,
]
)
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.RGB_LIGHT.upper()}", add_new_device
)
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{ZWaveMePlatform.RGBW_LIGHT.upper()}", add_new_device
)
class ZWaveMeRGB(ZWaveMeEntity, LightEntity):
"""Representation of a ZWaveMe light."""
def turn_off(self, **kwargs: Any) -> None:
"""Turn the device on."""
self.controller.zwave_api.send_command(self.device.id, "off")
def turn_on(self, **kwargs: Any):
"""Turn the device on."""
color = kwargs.get(ATTR_RGB_COLOR)
if color is None:
color = (122, 122, 122)
cmd = "exact?red={}&green={}&blue={}".format(*color)
self.controller.zwave_api.send_command(self.device.id, cmd)
@property
def is_on(self) -> bool:
"""Return true if the light is on."""
return self.device.level == "on"
@property
def brightness(self) -> int:
"""Return the brightness of a device."""
return max(self.device.color.values())
@property
def rgb_color(self) -> tuple[int, int, int]:
"""Return the rgb color value [int, int, int]."""
rgb = self.device.color
return rgb["r"], rgb["g"], rgb["b"]
@property
def supported_color_modes(self) -> set:
"""Return all color modes."""
return {COLOR_MODE_RGB}
@property
def color_mode(self) -> str:
"""Return current color mode."""
return COLOR_MODE_RGB

View File

@ -0,0 +1,60 @@
"""Representation of a doorlock."""
from __future__ import annotations
from typing import Any
from zwave_me_ws import ZWaveMeData
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
DEVICE_NAME = ZWaveMePlatform.LOCK
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the lock platform."""
@callback
def add_new_device(new_device: ZWaveMeData) -> None:
"""Add a new device."""
controller = hass.data[DOMAIN][config_entry.entry_id]
lock = ZWaveMeLock(controller, new_device)
async_add_entities(
[
lock,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeLock(ZWaveMeEntity, LockEntity):
"""Representation of a ZWaveMe lock."""
@property
def is_locked(self) -> bool:
"""Return the state of the lock."""
return self.device.level == "close"
def unlock(self, **kwargs: Any) -> None:
"""Send command to unlock the lock."""
self.controller.zwave_api.send_command(self.device.id, "open")
def lock(self, **kwargs: Any) -> None:
"""Send command to lock the lock."""
self.controller.zwave_api.send_command(self.device.id, "close")

View File

@ -0,0 +1,17 @@
{
"domain": "zwave_me",
"name": "Z-Wave.Me",
"documentation": "https://www.home-assistant.io/integrations/zwave_me",
"iot_class": "local_push",
"requirements": [
"zwave_me_ws==0.1.23",
"url-normalize==1.4.1"
],
"after_dependencies": ["zeroconf"],
"zeroconf": [{"type":"_hap._tcp.local.", "name": "*z.wave-me*"}],
"config_flow": true,
"codeowners": [
"@lawfulchaos",
"@Z-Wave-Me"
]
}

View File

@ -0,0 +1,45 @@
"""Representation of a switchMultilevel."""
from homeassistant.components.number import NumberEntity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
DEVICE_NAME = ZWaveMePlatform.NUMBER
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the number platform."""
@callback
def add_new_device(new_device):
controller = hass.data[DOMAIN][config_entry.entry_id]
switch = ZWaveMeNumber(controller, new_device)
async_add_entities(
[
switch,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeNumber(ZWaveMeEntity, NumberEntity):
"""Representation of a ZWaveMe Multilevel Switch."""
@property
def value(self):
"""Return the unit of measurement."""
return self.device.level
def set_value(self, value: float) -> None:
"""Update the current value."""
self.controller.zwave_api.send_command(
self.device.id, f"exact?level={str(round(value))}"
)

View File

@ -0,0 +1,120 @@
"""Representation of a sensorMultilevel."""
from __future__ import annotations
from zwave_me_ws import ZWaveMeData
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
LIGHT_LUX,
POWER_WATT,
SIGNAL_STRENGTH_DECIBELS,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ZWaveMeController, ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
SENSORS_MAP: dict[str, SensorEntityDescription] = {
"meterElectric_watt": SensorEntityDescription(
key="meterElectric_watt",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=POWER_WATT,
state_class=SensorStateClass.MEASUREMENT,
),
"meterElectric_kilowatt_hour": SensorEntityDescription(
key="meterElectric_kilowatt_hour",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
),
"meterElectric_voltage": SensorEntityDescription(
key="meterElectric_voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
state_class=SensorStateClass.MEASUREMENT,
),
"light": SensorEntityDescription(
key="light",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
"noise": SensorEntityDescription(
key="noise",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
state_class=SensorStateClass.MEASUREMENT,
),
"currentTemperature": SensorEntityDescription(
key="currentTemperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"temperature": SensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
"generic": SensorEntityDescription(
key="generic",
),
}
DEVICE_NAME = ZWaveMePlatform.SENSOR
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
@callback
def add_new_device(new_device: ZWaveMeData) -> None:
controller: ZWaveMeController = hass.data[DOMAIN][config_entry.entry_id]
description = SENSORS_MAP.get(new_device.probeType, SENSORS_MAP["generic"])
sensor = ZWaveMeSensor(controller, new_device, description)
async_add_entities(
[
sensor,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeSensor(ZWaveMeEntity, SensorEntity):
"""Representation of a ZWaveMe sensor."""
def __init__(
self,
controller: ZWaveMeController,
device: ZWaveMeData,
description: SensorEntityDescription,
) -> None:
"""Initialize the device."""
super().__init__(controller=controller, device=device)
self.entity_description = description
@property
def native_value(self) -> str:
"""Return the state of the sensor."""
return self.device.level

View File

@ -0,0 +1,20 @@
{
"config": {
"step": {
"user": {
"description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this).",
"data": {
"url": "[%key:common::config_flow::data::url%]",
"token": "Token"
}
}
},
"error": {
"no_valid_uuid_set": "No valid UUID set"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_valid_uuid_set": "No valid UUID set"
}
}
}

View File

@ -0,0 +1,67 @@
"""Representation of a switchBinary."""
import logging
from typing import Any
from homeassistant.components.switch import (
SwitchDeviceClass,
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from . import ZWaveMeEntity
from .const import DOMAIN, ZWaveMePlatform
_LOGGER = logging.getLogger(__name__)
DEVICE_NAME = ZWaveMePlatform.SWITCH
SWITCH_MAP: dict[str, SwitchEntityDescription] = {
"generic": SwitchEntityDescription(
key="generic",
device_class=SwitchDeviceClass.SWITCH,
)
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the switch platform."""
@callback
def add_new_device(new_device):
controller = hass.data[DOMAIN][config_entry.entry_id]
switch = ZWaveMeSwitch(controller, new_device, SWITCH_MAP["generic"])
async_add_entities(
[
switch,
]
)
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"ZWAVE_ME_NEW_{DEVICE_NAME.upper()}", add_new_device
)
)
class ZWaveMeSwitch(ZWaveMeEntity, SwitchEntity):
"""Representation of a ZWaveMe binary switch."""
def __init__(self, controller, device, description):
"""Initialize the device."""
super().__init__(controller, device)
self.entity_description = description
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.device.level == "on"
def turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
self.controller.zwave_api.send_command(self.device.id, "on")
def turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
self.controller.zwave_api.send_command(self.device.id, "off")

View File

@ -0,0 +1,20 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"no_valid_uuid_set": "No valid UUID set"
},
"error": {
"no_valid_uuid_set": "No valid UUID set"
},
"step": {
"user": {
"data": {
"token": "Token",
"url": "URL"
},
"description": "Input IP address of Z-Way server and Z-Way access token. IP address can be prefixed with wss:// if HTTPS should be used instead of HTTP. To get the token go to the Z-Way user interface > Menu > Settings > User > API token. It is suggested to create a new user for Home Assistant and grant access to devices you need to control from Home Assistant. It is also possible to use remote access via find.z-wave.me to connect a remote Z-Way. Input wss://find.z-wave.me in IP field and copy the token with Global scope (log-in to Z-Way via find.z-wave.me for this)."
}
}
}
}

View File

@ -378,5 +378,6 @@ FLOWS = [
"zerproc",
"zha",
"zwave",
"zwave_js"
"zwave_js",
"zwave_me"
]

View File

@ -144,6 +144,10 @@ ZEROCONF = {
"_hap._tcp.local.": [
{
"domain": "homekit_controller"
},
{
"domain": "zwave_me",
"name": "*z.wave-me*"
}
],
"_homekit._tcp.local.": [

View File

@ -2408,6 +2408,7 @@ upcloud-api==2.0.0
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
# homeassistant.components.zwave_me
url-normalize==1.4.1
# homeassistant.components.uscis
@ -2562,3 +2563,6 @@ zm-py==0.5.2
# homeassistant.components.zwave_js
zwave-js-server-python==0.34.0
# homeassistant.components.zwave_me
zwave_me_ws==0.1.23

View File

@ -1475,6 +1475,7 @@ upcloud-api==2.0.0
# homeassistant.components.huawei_lte
# homeassistant.components.syncthru
# homeassistant.components.zwave_me
url-normalize==1.4.1
# homeassistant.components.uvc
@ -1578,3 +1579,6 @@ zigpy==0.43.0
# homeassistant.components.zwave_js
zwave-js-server-python==0.34.0
# homeassistant.components.zwave_me
zwave_me_ws==0.1.23

View File

@ -0,0 +1 @@
"""Tests for the zwave_me integration."""

View File

@ -0,0 +1,184 @@
"""Test the zwave_me config flow."""
from unittest.mock import patch
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.components.zwave_me.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
FlowResult,
)
from tests.common import MockConfigEntry
MOCK_ZEROCONF_DATA = zeroconf.ZeroconfServiceInfo(
host="ws://192.168.1.14",
hostname="mock_hostname",
name="mock_name",
port=1234,
properties={
"deviceid": "aa:bb:cc:dd:ee:ff",
"manufacturer": "fake_manufacturer",
"model": "fake_model",
"serialNumber": "fake_serial",
},
type="mock_type",
)
async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
with patch(
"homeassistant.components.zwave_me.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.zwave_me.helpers.get_uuid",
return_value="test_uuid",
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": "192.168.1.14",
"token": "test-token",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "ws://192.168.1.14"
assert result2["data"] == {
"url": "ws://192.168.1.14",
"token": "test-token",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf(hass: HomeAssistant):
"""Test starting a flow from zeroconf."""
with patch(
"homeassistant.components.zwave_me.async_setup_entry",
return_value=True,
) as mock_setup_entry, patch(
"homeassistant.components.zwave_me.helpers.get_uuid",
return_value="test_uuid",
):
result: FlowResult = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=MOCK_ZEROCONF_DATA,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"token": "test-token",
},
)
await hass.async_block_till_done()
assert result2["type"] == RESULT_TYPE_CREATE_ENTRY
assert result2["title"] == "ws://192.168.1.14"
assert result2["data"] == {
"url": "ws://192.168.1.14",
"token": "test-token",
}
assert len(mock_setup_entry.mock_calls) == 1
async def test_error_handling_zeroconf(hass: HomeAssistant):
"""Test getting proper errors from no uuid."""
with patch("homeassistant.components.zwave_me.helpers.get_uuid", return_value=None):
result: FlowResult = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=MOCK_ZEROCONF_DATA,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "no_valid_uuid_set"
async def test_handle_error_user(hass: HomeAssistant):
"""Test getting proper errors from no uuid."""
with patch("homeassistant.components.zwave_me.helpers.get_uuid", return_value=None):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": "192.168.1.15",
"token": "test-token",
},
)
assert result2["errors"] == {"base": "no_valid_uuid_set"}
async def test_duplicate_user(hass: HomeAssistant):
"""Test getting proper errors from duplicate uuid."""
entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="ZWave_me",
data={
"url": "ws://192.168.1.15",
"token": "test-token",
},
unique_id="test_uuid",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.zwave_me.helpers.get_uuid",
return_value="test_uuid",
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": "192.168.1.15",
"token": "test-token",
},
)
assert result2["type"] == RESULT_TYPE_ABORT
assert result2["reason"] == "already_configured"
async def test_duplicate_zeroconf(hass: HomeAssistant):
"""Test getting proper errors from duplicate uuid."""
entry: MockConfigEntry = MockConfigEntry(
domain=DOMAIN,
title="ZWave_me",
data={
"url": "ws://192.168.1.14",
"token": "test-token",
},
unique_id="test_uuid",
)
entry.add_to_hass(hass)
with patch(
"homeassistant.components.zwave_me.helpers.get_uuid",
return_value="test_uuid",
):
result: FlowResult = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=MOCK_ZEROCONF_DATA,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "already_configured"