diff --git a/miio/descriptors.py b/miio/descriptors.py index 54f4761dd..a72023bca 100644 --- a/miio/descriptors.py +++ b/miio/descriptors.py @@ -11,7 +11,7 @@ If needed, you can override the methods listed to add more descriptors to your integration. """ from enum import Enum, auto -from typing import Callable, Dict, Optional, Type +from typing import Any, Callable, Dict, List, Optional, Type import attr @@ -33,6 +33,7 @@ class ActionDescriptor: name: str method_name: Optional[str] = attr.ib(default=None, repr=False) method: Optional[Callable] = attr.ib(default=None, repr=False) + inputs: Optional[List[Any]] = attr.ib(default=None, repr=True) extras: Dict = attr.ib(factory=dict, repr=False) diff --git a/miio/devtools/simulators/miotsimulator.py b/miio/devtools/simulators/miotsimulator.py index 7f65a240c..5aa9dfe71 100644 --- a/miio/devtools/simulators/miotsimulator.py +++ b/miio/devtools/simulators/miotsimulator.py @@ -185,8 +185,65 @@ def dump_properties(self, payload): def action(self, payload): """Handle action method.""" + params = payload["params"] + if ( + "did" not in params + or "siid" not in params + or "aiid" not in params + or "in" not in params + ): + raise ValueError("did, siid, or aiid missing") + + siid = params["siid"] + aiid = params["aiid"] + inputs = params["in"] + service = self._model.get_service_by_siid(siid) + + action = service.get_action_by_id(aiid) + action_inputs = action.inputs + if len(inputs) != len(action_inputs): + raise ValueError( + "Invalid parameter count, was expecting %s params, got %s" + % (len(inputs), len(action_inputs)) + ) + + for idx, param in enumerate(inputs): + wanted_input = action_inputs[idx] + + if wanted_input.choices: + if not isinstance(param, int): + raise TypeError( + "Param #%s: enum value expects an integer %s, got %s" + % (idx, wanted_input, param) + ) + for choice in wanted_input.choices: + if param == choice.value: + break + else: + raise ValueError( + "Param #%s: invalid value '%s' for %s" + % (idx, param, wanted_input.choices) + ) + + elif wanted_input.range: + if not isinstance(param, int): + raise TypeError( + "Param #%s: ranged value expects an integer %s, got %s" + % (idx, wanted_input, param) + ) + + min, max, step = wanted_input.range + if param < min or param > max: + raise ValueError( + "Param #%s: value '%s' out of range [%s, %s]" + % (idx, param, min, max) + ) + + elif wanted_input.format == str and not isinstance(param, str): + raise TypeError(f"Param #{idx}: expected string but got {type(param)}") + _LOGGER.info("Got called %s", payload) - return {"result": 0} + return {"result": ["ok"]} async def main(dev, model): diff --git a/miio/integrations/genericmiot/genericmiot.py b/miio/integrations/genericmiot/genericmiot.py index 08e6c9af1..3deff1440 100644 --- a/miio/integrations/genericmiot/genericmiot.py +++ b/miio/integrations/genericmiot/genericmiot.py @@ -60,8 +60,25 @@ def pretty_status(result: "GenericMiotStatus"): def pretty_actions(result: Dict[str, ActionDescriptor]): """Pretty print actions.""" out = "" + service = None for _, desc in result.items(): - out += f"{desc.id}\t\t{desc.name}\n" + miot_prop: MiotProperty = desc.extras["miot_action"] + # service is marked as optional due pydantic backrefs.. + serv = cast(MiotService, miot_prop.service) + if service is None or service.siid != serv.siid: + service = serv + out += f"[bold]{service.description} ({service.name})[/bold]\n" + + out += f"\t{desc.id}\t\t{desc.name}" + if desc.inputs: + for idx, input_ in enumerate(desc.inputs, start=1): + param = input_.extras[ + "miot_property" + ] # TODO: hack until descriptors get support for descriptions + param_desc = f"\n\t\tParameter #{idx}: {param.name} ({param.description}) ({param.format}) {param.pretty_input_constraints}" + out += param_desc + + out += "\n" return out @@ -213,13 +230,6 @@ def status(self) -> GenericMiotStatus: def _create_action(self, act: MiotAction) -> Optional[ActionDescriptor]: """Create action descriptor for miot action.""" - if act.inputs: - # TODO: need to figure out how to expose input parameters for downstreams - _LOGGER.warning( - "Got inputs for action, skipping as handling is unknown: %s", act - ) - return None - call_action = partial(self.call_action_by, act.siid, act.aiid) id_ = act.name @@ -229,10 +239,17 @@ def _create_action(self, act: MiotAction) -> Optional[ActionDescriptor]: extras["urn"] = act.urn extras["siid"] = act.siid extras["aiid"] = act.aiid + extras["miot_action"] = act + + inputs = act.inputs + if inputs: + # TODO: this is just temporarily here, pending refactoring the descriptor creation into the model + inputs = [self._descriptor_for_property(prop) for prop in act.inputs] return ActionDescriptor( id=id_, name=act.description, + inputs=inputs, method=call_action, extras=extras, ) diff --git a/miio/miot_models.py b/miio/miot_models.py index 9813e3169..48a0306b8 100644 --- a/miio/miot_models.py +++ b/miio/miot_models.py @@ -287,7 +287,8 @@ class DeviceModel(BaseModel): urn: URN = Field(alias="type") services: List[MiotService] = Field(repr=False) - # internal mappings to simplify accesses to a specific (siid, piid) + # internal mappings to simplify accesses + _services_by_id: Dict[int, MiotService] = PrivateAttr(default_factory=dict) _properties_by_id: Dict[int, Dict[int, MiotProperty]] = PrivateAttr( default_factory=dict ) @@ -302,6 +303,7 @@ def __init__(self, *args, **kwargs): """ super().__init__(*args, **kwargs) for serv in self.services: + self._services_by_id[serv.siid] = serv self._properties_by_name[serv.name] = dict() self._properties_by_id[serv.siid] = dict() for prop in serv.properties: @@ -313,6 +315,10 @@ def device_type(self) -> str: """Return device type as string.""" return self.urn.type + def get_service_by_siid(self, siid: int) -> MiotService: + """Return the service for given siid.""" + return self._services_by_id[siid] + def get_property(self, service: str, prop_name: str) -> MiotProperty: """Return the property model for given service and property name.""" return self._properties_by_name[service][prop_name]