File: lock.py

package info (click to toggle)
zwave-js-server-python 0.67.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,820 kB
  • sloc: python: 15,886; sh: 21; javascript: 16; makefile: 2
file content (227 lines) | stat: -rw-r--r-- 7,422 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
"""Utility functions for Z-Wave JS locks."""

from __future__ import annotations

from typing import TypedDict, cast

from ..const import CommandClass
from ..const.command_class.lock import (
    ATTR_CODE_SLOT,
    ATTR_IN_USE,
    ATTR_NAME,
    ATTR_USERCODE,
    CURRENT_AUTO_RELOCK_TIME_PROPERTY,
    CURRENT_BLOCK_TO_BLOCK_PROPERTY,
    CURRENT_HOLD_AND_RELEASE_TIME_PROPERTY,
    CURRENT_TWIST_ASSIST_PROPERTY,
    LOCK_USERCODE_ID_PROPERTY,
    LOCK_USERCODE_PROPERTY,
    LOCK_USERCODE_STATUS_PROPERTY,
    CodeSlotStatus,
    DoorLockCCConfigurationSetOptions,
    OperationType,
)
from ..exceptions import NotFoundError
from ..model.endpoint import Endpoint
from ..model.node import Node
from ..model.value import SetValueResult, SupervisionResult, Value, get_value_id_str


def get_code_slot_value(node: Node, code_slot: int, property_name: str) -> Value:
    """Get a code slot value."""
    value = node.values.get(
        get_value_id_str(
            node,
            CommandClass.USER_CODE,
            property_name,
            endpoint=0,
            property_key=code_slot,
        )
    )

    if not value:
        raise NotFoundError(f"{property_name} for code slot {code_slot} not found")

    return value


class CodeSlot(TypedDict, total=False):
    """Represent a code slot."""

    code_slot: int  # required
    name: str  # required
    in_use: bool | None  # required
    usercode: str | None


def _get_code_slots(node: Node, include_usercode: bool = False) -> list[CodeSlot]:
    """Get all code slots on the lock and optionally include usercode."""
    code_slot = 1
    slots: list[CodeSlot] = []

    # Loop until we can't find a code slot
    while True:
        try:
            value = get_code_slot_value(node, code_slot, LOCK_USERCODE_PROPERTY)
            status_value = get_code_slot_value(
                node, code_slot, LOCK_USERCODE_STATUS_PROPERTY
            )
        except NotFoundError:
            return slots

        code_slot = int(value.property_key)  # type: ignore[arg-type]
        in_use = (
            None
            if status_value.value is None
            else status_value.value == CodeSlotStatus.ENABLED
        )

        # we know that code slots will always have a property key
        # that is an int, so we can ignore mypy
        slot = {
            ATTR_CODE_SLOT: code_slot,
            ATTR_NAME: value.metadata.label,
            ATTR_IN_USE: in_use,
        }
        if include_usercode:
            slot[ATTR_USERCODE] = value.value

        slots.append(cast(CodeSlot, slot))
        code_slot += 1


def get_code_slots(node: Node) -> list[CodeSlot]:
    """Get all code slots on the lock and whether or not they are used."""
    return _get_code_slots(node, False)


def get_usercodes(node: Node) -> list[CodeSlot]:
    """Get all code slots and usercodes on the lock."""
    return _get_code_slots(node, True)


def get_usercode(node: Node, code_slot: int) -> CodeSlot:
    """Get usercode from slot X on the lock."""
    value = get_code_slot_value(node, code_slot, LOCK_USERCODE_PROPERTY)
    status_value = get_code_slot_value(node, code_slot, LOCK_USERCODE_STATUS_PROPERTY)

    code_slot = int(value.property_key)  # type: ignore[arg-type]
    in_use = (
        None
        if status_value.value is None
        else status_value.value == CodeSlotStatus.ENABLED
    )

    return cast(
        CodeSlot,
        {
            ATTR_CODE_SLOT: code_slot,
            ATTR_NAME: value.metadata.label,
            ATTR_IN_USE: in_use,
            ATTR_USERCODE: value.value,
        },
    )


async def get_usercode_from_node(node: Node, code_slot: int) -> CodeSlot:
    """
    Fetch a usercode directly from a node.

    Should be used when Z-Wave JS's ValueDB hasn't been populated for this code slot.
    This call will populate the ValueDB and trigger value update events from the
    driver.
    """
    # https://zwave-js.github.io/node-zwave-js/#/api/CCs/UserCode?id=get
    await node.async_invoke_cc_api(
        CommandClass.USER_CODE, "get", code_slot, wait_for_result=True
    )
    return get_usercode(node, code_slot)


async def set_usercode(
    node: Node, code_slot: int, usercode: str
) -> SetValueResult | None:
    """Set the usercode to index X on the lock."""
    value = get_code_slot_value(node, code_slot, LOCK_USERCODE_PROPERTY)
    return await node.async_set_value(value, usercode)


async def set_usercodes(node: Node, codes: dict[int, str]) -> SupervisionResult | None:
    """Set the usercode to index X on the lock."""
    cc_api_codes = [
        {
            LOCK_USERCODE_ID_PROPERTY: code_slot,
            LOCK_USERCODE_STATUS_PROPERTY: CodeSlotStatus.ENABLED,
            LOCK_USERCODE_PROPERTY: usercode,
        }
        for code_slot, usercode in codes.items()
    ]
    # https://zwave-js.github.io/node-zwave-js/#/api/CCs/UserCode?id=setmany
    data = await node.async_invoke_cc_api(
        CommandClass.USER_CODE, "setMany", cc_api_codes, wait_for_result=True
    )

    if not data:
        raise ValueError("Received unexpected response from User Code CC setMany API")

    return SupervisionResult(data)


async def clear_usercode(node: Node, code_slot: int) -> SetValueResult | None:
    """Clear a code slot on the lock."""
    value = get_code_slot_value(node, code_slot, LOCK_USERCODE_STATUS_PROPERTY)
    return await node.async_set_value(value, CodeSlotStatus.AVAILABLE)


async def set_configuration(
    endpoint: Endpoint, configuration: DoorLockCCConfigurationSetOptions
) -> SupervisionResult | None:
    """Set lock configuration."""
    # It is invalid to set the operation to timed with no timeout, or to constant
    # with a timeout
    if (configuration.operation_type == OperationType.CONSTANT) ^ (
        configuration.lock_timeout_configuration is None
    ):
        raise ValueError(
            "Invalid operation type and lock timeout configuration combination"
        )
    errors: list[str] = []

    for property_name, attr_name in (
        (CURRENT_AUTO_RELOCK_TIME_PROPERTY, "auto_relock_time"),
        (CURRENT_HOLD_AND_RELEASE_TIME_PROPERTY, "hold_and_release_time"),
        (CURRENT_TWIST_ASSIST_PROPERTY, "twist_assist"),
        (CURRENT_BLOCK_TO_BLOCK_PROPERTY, "block_to_block"),
    ):
        # It a value for a particular configuration value is not provided and it exists
        # on the node, use the cached value
        cached_value = next(
            (
                value
                for value in endpoint.values.values()
                if value.command_class == CommandClass.DOOR_LOCK
                and value.property_name == property_name
            ),
            None,
        )
        if (
            val := getattr(configuration, attr_name)
        ) is not None and cached_value is None:
            errors.append(
                f"- Can't provide value for {property_name} since it is unsupported"
            )
        elif cached_value is not None and val is None and not errors:
            setattr(configuration, attr_name, cached_value.value)

    if errors:
        raise ValueError("\n".join(errors))

    # https://zwave-js.github.io/node-zwave-js/#/api/CCs/UserCode?id=setconfiguration
    data = await endpoint.async_invoke_cc_api(
        CommandClass.DOOR_LOCK, "setConfiguration", configuration.to_dict()
    )

    if not data:
        return None

    return SupervisionResult(data)