Skip to content

Commit

Permalink
Merge branch 'rytilahti:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
st7105 authored Oct 4, 2023
2 parents 65a3783 + 76808d8 commit 2ba0b51
Show file tree
Hide file tree
Showing 23 changed files with 1,467 additions and 1,101 deletions.
35 changes: 34 additions & 1 deletion miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@

from miio.device import Device
from miio.devicestatus import DeviceStatus
from miio.exceptions import DeviceError, DeviceException, UnsupportedFeatureException
from miio.exceptions import (
DeviceError,
DeviceException,
UnsupportedFeatureException,
DeviceInfoUnavailableException,
)
from miio.miot_device import MiotDevice
from miio.deviceinfo import DeviceInfo

Expand Down Expand Up @@ -80,4 +85,32 @@

from miio.discovery import Discovery


def __getattr__(name):
"""Create deprecation warnings on classes that are going away."""
from warnings import warn

current_globals = globals()

def _is_miio_integration(x):
"""Return True if miio.integrations is in the module 'path'."""
module_ = current_globals[x]
if "miio.integrations" in str(module_):
return True

return False

deprecated_module_mapping = {
str(x): current_globals[x] for x in current_globals if _is_miio_integration(x)
}
if new_module := deprecated_module_mapping.get(name):
warn(
f"Importing {name} directly from 'miio' is deprecated, import {new_module} or use DeviceFactory.create() instead",
DeprecationWarning,
)
return globals()[new_module.__name__]

raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


__version__ = version("python-miio")
6 changes: 5 additions & 1 deletion miio/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
from typing import TYPE_CHECKING, Dict, Optional

import click
from pydantic import BaseModel, Field

try:
from pydantic.v1 import BaseModel, Field
except ImportError:
from pydantic import BaseModel, Field

try:
from rich import print as echo
Expand Down
18 changes: 12 additions & 6 deletions miio/devtools/simulators/miiosimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
from typing import List, Optional, Union

import click
from pydantic import BaseModel, Field, PrivateAttr

try:
from pydantic.v1 import BaseModel, Field, PrivateAttr
except ImportError:
from pydantic import BaseModel, Field, PrivateAttr
from yaml import safe_load

from miio import PushServer
Expand Down Expand Up @@ -90,10 +94,6 @@ def __init__(self, dev: SimulatedMiio, server: PushServer):
self._setters = {}
self._server = server

# If no model is given, use one from the supported ones
if self._dev._model is None:
self._dev._model = next(iter(self._dev.models)).model

# Add get_prop if device has properties defined
if self._dev.properties:
server.add_method("get_prop", self.get_prop)
Expand Down Expand Up @@ -135,7 +135,13 @@ def handle_set(self, payload):


async def main(dev):
did, mac = did_and_mac_for_model(dev)
if dev._model is None:
dev._model = next(iter(dev.models)).model
_LOGGER.warning(
"No --model defined, using the first supported one: %s", dev._model
)

did, mac = did_and_mac_for_model(dev._model)
server = PushServer(device_id=did)

_ = MiioSimulator(dev=dev, server=server)
Expand Down
5 changes: 4 additions & 1 deletion miio/devtools/simulators/miotsimulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
from typing import List, Union

import click
from pydantic import Field, validator

try:
from pydantic.v1 import Field, validator
except ImportError:
from pydantic import Field, validator
from miio import PushServer
from miio.miot_cloud import MiotCloud
from miio.miot_models import DeviceModel, MiotAccess, MiotProperty, MiotService
Expand Down
24 changes: 12 additions & 12 deletions miio/integrations/cgllc/airmonitor/airqualitymonitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def __init__(self, data):
@property
def power(self) -> Optional[str]:
"""Current power state."""
return self.data.get("power", None)
return self.data.get("power")

@property
def is_on(self) -> bool:
Expand All @@ -77,12 +77,12 @@ def usb_power(self) -> Optional[bool]:
@property
def aqi(self) -> Optional[int]:
"""Air quality index value (0..600)."""
return self.data.get("aqi", None)
return self.data.get("aqi")

@property
def battery(self) -> Optional[int]:
"""Current battery level (0..100)."""
return self.data.get("battery", None)
return self.data.get("battery")

@property
def display_clock(self) -> Optional[bool]:
Expand All @@ -101,47 +101,47 @@ def night_mode(self) -> Optional[bool]:
@property
def night_time_begin(self) -> Optional[str]:
"""Return the begin of the night time."""
return self.data.get("night_beg_time", None)
return self.data.get("night_beg_time")

@property
def night_time_end(self) -> Optional[str]:
"""Return the end of the night time."""
return self.data.get("night_end_time", None)
return self.data.get("night_end_time")

@property
def sensor_state(self) -> Optional[str]:
"""Sensor state."""
return self.data.get("sensor_state", None)
return self.data.get("sensor_state")

@property
def co2(self) -> Optional[int]:
"""Return co2 value (400...9999ppm)."""
return self.data.get("co2", None)
return self.data.get("co2")

@property
def co2e(self) -> Optional[int]:
"""Return co2e value (400...9999ppm)."""
return self.data.get("co2e", None)
return self.data.get("co2e")

@property
def humidity(self) -> Optional[float]:
"""Return humidity value (0...100%)."""
return self.data.get("humidity", None)
return self.data.get("humidity")

@property
def pm25(self) -> Optional[float]:
"""Return pm2.5 value (0...999μg/m³)."""
return self.data.get("pm25", None)
return self.data.get("pm25")

@property
def temperature(self) -> Optional[float]:
"""Return temperature value (-10...50°C)."""
return self.data.get("temperature", None)
return self.data.get("temperature")

@property
def tvoc(self) -> Optional[int]:
"""Return tvoc value."""
return self.data.get("tvoc", None)
return self.data.get("tvoc")


class AirQualityMonitor(Device):
Expand Down
1 change: 1 addition & 0 deletions miio/integrations/chuangmi/remote/chuangmi_ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ChuangmiIr(Device):
"chuangmi.ir.v2",
"chuangmi.remote.v2",
"chuangmi.remote.h102a03",
"xiaomi.wifispeaker.l05g",
]

PRONTO_RE = re.compile(r"^([\da-f]{4}\s?){3,}([\da-f]{4})$", re.IGNORECASE)
Expand Down
2 changes: 2 additions & 0 deletions miio/integrations/dreame/vacuum/dreamevacuum_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
DREAME_MOP_2_ULTRA = "dreame.vacuum.p2150a"
DREAME_MOP_2 = "dreame.vacuum.p2150o"
DREAME_TROUVER_FINDER = "dreame.vacuum.p2036"
DREAME_D10_PLUS = "dreame.vacuum.r2205"

_DREAME_1C_MAPPING: MiotMapping = {
# https://home.miot-spec.com/spec/dreame.vacuum.mc1808
Expand Down Expand Up @@ -179,6 +180,7 @@
DREAME_MOP_2_ULTRA: _DREAME_F9_MAPPING,
DREAME_MOP_2: _DREAME_F9_MAPPING,
DREAME_TROUVER_FINDER: _DREAME_TROUVER_FINDER_MAPPING,
DREAME_D10_PLUS: _DREAME_TROUVER_FINDER_MAPPING,
}


Expand Down
2 changes: 1 addition & 1 deletion miio/integrations/genericmiot/genericmiot.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def call_action(self, name: str, params=None):
def change_setting(self, name: str, params=None):
"""Change setting value."""
params = params if params is not None else []
setting = self._properties.get(name, None)
setting = self._properties.get(name)
if setting is None:
raise ValueError("No property found for name %s" % name)
if setting.access & AccessFlags.Write == 0:
Expand Down
48 changes: 38 additions & 10 deletions miio/integrations/genericmiot/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,44 @@ class GenericMiotStatus(DeviceStatus):
def __init__(self, response, dev):
self._model: DeviceModel = dev._miot_model
self._dev = dev
self._data = {elem["did"]: elem["value"] for elem in response}
# for hardcoded json output.. see click_common.json_output
self.data = self._data

self._data_by_siid_piid = {
(elem["siid"], elem["piid"]): elem["value"] for elem in response
}
self._data_by_normalized_name = {
self._normalize_name(elem["did"]): elem["value"] for elem in response
}
self._data = {}
self._data_by_siid_piid = {}
self._data_by_normalized_name = {}
self._initialize_data(response)

def _initialize_data(self, response):
def _is_valid_property_response(elem):
code = elem.get("code")
if code is None:
_LOGGER.debug("Ignoring due to missing 'code': %s", elem)
return False

if code != 0:
_LOGGER.warning("Ignoring due to error code '%s': %s", code, elem)
return False

needed_keys = ("did", "piid", "siid", "value")
for key in needed_keys:
if key not in elem:
_LOGGER.debug("Ignoring due to missing '%s': %s", key, elem)
return False

return True

for prop in response:
if not _is_valid_property_response(prop):
continue

self._data[prop["did"]] = prop["value"]
self._data_by_siid_piid[(prop["siid"], prop["piid"])] = prop["value"]
self._data_by_normalized_name[self._normalize_name(prop["did"])] = prop[
"value"
]

@property
def data(self):
"""Implemented to support json output."""
return self._data

def _normalize_name(self, id_: str) -> str:
"""Return a cleaned id for dict searches."""
Expand Down
38 changes: 38 additions & 0 deletions miio/integrations/genericmiot/tests/test_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import logging
from unittest.mock import Mock

import pytest

from ..status import GenericMiotStatus


@pytest.fixture(scope="session")
def mockdev():
yield Mock()


VALID_RESPONSE = {"code": 0, "did": "valid-response", "piid": 1, "siid": 1, "value": 1}


@pytest.mark.parametrize("key", ("did", "piid", "siid", "value", "code"))
def test_response_with_missing_value(key, mockdev, caplog: pytest.LogCaptureFixture):
"""Verify that property responses without necessary keys are ignored."""
caplog.set_level(logging.DEBUG)

prop = {"code": 0, "did": f"no-{key}-in-response", "piid": 1, "siid": 1, "value": 1}
prop.pop(key)

status = GenericMiotStatus([VALID_RESPONSE, prop], mockdev)
assert f"Ignoring due to missing '{key}'" in caplog.text
assert len(status.data) == 1


@pytest.mark.parametrize("code", (-123, 123))
def test_response_with_error_codes(code, mockdev, caplog: pytest.LogCaptureFixture):
caplog.set_level(logging.WARNING)

did = f"error-code-{code}"
prop = {"code": code, "did": did, "piid": 1, "siid": 1}
status = GenericMiotStatus([VALID_RESPONSE, prop], mockdev)
assert f"Ignoring due to error code '{code}'" in caplog.text
assert len(status.data) == 1
4 changes: 2 additions & 2 deletions miio/integrations/lumi/gateway/devices/subdevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(
self.get_prop_exp_dict = {}
for prop in model_info.get("properties", []):
prop_name = prop.get("name", prop["property"])
self._props[prop_name] = prop.get("default", None)
self._props[prop_name] = prop.get("default")
if prop.get("get") == "get_property_exp":
self.get_prop_exp_dict[prop["property"]] = prop

Expand Down Expand Up @@ -313,7 +313,7 @@ async def subscribe_events(self):
extra=self.push_events[action]["extra"],
source_sid=self.sid,
source_model=self.zigbee_model,
event=self.push_events[action].get("event", None),
event=self.push_events[action].get("event"),
command_extra=self.push_events[action].get("command_extra", ""),
trigger_value=self.push_events[action].get("trigger_value"),
)
Expand Down
4 changes: 3 additions & 1 deletion miio/integrations/lumi/gateway/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,10 @@ def setup_device(self, dev_info, model_info):
return

# Obtain the correct subdevice class
# TODO: is there a better way to obtain this information?
subdevice_cls = getattr(
sys.modules["miio.gateway.devices"], model_info.get("class")
sys.modules["miio.integrations.lumi.gateway.devices"],
model_info.get("class"),
)
if subdevice_cls is None:
subdevice_cls = SubDevice
Expand Down
2 changes: 1 addition & 1 deletion miio/integrations/roborock/vacuum/vacuumcontainers.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,7 @@ def progress(self) -> int:
def sid(self) -> int:
"""Sound ID for the sound being installed."""
# this is missing on install confirmation, so let's use get
return self.data.get("sid_in_progress", None)
return self.data.get("sid_in_progress")

@property
def error(self) -> int:
Expand Down
4 changes: 2 additions & 2 deletions miio/miioprotocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import codecs
import logging
import socket
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pprint import pformat as pf
from typing import Any, Dict, List, Optional

Expand Down Expand Up @@ -48,7 +48,7 @@ def __init__(

self._discovered = False
# these come from the device, but we initialize them here to make mypy happy
self._device_ts: datetime = datetime.utcnow()
self._device_ts: datetime = datetime.now(tz=timezone.utc)
self._device_id = b""

def send_handshake(self, *, retry_count=3) -> Message:
Expand Down
Loading

0 comments on commit 2ba0b51

Please sign in to comment.