Skip to content

Commit c373fc9

Browse files
authoredOct 5, 2024
Merge pull request #36 from tmenguy/master
2 parents 9aa2e9a + b7001c7 commit c373fc9

21 files changed

+516
-296
lines changed
 

‎custom_components/netatmo/icons.json

+30-10
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,35 @@
3434
}
3535
},
3636
"services": {
37-
"set_camera_light": "mdi:led-on",
38-
"set_schedule": "mdi:calendar-clock",
39-
"set_preset_mode_with_end_datetime": "mdi:calendar-clock",
40-
"set_temperature_with_end_datetime": "mdi:thermometer",
41-
"set_temperature_with_time_period": "mdi:thermometer",
42-
"clear_temperature_setting": "mdi:thermometer",
43-
"set_persons_home": "mdi:home",
44-
"set_person_away": "mdi:walk",
45-
"register_webhook": "mdi:link-variant",
46-
"unregister_webhook": "mdi:link-variant-off"
37+
"set_camera_light": {
38+
"service": "mdi:led-on"
39+
},
40+
"set_schedule": {
41+
"service": "mdi:calendar-clock"
42+
},
43+
"set_preset_mode_with_end_datetime": {
44+
"service": "mdi:calendar-clock"
45+
},
46+
"set_temperature_with_end_datetime": {
47+
"service": "mdi:thermometer"
48+
},
49+
"set_temperature_with_time_period": {
50+
"service": "mdi:thermometer"
51+
},
52+
"clear_temperature_setting": {
53+
"service": "mdi:thermometer"
54+
},
55+
"set_persons_home": {
56+
"service": "mdi:home"
57+
},
58+
"set_person_away": {
59+
"service": "mdi:walk"
60+
},
61+
"register_webhook": {
62+
"service": "mdi:link-variant"
63+
},
64+
"unregister_webhook": {
65+
"service": "mdi:link-variant-off"
66+
}
4767
}
4868
}

‎custom_components/netatmo/light.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ def __init__(self, netatmo_device: NetatmoDevice) -> None:
176176
async def async_turn_on(self, **kwargs: Any) -> None:
177177
"""Turn light on."""
178178
if ATTR_BRIGHTNESS in kwargs:
179-
await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
179+
await self.device.async_set_brightness(
180+
round(kwargs[ATTR_BRIGHTNESS] / 2.55)
181+
)
180182

181183
else:
182184
await self.device.async_on()
@@ -197,6 +199,6 @@ def async_update_callback(self) -> None:
197199

198200
if (brightness := self.device.brightness) is not None:
199201
# Netatmo uses a range of [0, 100] to control brightness
200-
self._attr_brightness = round((brightness / 100) * 255)
202+
self._attr_brightness = round(brightness * 2.55)
201203
else:
202204
self._attr_brightness = None

‎custom_components/netatmo/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "netatmo",
33
"name": "Netatmo",
4-
"version": "2024.06.31",
4+
"version": "2024.09.26",
55
"after_dependencies": ["cloud", "media_source"],
66
"codeowners": ["@cgtobi"],
77
"config_flow": true,

‎custom_components/netatmo/pyatmo/account.py

+19-16
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6-
from typing import TYPE_CHECKING, Any
6+
from typing import TYPE_CHECKING, Any, cast
77
from uuid import uuid4
88

99
from . import modules
@@ -20,7 +20,7 @@
2020
)
2121
from .helpers import extract_raw_data
2222
from .home import Home
23-
from .modules.module import MeasureInterval, Module
23+
from .modules.module import Energy, MeasureInterval, Module
2424

2525
if TYPE_CHECKING:
2626
from .auth import AbstractAsyncAuth
@@ -31,7 +31,11 @@
3131
class AsyncAccount:
3232
"""Async class of a Netatmo account."""
3333

34-
def __init__(self, auth: AbstractAsyncAuth, favorite_stations: bool = True) -> None:
34+
def __init__(
35+
self,
36+
auth: AbstractAsyncAuth,
37+
favorite_stations: bool = True,
38+
) -> None:
3539
"""Initialize the Netatmo account."""
3640

3741
self.auth: AbstractAsyncAuth = auth
@@ -57,7 +61,6 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None:
5761
disabled_homes_ids = []
5862

5963
for home in self.raw_data["homes"]:
60-
6164
home_id = home.get("id", "Unknown")
6265
home_name = home.get("name", "Unknown")
6366
self.all_homes_id[home_id] = home_name
@@ -72,10 +75,9 @@ def process_topology(self, disabled_homes_ids: list[str] | None = None) -> None:
7275
else:
7376
self.homes[home_id] = Home(self.auth, raw_data=home)
7477

75-
LOG.debug("account.process_topology for home %s %s", home_id, self.homes[home_id].name)
76-
7778
async def async_update_topology(
78-
self, disabled_homes_ids: list[str] | None = None
79+
self,
80+
disabled_homes_ids: list[str] | None = None,
7981
) -> None:
8082
"""Retrieve topology data from /homesdata."""
8183

@@ -129,12 +131,15 @@ async def async_update_measures(
129131
) -> None:
130132
"""Retrieve measures data from /getmeasure."""
131133

132-
await getattr(self.homes[home_id].modules[module_id], "async_update_measures")(
133-
start_time=start_time,
134-
end_time=end_time,
135-
interval=interval,
136-
days=days,
137-
)
134+
module = self.homes[home_id].modules[module_id]
135+
if module.has_feature("historical_data"):
136+
module = cast(Energy, module)
137+
await module.async_update_measures(
138+
start_time=start_time,
139+
end_time=end_time,
140+
interval=interval,
141+
days=days,
142+
)
138143

139144
def register_public_weather_area(
140145
self,
@@ -235,13 +240,11 @@ async def update_devices(
235240
"modules": modules_data,
236241
},
237242
)
238-
239-
LOG.debug("update_devices New home %s %s found.", home_id, self.homes[home_id].name)
240243
await self.homes[home_id].update(
241244
{HOME: {"modules": [normalize_weather_attributes(device_data)]}},
242245
)
243246
else:
244-
LOG.debug("No home %s found.", home_id)
247+
LOG.debug("No home %s (%s) found.", home_id, home_id)
245248

246249
for module_data in device_data.get("modules", []):
247250
module_data["home_id"] = home_id

‎custom_components/netatmo/pyatmo/auth.py

+46-21
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
import logging
99
from typing import Any
1010

11-
from aiohttp import ClientError, ClientResponse, ClientSession, ContentTypeError
11+
from aiohttp import (
12+
ClientError,
13+
ClientResponse,
14+
ClientSession,
15+
ClientTimeout,
16+
ContentTypeError,
17+
)
1218

1319
from .const import (
1420
AUTHORIZATION_HEADER,
@@ -51,7 +57,9 @@ async def async_get_image(
5157
try:
5258
access_token = await self.async_get_access_token()
5359
except ClientError as err:
54-
raise ApiError(f"Access token failure: {err}") from err
60+
error_type = type(err).__name__
61+
msg = f"Access token failure: {error_type} - {err}"
62+
raise ApiError(msg) from err
5563
headers = {AUTHORIZATION_HEADER: f"Bearer {access_token}"}
5664

5765
req_args = {"data": params if params is not None else {}}
@@ -61,17 +69,20 @@ async def async_get_image(
6169
url,
6270
**req_args, # type: ignore
6371
headers=headers,
64-
timeout=timeout,
72+
timeout=ClientTimeout(total=timeout),
6573
) as resp:
6674
resp_content = await resp.read()
6775

6876
if resp.headers.get("content-type") == "image/jpeg":
6977
return resp_content
7078

71-
raise ApiError(
79+
msg = (
7280
f"{resp.status} - "
7381
f"invalid content-type in response"
74-
f"when accessing '{url}'",
82+
f"when accessing '{url}'"
83+
)
84+
raise ApiError(
85+
msg,
7586
)
7687

7788
async def async_post_api_request(
@@ -104,20 +115,21 @@ async def async_post_request(
104115

105116
async with self.websession.post(
106117
url,
107-
**req_args,
118+
**req_args, # type: ignore
108119
headers=headers,
109-
timeout=timeout,
120+
timeout=ClientTimeout(total=timeout),
110121
) as resp:
111122
return await self.process_response(resp, url)
112123

113-
async def get_access_token(self):
124+
async def get_access_token(self) -> str:
114125
"""Get access token."""
115126
try:
116127
return await self.async_get_access_token()
117128
except ClientError as err:
118-
raise ApiError(f"Access token failure: {err}") from err
129+
msg = f"Access token failure: {err}"
130+
raise ApiError(msg) from err
119131

120-
def prepare_request_arguments(self, params):
132+
def prepare_request_arguments(self, params: dict | None) -> dict:
121133
"""Prepare request arguments."""
122134
req_args = {"data": params if params is not None else {}}
123135

@@ -131,7 +143,7 @@ def prepare_request_arguments(self, params):
131143

132144
return req_args
133145

134-
async def process_response(self, resp, url):
146+
async def process_response(self, resp: ClientResponse, url: str) -> ClientResponse:
135147
"""Process response."""
136148
resp_status = resp.status
137149
resp_content = await resp.read()
@@ -142,7 +154,12 @@ async def process_response(self, resp, url):
142154

143155
return await self.handle_success_response(resp, resp_content)
144156

145-
async def handle_error_response(self, resp, resp_status, url):
157+
async def handle_error_response(
158+
self,
159+
resp: ClientResponse,
160+
resp_status: int,
161+
url: str,
162+
) -> None:
146163
"""Handle error response."""
147164
try:
148165
resp_json = await resp.json()
@@ -159,19 +176,25 @@ async def handle_error_response(self, resp, resp_status, url):
159176
raise ApiErrorThrottling(
160177
message,
161178
)
162-
else:
163-
raise ApiError(
164-
message,
165-
)
179+
raise ApiError(
180+
message,
181+
)
166182

167183
except (JSONDecodeError, ContentTypeError) as exc:
168-
raise ApiError(
184+
msg = (
169185
f"{resp_status} - "
170186
f"{ERRORS.get(resp_status, '')} - "
171-
f"when accessing '{url}'",
187+
f"when accessing '{url}'"
188+
)
189+
raise ApiError(
190+
msg,
172191
) from exc
173192

174-
async def handle_success_response(self, resp, resp_content):
193+
async def handle_success_response(
194+
self,
195+
resp: ClientResponse,
196+
resp_content: bytes,
197+
) -> ClientResponse:
175198
"""Handle success response."""
176199
try:
177200
if "application/json" in resp.headers.get("content-type", []):
@@ -193,7 +216,8 @@ async def async_addwebhook(self, webhook_url: str) -> None:
193216
params={"url": webhook_url},
194217
)
195218
except asyncio.exceptions.TimeoutError as exc:
196-
raise ApiError("Webhook registration timed out") from exc
219+
msg = "Webhook registration timed out"
220+
raise ApiError(msg) from exc
197221
else:
198222
LOG.debug("addwebhook: %s", resp)
199223

@@ -205,6 +229,7 @@ async def async_dropwebhook(self) -> None:
205229
params={"app_types": "app_security"},
206230
)
207231
except asyncio.exceptions.TimeoutError as exc:
208-
raise ApiError("Webhook registration timed out") from exc
232+
msg = "Webhook registration timed out"
233+
raise ApiError(msg) from exc
209234
else:
210235
LOG.debug("dropwebhook: %s", resp)

‎custom_components/netatmo/pyatmo/const.py

+11-2
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,20 @@
8181
"write_thermostat", # Netatmo climate products
8282
]
8383

84+
EVENTS = "events"
85+
SCHEDULES = "schedules"
86+
8487
MANUAL = "manual"
8588
MAX = "max"
8689
HOME = "home"
8790
FROSTGUARD = "hg"
88-
SCHEDULES = "schedules"
89-
EVENTS = "events"
91+
SCHEDULE = "schedule"
92+
OFF = "off"
93+
AWAY = "away"
9094

95+
HEATING = "heating"
96+
COOLING = "cooling"
97+
IDLE = "idle"
9198

9299
STATION_TEMPERATURE_TYPE = "temperature"
93100
STATION_PRESSURE_TYPE = "pressure"
@@ -105,3 +112,5 @@
105112

106113
# 2 days of dynamic historical data stored
107114
MAX_HISTORY_TIME_FRAME = 24 * 2 * 3600
115+
116+
UNKNOWN = "unknown"

‎custom_components/netatmo/pyatmo/event.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from dataclasses import dataclass
66
from enum import Enum
7+
from typing import TYPE_CHECKING
78

8-
from .const import RawData
9+
if TYPE_CHECKING:
10+
from .const import RawData
911

1012
EVENT_ATTRIBUTES_MAP = {"id": "entity_id", "type": "event_type", "time": "event_time"}
1113

@@ -86,6 +88,7 @@ class Event:
8688
message: str | None = None
8789
camera_id: str | None = None
8890
device_id: str | None = None
91+
module_id: str | None = None
8992
person_id: str | None = None
9093
video_id: str | None = None
9194
sub_type: int | None = None

0 commit comments

Comments
 (0)