File: miot_device.py

package info (click to toggle)
python-miio 0.5.12-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,888 kB
  • sloc: python: 23,425; makefile: 9
file content (185 lines) | stat: -rw-r--r-- 5,896 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
import logging
import sys
from enum import Enum
from functools import partial
from typing import Any, Dict, Union

import click

from .click_common import EnumType, LiteralParamType, command
from .device import Device, DeviceStatus  # noqa: F401
from .exceptions import DeviceException

if sys.version_info >= (3, 11):
    from enum import member

_LOGGER = logging.getLogger(__name__)


# partial is required here for str2bool, see https://stackoverflow.com/a/40339397
class MiotValueType(Enum):
    def _str2bool(x):
        """Helper to convert string to boolean."""
        return x.lower() in ("true", "1")

    Int = int
    Float = float

    if sys.version_info >= (3, 11):
        Bool = member(partial(_str2bool))
    else:
        Bool = partial(_str2bool)

    Str = str


MiotMapping = Dict[str, Dict[str, Any]]


class MiotDevice(Device):
    """Main class representing a MIoT device.

    The inheriting class should use the `_mappings` to set the `MiotMapping` keyed by
    the model names to inform which mapping is to be used for methods contained in this
    class. Defining the mappiong using `mapping` class variable is deprecated but
    remains in-place for backwards compatibility.
    """

    mapping: MiotMapping  # Deprecated, use _mappings instead
    _mappings: Dict[str, MiotMapping] = {}

    def __init__(
        self,
        ip: str = None,
        token: str = None,
        start_id: int = 0,
        debug: int = 0,
        lazy_discover: bool = True,
        timeout: int = None,
        *,
        model: str = None,
        mapping: MiotMapping = None,
    ):
        """Overloaded to accept keyword-only `mapping` parameter."""
        super().__init__(
            ip, token, start_id, debug, lazy_discover, timeout, model=model
        )

        if mapping is None and not hasattr(self, "mapping") and not self._mappings:
            _LOGGER.warning("Neither the class nor the parameter defines the mapping")

        if mapping is not None:
            self.mapping = mapping

    def get_properties_for_mapping(self, *, max_properties=15) -> list:
        """Retrieve raw properties based on mapping."""

        # We send property key in "did" because it's sent back via response and we can identify the property.
        mapping = self._get_mapping()
        properties = [{"did": k, **v} for k, v in mapping.items() if "aiid" not in v]

        return self.get_properties(
            properties, property_getter="get_properties", max_properties=max_properties
        )

    @command(
        click.argument("name", type=str),
        click.argument("params", type=LiteralParamType(), required=False),
    )
    def call_action(self, name: str, params=None):
        """Call an action by a name in the mapping."""
        mapping = self._get_mapping()
        if name not in mapping:
            raise DeviceException(f"Unable to find {name} in the mapping")

        action = mapping[name]

        if "siid" not in action or "aiid" not in action:
            raise DeviceException(f"{name} is not an action (missing siid or aiid)")

        return self.call_action_by(action["siid"], action["aiid"], params)

    @command(
        click.argument("siid", type=int),
        click.argument("aiid", type=int),
        click.argument("params", type=LiteralParamType(), required=False),
    )
    def call_action_by(self, siid, aiid, params=None):
        """Call an action."""
        if params is None:
            params = []
        payload = {
            "did": f"call-{siid}-{aiid}",
            "siid": siid,
            "aiid": aiid,
            "in": params,
        }

        return self.send("action", payload)

    @command(
        click.argument("siid", type=int),
        click.argument("piid", type=int),
    )
    def get_property_by(self, siid: int, piid: int):
        """Get a single property (siid/piid)."""
        return self.send(
            "get_properties", [{"did": f"{siid}-{piid}", "siid": siid, "piid": piid}]
        )

    @command(
        click.argument("siid", type=int),
        click.argument("piid", type=int),
        click.argument("value"),
        click.argument(
            "value_type", type=EnumType(MiotValueType), required=False, default=None
        ),
    )
    def set_property_by(
        self,
        siid: int,
        piid: int,
        value: Union[int, float, str, bool],
        value_type: Any = None,
    ):
        """Set a single property (siid/piid) to given value.

        value_type can be given to convert the value to wanted type, allowed types are:
        int, float, bool, str
        """
        if value_type is not None:
            value = value_type.value(value)

        return self.send(
            "set_properties",
            [{"did": f"set-{siid}-{piid}", "siid": siid, "piid": piid, "value": value}],
        )

    def set_property(self, property_key: str, value):
        """Sets property value using the existing mapping."""
        mapping = self._get_mapping()
        return self.send(
            "set_properties",
            [{"did": property_key, **mapping[property_key], "value": value}],
        )

    def _get_mapping(self) -> MiotMapping:
        """Return the protocol mapping to use.

        The logic is as follows:
        1. Use device model as key to lookup _mappings for the mapping
        2. If no match is found, but _mappings is defined, use the first item
        3. Fallback to class-defined `mapping` for backwards compat
        """
        if not self._mappings:
            return self.mapping
        mapping = self._mappings.get(self.model)
        if mapping is not None:
            return mapping

        first_model, first_mapping = list(self._mappings.items())[0]
        _LOGGER.warning(
            "Unable to find mapping for %s, falling back to %s", self.model, first_model
        )

        return first_mapping