Skip to content

Commit

Permalink
Add miottemplate tool to simplify adding support for new miot devices (
Browse files Browse the repository at this point in the history
…#656)

Related to #543
  • Loading branch information
rytilahti authored Mar 28, 2020
1 parent 86de386 commit 18fc0d6
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 2 deletions.
11 changes: 11 additions & 0 deletions devtools/README.md
Original file line number Diff line number Diff line change
@@ -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 <type>` to download the description file.
3. Execute `python miottemplate.py generate <file>` to generate pseudo-python for the device.
199 changes: 199 additions & 0 deletions devtools/containers.py
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions devtools/miottemplate.py
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 7 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -101,3 +101,8 @@ basepython = python3.7
deps = check-manifest
skip_install = true
commands = check-manifest

[check-manifest]
ignore =
devtools
devtools/*

0 comments on commit 18fc0d6

Please sign in to comment.