homeassistant 2025.10.0b0__py3-none-any.whl → 2025.10.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of homeassistant might be problematic. Click here for more details.

Files changed (82) hide show
  1. homeassistant/components/accuweather/manifest.json +1 -1
  2. homeassistant/components/accuweather/translations/nl.json +7 -1
  3. homeassistant/components/airzone/translations/nl.json +5 -0
  4. homeassistant/components/airzone/translations/zh-Hans.json +10 -0
  5. homeassistant/components/alexa_devices/binary_sensor.py +36 -38
  6. homeassistant/components/alexa_devices/config_flow.py +2 -2
  7. homeassistant/components/alexa_devices/coordinator.py +1 -1
  8. homeassistant/components/alexa_devices/diagnostics.py +1 -3
  9. homeassistant/components/alexa_devices/icons.json +0 -40
  10. homeassistant/components/alexa_devices/manifest.json +2 -2
  11. homeassistant/components/alexa_devices/sensor.py +13 -0
  12. homeassistant/components/alexa_devices/strings.json +0 -20
  13. homeassistant/components/alexa_devices/switch.py +27 -7
  14. homeassistant/components/alexa_devices/translations/bg.json +0 -13
  15. homeassistant/components/alexa_devices/translations/cs.json +0 -20
  16. homeassistant/components/alexa_devices/translations/de.json +0 -20
  17. homeassistant/components/alexa_devices/translations/el.json +0 -20
  18. homeassistant/components/alexa_devices/translations/en-GB.json +0 -20
  19. homeassistant/components/alexa_devices/translations/en.json +0 -20
  20. homeassistant/components/alexa_devices/translations/es.json +0 -20
  21. homeassistant/components/alexa_devices/translations/et.json +0 -20
  22. homeassistant/components/alexa_devices/translations/ga.json +0 -20
  23. homeassistant/components/alexa_devices/translations/he.json +0 -7
  24. homeassistant/components/alexa_devices/translations/hu.json +0 -20
  25. homeassistant/components/alexa_devices/translations/it.json +0 -20
  26. homeassistant/components/alexa_devices/translations/ja.json +0 -19
  27. homeassistant/components/alexa_devices/translations/lt.json +0 -20
  28. homeassistant/components/alexa_devices/translations/mk.json +0 -14
  29. homeassistant/components/alexa_devices/translations/nl.json +0 -20
  30. homeassistant/components/alexa_devices/translations/pl.json +0 -5
  31. homeassistant/components/alexa_devices/translations/pt.json +0 -20
  32. homeassistant/components/alexa_devices/translations/ru.json +0 -5
  33. homeassistant/components/alexa_devices/translations/sk.json +0 -20
  34. homeassistant/components/alexa_devices/translations/sv.json +0 -20
  35. homeassistant/components/alexa_devices/translations/tr.json +0 -5
  36. homeassistant/components/alexa_devices/translations/zh-Hans.json +0 -20
  37. homeassistant/components/alexa_devices/translations/zh-Hant.json +0 -20
  38. homeassistant/components/alexa_devices/utils.py +24 -1
  39. homeassistant/components/assist_pipeline/translations/nl.json +1 -0
  40. homeassistant/components/blue_current/translations/zh-Hans.json +44 -0
  41. homeassistant/components/comelit/manifest.json +1 -1
  42. homeassistant/components/cync/translations/cs.json +32 -0
  43. homeassistant/components/cync/translations/nl.json +20 -0
  44. homeassistant/components/cync/translations/sk.json +32 -0
  45. homeassistant/components/cync/translations/zh-Hans.json +32 -0
  46. homeassistant/components/cync/translations/zh-Hant.json +32 -0
  47. homeassistant/components/ekeybionyx/translations/nl.json +24 -0
  48. homeassistant/components/ekeybionyx/translations/zh-Hans.json +66 -0
  49. homeassistant/components/esphome/config_flow.py +9 -1
  50. homeassistant/components/esphome/manager.py +10 -0
  51. homeassistant/components/esphome/manifest.json +1 -1
  52. homeassistant/components/esphome/translations/nl.json +1 -1
  53. homeassistant/components/frontend/manifest.json +1 -1
  54. homeassistant/components/letpot/translations/nl.json +5 -0
  55. homeassistant/components/letpot/translations/zh-Hans.json +10 -0
  56. homeassistant/components/libre_hardware_monitor/manifest.json +1 -1
  57. homeassistant/components/mvglive/manifest.json +2 -4
  58. homeassistant/components/mvglive/sensor.py +116 -86
  59. homeassistant/components/portainer/binary_sensor.py +9 -1
  60. homeassistant/components/portainer/entity.py +1 -1
  61. homeassistant/components/roborock/coordinator.py +3 -7
  62. homeassistant/components/route_b_smart_meter/translations/nl.json +24 -0
  63. homeassistant/components/route_b_smart_meter/translations/zh-Hans.json +42 -0
  64. homeassistant/components/smartthings/manifest.json +1 -1
  65. homeassistant/components/smartthings/translations/nl.json +7 -0
  66. homeassistant/components/smartthings/translations/zh-Hans.json +21 -0
  67. homeassistant/components/switchbot/translations/nl.json +5 -0
  68. homeassistant/components/switchbot_cloud/translations/nl.json +2 -1
  69. homeassistant/components/tolo/translations/nl.json +2 -1
  70. homeassistant/components/usage_prediction/common_control.py +56 -59
  71. homeassistant/components/voip/translations/nl.json +1 -1
  72. homeassistant/components/wyoming/translations/nl.json +1 -1
  73. homeassistant/const.py +1 -1
  74. homeassistant/loader.py +3 -0
  75. homeassistant/package_constraints.txt +1 -1
  76. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/METADATA +1 -1
  77. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/RECORD +82 -73
  78. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/WHEEL +0 -0
  79. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/entry_points.txt +0 -0
  80. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/licenses/LICENSE.md +0 -0
  81. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/licenses/homeassistant/backports/LICENSE.Python +0 -0
  82. {homeassistant-2025.10.0b0.dist-info → homeassistant-2025.10.0b2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,66 @@
1
+ {
2
+ "config": {
3
+ "abort": {
4
+ "already_configured": "\u8d26\u6237\u5df2\u914d\u7f6e",
5
+ "already_in_progress": "\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d",
6
+ "authorize_url_timeout": "\u751f\u6210\u6388\u6743\u7f51\u5740\u8d85\u65f6\u3002",
7
+ "cannot_connect": "\u8fde\u63a5\u5230 {ekeybionyx} \u5931\u8d25\u3002\u8bf7\u68c0\u67e5\u60a8\u7684\u4e92\u8054\u7f51\u8fde\u63a5\uff0c\u7136\u540e\u91cd\u8bd5\u3002",
8
+ "missing_configuration": "\u6b64\u7ec4\u4ef6\u5c1a\u672a\u914d\u7f6e\u3002\u8bf7\u53c2\u9605\u6587\u6863\u3002",
9
+ "no_available_webhooks": "{ekeybionyx} \u7cfb\u7edf\u4e2d\u6ca1\u6709\u53ef\u7528\u7684 webhook \u3002\u8bf7\u5220\u9664\u4e00\u4e9b webhook \u540e\u91cd\u8bd5\u3002",
10
+ "no_own_systems": "\u60a8\u7684\u8d26\u6237\u4e0d\u5177\u5907\u4efb\u4f55\u7cfb\u7edf\u7684\u7ba1\u7406\u5458\u8bbf\u95ee\u6743\u9650\u3002",
11
+ "no_url_available": "\u6ca1\u6709\u53ef\u7528\u7684\u7f51\u5740\u3002\u6709\u5173\u6b64\u9519\u8bef\u7684\u4fe1\u606f\uff0c\u8bf7[\u68c0\u67e5\u5e2e\u52a9\u7ae0\u8282]({docs_url})",
12
+ "oauth_error": "\u6536\u5230\u65e0\u6548\u7684\u4ee4\u724c\u6570\u636e\u3002",
13
+ "oauth_failed": "\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\u65f6\u51fa\u9519\u3002",
14
+ "oauth_timeout": "\u89e3\u6790 OAuth \u4ee4\u724c\u8d85\u65f6\u3002",
15
+ "oauth_unauthorized": "\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\u65f6\u51fa\u73b0 OAuth \u6388\u6743\u9519\u8bef\u3002",
16
+ "user_rejected_authorize": "\u8d26\u6237\u94fe\u63a5\u88ab\u62d2\u7edd\uff1a{error}"
17
+ },
18
+ "create_entry": {
19
+ "default": "\u8ba4\u8bc1\u6210\u529f"
20
+ },
21
+ "error": {
22
+ "invalid_name": "\u540d\u79f0\u65e0\u6548",
23
+ "invalid_url": "\u7f51\u5740\u65e0\u6548",
24
+ "no_webhooks_provided": "\u672a\u63d0\u4f9b\u4e8b\u4ef6\u540d\u79f0"
25
+ },
26
+ "progress": {
27
+ "check_deletion_status": "\u8bf7\u6253\u5f00 {ekeybionyx} \u5e94\u7528\u5e76\u786e\u8ba4\u5220\u9664\u8be5\u529f\u80fd\u3002"
28
+ },
29
+ "step": {
30
+ "choose_system": {
31
+ "data": {
32
+ "system": "\u7cfb\u7edf"
33
+ },
34
+ "data_description": {
35
+ "system": "\u5e94\u8bbe\u7f6e\u4e8b\u4ef6\u5b9e\u4f53\u7684\u7cfb\u7edf\u3002"
36
+ },
37
+ "description": "\u8bf7\u9009\u62e9\u60a8\u60f3\u8981\u8fde\u63a5\u5230 Home Assistant \u7684 {ekeybionyx} \u7cfb\u7edf\u3002"
38
+ },
39
+ "delete_webhooks": {
40
+ "description": "\u6b64\u7cfb\u7edf\u5df2\u8fde\u63a5\u81f3 Home Assistant \u3002\u5982\u679c\u7ee7\u7eed\u64cd\u4f5c\uff0c\u4e4b\u524d\u914d\u7f6e\u7684\u529f\u80fd\u5c06\u88ab\u5220\u9664\u3002"
41
+ },
42
+ "pick_implementation": {
43
+ "title": "\u9009\u62e9\u8eab\u4efd\u9a8c\u8bc1\u65b9\u6cd5"
44
+ },
45
+ "webhooks": {
46
+ "data": {
47
+ "url": "Home Assistant \u7f51\u5740",
48
+ "webhook1": "\u4e8b\u4ef6\u5b9e\u4f53 1",
49
+ "webhook2": "\u4e8b\u4ef6\u5b9e\u4f53 2",
50
+ "webhook3": "\u4e8b\u4ef6\u5b9e\u4f53 3",
51
+ "webhook4": "\u4e8b\u4ef6\u5b9e\u4f53 4",
52
+ "webhook5": "\u4e8b\u4ef6\u5b9e\u4f53 5"
53
+ },
54
+ "data_description": {
55
+ "url": "\u53ef\u4ee5\u4ece\u6307\u7eb9\u63a7\u5236\u5668\u8bbf\u95ee\u7684 Home Assistant \u5b9e\u4f8b\u7f51\u5740",
56
+ "webhook1": "\u88ab\u6620\u5c04\u5230\u51fd\u6570\u7684\u4e8b\u4ef6\u5b9e\u4f53 1 \u7684\u540d\u79f0",
57
+ "webhook2": "\u88ab\u6620\u5c04\u5230\u51fd\u6570\u7684\u4e8b\u4ef6\u5b9e\u4f53 2 \u7684\u540d\u79f0",
58
+ "webhook3": "\u88ab\u6620\u5c04\u5230\u51fd\u6570\u7684\u4e8b\u4ef6\u5b9e\u4f53 3 \u7684\u540d\u79f0",
59
+ "webhook4": "\u88ab\u6620\u5c04\u5230\u51fd\u6570\u7684\u4e8b\u4ef6\u5b9e\u4f53 4 \u7684\u540d\u79f0",
60
+ "webhook5": "\u88ab\u6620\u5c04\u5230\u51fd\u6570\u7684\u4e8b\u4ef6\u5b9e\u4f53 5 \u7684\u540d\u79f0"
61
+ },
62
+ "description": "\u8bf7\u4e3a\u60a8\u7684\u4e8b\u4ef6\u5b9e\u4f53\u547d\u540d\u3002\u8fd9\u4e9b\u4e8b\u4ef6\u5b9e\u4f53\u5c06\u5728 {ekeybionyx} \u5e94\u7528\u4e2d\u6620\u5c04\u4e3a\u51fd\u6570\u3002\u60a8\u6700\u591a\u53ef\u4ee5\u914d\u7f6e {webhooks_available} \u4e2a\u4e8b\u4ef6\u5b9e\u4f53\u3002\u5982\u679c\u540d\u79f0\u7559\u7a7a\uff0c\u5219\u8df3\u8fc7\u8be5\u4e8b\u4ef6\u5b9e\u4f53\u7684\u8bbe\u7f6e\u3002"
63
+ }
64
+ }
65
+ }
66
+ }
@@ -57,6 +57,7 @@ from .manager import async_replace_device
57
57
 
58
58
  ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
59
59
  ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
60
+ ERROR_INVALID_PASSWORD_AUTH = "invalid_auth"
60
61
  _LOGGER = logging.getLogger(__name__)
61
62
 
62
63
  ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
@@ -137,6 +138,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
137
138
  self._password = ""
138
139
  return await self._async_authenticate_or_add()
139
140
 
141
+ if error == ERROR_INVALID_PASSWORD_AUTH or (
142
+ error is None and self._device_info and self._device_info.uses_password
143
+ ):
144
+ return await self.async_step_authenticate()
145
+
140
146
  if error is None and entry_data.get(CONF_NOISE_PSK):
141
147
  # Device was configured with encryption but now connects without it.
142
148
  # Check if it's the same device before offering to remove encryption.
@@ -690,13 +696,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
690
696
  cli = APIClient(
691
697
  host,
692
698
  port or DEFAULT_PORT,
693
- "",
699
+ self._password or "",
694
700
  zeroconf_instance=zeroconf_instance,
695
701
  noise_psk=noise_psk,
696
702
  )
697
703
  try:
698
704
  await cli.connect()
699
705
  self._device_info = await cli.device_info()
706
+ except InvalidAuthAPIError:
707
+ return ERROR_INVALID_PASSWORD_AUTH
700
708
  except RequiresEncryptionAPIError:
701
709
  return ERROR_REQUIRES_ENCRYPTION_KEY
702
710
  except InvalidEncryptionKeyAPIError as ex:
@@ -372,6 +372,9 @@ class ESPHomeManager:
372
372
  """Subscribe to states and list entities on successful API login."""
373
373
  try:
374
374
  await self._on_connect()
375
+ except InvalidAuthAPIError as err:
376
+ _LOGGER.warning("Authentication failed for %s: %s", self.host, err)
377
+ await self._start_reauth_and_disconnect()
375
378
  except APIConnectionError as err:
376
379
  _LOGGER.warning(
377
380
  "Error getting setting up connection for %s: %s", self.host, err
@@ -641,7 +644,14 @@ class ESPHomeManager:
641
644
  if self.reconnect_logic:
642
645
  await self.reconnect_logic.stop()
643
646
  return
647
+ await self._start_reauth_and_disconnect()
648
+
649
+ async def _start_reauth_and_disconnect(self) -> None:
650
+ """Start reauth flow and stop reconnection attempts."""
644
651
  self.entry.async_start_reauth(self.hass)
652
+ await self.cli.disconnect()
653
+ if self.reconnect_logic:
654
+ await self.reconnect_logic.stop()
645
655
 
646
656
  async def _handle_dynamic_encryption_key(
647
657
  self, device_info: EsphomeDeviceInfo
@@ -17,7 +17,7 @@
17
17
  "mqtt": ["esphome/discover/#"],
18
18
  "quality_scale": "platinum",
19
19
  "requirements": [
20
- "aioesphomeapi==41.9.0",
20
+ "aioesphomeapi==41.10.0",
21
21
  "esphome-dashboard-api==1.3.0",
22
22
  "bleak-esphome==3.3.0"
23
23
  ],
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "select": {
60
60
  "pipeline": {
61
- "name": "Assistent",
61
+ "name": "Assistent{index}",
62
62
  "state": {
63
63
  "preferred": "Voorkeur"
64
64
  }
@@ -20,5 +20,5 @@
20
20
  "documentation": "https://www.home-assistant.io/integrations/frontend",
21
21
  "integration_type": "system",
22
22
  "quality_scale": "internal",
23
- "requirements": ["home-assistant-frontend==20250924.0"]
23
+ "requirements": ["home-assistant-frontend==20250925.1"]
24
24
  }
@@ -43,6 +43,11 @@
43
43
  "name": "Pompfout"
44
44
  }
45
45
  },
46
+ "number": {
47
+ "plant_days": {
48
+ "unit_of_measurement": "dagen"
49
+ }
50
+ },
46
51
  "select": {
47
52
  "display_temperature_unit": {
48
53
  "name": "Temperatuureenheid op het display",
@@ -49,6 +49,15 @@
49
49
  "name": "\u91cd\u65b0\u586b\u5145\u51fa\u9519"
50
50
  }
51
51
  },
52
+ "number": {
53
+ "light_brightness": {
54
+ "name": "\u706f\u5149\u4eae\u5ea6"
55
+ },
56
+ "plant_days": {
57
+ "name": "\u690d\u7269\u5e74\u9f84",
58
+ "unit_of_measurement": "\u5929"
59
+ }
60
+ },
52
61
  "select": {
53
62
  "display_temperature_unit": {
54
63
  "name": "\u6e29\u5ea6\u663e\u793a\u5355\u4f4d",
@@ -58,6 +67,7 @@
58
67
  }
59
68
  },
60
69
  "light_brightness": {
70
+ "name": "\u706f\u5149\u4eae\u5ea6",
61
71
  "state": {
62
72
  "high": "\u9ad8",
63
73
  "low": "\u4f4e"
@@ -6,5 +6,5 @@
6
6
  "documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
7
7
  "iot_class": "local_polling",
8
8
  "quality_scale": "silver",
9
- "requirements": ["librehardwaremonitor-api==1.3.1"]
9
+ "requirements": ["librehardwaremonitor-api==1.4.0"]
10
10
  }
@@ -2,10 +2,8 @@
2
2
  "domain": "mvglive",
3
3
  "name": "MVG",
4
4
  "codeowners": [],
5
- "disabled": "This integration is disabled because it uses non-open source code to operate.",
6
5
  "documentation": "https://www.home-assistant.io/integrations/mvglive",
7
6
  "iot_class": "cloud_polling",
8
- "loggers": ["MVGLive"],
9
- "quality_scale": "legacy",
10
- "requirements": ["PyMVGLive==1.1.4"]
7
+ "loggers": ["MVG"],
8
+ "requirements": ["mvg==1.4.0"]
11
9
  }
@@ -1,13 +1,14 @@
1
1
  """Support for departure information for public transport in Munich."""
2
2
 
3
- # mypy: ignore-errors
4
3
  from __future__ import annotations
5
4
 
5
+ from collections.abc import Mapping
6
6
  from copy import deepcopy
7
7
  from datetime import timedelta
8
8
  import logging
9
+ from typing import Any
9
10
 
10
- import MVGLive
11
+ from mvg import MvgApi, MvgApiError, TransportType
11
12
  import voluptuous as vol
12
13
 
13
14
  from homeassistant.components.sensor import (
@@ -19,6 +20,7 @@ from homeassistant.core import HomeAssistant
19
20
  from homeassistant.helpers import config_validation as cv
20
21
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
21
22
  from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
23
+ import homeassistant.util.dt as dt_util
22
24
 
23
25
  _LOGGER = logging.getLogger(__name__)
24
26
 
@@ -44,53 +46,55 @@ ICONS = {
44
46
  "SEV": "mdi:checkbox-blank-circle-outline",
45
47
  "-": "mdi:clock",
46
48
  }
47
- ATTRIBUTION = "Data provided by MVG-live.de"
49
+
50
+ ATTRIBUTION = "Data provided by mvg.de"
48
51
 
49
52
  SCAN_INTERVAL = timedelta(seconds=30)
50
53
 
51
- PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
52
- {
53
- vol.Required(CONF_NEXT_DEPARTURE): [
54
- {
55
- vol.Required(CONF_STATION): cv.string,
56
- vol.Optional(CONF_DESTINATIONS, default=[""]): cv.ensure_list_csv,
57
- vol.Optional(CONF_DIRECTIONS, default=[""]): cv.ensure_list_csv,
58
- vol.Optional(CONF_LINES, default=[""]): cv.ensure_list_csv,
59
- vol.Optional(
60
- CONF_PRODUCTS, default=DEFAULT_PRODUCT
61
- ): cv.ensure_list_csv,
62
- vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int,
63
- vol.Optional(CONF_NUMBER, default=1): cv.positive_int,
64
- vol.Optional(CONF_NAME): cv.string,
65
- }
66
- ]
67
- }
54
+ PLATFORM_SCHEMA = vol.All(
55
+ cv.deprecated(CONF_DIRECTIONS),
56
+ SENSOR_PLATFORM_SCHEMA.extend(
57
+ {
58
+ vol.Required(CONF_NEXT_DEPARTURE): [
59
+ {
60
+ vol.Required(CONF_STATION): cv.string,
61
+ vol.Optional(CONF_DESTINATIONS, default=[""]): cv.ensure_list_csv,
62
+ vol.Optional(CONF_DIRECTIONS, default=[""]): cv.ensure_list_csv,
63
+ vol.Optional(CONF_LINES, default=[""]): cv.ensure_list_csv,
64
+ vol.Optional(
65
+ CONF_PRODUCTS, default=DEFAULT_PRODUCT
66
+ ): cv.ensure_list_csv,
67
+ vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int,
68
+ vol.Optional(CONF_NUMBER, default=1): cv.positive_int,
69
+ vol.Optional(CONF_NAME): cv.string,
70
+ }
71
+ ]
72
+ }
73
+ ),
68
74
  )
69
75
 
70
76
 
71
- def setup_platform(
77
+ async def async_setup_platform(
72
78
  hass: HomeAssistant,
73
79
  config: ConfigType,
74
80
  add_entities: AddEntitiesCallback,
75
81
  discovery_info: DiscoveryInfoType | None = None,
76
82
  ) -> None:
77
83
  """Set up the MVGLive sensor."""
78
- add_entities(
79
- (
80
- MVGLiveSensor(
81
- nextdeparture.get(CONF_STATION),
82
- nextdeparture.get(CONF_DESTINATIONS),
83
- nextdeparture.get(CONF_DIRECTIONS),
84
- nextdeparture.get(CONF_LINES),
85
- nextdeparture.get(CONF_PRODUCTS),
86
- nextdeparture.get(CONF_TIMEOFFSET),
87
- nextdeparture.get(CONF_NUMBER),
88
- nextdeparture.get(CONF_NAME),
89
- )
90
- for nextdeparture in config[CONF_NEXT_DEPARTURE]
91
- ),
92
- True,
93
- )
84
+ sensors = [
85
+ MVGLiveSensor(
86
+ hass,
87
+ nextdeparture.get(CONF_STATION),
88
+ nextdeparture.get(CONF_DESTINATIONS),
89
+ nextdeparture.get(CONF_LINES),
90
+ nextdeparture.get(CONF_PRODUCTS),
91
+ nextdeparture.get(CONF_TIMEOFFSET),
92
+ nextdeparture.get(CONF_NUMBER),
93
+ nextdeparture.get(CONF_NAME),
94
+ )
95
+ for nextdeparture in config[CONF_NEXT_DEPARTURE]
96
+ ]
97
+ add_entities(sensors, True)
94
98
 
95
99
 
96
100
  class MVGLiveSensor(SensorEntity):
@@ -100,38 +104,38 @@ class MVGLiveSensor(SensorEntity):
100
104
 
101
105
  def __init__(
102
106
  self,
103
- station,
107
+ hass: HomeAssistant,
108
+ station_name,
104
109
  destinations,
105
- directions,
106
110
  lines,
107
111
  products,
108
112
  timeoffset,
109
113
  number,
110
114
  name,
111
- ):
115
+ ) -> None:
112
116
  """Initialize the sensor."""
113
- self._station = station
114
117
  self._name = name
118
+ self._station_name = station_name
115
119
  self.data = MVGLiveData(
116
- station, destinations, directions, lines, products, timeoffset, number
120
+ hass, station_name, destinations, lines, products, timeoffset, number
117
121
  )
118
122
  self._state = None
119
123
  self._icon = ICONS["-"]
120
124
 
121
125
  @property
122
- def name(self):
126
+ def name(self) -> str | None:
123
127
  """Return the name of the sensor."""
124
128
  if self._name:
125
129
  return self._name
126
- return self._station
130
+ return self._station_name
127
131
 
128
132
  @property
129
- def native_value(self):
133
+ def native_value(self) -> str | None:
130
134
  """Return the next departure time."""
131
135
  return self._state
132
136
 
133
137
  @property
134
- def extra_state_attributes(self):
138
+ def extra_state_attributes(self) -> Mapping[str, Any] | None:
135
139
  """Return the state attributes."""
136
140
  if not (dep := self.data.departures):
137
141
  return None
@@ -140,88 +144,114 @@ class MVGLiveSensor(SensorEntity):
140
144
  return attr
141
145
 
142
146
  @property
143
- def icon(self):
147
+ def icon(self) -> str | None:
144
148
  """Icon to use in the frontend, if any."""
145
149
  return self._icon
146
150
 
147
151
  @property
148
- def native_unit_of_measurement(self):
152
+ def native_unit_of_measurement(self) -> str | None:
149
153
  """Return the unit this state is expressed in."""
150
154
  return UnitOfTime.MINUTES
151
155
 
152
- def update(self) -> None:
156
+ async def async_update(self) -> None:
153
157
  """Get the latest data and update the state."""
154
- self.data.update()
158
+ await self.data.update()
155
159
  if not self.data.departures:
156
- self._state = "-"
160
+ self._state = None
157
161
  self._icon = ICONS["-"]
158
162
  else:
159
- self._state = self.data.departures[0].get("time", "-")
160
- self._icon = ICONS[self.data.departures[0].get("product", "-")]
163
+ self._state = self.data.departures[0].get("time_in_mins", "-")
164
+ self._icon = self.data.departures[0].get("icon", ICONS["-"])
165
+
166
+
167
+ def _get_minutes_until_departure(departure_time: int) -> int:
168
+ """Calculate the time difference in minutes between the current time and a given departure time.
169
+
170
+ Args:
171
+ departure_time: Unix timestamp of the departure time, in seconds.
172
+
173
+ Returns:
174
+ The time difference in minutes, as an integer.
175
+
176
+ """
177
+ current_time = dt_util.utcnow()
178
+ departure_datetime = dt_util.utc_from_timestamp(departure_time)
179
+ time_difference = (departure_datetime - current_time).total_seconds()
180
+ return int(time_difference / 60.0)
161
181
 
162
182
 
163
183
  class MVGLiveData:
164
- """Pull data from the mvg-live.de web page."""
184
+ """Pull data from the mvg.de web page."""
165
185
 
166
186
  def __init__(
167
- self, station, destinations, directions, lines, products, timeoffset, number
168
- ):
187
+ self,
188
+ hass: HomeAssistant,
189
+ station_name,
190
+ destinations,
191
+ lines,
192
+ products,
193
+ timeoffset,
194
+ number,
195
+ ) -> None:
169
196
  """Initialize the sensor."""
170
- self._station = station
197
+ self._hass = hass
198
+ self._station_name = station_name
199
+ self._station_id = None
171
200
  self._destinations = destinations
172
- self._directions = directions
173
201
  self._lines = lines
174
202
  self._products = products
175
203
  self._timeoffset = timeoffset
176
204
  self._number = number
177
- self._include_ubahn = "U-Bahn" in self._products
178
- self._include_tram = "Tram" in self._products
179
- self._include_bus = "Bus" in self._products
180
- self._include_sbahn = "S-Bahn" in self._products
181
- self.mvg = MVGLive.MVGLive()
182
- self.departures = []
205
+ self.departures: list[dict[str, Any]] = []
183
206
 
184
- def update(self):
207
+ async def update(self):
185
208
  """Update the connection data."""
209
+ if self._station_id is None:
210
+ try:
211
+ station = await MvgApi.station_async(self._station_name)
212
+ self._station_id = station["id"]
213
+ except MvgApiError as err:
214
+ _LOGGER.error(
215
+ "Failed to resolve station %s: %s", self._station_name, err
216
+ )
217
+ self.departures = []
218
+ return
219
+
186
220
  try:
187
- _departures = self.mvg.getlivedata(
188
- station=self._station,
189
- timeoffset=self._timeoffset,
190
- ubahn=self._include_ubahn,
191
- tram=self._include_tram,
192
- bus=self._include_bus,
193
- sbahn=self._include_sbahn,
221
+ _departures = await MvgApi.departures_async(
222
+ station_id=self._station_id,
223
+ offset=self._timeoffset,
224
+ limit=self._number,
225
+ transport_types=[
226
+ transport_type
227
+ for transport_type in TransportType
228
+ if transport_type.value[0] in self._products
229
+ ]
230
+ if self._products
231
+ else None,
194
232
  )
195
233
  except ValueError:
196
234
  self.departures = []
197
235
  _LOGGER.warning("Returned data not understood")
198
236
  return
199
237
  self.departures = []
200
- for i, _departure in enumerate(_departures):
201
- # find the first departure meeting the criteria
238
+ for _departure in _departures:
202
239
  if (
203
240
  "" not in self._destinations[:1]
204
241
  and _departure["destination"] not in self._destinations
205
242
  ):
206
243
  continue
207
244
 
208
- if (
209
- "" not in self._directions[:1]
210
- and _departure["direction"] not in self._directions
211
- ):
245
+ if "" not in self._lines[:1] and _departure["line"] not in self._lines:
212
246
  continue
213
247
 
214
- if "" not in self._lines[:1] and _departure["linename"] not in self._lines:
215
- continue
248
+ time_to_departure = _get_minutes_until_departure(_departure["time"])
216
249
 
217
- if _departure["time"] < self._timeoffset:
250
+ if time_to_departure < self._timeoffset:
218
251
  continue
219
252
 
220
- # now select the relevant data
221
253
  _nextdep = {}
222
- for k in ("destination", "linename", "time", "direction", "product"):
254
+ for k in ("destination", "line", "type", "cancelled", "icon"):
223
255
  _nextdep[k] = _departure.get(k, "")
224
- _nextdep["time"] = int(_nextdep["time"])
256
+ _nextdep["time_in_mins"] = time_to_departure
225
257
  self.departures.append(_nextdep)
226
- if i == self._number - 1:
227
- break
@@ -131,7 +131,15 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
131
131
  self.entity_description = entity_description
132
132
  super().__init__(device_info, coordinator, via_device)
133
133
 
134
- self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
134
+ # Container ID's are ephemeral, so use the container name for the unique ID
135
+ # The first one, should always be unique, it's fine if users have aliases
136
+ # According to Docker's API docs, the first name is unique
137
+ device_identifier = (
138
+ self._device_info.names[0].replace("/", " ").strip()
139
+ if self._device_info.names
140
+ else None
141
+ )
142
+ self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_identifier}_{entity_description.key}"
135
143
 
136
144
  @property
137
145
  def available(self) -> bool:
@@ -60,7 +60,7 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
60
60
 
61
61
  self._attr_device_info = DeviceInfo(
62
62
  identifiers={
63
- (DOMAIN, f"{self.coordinator.config_entry.entry_id}_{self.device_id}")
63
+ (DOMAIN, f"{self.coordinator.config_entry.entry_id}_{device_name}")
64
64
  },
65
65
  manufacturer=DEFAULT_NAME,
66
66
  model="Container",
@@ -351,13 +351,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
351
351
  def _set_current_map(self) -> None:
352
352
  if (
353
353
  self.roborock_device_info.props.status is not None
354
- and self.roborock_device_info.props.status.map_status is not None
354
+ and self.roborock_device_info.props.status.current_map is not None
355
355
  ):
356
- # The map status represents the map flag as flag * 4 + 3 -
357
- # so we have to invert that in order to get the map flag that we can use to set the current map.
358
- self.current_map = (
359
- self.roborock_device_info.props.status.map_status - 3
360
- ) // 4
356
+ self.current_map = self.roborock_device_info.props.status.current_map
361
357
 
362
358
  async def set_current_map_rooms(self) -> None:
363
359
  """Fetch all of the rooms for the current map and set on RoborockMapInfo."""
@@ -440,7 +436,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
440
436
  # If either of these fail, we don't care, and we want to continue.
441
437
  await asyncio.gather(*tasks, return_exceptions=True)
442
438
 
443
- if len(self.maps) != 1:
439
+ if len(self.maps) > 1:
444
440
  # Set the map back to the map the user previously had selected so that it
445
441
  # does not change the end user's app.
446
442
  # Only needs to happen when we changed maps above.
@@ -0,0 +1,24 @@
1
+ {
2
+ "config": {
3
+ "abort": {
4
+ "already_configured": "Apparaat is al geconfigureerd"
5
+ },
6
+ "error": {
7
+ "cannot_connect": "Kan geen verbinding maken",
8
+ "invalid_auth": "Ongeldige authenticatie",
9
+ "unknown": "Onverwachte fout"
10
+ },
11
+ "step": {
12
+ "user": {
13
+ "data": {
14
+ "device": "Apparaat",
15
+ "password": "Wachtwoord"
16
+ },
17
+ "data_description": {
18
+ "device": "Apparaat",
19
+ "password": "Wachtwoord"
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "config": {
3
+ "abort": {
4
+ "already_configured": "\u8bbe\u5907\u5df2\u914d\u7f6e"
5
+ },
6
+ "error": {
7
+ "cannot_connect": "\u8fde\u63a5\u5931\u8d25",
8
+ "invalid_auth": "\u8eab\u4efd\u9a8c\u8bc1\u65e0\u6548",
9
+ "unknown": "\u610f\u5916\u9519\u8bef"
10
+ },
11
+ "step": {
12
+ "user": {
13
+ "data": {
14
+ "device": "\u8bbe\u5907",
15
+ "id": "B \u8def\u7ebf ID",
16
+ "password": "\u5bc6\u7801"
17
+ },
18
+ "data_description": {
19
+ "device": "\u8bbe\u5907",
20
+ "id": "B \u8def\u7ebf ID",
21
+ "password": "\u5bc6\u7801"
22
+ }
23
+ }
24
+ }
25
+ },
26
+ "entity": {
27
+ "sensor": {
28
+ "instantaneous_current_r_phase": {
29
+ "name": "\u77ac\u65f6\u7535\u6d41 R \u76f8"
30
+ },
31
+ "instantaneous_current_t_phase": {
32
+ "name": "\u77ac\u65f6\u7535\u6d41 T \u76f8"
33
+ },
34
+ "instantaneous_power": {
35
+ "name": "\u77ac\u65f6\u529f\u7387"
36
+ },
37
+ "total_consumption": {
38
+ "name": "\u603b\u6d88\u8017"
39
+ }
40
+ }
41
+ }
42
+ }