diff --git a/devtools/README.md b/devtools/README.md new file mode 100644 index 000000000..739fb2041 --- /dev/null +++ b/devtools/README.md @@ -0,0 +1,11 @@ +# Devtools + +This directory contains tooling useful for developers + +## MiOT generator + +This tool generates some boilerplate code for adding support for MIoT devices + +1. Obtain device type from http://miot-spec.org/miot-spec-v2/instances?status=all +2. Execute `python miottemplate.py download ` to download the description file. +3. Execute `python miottemplate.py generate ` to generate pseudo-python for the device. diff --git a/devtools/containers.py b/devtools/containers.py new file mode 100644 index 000000000..e8c91644b --- /dev/null +++ b/devtools/containers.py @@ -0,0 +1,199 @@ +from dataclasses import dataclass, field +from typing import List + +from dataclasses_json import DataClassJsonMixin, config + + +def pretty_name(name): + return name.replace(" ", "_").replace("-", "_") + + +def python_type_for_type(x): + if "int" in x: + return "int" + if x == "string": + return "str" + if x == "float" or x == "bool": + return x + + return f"unknown type {x}" + + +def indent(data, level=4): + indented = "" + for x in data.splitlines(keepends=True): + indented += " " * level + x + + return indented + + +@dataclass +class Property(DataClassJsonMixin): + iid: int + type: str + description: str + format: str + access: List[str] + + value_list: List = field( + default_factory=list, metadata=config(field_name="value-list") + ) + value_range: List = field(default=None, metadata=config(field_name="value-range")) + + unit: str = None + + def __repr__(self): + return f"piid: {self.iid} ({self.description}): ({self.format}, unit: {self.unit}) (acc: {self.access}, value-list: {self.value_list}, value-range: {self.value_range})" + + def __str__(self): + return self.__repr__() + + def _generate_enum(self): + s = f"class {self.pretty_name()}Enum(enum.Enum):\n" + for value in self.value_list: + s += f" {pretty_name(value['description'])} = {value['value']}\n" + s += "\n" + return s + + def pretty_name(self): + return pretty_name(self.description) + + def _generate_value_and_range(self): + s = "" + if self.value_range: + s += f" Range: {self.value_range}\n" + if self.value_list: + s += f" Values: {self.pretty_name()}Enum\n" + return s + + def _generate_docstring(self): + return ( + f"{self.description} (siid: {self.siid}, piid: {self.iid}) - {self.type} " + ) + + def _generate_getter(self): + s = "" + s += ( + f"def read_{self.pretty_name()}() -> {python_type_for_type(self.format)}:\n" + ) + s += f' """{self._generate_docstring()}\n' + s += self._generate_value_and_range() + s += ' """\n\n' + + return s + + def _generate_setter(self): + s = "" + s += f"def write_{self.pretty_name()}(var: {python_type_for_type(self.format)}):\n" + s += f' """{self._generate_docstring()}\n' + s += self._generate_value_and_range() + s += ' """\n' + s += "\n" + return s + + def as_code(self, siid): + s = "" + self.siid = siid + + if self.value_list: + s += self._generate_enum() + + if "read" in self.access: + s += self._generate_getter() + if "write" in self.access: + s += self._generate_setter() + + return s + + +@dataclass +class Action(DataClassJsonMixin): + iid: int + type: str + description: str + out: List = field(default_factory=list) + in_: List = field(default_factory=list, metadata=config(field_name="in")) + + def __repr__(self): + return f"aiid {self.iid} {self.description}: in: {self.in_} -> out: {self.out}" + + def __str__(self): + return self.__repr__() + + def pretty_name(self): + return pretty_name(self.description) + + def as_code(self, siid): + self.siid = siid + s = "" + s += f"def {self.pretty_name()}({self.in_}) -> {self.out}:\n" + s += f' """{self.description} (siid: {self.siid}, aiid: {self.iid}) {self.type}"""\n\n' + return s + + +@dataclass +class Event(DataClassJsonMixin): + iid: int + type: str + description: str + arguments: List + + def __repr__(self): + return f"eiid {self.iid} ({self.description}): (args: {self.arguments})" + + def __str__(self): + return self.__repr__() + + +@dataclass +class Service(DataClassJsonMixin): + iid: int + type: str + description: str + properties: List[Property] = field(default_factory=list) + actions: List[Action] = field(default_factory=list) + events: List[Event] = field(default_factory=list) + + def __repr__(self): + return f"siid {self.iid}: ({self.description}): {len(self.properties)} props, {len(self.actions)} actions" + + def __str__(self): + return self.__repr__() + + def as_code(self): + s = "" + s += f"class {pretty_name(self.description)}(MiOTService):\n" + s += f' """\n' + s += f" {self.description} ({self.type}) (siid: {self.iid})\n" + s += f" Events: {len(self.events)}\n" + s += f" Properties: {len(self.properties)}\n" + s += f" Actions: {len(self.actions)}\n" + s += f' """\n\n' + s += "#### PROPERTIES ####\n" + for property in self.properties: + s += indent(property.as_code(self.iid)) + s += "#### PROPERTIES END ####\n\n" + s += "#### ACTIONS ####\n" + for act in self.actions: + s += indent(act.as_code(self.iid)) + s += "#### ACTIONS END ####\n\n" + return s + + +@dataclass +class Device(DataClassJsonMixin): + type: str + description: str + services: List[Service] = field(default_factory=list) + + def as_code(self): + s = "" + s += f'"""' + s += f"Support template for {self.description} ({self.type})\n\n" + s += f"Contains {len(self.services)} services\n" + s += f'"""\n\n' + + for serv in self.services: + s += serv.as_code() + + return s diff --git a/devtools/miottemplate.py b/devtools/miottemplate.py new file mode 100644 index 000000000..0cb3e2f2e --- /dev/null +++ b/devtools/miottemplate.py @@ -0,0 +1,65 @@ +import logging + +import click + +from containers import Device + +_LOGGER = logging.getLogger(__name__) + + +@click.group() +@click.option("-d", "--debug") +def cli(debug): + lvl = logging.INFO + if debug: + lvl = logging.DEBUG + + logging.basicConfig(level=lvl) + + +class Generator: + def __init__(self, data): + self.data = data + + def generate(self): + dev = Device.from_json(self.data) + + for serv in dev.services: + _LOGGER.info("Service: %s", serv) + for prop in serv.properties: + _LOGGER.info(" * Property %s", prop) + + for act in serv.actions: + _LOGGER.info(" * Action %s", act) + + for ev in serv.events: + _LOGGER.info(" * Event %s", ev) + + return dev.as_code() + + +@cli.command() +@click.argument("file", type=click.File()) +def generate(file): + """Generate pseudo-code python for given file.""" + data = file.read() + gen = Generator(data) + print(gen.generate()) + + +@cli.command() +@click.argument("type") +def download(type): + """Download description file for model.""" + import requests + + url = f"https://miot-spec.org/miot-spec-v2/instance?type={type}" + content = requests.get(url) + save_to = f"{type}.json" + click.echo(f"Saving data to {save_to}") + with open(save_to, "w") as f: + f.write(content.text) + + +if __name__ == "__main__": + cli() diff --git a/tox.ini b/tox.ini index f8481438c..8c4182aee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,10 @@ [tox] -envlist=py35,py36,py37,flake8,docs,manifest,pypi-description +envlist=py36,py37,py38,flake8,docs,manifest,pypi-description [tox:travis] -3.5 = py35 3.6 = py36 3.7 = py37 +3.8 = py38 [testenv] passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH @@ -101,3 +101,8 @@ basepython = python3.7 deps = check-manifest skip_install = true commands = check-manifest + +[check-manifest] +ignore = + devtools + devtools/*