Skip to content

Commit 57f4b7c

Browse files
authored
Merge branch 'dev' into content-fix
2 parents d901ab1 + 2208650 commit 57f4b7c

File tree

17 files changed

+531
-48
lines changed

17 files changed

+531
-48
lines changed

homeassistant/components/idasen_desk/strings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"address": "Device"
88
},
99
"data_description": {
10-
"address": "The bluetooth device for the desk."
10+
"address": "The Bluetooth device for the desk."
1111
}
1212
}
1313
},

homeassistant/components/jewish_calendar/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ def update_unique_id(
113113
"first_stars": "tset_hakohavim_tsom",
114114
"three_stars": "tset_hakohavim_shabbat",
115115
}
116-
new_keys = tuple(key_translations.values())
117-
if not entity_entry.unique_id.endswith(new_keys):
116+
old_keys = tuple(key_translations.keys())
117+
if entity_entry.unique_id.endswith(old_keys):
118118
old_key = entity_entry.unique_id.split("-")[1]
119119
new_unique_id = f"{config_entry.entry_id}-{key_translations[old_key]}"
120120
return {"new_unique_id": new_unique_id}
+172
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
"""Support for Qbus thermostat."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from qbusmqttapi.const import KEY_PROPERTIES_REGIME, KEY_PROPERTIES_SET_TEMPERATURE
7+
from qbusmqttapi.discovery import QbusMqttOutput
8+
from qbusmqttapi.state import QbusMqttThermoState, StateType
9+
10+
from homeassistant.components.climate import (
11+
ClimateEntity,
12+
ClimateEntityFeature,
13+
HVACAction,
14+
HVACMode,
15+
)
16+
from homeassistant.components.mqtt import ReceiveMessage, client as mqtt
17+
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
18+
from homeassistant.core import HomeAssistant
19+
from homeassistant.exceptions import ServiceValidationError
20+
from homeassistant.helpers.debounce import Debouncer
21+
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
22+
23+
from .const import DOMAIN
24+
from .coordinator import QbusConfigEntry
25+
from .entity import QbusEntity, add_new_outputs
26+
27+
PARALLEL_UPDATES = 0
28+
29+
STATE_REQUEST_DELAY = 2
30+
31+
_LOGGER = logging.getLogger(__name__)
32+
33+
34+
async def async_setup_entry(
35+
hass: HomeAssistant,
36+
entry: QbusConfigEntry,
37+
async_add_entities: AddConfigEntryEntitiesCallback,
38+
) -> None:
39+
"""Set up climate entities."""
40+
41+
coordinator = entry.runtime_data
42+
added_outputs: list[QbusMqttOutput] = []
43+
44+
def _check_outputs() -> None:
45+
add_new_outputs(
46+
coordinator,
47+
added_outputs,
48+
lambda output: output.type == "thermo",
49+
QbusClimate,
50+
async_add_entities,
51+
)
52+
53+
_check_outputs()
54+
entry.async_on_unload(coordinator.async_add_listener(_check_outputs))
55+
56+
57+
class QbusClimate(QbusEntity, ClimateEntity):
58+
"""Representation of a Qbus climate entity."""
59+
60+
_attr_hvac_modes = [HVACMode.HEAT]
61+
_attr_supported_features = (
62+
ClimateEntityFeature.PRESET_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
63+
)
64+
_attr_temperature_unit = UnitOfTemperature.CELSIUS
65+
66+
def __init__(self, mqtt_output: QbusMqttOutput) -> None:
67+
"""Initialize climate entity."""
68+
69+
super().__init__(mqtt_output)
70+
71+
self._attr_hvac_action = HVACAction.IDLE
72+
self._attr_hvac_mode = HVACMode.HEAT
73+
74+
set_temp: dict[str, Any] = mqtt_output.properties.get(
75+
KEY_PROPERTIES_SET_TEMPERATURE, {}
76+
)
77+
current_regime: dict[str, Any] = mqtt_output.properties.get(
78+
KEY_PROPERTIES_REGIME, {}
79+
)
80+
81+
self._attr_min_temp: float = set_temp.get("min", 0)
82+
self._attr_max_temp: float = set_temp.get("max", 35)
83+
self._attr_target_temperature_step: float = set_temp.get("step", 0.5)
84+
self._attr_preset_modes: list[str] = current_regime.get("enumValues", [])
85+
self._attr_preset_mode: str = (
86+
self._attr_preset_modes[0] if len(self._attr_preset_modes) > 0 else ""
87+
)
88+
89+
self._request_state_debouncer: Debouncer | None = None
90+
91+
async def async_added_to_hass(self) -> None:
92+
"""Run when entity about to be added to hass."""
93+
self._request_state_debouncer = Debouncer(
94+
self.hass,
95+
_LOGGER,
96+
cooldown=STATE_REQUEST_DELAY,
97+
immediate=False,
98+
function=self._async_request_state,
99+
)
100+
await super().async_added_to_hass()
101+
102+
async def async_set_preset_mode(self, preset_mode: str) -> None:
103+
"""Set new target preset mode."""
104+
105+
if preset_mode not in self._attr_preset_modes:
106+
raise ServiceValidationError(
107+
translation_domain=DOMAIN,
108+
translation_key="invalid_preset",
109+
translation_placeholders={
110+
"preset": preset_mode,
111+
"options": ", ".join(self._attr_preset_modes),
112+
},
113+
)
114+
115+
state = QbusMqttThermoState(id=self._mqtt_output.id, type=StateType.STATE)
116+
state.write_regime(preset_mode)
117+
118+
await self._async_publish_output_state(state)
119+
120+
async def async_set_temperature(self, **kwargs: Any) -> None:
121+
"""Set new target temperature."""
122+
temperature = kwargs.get(ATTR_TEMPERATURE)
123+
124+
if temperature is not None and isinstance(temperature, float):
125+
state = QbusMqttThermoState(id=self._mqtt_output.id, type=StateType.STATE)
126+
state.write_set_temperature(temperature)
127+
128+
await self._async_publish_output_state(state)
129+
130+
async def _state_received(self, msg: ReceiveMessage) -> None:
131+
state = self._message_factory.parse_output_state(
132+
QbusMqttThermoState, msg.payload
133+
)
134+
135+
if state is None:
136+
return
137+
138+
if preset_mode := state.read_regime():
139+
self._attr_preset_mode = preset_mode
140+
141+
if current_temperature := state.read_current_temperature():
142+
self._attr_current_temperature = current_temperature
143+
144+
if target_temperature := state.read_set_temperature():
145+
self._attr_target_temperature = target_temperature
146+
147+
self._set_hvac_action()
148+
149+
# When the state type is "event", the payload only contains the changed
150+
# property. Request the state to get the full payload. However, changing
151+
# temperature step by step could cause a flood of state requests, so we're
152+
# holding off a few seconds before requesting the full state.
153+
if state.type == StateType.EVENT:
154+
assert self._request_state_debouncer is not None
155+
await self._request_state_debouncer.async_call()
156+
157+
self.async_schedule_update_ha_state()
158+
159+
def _set_hvac_action(self) -> None:
160+
if self.target_temperature is None or self.current_temperature is None:
161+
self._attr_hvac_action = HVACAction.IDLE
162+
return
163+
164+
self._attr_hvac_action = (
165+
HVACAction.HEATING
166+
if self.target_temperature > self.current_temperature
167+
else HVACAction.IDLE
168+
)
169+
170+
async def _async_request_state(self) -> None:
171+
request = self._message_factory.create_state_request([self._mqtt_output.id])
172+
await mqtt.async_publish(self.hass, request.topic, request.payload)

homeassistant/components/qbus/const.py

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
DOMAIN: Final = "qbus"
88
PLATFORMS: list[Platform] = [
9+
Platform.CLIMATE,
910
Platform.LIGHT,
1011
Platform.SWITCH,
1112
]

homeassistant/components/qbus/strings.json

+5
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@
1515
"error": {
1616
"no_controller": "No controllers were found"
1717
}
18+
},
19+
"exceptions": {
20+
"invalid_preset": {
21+
"message": "Preset mode \"{preset}\" is not valid. Valid preset modes are: {options}."
22+
}
1823
}
1924
}

homeassistant/components/reolink/entity.py

+5
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,13 @@ def __init__(
178178
else:
179179
self._dev_id = f"{self._host.unique_id}_ch{dev_ch}"
180180

181+
connections = set()
182+
if mac := self._host.api.baichuan.mac_address(dev_ch):
183+
connections.add((CONNECTION_NETWORK_MAC, mac))
184+
181185
self._attr_device_info = DeviceInfo(
182186
identifiers={(DOMAIN, self._dev_id)},
187+
connections=connections,
183188
via_device=(DOMAIN, self._host.unique_id),
184189
name=self._host.api.camera_name(dev_ch),
185190
model=self._host.api.camera_model(dev_ch),

homeassistant/components/reolink/strings.json

+6
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@
103103
},
104104
"config_entry_not_ready": {
105105
"message": "Error while trying to set up {host}: {err}"
106+
},
107+
"update_already_running": {
108+
"message": "Reolink firmware update already running, wait on completion before starting another"
109+
},
110+
"firmware_rate_limit": {
111+
"message": "Reolink firmware update server reached hourly rate limit: updating can be tried again in 1 hour"
106112
}
107113
},
108114
"issues": {

homeassistant/components/reolink/update.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
ReolinkHostCoordinatorEntity,
3232
ReolinkHostEntityDescription,
3333
)
34-
from .util import ReolinkConfigEntry, ReolinkData
34+
from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error
3535

3636
PARALLEL_UPDATES = 0
3737
RESUME_AFTER_INSTALL = 15
@@ -184,6 +184,7 @@ async def async_release_notes(self) -> str | None:
184184
f"## Release notes\n\n{new_firmware.release_notes}"
185185
)
186186

187+
@raise_translated_error
187188
async def async_install(
188189
self, version: str | None, backup: bool, **kwargs: Any
189190
) -> None:
@@ -196,6 +197,8 @@ async def async_install(
196197
try:
197198
await self._host.api.update_firmware(self._channel)
198199
except ReolinkError as err:
200+
if err.translation_key:
201+
raise
199202
raise HomeAssistantError(
200203
translation_domain=DOMAIN,
201204
translation_key="firmware_install_error",

homeassistant/components/reolink/util.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
2828
from homeassistant.helpers import device_registry as dr
2929
from homeassistant.helpers.storage import Store
30+
from homeassistant.helpers.translation import async_get_exception_message
3031
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
3132

3233
from .const import DOMAIN
@@ -97,6 +98,16 @@ def get_device_uid_and_ch(
9798
return (device_uid, ch, is_chime)
9899

99100

101+
def check_translation_key(err: ReolinkError) -> str | None:
102+
"""Check if the translation key from the upstream library is present."""
103+
if not err.translation_key:
104+
return None
105+
if async_get_exception_message(DOMAIN, err.translation_key) == err.translation_key:
106+
# translation key not found in strings.json
107+
return None
108+
return err.translation_key
109+
110+
100111
# Decorators
101112
def raise_translated_error[**P, R](
102113
func: Callable[P, Awaitable[R]],
@@ -110,73 +121,73 @@ async def decorator_raise_translated_error(*args: P.args, **kwargs: P.kwargs) ->
110121
except InvalidParameterError as err:
111122
raise ServiceValidationError(
112123
translation_domain=DOMAIN,
113-
translation_key="invalid_parameter",
124+
translation_key=check_translation_key(err) or "invalid_parameter",
114125
translation_placeholders={"err": str(err)},
115126
) from err
116127
except ApiError as err:
117128
raise HomeAssistantError(
118129
translation_domain=DOMAIN,
119-
translation_key="api_error",
130+
translation_key=check_translation_key(err) or "api_error",
120131
translation_placeholders={"err": str(err)},
121132
) from err
122133
except InvalidContentTypeError as err:
123134
raise HomeAssistantError(
124135
translation_domain=DOMAIN,
125-
translation_key="invalid_content_type",
136+
translation_key=check_translation_key(err) or "invalid_content_type",
126137
translation_placeholders={"err": str(err)},
127138
) from err
128139
except CredentialsInvalidError as err:
129140
raise HomeAssistantError(
130141
translation_domain=DOMAIN,
131-
translation_key="invalid_credentials",
142+
translation_key=check_translation_key(err) or "invalid_credentials",
132143
translation_placeholders={"err": str(err)},
133144
) from err
134145
except LoginError as err:
135146
raise HomeAssistantError(
136147
translation_domain=DOMAIN,
137-
translation_key="login_error",
148+
translation_key=check_translation_key(err) or "login_error",
138149
translation_placeholders={"err": str(err)},
139150
) from err
140151
except NoDataError as err:
141152
raise HomeAssistantError(
142153
translation_domain=DOMAIN,
143-
translation_key="no_data",
154+
translation_key=check_translation_key(err) or "no_data",
144155
translation_placeholders={"err": str(err)},
145156
) from err
146157
except UnexpectedDataError as err:
147158
raise HomeAssistantError(
148159
translation_domain=DOMAIN,
149-
translation_key="unexpected_data",
160+
translation_key=check_translation_key(err) or "unexpected_data",
150161
translation_placeholders={"err": str(err)},
151162
) from err
152163
except NotSupportedError as err:
153164
raise HomeAssistantError(
154165
translation_domain=DOMAIN,
155-
translation_key="not_supported",
166+
translation_key=check_translation_key(err) or "not_supported",
156167
translation_placeholders={"err": str(err)},
157168
) from err
158169
except SubscriptionError as err:
159170
raise HomeAssistantError(
160171
translation_domain=DOMAIN,
161-
translation_key="subscription_error",
172+
translation_key=check_translation_key(err) or "subscription_error",
162173
translation_placeholders={"err": str(err)},
163174
) from err
164175
except ReolinkConnectionError as err:
165176
raise HomeAssistantError(
166177
translation_domain=DOMAIN,
167-
translation_key="connection_error",
178+
translation_key=check_translation_key(err) or "connection_error",
168179
translation_placeholders={"err": str(err)},
169180
) from err
170181
except ReolinkTimeoutError as err:
171182
raise HomeAssistantError(
172183
translation_domain=DOMAIN,
173-
translation_key="timeout",
184+
translation_key=check_translation_key(err) or "timeout",
174185
translation_placeholders={"err": str(err)},
175186
) from err
176187
except ReolinkError as err:
177188
raise HomeAssistantError(
178189
translation_domain=DOMAIN,
179-
translation_key="unexpected",
190+
translation_key=check_translation_key(err) or "unexpected",
180191
translation_placeholders={"err": str(err)},
181192
) from err
182193

0 commit comments

Comments
 (0)