Skip to content

Commit

Permalink
Merge branch 'master' into feat/viomi_improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
rytilahti authored Nov 19, 2022
2 parents 0fe140a + 0db7997 commit 89795d1
Show file tree
Hide file tree
Showing 37 changed files with 1,482 additions and 513 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ jobs:
uses: "codecov/codecov-action@v2"
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
- id: docformatter
args: [--in-place, --wrap-summaries, '88', --wrap-descriptions, '88']

- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
Expand Down
53 changes: 29 additions & 24 deletions docs/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Development checklist
listing the known models (as reported by :meth:`~miio.device.Device.info()`).
4. Status containers is derived from :class:`~miio.devicestatus.DeviceStatus` class and all properties should
have type annotations for their return values. The information that should be exposed directly
to end users should be decorated using appropriate decorators (e.g., `@sensor` or `@switch`) to make
to end users should be decorated using appropriate decorators (e.g., `@sensor` or `@setting`) to make
them discoverable (:ref:`status_containers`).
5. Add tests at least for the status container handling (:ref:`adding_tests`).
6. Updating documentation is generally not needed as the API documentation
Expand Down Expand Up @@ -293,36 +293,19 @@ This will make all decorated sensors accessible through :meth:`~miio.device.Devi
device class information to Home Assistant.


Switches
""""""""

Use :meth:`@switch <miio.devicestatus.switch>` to create :class:`~miio.descriptors.SwitchDescriptor` objects.
This will make all decorated switches accessible through :meth:`~miio.device.Device.switches` for downstream users.

.. code-block::
@property
@switch(name="Power", setter_name="set_power")
def power(self) -> bool:
"""Return if device is turned on."""
You can either use *setter* to define a callable that can be used to adjust the value of the property,
or alternatively define *setter_name* which will be used to bind the method during the initialization
to the the :meth:`~miio.descriptors.SwitchDescriptor.setter` callable.


Settings
""""""""

Use :meth:`@switch <miio.devicestatus.setting>` to create :meth:`~miio.descriptors.SettingDescriptor` objects.
Use :meth:`@setting <miio.devicestatus.setting>` to create :meth:`~miio.descriptors.SettingDescriptor` objects.
This will make all decorated settings accessible through :meth:`~miio.device.Device.settings` for downstream users.

The type of the descriptor depends on the input parameters:

* Passing *min_value* or *max_value* will create a :class:`~miio.descriptors.NumberSettingDescriptor`,
which is useful for presenting ranges of values.
* Passing an Enum object using *choices* will create a :class:`~miio.descriptors.EnumSettingDescriptor`,
which is useful for presenting a fixed set of options.
* Passing an :class:`enum.Enum` object using *choices* will create a
:class:`~miio.descriptors.EnumSettingDescriptor`, which is useful for presenting a fixed set of options.
* Otherwise, the setting is considered to be boolean switch.


You can either use *setter* to define a callable that can be used to adjust the value of the property,
Expand All @@ -338,7 +321,7 @@ The *max_value* is the only mandatory parameter. If not given, *min_value* defau
.. code-block::
@property
@switch(name="Fan Speed", min_value=0, max_value=100, steps=5, setter_name="set_fan_speed")
@setting(name="Fan Speed", min_value=0, max_value=100, steps=5, setter_name="set_fan_speed")
def fan_speed(self) -> int:
"""Return the current fan speed."""
Expand All @@ -356,11 +339,33 @@ If the device has a setting with some pre-defined values, you want to use this.
Off = 2
@property
@switch(name="LED Brightness", choices=SomeEnum, setter_name="set_led_brightness")
@setting(name="LED Brightness", choices=SomeEnum, setter_name="set_led_brightness")
def led_brightness(self) -> LedBrightness:
"""Return the LED brightness."""
Actions
"""""""

Use :meth:`@action <miio.devicestatus.action>` to create :class:`~miio.descriptors.ActionDescriptor`
objects for the device.
This will make all decorated actions accessible through :meth:`~miio.device.Device.actions` for downstream users.

.. code-block:: python
@command()
@action(name="Do Something", some_kwarg_for_downstream="hi there")
def do_something(self):
"""Execute some action on the device."""
.. note::

All keywords arguments not defined in the decorator signature will be available
through the :attr:`~miio.descriptors.ActionDescriptor.extras` variable.

This information can be used to pass information to the downstream users.


.. _adding_tests:

Adding tests
Expand Down
58 changes: 57 additions & 1 deletion docs/simulator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,60 @@ concrete example for a device using custom method names for obtaining the status
MiOT Simulator
--------------

.. note:: TBD.
The ``miiocli devtools miot-simulator`` command can be used to simulate MiOT devices for a given description file.
You can command the simulated devices using the ``miiocli`` tool or any other implementation that supports the device.

Behind the scenes, the simulator uses :class:`the push server <miio.push_server.server.PushServer>` to
handle the low-level protocol handling.

The simulator implements the following methods:

* ``miIO.info`` returns the device information
* ``get_properties`` returns randomized (leveraging the schema limits) values for the given ``siid`` and ``piid``
* ``set_properties`` allows setting the property for the given ``siid`` and ``piid`` combination
* ``action`` to call actions that simply respond that the action succeeded

Furthermore, two custom methods are implemented help with development:

* ``dump_services`` returns the :ref:`list of available services <dump_services>`
* ``dump_properties`` returns the :ref:`available properties and their values <dump_properties>` the given ``siid``


Usage
"""""

You start the simulator like this::

miiocli devtools miot-simulator --file some.vacuum.model.json --model some.vacuum.model

The mandatory ``--file`` option takes a path to a MiOT description file, while ``--model`` defines the model
the simulator should report in its ``miIO.info`` response.

.. note::

The default token is hardcoded to full of zeros (``00000000000000000000000000000000``).


.. _dump_services:

Dump Service Information
~~~~~~~~~~~~~~~~~~~~~~~~

``dump_services`` method that returns a JSON dictionary keyed with the ``siid`` containing the simulated services::


$ miiocli device --ip 127.0.0.1 --token 00000000000000000000000000000000 raw_command dump_services
Running command raw_command
{'services': {'1': {'siid': 1, 'description': 'Device Information'}, '2': {'siid': 2, 'description': 'Heater'}, '3': {'siid': 3, 'description': 'Countdown'}, '4': {'siid': 4, 'description': 'Environment'}, '5': {'siid': 5, 'description': 'Physical Control Locked'}, '6': {'siid': 6, 'description': 'Alarm'}, '7': {'siid': 7, 'description': 'Indicator Light'}, '8': {'siid': 8, 'description': '私有服务'}}, 'id': 2}


.. _dump_properties:

Dump Service Properties
~~~~~~~~~~~~~~~~~~~~~~~

``dump_properties`` method can be used to return the current state of the device on service-basis::

$ miiocli device --ip 127.0.0.1 --token 00000000000000000000000000000000 raw_command dump_properties '{"siid": 2}'
Running command raw_command
[{'siid': 2, 'piid': 1, 'prop': 'Switch Status', 'value': False}, {'siid': 2, 'piid': 2, 'prop': 'Device Fault', 'value': 167}, {'siid': 2, 'piid': 5, 'prop': 'Target Temperature', 'value': 28}]
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from miio.exceptions import DeviceError, DeviceException, UnsupportedFeatureException
from miio.miot_device import MiotDevice
from miio.deviceinfo import DeviceInfo
from miio.interfaces import VacuumInterface, LightInterface, ColorTemperatureRange

# isort: on

Expand Down
22 changes: 17 additions & 5 deletions miio/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any, Dict

import click

Expand Down Expand Up @@ -29,11 +30,22 @@
@click.version_option()
@click.pass_context
def cli(ctx, debug: int, output: str):
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)
logging_config: Dict[str, Any] = {
"level": logging.DEBUG if debug > 0 else logging.INFO
}
try:
from rich.logging import RichHandler

rich_config = {
"show_time": False,
}
logging_config["handlers"] = [RichHandler(**rich_config)]
logging_config["format"] = "%(message)s"
except ImportError:
pass

# The configuration should be converted to use dictConfig, but this keeps mypy happy for now
logging.basicConfig(**logging_config) # type: ignore

if output in ("json", "json_pretty"):
output_func = json_output(pretty=output == "json_pretty")
Expand Down
25 changes: 12 additions & 13 deletions miio/click_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@

import click

import miio

from .exceptions import DeviceError

try:
from rich import print as echo
except ImportError:
echo = click.echo

_LOGGER = logging.getLogger(__name__)


Expand Down Expand Up @@ -49,9 +52,8 @@ class ExceptionHandlerGroup(click.Group):
def __call__(self, *args, **kwargs):
try:
return self.main(*args, **kwargs)
except (ValueError, miio.DeviceException) as ex:
_LOGGER.debug("Exception: %s", ex, exc_info=True)
click.echo(click.style("Error: %s" % ex, fg="red", bold=True))
except Exception as ex:
_LOGGER.exception("Exception: %s", ex)


class EnumType(click.Choice):
Expand Down Expand Up @@ -179,10 +181,7 @@ def _wrap(self, *args, **kwargs):
and self._model is None
and self._info is None
):
_LOGGER.debug(
"Unknown model, trying autodetection. %s %s"
% (self._model, self._info)
)
_LOGGER.debug("Unknown model, trying autodetection")
self._fetch_info()
return func(self, *args, **kwargs)

Expand Down Expand Up @@ -304,15 +303,15 @@ def wrap(*args, **kwargs):
else:
msg = msg_fmt.format(**kwargs)
if msg:
click.echo(msg.strip())
echo(msg.strip())
kwargs["result"] = func(*args, **kwargs)
if result_msg_fmt:
if callable(result_msg_fmt):
result_msg = result_msg_fmt(**kwargs)
else:
result_msg = result_msg_fmt.format(**kwargs)
if result_msg:
click.echo(result_msg.strip())
echo(result_msg.strip())

return wrap

Expand All @@ -328,7 +327,7 @@ def wrap(*args, **kwargs):
try:
result = func(*args, **kwargs)
except DeviceError as ex:
click.echo(json.dumps(ex.args[0], indent=indent))
echo(json.dumps(ex.args[0], indent=indent))
return

get_json_data_func = getattr(result, "__json__", None)
Expand All @@ -337,7 +336,7 @@ def wrap(*args, **kwargs):
result = get_json_data_func()
elif data_variable is not None:
result = data_variable
click.echo(json.dumps(result, indent=indent))
echo(json.dumps(result, indent=indent))

return wrap

Expand Down
Loading

0 comments on commit 89795d1

Please sign in to comment.