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

Fix issue in switchboxd boxtype manifest causing errors when toggling switchboxd relays #152

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
44 changes: 39 additions & 5 deletions blebox_uniapi/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ class Box:
_name: str
_data_path: str
_last_real_update: Optional[float]
_last_data: Optional[Any]

api_session: ApiHost
info: dict
config: dict
Expand All @@ -49,6 +51,7 @@ def __init__(
config,
extended_state,
) -> None:
self._last_data = None
self._last_real_update = None
self._sem = asyncio.BoundedSemaphore()
self._session = api_session
Expand Down Expand Up @@ -243,6 +246,38 @@ async def async_update_data(self) -> None:
await self._async_api(True, "GET", self._data_path)

def _update_last_data(self, new_data: Optional[dict]) -> None:
# Note: on certain more complex devices that inlcude multiple features
# like switches and sensors (e.g. SwitchboxD) it may happen that activating
# single feature would result only in partial update of the self._last_data.
# We can know that by comparing keys of both states.
#
# Notable example of device exhibiting this behavior is SwitchboxD (20200831)
# It has two relays (switches) and power measurement capabilities (sensor).
# Toggling the relay does return state of all relays, but does not return
# sensor information (partial state). Accepting the new state as-is would
# break the update of sensory information.
#
# Note that SwitchboxD is just an example. It is possible that APIs of other
# box types also exhibit this kind of behavior.
if (
isinstance(self._last_data, dict) and
isinstance(new_data, dict) and
self._last_data.keys() != new_data.keys()
):
# ... In such a case we need to merge both states instead of overwriting
# the old one as-is.
#
# The only risk is that if certain features are somehow coupled inside
# the device, we will have inconsistent information about the device.
# However, this should be eventually consistent as new updates arrive.
# In the end, it is better to have an inconsistent state, rather than
# have broken one (e.g. missing keys) that results in broken features.
#
# Refs:
# - https://github.com/blebox/blebox_uniapi/pull/152
# - https://github.com/blebox/blebox_uniapi/issues/137
new_data = {**self._last_data, **new_data}

self._last_data = new_data
for feature_set in self._features.values():
for feature in feature_set:
Expand All @@ -255,7 +290,7 @@ async def async_api_command(self, command: str, value: Any = None) -> None:

def follow(self, data: dict, path: str) -> Any:
"""
Return payloadu from device response json.
Return payload from device response json.
:param self:
:param data:
:param path:
Expand All @@ -268,8 +303,7 @@ def follow(self, data: dict, path: str) -> Any:
current_tree = data

for chunk in results:
with_string_value = re.compile("^\\[(.*)='(.*)'\\]$")

with_string_value = re.compile(r"^\[(.*)='(.*)']$")
match = with_string_value.match(chunk)
if match:
name = match.group(1)
Expand All @@ -288,7 +322,7 @@ def follow(self, data: dict, path: str) -> Any:

continue # pragma: no cover

with_int_value = re.compile("^\\[(.*)=(\\d+)\\]$")
with_int_value = re.compile(r"^\[(.*)=(\d+)]$")
match = with_int_value.match(chunk)
if match:
name = match.group(1)
Expand All @@ -311,7 +345,7 @@ def follow(self, data: dict, path: str) -> Any:
raise JPathFailed(f"with: {name}={value}", path, data)
continue # pragma: no cover

with_index = re.compile("^\\[(\\d+)\\]$")
with_index = re.compile(r"^\[(\d+)]$")
match = with_index.match(chunk)
if match:
index = int(match.group(1))
Expand Down
146 changes: 132 additions & 14 deletions blebox_uniapi/box_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,17 +245,6 @@ def get_latest_api_level(product_type: str) -> Union[dict, int]:
}
},
"switchBox": {
20220114: {
"api_path": "/state",
"extended_state_path": "/state/extended",
"api": {
"on": lambda x=None: ("GET", f"/s/{x}/1", None),
"off": lambda x=None: ("GET", f"/s/{x}/0", None),
},
"switches": [
["relay", {"state": lambda x: f"relays/[relay={x}]/state"}, "relay"]
],
},
20180604: {
"model": "switchBox",
"api_path": "/api/relay/state",
Expand All @@ -265,6 +254,7 @@ def get_latest_api_level(product_type: str) -> Union[dict, int]:
"off": lambda x=None: ("GET", "/s/0", None),
},
"switches": [["0.relay", {"state": "[relay=0]/state"}, "relay"]],
# note: does not support power measurement
},
20190808: {
"api_path": "/api/relay/extended/state",
Expand All @@ -287,11 +277,107 @@ def get_latest_api_level(product_type: str) -> Union[dict, int]:
]
],
},
20200229: {
"extended_state_path": "/state/extended",
"api_path": "/state/extended",
"api": {
"on": lambda x=None: ("GET", "/s/1", None),
"off": lambda x=None: ("GET", "/s/0", None),
},
"switches": [
["0.relay", {"state": lambda x: f"relays/[relay={x}]/state"}, "relay"]
],
"sensors": [
[
"switchBox.energy",
{
"energy": "powerMeasuring/powerConsumption/[0]/value",
"periodS": "powerMeasuring/powerConsumption/[0]/periodS",
"measurment_enabled": "powerMeasuring/enabled",
},
]
],
},
20200831: {
"extended_state_path": "/state/extended",
"api_path": "/state/extended",
"api": {
"on": lambda x=None: ("GET", "/s/1", None),
"off": lambda x=None: ("GET", "/s/0", None),
},
"switches": [
["0.relay", {"state": lambda x: f"relays/[relay={x}]/state"}, "relay"]
],
"sensors": [
[
"switchBox.energy",
{
"energy": "powerMeasuring/powerConsumption/[0]/value",
"periodS": "powerMeasuring/powerConsumption/[0]/periodS",
"measurment_enabled": "powerMeasuring/enabled",
},
]
],
},
20220114: {
"api_path": "/state/extended",
"extended_state_path": "/state/extended",
"api": {
# note: old control api (i.e. /s/0, /s/1, /s/2) still supported but
# now deprecated. switchBox has now API consistent with switchBoxD
"on": lambda x=None: ("GET", f"/s/{x}/1", None),
"off": lambda x=None: ("GET", f"/s/{x}/0", None),
},
"switches": [
["relay", {"state": lambda x: f"relays/[relay={x}]/state"}, "relay"]
],
"sensors": [
[
"switchBox.energy",
{
"energy": "powerMeasuring/powerConsumption/[0]/value",
"periodS": "powerMeasuring/powerConsumption/[0]/periodS",
"measurment_enabled": "powerMeasuring/enabled",
},
]
],
},
},
# switchBoxD
"switchBoxD": {
20190808: {
"extended_state_path": "/state/extended", # tylko dla testów do usunięcia nie w tym api
"extended_state_path": "/api/relay/extended/state",
"api_path": "/api/relay/extended/state",
"api": {
"on": lambda x: ("GET", f"/s/{int(x)}/1", None),
"off": lambda x=None: ("GET", f"/s/{int(x)}/0", None),
},
"switches": [
[
"0.relay",
{"state": lambda x: f"relays/[relay={x}]/state"},
"relay",
0,
],
[
"1.relay",
{"state": lambda x: f"relays/[relay={x}]/state"},
"relay",
1,
],
],
"sensors": [
[
"switchBox.energy",
{
"energy": "powerMeasuring/powerConsumption/[0]/value",
"periodS": "powerMeasuring/powerConsumption/[0]/periodS",
"measurment_enabled": "powerMeasuring/enabled",
},
]
],
},
20200229: {
"extended_state_path": "/state/extended",
"api_path": "/state/extended",
"api": {
"on": lambda x: ("GET", f"/s/{int(x)}/1", None),
Expand Down Expand Up @@ -321,7 +407,39 @@ def get_latest_api_level(product_type: str) -> Union[dict, int]:
},
]
],
}
},
20200831: {
"extended_state_path": "/state/extended",
"api_path": "/state/extended",
"api": {
"on": lambda x: ("GET", f"/s/{int(x)}/1", None),
"off": lambda x=None: ("GET", f"/s/{int(x)}/0", None),
},
"switches": [
[
"0.relay",
{"state": lambda x: f"relays/[relay={x}]/state"},
"relay",
0,
],
[
"1.relay",
{"state": lambda x: f"relays/[relay={x}]/state"},
"relay",
1,
],
],
"sensors": [
[
"switchBox.energy",
{
"energy": "powerMeasuring/powerConsumption/[0]/value",
"periodS": "powerMeasuring/powerConsumption/[0]/periodS",
"measurment_enabled": "powerMeasuring/enabled",
},
]
],
},
},
# tempSensor
"tempSensor": {
Expand Down
2 changes: 1 addition & 1 deletion tests/test_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ class TestSwitchBoxD(DefaultBoxTest):
"fv": "0.200",
"hv": "0.7",
"id": "1afe34e750b8",
"apiLevel": "20190808"
"apiLevel": "20200831"
}
}
"""
Expand Down
Loading