Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add zwave_js.set_config_parameter service #46673

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
00ceed7
create zwave_js.set_config_value service
raman325 Feb 16, 2021
1c4e4bd
update docstring
raman325 Feb 17, 2021
4691993
PR comments
raman325 Feb 17, 2021
c3616b9
make proposed changes
raman325 Feb 17, 2021
fb2481e
handle providing a label for the new value
raman325 Feb 17, 2021
e5472bd
fix docstring
raman325 Feb 17, 2021
05e9611
use new library function
raman325 Feb 18, 2021
c7e843f
config param endpoint is always 0
raman325 Feb 18, 2021
69be006
corresponding changes from upstream PR
raman325 Feb 19, 2021
ddf5afd
bug fixes and add tests
raman325 Feb 19, 2021
fec298f
create zwave_js.set_config_value service
raman325 Feb 16, 2021
6e506f8
update docstring
raman325 Feb 17, 2021
82d53a6
PR comments
raman325 Feb 17, 2021
60f4b17
make proposed changes
raman325 Feb 17, 2021
7fa4a59
handle providing a label for the new value
raman325 Feb 17, 2021
92691bb
fix docstring
raman325 Feb 17, 2021
a0408dc
use new library function
raman325 Feb 18, 2021
1ec0fa3
config param endpoint is always 0
raman325 Feb 18, 2021
c2f2ce5
corresponding changes from upstream PR
raman325 Feb 19, 2021
2de51da
bug fixes and add tests
raman325 Feb 19, 2021
48b66bb
use lambda to avoid extra function
raman325 Feb 20, 2021
97cdfde
add services description file
marcelveldt Feb 20, 2021
4077f80
bring back the missing selector
marcelveldt Feb 20, 2021
2b56a66
move helper functions to helper file for reuse
raman325 Feb 20, 2021
a19cea6
allow target selector for automation editor
marcelveldt Feb 20, 2021
7b0b477
formatting
marcelveldt Feb 20, 2021
dcd3760
fix service schema
raman325 Feb 20, 2021
6dc673f
update docstrings
raman325 Feb 22, 2021
71ff5a1
raise error in service if call to set value is unsuccessful
raman325 Feb 22, 2021
9bb353b
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
7e3362a
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
6f16e18
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
97a0f37
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
af44503
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
76d7dbd
Update homeassistant/components/zwave_js/services.yaml
raman325 Feb 22, 2021
75e51e6
remove extra param to vol.Optional
raman325 Feb 22, 2021
68e9375
switch to set over list for nodes
raman325 Feb 23, 2021
7d17381
switch to set over list for nodes
raman325 Feb 23, 2021
06becbd
Merge branch 'dev' into zwave_js_set_config_value_service
raman325 Feb 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion homeassistant/components/zwave_js/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
ZWAVE_JS_EVENT,
)
from .discovery import async_discover_values
from .entity import get_device_id
from .helpers import get_device_id
from .services import ZWaveServices

LOGGER = logging.getLogger(__package__)
CONNECT_TIMEOUT = 10
Expand Down Expand Up @@ -192,6 +193,9 @@ def async_on_notification(notification: Notification) -> None:
DATA_UNSUBSCRIBE: unsubscribe_callbacks,
}

services = ZWaveServices(hass)
services.async_register()

# Set up websocket API
async_register_api(hass)

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/zwave_js/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@
ATTR_PROPERTY_KEY_NAME = "property_key_name"
ATTR_PARAMETERS = "parameters"

# service constants
SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter"

ATTR_CONFIG_PARAMETER = "parameter"
ATTR_CONFIG_PARAMETER_BITMASK = "bitmask"
ATTR_CONFIG_VALUE = "value"

ADDON_SLUG = "core_zwave_js"
11 changes: 2 additions & 9 deletions homeassistant/components/zwave_js/entity.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,23 @@
"""Generic Z-Wave Entity Class."""

import logging
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Union

from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.model.value import Value as ZwaveValue, get_value_id

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity

from .const import DOMAIN
from .discovery import ZwaveDiscoveryInfo
from .helpers import get_device_id

LOGGER = logging.getLogger(__name__)

EVENT_VALUE_UPDATED = "value updated"


@callback
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
"""Get device registry identifier for Z-Wave node."""
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")


class ZWaveBaseEntity(Entity):
"""Generic Entity Class for a Z-Wave Device."""

Expand Down
100 changes: 100 additions & 0 deletions homeassistant/components/zwave_js/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Helper functions for Z-Wave JS integration."""
from typing import List, Tuple, cast

from zwave_js_server.client import Client as ZwaveClient
from zwave_js_server.model.node import Node as ZwaveNode

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import async_get as async_get_dev_reg
from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg

from .const import DATA_CLIENT, DOMAIN


@callback
def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]:
"""Get device registry identifier for Z-Wave node."""
return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")


@callback
def get_home_and_node_id_from_device_id(device_id: Tuple[str, str]) -> List[str]:
"""
Get home ID and node ID for Z-Wave device registry entry.

Returns [home_id, node_id]
"""
return device_id[1].split("-")


@callback
def async_get_node_from_device_id(hass: HomeAssistant, device_id: str) -> ZwaveNode:
"""
Get node from a device ID.

Raises ValueError if device is invalid or node can't be found.
"""
device_entry = async_get_dev_reg(hass).async_get(device_id)

if not device_entry:
raise ValueError("Device ID is not valid")

# Use device config entry ID's to validate that this is a valid zwave_js device
# and to get the client
config_entry_ids = device_entry.config_entries
config_entry_id = next(
(
config_entry_id
for config_entry_id in config_entry_ids
if cast(
ConfigEntry,
hass.config_entries.async_get_entry(config_entry_id),
).domain
== DOMAIN
),
None,
)
if config_entry_id is None or config_entry_id not in hass.data[DOMAIN]:
raise ValueError("Device is not from an existing zwave_js config entry")

client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT]

# Get node ID from device identifier, perform some validation, and then get the
# node
identifier = next(
(
get_home_and_node_id_from_device_id(identifier)
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
),
None,
)

node_id = int(identifier[1]) if identifier is not None else None

if node_id is None or node_id not in client.driver.controller.nodes:
raise ValueError("Device node can't be found")

return client.driver.controller.nodes[node_id]


@callback
def async_get_node_from_entity_id(hass: HomeAssistant, entity_id: str) -> ZwaveNode:
"""
Get node from an entity ID.

Raises ValueError if entity is invalid.
"""
entity_entry = async_get_ent_reg(hass).async_get(entity_id)

if not entity_entry:
raise ValueError("Entity ID is not valid")

if entity_entry.platform != DOMAIN:
raise ValueError("Entity is not from zwave_js integration")

# Assert for mypy, safe because we know that zwave_js entities are always
# tied to a device
assert entity_entry.device_id
return async_get_node_from_device_id(hass, entity_entry.device_id)
110 changes: 110 additions & 0 deletions homeassistant/components/zwave_js/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Methods and classes related to executing Z-Wave commands and publishing these to hass."""

import logging
from typing import Dict, Set, Union

import voluptuous as vol
from zwave_js_server.model.node import Node as ZwaveNode
from zwave_js_server.util.node import async_set_config_parameter

from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
import homeassistant.helpers.config_validation as cv

from . import const
from .helpers import async_get_node_from_device_id, async_get_node_from_entity_id

_LOGGER = logging.getLogger(__name__)


def parameter_name_does_not_need_bitmask(
val: Dict[str, Union[int, str]]
) -> Dict[str, Union[int, str]]:
"""Validate that if a parameter name is provided, bitmask is not as well."""
if isinstance(val[const.ATTR_CONFIG_PARAMETER], str) and (
val.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
):
raise vol.Invalid(
"Don't include a bitmask when a parameter name is specified",
path=[const.ATTR_CONFIG_PARAMETER, const.ATTR_CONFIG_PARAMETER_BITMASK],
)
return val


# Validates that a bitmask is provided in hex form and converts it to decimal
# int equivalent since that's what the library uses
BITMASK_SCHEMA = vol.All(
cv.string, vol.Lower, vol.Match(r"^(0x)?[0-9a-f]+$"), lambda value: int(value, 16)
)


class ZWaveServices:
"""Class that holds our services (Zwave Commands) that should be published to hass."""

def __init__(self, hass: HomeAssistant):
"""Initialize with hass object."""
self._hass = hass

@callback
def async_register(self) -> None:
"""Register all our services."""
self._hass.services.async_register(
const.DOMAIN,
const.SERVICE_SET_CONFIG_PARAMETER,
self.async_set_config_parameter,
schema=vol.All(
{
vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any(
vol.Coerce(int), cv.string
),
vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any(
raman325 marked this conversation as resolved.
Show resolved Hide resolved
vol.Coerce(int), BITMASK_SCHEMA
),
vol.Required(const.ATTR_CONFIG_VALUE): vol.Any(
vol.Coerce(int), cv.string
),
},
cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID),
parameter_name_does_not_need_bitmask,
),
)

async def async_set_config_parameter(self, service: ServiceCall) -> None:
"""Set a config value on a node."""
nodes: Set[ZwaveNode] = set()
raman325 marked this conversation as resolved.
Show resolved Hide resolved
if ATTR_ENTITY_ID in service.data:
nodes |= {
async_get_node_from_entity_id(self._hass, entity_id)
for entity_id in service.data[ATTR_ENTITY_ID]
}
if ATTR_DEVICE_ID in service.data:
nodes |= {
async_get_node_from_device_id(self._hass, device_id)
for device_id in service.data[ATTR_DEVICE_ID]
}
property_or_property_name = service.data[const.ATTR_CONFIG_PARAMETER]
property_key = service.data.get(const.ATTR_CONFIG_PARAMETER_BITMASK)
new_value = service.data[const.ATTR_CONFIG_VALUE]

for node in nodes:
zwave_value = await async_set_config_parameter(
node,
new_value,
property_or_property_name,
property_key=property_key,
)

if zwave_value:
_LOGGER.info(
"Set configuration parameter %s on Node %s with value %s",
zwave_value,
node,
new_value,
)
else:
raise ValueError(
f"Unable to set configuration parameter on Node {node} with "
f"value {new_value}"
)
28 changes: 28 additions & 0 deletions homeassistant/components/zwave_js/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,31 @@ set_lock_usercode:
example: 1234
raman325 marked this conversation as resolved.
Show resolved Hide resolved
selector:
text:

set_config_parameter:
name: Set a Z-Wave device configuration parameter
description: Allow for changing configuration parameters of your Z-Wave devices.
target:
entity:
integration: zwave_js
fields:
parameter:
name: Parameter
description: The (name or id of the) configuration parameter you want to configure.
example: Minimum brightness level
required: true
selector:
text:
value:
name: Value
description: The new value to set for this configuration parameter.
example: 5
required: true
selector:
object:
bitmask:
name: Bitmask
description: Target a specific bitmask (see the documentation for more information).
advanced: true
selector:
object:
2 changes: 1 addition & 1 deletion tests/components/zwave_js/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from homeassistant.components.hassio.handler import HassioAPIError
from homeassistant.components.zwave_js.const import DOMAIN
from homeassistant.components.zwave_js.entity import get_device_id
from homeassistant.components.zwave_js.helpers import get_device_id
from homeassistant.config_entries import (
CONN_CLASS_LOCAL_PUSH,
ENTRY_STATE_LOADED,
Expand Down
Loading