Skip to content

Commit 4e9771c

Browse files
committed
Use micloud for miotspec cloud connectivity
Depends on Squachen/micloud#11
1 parent cae59eb commit 4e9771c

File tree

1 file changed

+54
-51
lines changed

1 file changed

+54
-51
lines changed

miio/miot_cloud.py

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
"""Module implementing handling of miot schema files."""
2+
import json
23
import logging
34
from datetime import datetime, timedelta
45
from operator import attrgetter
56
from pathlib import Path
6-
from typing import List
7+
from typing import Dict, List, Optional
78

89
import appdirs
9-
import requests # TODO: externalize HTTP requests to avoid direct dependency
10-
from pydantic import BaseModel
10+
from micloud import MiotSpec
11+
from pydantic import BaseModel, Field
1112

1213
from miio.miot_models import DeviceModel
1314

1415
_LOGGER = logging.getLogger(__name__)
1516

1617

1718
class ReleaseInfo(BaseModel):
19+
"""Information about individual miotspec release."""
20+
1821
model: str
19-
status: str
22+
status: Optional[str] # only available on full listing
2023
type: str
2124
version: int
2225

@@ -26,87 +29,87 @@ def filename(self) -> str:
2629

2730

2831
class ReleaseList(BaseModel):
29-
instances: List[ReleaseInfo]
32+
"""Model for miotspec release list."""
33+
34+
releases: List[ReleaseInfo] = Field(alias="instances")
3035

3136
def info_for_model(self, model: str, *, status_filter="released") -> ReleaseInfo:
32-
matches = [inst for inst in self.instances if inst.model == model]
37+
releases = [inst for inst in self.releases if inst.model == model]
3338

34-
if len(matches) > 1:
39+
if not releases:
40+
raise Exception(f"No releases found for {model=} with {status_filter=}")
41+
elif len(releases) > 1:
3542
_LOGGER.warning(
36-
"more than a single match for model %s: %s, filtering with status=%s",
43+
"%s versions found for model %s: %s, using the newest one",
44+
len(releases),
3745
model,
38-
matches,
46+
releases,
3947
status_filter,
4048
)
4149

42-
released_versions = [inst for inst in matches if inst.status == status_filter]
43-
if not released_versions:
44-
raise Exception(f"No releases for {model}, adjust status_filter?")
45-
46-
_LOGGER.debug("Got %s releases, picking the newest one", released_versions)
50+
newest_release = max(releases, key=attrgetter("version"))
51+
_LOGGER.debug("Using %s", newest_release)
4752

48-
match = max(released_versions, key=attrgetter("version"))
49-
_LOGGER.debug("Using %s", match)
50-
51-
return match
53+
return newest_release
5254

5355

5456
class MiotCloud:
57+
"""Interface for miotspec data."""
58+
5559
def __init__(self):
5660
self._cache_dir = Path(appdirs.user_cache_dir("python-miio"))
5761

5862
def get_device_model(self, model: str) -> DeviceModel:
5963
"""Get device model for model name."""
6064
file = self._cache_dir / f"{model}.json"
61-
if file.exists():
62-
_LOGGER.debug("Using cached %s", file)
63-
return DeviceModel.parse_raw(file.read_text())
65+
spec = self.file_from_cache(file)
66+
if spec is not None:
67+
return DeviceModel.parse_obj(spec)
6468

65-
return DeviceModel.parse_raw(self.get_model_schema(model))
69+
return DeviceModel.parse_obj(self.get_model_schema(model))
6670

67-
def get_model_schema(self, model: str) -> str:
71+
def get_model_schema(self, model: str) -> Dict:
6872
"""Get the preferred schema for the model."""
69-
instances = self.fetch_release_list()
70-
release_info = instances.info_for_model(model)
73+
specs = self.get_release_list()
74+
release_info = specs.info_for_model(model)
7175

7276
model_file = self._cache_dir / f"{release_info.model}.json"
73-
url = f"https://miot-spec.org/miot-spec-v2/instance?type={release_info.type}"
74-
75-
data = self._fetch(url, model_file)
77+
spec = self.file_from_cache(model_file)
78+
if spec is not None:
79+
return spec
7680

77-
return data
81+
spec = json.loads(MiotSpec.get_spec_for_urn(device_urn=release_info.type))
7882

79-
def fetch_release_list(self):
80-
"""Fetch a list of available schemas."""
81-
mapping_file = "model-to-urn.json"
82-
url = "http://miot-spec.org/miot-spec-v2/instances?status=all"
83-
data = self._fetch(url, self._cache_dir / mapping_file)
84-
85-
return ReleaseList.parse_raw(data)
86-
87-
def _fetch(self, url: str, target_file: Path, cache_hours=6):
88-
"""Fetch the URL and cache results, if expired."""
83+
return spec
8984

90-
def valid_cache():
85+
def file_from_cache(self, file, cache_hours=6) -> Optional[Dict]:
86+
def _valid_cache():
9187
expiration = timedelta(hours=cache_hours)
9288
if (
93-
datetime.fromtimestamp(target_file.stat().st_mtime) + expiration
89+
datetime.fromtimestamp(file.stat().st_mtime) + expiration
9490
> datetime.utcnow()
9591
):
9692
return True
9793

9894
return False
9995

100-
if target_file.exists() and valid_cache():
101-
_LOGGER.debug("Returning data from cache: %s", target_file)
102-
return target_file.read_text()
96+
if file.exists() and _valid_cache():
97+
_LOGGER.debug("Returning data from cache file %s", file)
98+
return json.loads(file.read_text())
99+
100+
_LOGGER.debug("Cache file %s not found or it is stale", file)
101+
return None
102+
103+
def get_release_list(self) -> ReleaseList:
104+
"""Fetch a list of available releases."""
105+
mapping_file = "model-to-urn.json"
103106

104-
_LOGGER.debug("Going to download %s to %s", url, target_file)
105-
content = requests.get(url)
106-
content.raise_for_status()
107+
cache_file = self._cache_dir / mapping_file
108+
mapping = self.file_from_cache(cache_file)
109+
if mapping is not None:
110+
return ReleaseList.parse_obj(mapping)
107111

108-
response = content.text
109-
written = target_file.write_text(response)
110-
_LOGGER.debug("Written %s bytes to %s", written, target_file)
112+
specs = MiotSpec.get_specs()
113+
cache_file.write_text(json.dumps(specs))
111114

112-
return response
115+
return ReleaseList.parse_obj(specs)

0 commit comments

Comments
 (0)