pywemo 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywemo/README.md +69 -0
- pywemo/__init__.py +33 -0
- pywemo/color.py +79 -0
- pywemo/discovery.py +194 -0
- pywemo/exceptions.py +94 -0
- pywemo/ouimeaux_device/LICENSE +12 -0
- pywemo/ouimeaux_device/__init__.py +679 -0
- pywemo/ouimeaux_device/api/__init__.py +1 -0
- pywemo/ouimeaux_device/api/attributes.py +131 -0
- pywemo/ouimeaux_device/api/db_orm.py +197 -0
- pywemo/ouimeaux_device/api/long_press.py +168 -0
- pywemo/ouimeaux_device/api/rules_db.py +467 -0
- pywemo/ouimeaux_device/api/service.py +363 -0
- pywemo/ouimeaux_device/api/wemo_services.py +25 -0
- pywemo/ouimeaux_device/api/wemo_services.pyi +241 -0
- pywemo/ouimeaux_device/api/xsd/__init__.py +1 -0
- pywemo/ouimeaux_device/api/xsd/device.py +3888 -0
- pywemo/ouimeaux_device/api/xsd/device.xsd +95 -0
- pywemo/ouimeaux_device/api/xsd/service.py +3872 -0
- pywemo/ouimeaux_device/api/xsd/service.xsd +93 -0
- pywemo/ouimeaux_device/api/xsd_types.py +222 -0
- pywemo/ouimeaux_device/bridge.py +506 -0
- pywemo/ouimeaux_device/coffeemaker.py +92 -0
- pywemo/ouimeaux_device/crockpot.py +157 -0
- pywemo/ouimeaux_device/dimmer.py +70 -0
- pywemo/ouimeaux_device/humidifier.py +223 -0
- pywemo/ouimeaux_device/insight.py +191 -0
- pywemo/ouimeaux_device/lightswitch.py +11 -0
- pywemo/ouimeaux_device/maker.py +54 -0
- pywemo/ouimeaux_device/motion.py +6 -0
- pywemo/ouimeaux_device/outdoor_plug.py +6 -0
- pywemo/ouimeaux_device/switch.py +32 -0
- pywemo/py.typed +0 -0
- pywemo/ssdp.py +372 -0
- pywemo/subscribe.py +782 -0
- pywemo/util.py +139 -0
- pywemo-1.4.0.dist-info/LICENSE +54 -0
- pywemo-1.4.0.dist-info/METADATA +192 -0
- pywemo-1.4.0.dist-info/RECORD +40 -0
- pywemo-1.4.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Attribute device helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from typing import Any, get_type_hints
|
|
6
|
+
|
|
7
|
+
from lxml import etree as et
|
|
8
|
+
|
|
9
|
+
from ..switch import Switch
|
|
10
|
+
from .service import RequiredService
|
|
11
|
+
from .xsd_types import quote_xml
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AttributeDevice(Switch):
|
|
17
|
+
"""Handles all parsing/getting/setting of attribute lists.
|
|
18
|
+
|
|
19
|
+
This is intended to be used as the base class for all devices that support
|
|
20
|
+
the deviceevent.GetAttributes() method.
|
|
21
|
+
|
|
22
|
+
Subclasses can use the _attributes property to fetch the string values of
|
|
23
|
+
all attributes. Subclasses must provide the name of the property to use
|
|
24
|
+
for self._state in a property named _state_property. Subclasses must also
|
|
25
|
+
define a TypedDict to hold the attributes and add the TypedDict subclass as
|
|
26
|
+
a type hint for the _attributes property of the class.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
EVENT_TYPE_ATTRIBUTE_LIST = "attributeList"
|
|
30
|
+
|
|
31
|
+
_state_property: str
|
|
32
|
+
|
|
33
|
+
_attr_name = "_attributes"
|
|
34
|
+
"""Name of the TypedDict attribute that holds values for this device."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
37
|
+
"""Create a Attributes device."""
|
|
38
|
+
assert isinstance(self._state_property, str)
|
|
39
|
+
setattr(self, self._attr_name, {})
|
|
40
|
+
class_hints = get_type_hints(type(self))
|
|
41
|
+
assert (attr_type := class_hints.get(self._attr_name)) is not None
|
|
42
|
+
self._attribute_type_hints = get_type_hints(attr_type)
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
self.update_attributes()
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def _required_services(self) -> list[RequiredService]:
|
|
48
|
+
return super()._required_services + [
|
|
49
|
+
RequiredService(
|
|
50
|
+
name="deviceevent", actions=["GetAttributes", "SetAttributes"]
|
|
51
|
+
),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
def _update_attributes_dict(self, xml_blob: str) -> None:
|
|
55
|
+
xml_blob = "<attributes>" + xml_blob + "</attributes>"
|
|
56
|
+
xml_blob = xml_blob.replace(">", ">")
|
|
57
|
+
xml_blob = xml_blob.replace("<", "<")
|
|
58
|
+
|
|
59
|
+
for attribute in et.fromstring(
|
|
60
|
+
xml_blob, parser=et.XMLParser(resolve_entities=False)
|
|
61
|
+
):
|
|
62
|
+
if len(attribute) < 2:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"Too few elements: {et.tostring(attribute).decode()}"
|
|
65
|
+
)
|
|
66
|
+
if (key := attribute[0].text) is None:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Key is not present: {et.tostring(attribute[0]).decode()}"
|
|
69
|
+
)
|
|
70
|
+
if (value := attribute[1].text) is None:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"Value is not present: "
|
|
73
|
+
f"{et.tostring(attribute[1]).decode()}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if (constructor := self._attribute_type_hints.get(key)) is None:
|
|
77
|
+
continue # Ignore unexpected attributes
|
|
78
|
+
try:
|
|
79
|
+
getattr(self, self._attr_name)[key] = constructor(value)
|
|
80
|
+
except (TypeError, ValueError) as err:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Unexpected value for {key}: {value}"
|
|
83
|
+
) from err
|
|
84
|
+
|
|
85
|
+
state: int | None = getattr(self, self._state_property)
|
|
86
|
+
self._state = state
|
|
87
|
+
|
|
88
|
+
def update_attributes(self) -> None:
|
|
89
|
+
"""Request state from device."""
|
|
90
|
+
resp = self.deviceevent.GetAttributes().get(
|
|
91
|
+
self.EVENT_TYPE_ATTRIBUTE_LIST
|
|
92
|
+
)
|
|
93
|
+
assert resp is not None
|
|
94
|
+
self._update_attributes_dict(resp)
|
|
95
|
+
|
|
96
|
+
def subscription_update(self, _type: str, _params: str) -> bool:
|
|
97
|
+
"""Handle subscription push-events from device."""
|
|
98
|
+
if _type == self.EVENT_TYPE_ATTRIBUTE_LIST:
|
|
99
|
+
try:
|
|
100
|
+
self._update_attributes_dict(_params)
|
|
101
|
+
except (et.XMLSyntaxError, ValueError) as err:
|
|
102
|
+
LOG.error(
|
|
103
|
+
"Unexpected %s value `%s` for device %s: %s",
|
|
104
|
+
self.EVENT_TYPE_ATTRIBUTE_LIST,
|
|
105
|
+
_params,
|
|
106
|
+
self.name,
|
|
107
|
+
repr(err),
|
|
108
|
+
)
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
return super().subscription_update(_type, _params)
|
|
112
|
+
|
|
113
|
+
def get_state(self, force_update: bool = False) -> int:
|
|
114
|
+
"""Return 0 if off and 1 if on."""
|
|
115
|
+
if force_update or self._state is None:
|
|
116
|
+
self.update_attributes()
|
|
117
|
+
|
|
118
|
+
assert self._state is not None
|
|
119
|
+
return self._state
|
|
120
|
+
|
|
121
|
+
def _set_attributes(self, *args: tuple[str, str | int | float]) -> None:
|
|
122
|
+
"""Set the specified attributes on the device."""
|
|
123
|
+
attribute_xml = "</attribute><attribute>".join(
|
|
124
|
+
f"<name>{name}</name><value>{value}</value>"
|
|
125
|
+
for name, value in args
|
|
126
|
+
)
|
|
127
|
+
attribute_xml = f"<attribute>{attribute_xml}</attribute>"
|
|
128
|
+
self.deviceevent.SetAttributes(attributeList=quote_xml(attribute_xml))
|
|
129
|
+
|
|
130
|
+
# Refresh the device state
|
|
131
|
+
self.get_state(True)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Light-weight mapping between sqlite3 and python data structures."""
|
|
2
|
+
import logging
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Any, Callable, Dict
|
|
5
|
+
|
|
6
|
+
LOG = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseRow:
|
|
10
|
+
"""Base class for sqlite Row schemas."""
|
|
11
|
+
|
|
12
|
+
TABLE_NAME: str
|
|
13
|
+
FIELDS: Dict[str, Callable[[Any], Any]]
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
"""Initialize a row with the supplied values."""
|
|
17
|
+
for key, value in kwargs.items():
|
|
18
|
+
if key not in self.FIELDS:
|
|
19
|
+
raise AttributeError(
|
|
20
|
+
f"{key} is not a valid attribute of {type(self).__name__}"
|
|
21
|
+
)
|
|
22
|
+
setattr(self, key, value)
|
|
23
|
+
self._modified = False
|
|
24
|
+
|
|
25
|
+
def __setattr__(self, name, value):
|
|
26
|
+
"""Update one of the attributes of the Row."""
|
|
27
|
+
if name in self.FIELDS and value is not None:
|
|
28
|
+
super().__setattr__(name, self.FIELDS[name](value))
|
|
29
|
+
else:
|
|
30
|
+
super().__setattr__(name, value)
|
|
31
|
+
if name != "_modified":
|
|
32
|
+
super().__setattr__("_modified", True)
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
"""Return a string representation of the Row."""
|
|
36
|
+
values = []
|
|
37
|
+
for name in self.FIELDS.keys():
|
|
38
|
+
if hasattr(self, name):
|
|
39
|
+
values.append(f"{name}={repr(getattr(self, name))}")
|
|
40
|
+
class_name = self.__class__.__name__
|
|
41
|
+
values_str = ", ".join(values)
|
|
42
|
+
return f"{class_name}({values_str})"
|
|
43
|
+
|
|
44
|
+
def __eq__(self, other):
|
|
45
|
+
"""Test for equality between two instances."""
|
|
46
|
+
return isinstance(other, self.__class__) and repr(self) == repr(other)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def modified(self):
|
|
50
|
+
"""Return True if any fields in the Row have been modified."""
|
|
51
|
+
return self._modified
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_sqlite_row(cls, row):
|
|
55
|
+
"""Initialize a Row from a sqllite cursor row."""
|
|
56
|
+
kwargs = {}
|
|
57
|
+
for key in row.keys():
|
|
58
|
+
kwargs[key] = row[key]
|
|
59
|
+
return cls(**kwargs)
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def select_all(cls, cursor):
|
|
63
|
+
"""Select all Row entries from the underlying sqlite table."""
|
|
64
|
+
names = ",".join(cls.FIELDS.keys())
|
|
65
|
+
cursor.execute(f"SELECT {names} FROM {cls.TABLE_NAME}")
|
|
66
|
+
for row in cursor.fetchall():
|
|
67
|
+
yield cls.from_sqlite_row(row)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def create_sqlite_table_from_row_schema(cls, cursor):
|
|
71
|
+
"""Create a sqlite table based on the schema for this Row class."""
|
|
72
|
+
fields = []
|
|
73
|
+
for name, value in cls.FIELDS.items():
|
|
74
|
+
if isinstance(value, SQLType):
|
|
75
|
+
fields.append(f"{name} {value.sql_type}")
|
|
76
|
+
else:
|
|
77
|
+
value = SQLType.TYPE_MAP[value]
|
|
78
|
+
fields.append(f"{name} {value}")
|
|
79
|
+
fields_str = ", ".join(fields)
|
|
80
|
+
sql = f"CREATE TABLE {cls.TABLE_NAME}({fields_str})"
|
|
81
|
+
try:
|
|
82
|
+
cursor.execute(sql)
|
|
83
|
+
except sqlite3.Error:
|
|
84
|
+
LOG.exception("Query failed: %s", sql)
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
def primary_key_name(self):
|
|
88
|
+
"""Return the primary key for this Row."""
|
|
89
|
+
for name, value in self.FIELDS.items():
|
|
90
|
+
if isinstance(value, PrimaryKey):
|
|
91
|
+
return name
|
|
92
|
+
raise RuntimeError(f"No primary key for table {self.TABLE_NAME}")
|
|
93
|
+
|
|
94
|
+
def primary_key_value(self):
|
|
95
|
+
"""Return the value of the primary key for this Row."""
|
|
96
|
+
return getattr(self, self.primary_key_name())
|
|
97
|
+
|
|
98
|
+
def update_db(self, cursor):
|
|
99
|
+
"""Update the sqlite database to reflect any changes to this Row."""
|
|
100
|
+
column_list = []
|
|
101
|
+
values = []
|
|
102
|
+
for name in self.FIELDS.keys():
|
|
103
|
+
if hasattr(self, name):
|
|
104
|
+
column_list.append(name)
|
|
105
|
+
values.append(getattr(self, name))
|
|
106
|
+
column_list_str = ", ".join(column_list)
|
|
107
|
+
value_placeholders = ", ".join(["?"] * len(values))
|
|
108
|
+
sql = (
|
|
109
|
+
f"INSERT OR REPLACE INTO {self.TABLE_NAME} ({column_list_str}) "
|
|
110
|
+
f"VALUES ({value_placeholders})"
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
cursor.execute(sql, values)
|
|
114
|
+
except sqlite3.Error:
|
|
115
|
+
LOG.exception("Query failed: %s %s", sql, values)
|
|
116
|
+
raise
|
|
117
|
+
try:
|
|
118
|
+
pk_name = self.primary_key_name()
|
|
119
|
+
except RuntimeError:
|
|
120
|
+
pass
|
|
121
|
+
else:
|
|
122
|
+
pk_type = self.FIELDS[pk_name]
|
|
123
|
+
if pk_type.auto_increment and not hasattr(self, pk_name):
|
|
124
|
+
setattr(self, pk_name, cursor.lastrowid)
|
|
125
|
+
|
|
126
|
+
def remove_from_db(self, cursor):
|
|
127
|
+
"""Remove the Row from the sqlite database."""
|
|
128
|
+
pk_name = self.primary_key_name()
|
|
129
|
+
pk_value = self.primary_key_value()
|
|
130
|
+
if pk_value is None:
|
|
131
|
+
sql = f"DELETE FROM {self.TABLE_NAME} WHERE {pk_name} IS NULL"
|
|
132
|
+
values = ()
|
|
133
|
+
else:
|
|
134
|
+
sql = f"DELETE FROM {self.TABLE_NAME} WHERE {pk_name}=?"
|
|
135
|
+
values = (pk_value,)
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
cursor.execute(sql, values)
|
|
139
|
+
except sqlite3.Error:
|
|
140
|
+
LOG.exception("Query failed: %s %s", sql, pk_value)
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SQLType:
|
|
145
|
+
"""Base class for custom sqlite schema types."""
|
|
146
|
+
|
|
147
|
+
TYPE_MAP = {
|
|
148
|
+
int: "INTEGER",
|
|
149
|
+
float: "REAL",
|
|
150
|
+
str: "TEXT",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
def __init__(self, type_constructor, *, sql_type=None, not_null=False):
|
|
154
|
+
"""Create a SQLType.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
type_constructor: Callable(value) that will receive the value from
|
|
158
|
+
the database cursor and convert it to a Python type.
|
|
159
|
+
sql_type: The sqlite field type.
|
|
160
|
+
not_null: Whether or not the sqlite field can be null.
|
|
161
|
+
"""
|
|
162
|
+
self.type_constructor = type_constructor
|
|
163
|
+
self._sql_type = (
|
|
164
|
+
sql_type
|
|
165
|
+
if sql_type is not None
|
|
166
|
+
else SQLType.TYPE_MAP[type_constructor]
|
|
167
|
+
)
|
|
168
|
+
self.not_null = not_null
|
|
169
|
+
|
|
170
|
+
def __call__(self, value):
|
|
171
|
+
"""Convert the sqlite row value to a Python type."""
|
|
172
|
+
return self.type_constructor(value)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def sql_type(self):
|
|
176
|
+
"""Return the sqlite type name for this type."""
|
|
177
|
+
if self.not_null:
|
|
178
|
+
return f"{self._sql_type} NOT NULL".lstrip()
|
|
179
|
+
return self._sql_type.lstrip()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class PrimaryKey(SQLType): # pylint: disable=too-few-public-methods
|
|
183
|
+
"""Class used to indicate the primary key field for a Row."""
|
|
184
|
+
|
|
185
|
+
def __init__(self, *args, auto_increment=False, **kwargs):
|
|
186
|
+
"""Create a PrimaryKey instance."""
|
|
187
|
+
super().__init__(*args, **kwargs)
|
|
188
|
+
self.auto_increment = auto_increment
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def sql_type(self):
|
|
192
|
+
"""Return the sqlite type name for this type."""
|
|
193
|
+
value = super().sql_type
|
|
194
|
+
value = f"{value} PRIMARY KEY"
|
|
195
|
+
if self.auto_increment:
|
|
196
|
+
value = f"{value} AUTOINCREMENT"
|
|
197
|
+
return value.lstrip()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Methods to make changes to the long press rules for a device.
|
|
2
|
+
|
|
3
|
+
Wemo devices store a database of rules that configure actions for the device. A
|
|
4
|
+
long press rule is activated when the button on the device is pressed for 2
|
|
5
|
+
seconds. A person can press the button for 2 seconds and, based on the rules
|
|
6
|
+
configured for the device, it will turn on/off/toggle other Wemo devices on the
|
|
7
|
+
network. The methods in this mixin allow editing of the devices that are
|
|
8
|
+
controlled by a long press.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Iterable, no_type_check
|
|
15
|
+
|
|
16
|
+
from .rules_db import RuleDevicesRow, RulesDb, RulesRow, rules_db_from_device
|
|
17
|
+
from .service import RequiredService, RequiredServicesMixin
|
|
18
|
+
|
|
19
|
+
LOG = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# RulesRow.Type values.
|
|
22
|
+
RULE_TYPE_LONG_PRESS = "Long Press"
|
|
23
|
+
|
|
24
|
+
VIRTUAL_DEVICE_UDN = "uuid:Socket-1_0-PyWemoVirtualDevice"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ActionType(Enum):
|
|
28
|
+
"""Action to perform when a long press rule is triggered."""
|
|
29
|
+
|
|
30
|
+
TOGGLE = 2.0
|
|
31
|
+
ON = 1.0
|
|
32
|
+
OFF = 0.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@no_type_check
|
|
36
|
+
def ensure_long_press_rule_exists(
|
|
37
|
+
rules_db: RulesDb, device_name: str, device_udn: str
|
|
38
|
+
) -> RulesRow:
|
|
39
|
+
"""Ensure that a long press rule exists and is enabled for the device.
|
|
40
|
+
|
|
41
|
+
Returns the long press rule.
|
|
42
|
+
"""
|
|
43
|
+
current_rules = rules_db.rules_for_device(rule_type=RULE_TYPE_LONG_PRESS)
|
|
44
|
+
for rule, _ in current_rules:
|
|
45
|
+
if rule.State != "1":
|
|
46
|
+
LOG.info("Enabling long press rule for device %s", device_name)
|
|
47
|
+
rule.State = "1"
|
|
48
|
+
rule.update_db(rules_db.cursor())
|
|
49
|
+
return rule
|
|
50
|
+
|
|
51
|
+
LOG.info("Adding long press rule for device %s", device_name)
|
|
52
|
+
rule_orders = (
|
|
53
|
+
r.RuleOrder for r in rules_db.rules.values() if r.RuleOrder is not None
|
|
54
|
+
)
|
|
55
|
+
new_rule = RulesRow(
|
|
56
|
+
Name=f"{device_name} Long Press Rule",
|
|
57
|
+
Type=RULE_TYPE_LONG_PRESS,
|
|
58
|
+
RuleOrder=max(rule_orders, default=-1) + 1,
|
|
59
|
+
StartDate="12201982",
|
|
60
|
+
EndDate="07301982",
|
|
61
|
+
State="1",
|
|
62
|
+
Sync="NOSYNC",
|
|
63
|
+
)
|
|
64
|
+
rules_db.add_rule(new_rule)
|
|
65
|
+
rules_db.add_rule_devices(
|
|
66
|
+
RuleDevicesRow(
|
|
67
|
+
RuleID=new_rule.RuleID, # pylint: disable=no-member
|
|
68
|
+
DeviceID=device_udn,
|
|
69
|
+
GroupID=0,
|
|
70
|
+
DayID=-1,
|
|
71
|
+
StartTime=60,
|
|
72
|
+
RuleDuration=86340,
|
|
73
|
+
StartAction=ActionType.TOGGLE.value,
|
|
74
|
+
EndAction=-1.0,
|
|
75
|
+
SensorDuration=-1,
|
|
76
|
+
Type=-1,
|
|
77
|
+
Value=-1,
|
|
78
|
+
Level=-1,
|
|
79
|
+
ZBCapabilityStart="",
|
|
80
|
+
ZBCapabilityEnd="",
|
|
81
|
+
OnModeOffset=-1,
|
|
82
|
+
OffModeOffset=-1,
|
|
83
|
+
CountdownTime=-1,
|
|
84
|
+
EndTime=86400,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return new_rule
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class LongPressMixin(RequiredServicesMixin):
|
|
91
|
+
"""Methods to make changes to the long press rules for a device."""
|
|
92
|
+
|
|
93
|
+
EVENT_TYPE_LONG_PRESS = "LongPress"
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def _required_services(self) -> list[RequiredService]:
|
|
97
|
+
return super()._required_services + [
|
|
98
|
+
RequiredService(name="rules", actions=["FetchRules", "StoreRules"])
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
@no_type_check
|
|
102
|
+
def list_long_press_udns(self) -> frozenset[str]:
|
|
103
|
+
"""Return a list of device UDNs that are configured for long press."""
|
|
104
|
+
devices = []
|
|
105
|
+
with rules_db_from_device(self) as rules_db:
|
|
106
|
+
for rule, _ in rules_db.rules_for_device(
|
|
107
|
+
rule_type=RULE_TYPE_LONG_PRESS
|
|
108
|
+
):
|
|
109
|
+
devices.extend(rules_db.get_target_devices_for_rule(rule))
|
|
110
|
+
return frozenset(devices)
|
|
111
|
+
|
|
112
|
+
@no_type_check
|
|
113
|
+
def add_long_press_udns(self, device_udns: Iterable[str]) -> None:
|
|
114
|
+
"""Add a list of device UDNs to be configured for long press."""
|
|
115
|
+
with rules_db_from_device(self) as rules_db:
|
|
116
|
+
rule = ensure_long_press_rule_exists(rules_db, self.name, self.udn)
|
|
117
|
+
for udn in device_udns:
|
|
118
|
+
if not udn:
|
|
119
|
+
continue
|
|
120
|
+
if udn not in rules_db.get_target_devices_for_rule(rule):
|
|
121
|
+
rules_db.add_target_device_to_rule(rule, udn)
|
|
122
|
+
|
|
123
|
+
@no_type_check
|
|
124
|
+
def remove_long_press_udns(self, device_udns: Iterable[str]) -> None:
|
|
125
|
+
"""Remove a list of device UDNs from the long press configuration."""
|
|
126
|
+
with rules_db_from_device(self) as rules_db:
|
|
127
|
+
for rule, _ in rules_db.rules_for_device(
|
|
128
|
+
rule_type=RULE_TYPE_LONG_PRESS
|
|
129
|
+
):
|
|
130
|
+
for udn in device_udns:
|
|
131
|
+
if udn in rules_db.get_target_devices_for_rule(rule):
|
|
132
|
+
rules_db.remove_target_device_from_rule(rule, udn)
|
|
133
|
+
|
|
134
|
+
@no_type_check
|
|
135
|
+
def get_long_press_action(self) -> ActionType | None:
|
|
136
|
+
"""Fetch the ActionType for the long press rule.
|
|
137
|
+
|
|
138
|
+
Will return None if no long press rule is configured for the device.
|
|
139
|
+
"""
|
|
140
|
+
with rules_db_from_device(self) as rules_db:
|
|
141
|
+
for _, device in rules_db.rules_for_device(
|
|
142
|
+
rule_type=RULE_TYPE_LONG_PRESS
|
|
143
|
+
):
|
|
144
|
+
return ActionType(device.StartAction)
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
@no_type_check
|
|
148
|
+
def set_long_press_action(self, action: ActionType) -> None:
|
|
149
|
+
"""Set the ActionType for the long press rule."""
|
|
150
|
+
with rules_db_from_device(self) as rules_db:
|
|
151
|
+
ensure_long_press_rule_exists(rules_db, self.name, self.udn)
|
|
152
|
+
for _, device in rules_db.rules_for_device(
|
|
153
|
+
rule_type=RULE_TYPE_LONG_PRESS
|
|
154
|
+
):
|
|
155
|
+
device.StartAction = action.value
|
|
156
|
+
|
|
157
|
+
def ensure_long_press_virtual_device(self) -> None:
|
|
158
|
+
"""Configure the device to notify pywemo when a long-press happens.
|
|
159
|
+
|
|
160
|
+
The ensure_long_press_virtual_device method ensures that the pywemo
|
|
161
|
+
virtual device is configured in the rules database for when a long
|
|
162
|
+
press rule is triggered.
|
|
163
|
+
"""
|
|
164
|
+
self.add_long_press_udns([VIRTUAL_DEVICE_UDN])
|
|
165
|
+
|
|
166
|
+
def remove_long_press_virtual_device(self) -> None:
|
|
167
|
+
"""Remove the pywemo virtual device from the long press."""
|
|
168
|
+
self.remove_long_press_udns([VIRTUAL_DEVICE_UDN])
|