Skip to content

Commit 6d3083e

Browse files
committed
Sync changes from hacore2/dwd_global_rad_api_server
1 parent 41a253b commit 6d3083e

File tree

5 files changed

+215
-47
lines changed

5 files changed

+215
-47
lines changed

custom_components/dwd_global_rad/__init__.py

Lines changed: 95 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from homeassistant.components.hassio import async_get_addon_info, async_start_addon
1414
from homeassistant.config_entries import ConfigEntry
15-
from homeassistant.const import Platform
15+
from homeassistant.const import CONF_NAME, Platform
1616
from homeassistant.core import HomeAssistant
1717
from homeassistant.exceptions import ConfigEntryNotReady
1818
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -78,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
7878
_LOGGER.debug("Setup with data %s", entry.data)
7979
# entry.async_on_unload(entry.add_update_listener(update_listener))
8080

81-
use_addon = True
81+
use_addon = False
8282

8383
if use_addon:
8484
addon_info = await get_addon_config(hass, ADDON_SLUG)
@@ -91,7 +91,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
9191
# Ensure the add-on is started
9292
await ensure_addon_started(hass, ADDON_SLUG)
9393
else:
94-
hostname = "homeassistant.local"
94+
hostname = "192.168.2.165"
9595
port_number = "5001"
9696

9797
hass.data.setdefault(DOMAIN, {})
@@ -104,17 +104,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
104104
hass.data[DOMAIN]["rest_api_setup"] = True
105105

106106
api_client = hass.data[DOMAIN]["api_client"]
107-
latitude = entry.data["latitude"]
108-
longitude = entry.data["longitude"]
109-
name = entry.data["name"]
110-
111107
if not await wait_for_api_server(api_client):
112108
raise ConfigEntryNotReady("API server not available after multiple attempts")
113109

114-
location = await api_client.get_location_by_name(name)
115-
if location is None:
116-
await api_client.add_location(name=name, latitude=latitude, longitude=longitude)
117-
118110
# Initialize ForecastUpdateCoordinator
119111
if "forecast_coordinator" not in hass.data[DOMAIN]:
120112
hass.data[DOMAIN]["forecast_coordinator"] = ForecastUpdateCoordinator(
@@ -132,26 +124,58 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
132124
await hass.data[DOMAIN][
133125
"measurement_coordinator"
134126
].async_config_entry_first_refresh()
127+
# Ensure camera entity is created
128+
if not any(
129+
e.data.get("category") == "forecast_camera"
130+
for e in hass.config_entries.async_entries(DOMAIN)
131+
):
132+
forecast_camera_entry = {
133+
CONF_NAME: "DWD Global Radiation Forecast Camera",
134+
"category": "forecast_camera",
135+
"unique_id": "dwd_global_radiation_forecast_camera",
136+
}
137+
hass.async_create_task(
138+
hass.config_entries.flow.async_init(
139+
DOMAIN,
140+
context={"source": "implicit_config_entry_create"},
141+
data=forecast_camera_entry,
142+
)
143+
)
144+
if entry.data.get("category") != "forecast_camera":
145+
latitude = entry.data["latitude"]
146+
longitude = entry.data["longitude"]
147+
name = entry.data["name"]
148+
149+
location = await api_client.get_location_by_name(name)
150+
if location is None:
151+
await api_client.add_location(
152+
name=name, latitude=latitude, longitude=longitude
153+
)
135154

136-
# Initialize LocationDataUpdateCoordinator
137-
name = entry.data["name"]
138-
location_coordinator = LocationDataUpdateCoordinator(
139-
hass,
140-
hass.data[DOMAIN]["forecast_coordinator"],
141-
hass.data[DOMAIN]["measurement_coordinator"],
142-
name,
143-
)
144-
await location_coordinator.async_config_entry_first_refresh()
145-
146-
# Store only the location_coordinator in the entry's data
147-
hass.data[DOMAIN][entry.entry_id] = {"location_coordinator": location_coordinator}
148-
149-
# Perform the first data fetch for the new location immediately
150-
await hass.data[DOMAIN]["measurement_coordinator"].async_request_refresh()
151-
await hass.data[DOMAIN]["forecast_coordinator"].async_request_refresh()
155+
# Initialize LocationDataUpdateCoordinator
156+
name = entry.data["name"]
157+
location_coordinator = LocationDataUpdateCoordinator(
158+
hass,
159+
hass.data[DOMAIN]["forecast_coordinator"],
160+
hass.data[DOMAIN]["measurement_coordinator"],
161+
name,
162+
)
163+
await location_coordinator.async_config_entry_first_refresh()
164+
165+
# Store only the location_coordinator in the entry's data
166+
hass.data[DOMAIN][entry.entry_id] = {
167+
"location_coordinator": location_coordinator
168+
}
169+
170+
# Perform the first data fetch for the new location immediately
171+
await hass.data[DOMAIN]["measurement_coordinator"].async_request_refresh()
172+
173+
# Forward setup to the sensor platform
174+
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
175+
else:
176+
await hass.config_entries.async_forward_entry_setups(entry, ["camera"])
152177

153-
# Setup platforms (e.g., sensor)
154-
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
178+
await hass.data[DOMAIN]["forecast_coordinator"].async_request_refresh()
155179

156180
return True
157181

@@ -181,22 +205,48 @@ async def ensure_addon_started(
181205

182206
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
183207
"""Unload a config entry."""
184-
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
185-
hass.data[DOMAIN].pop(entry.entry_id)
208+
if entry.data.get("category") == "forecast_camera":
209+
platforms = ["camera"]
210+
else:
211+
platforms = ["sensor"]
212+
213+
# Unload the specified platforms for the config entry
214+
unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms)
215+
216+
if unload_ok:
217+
# Remove entry-specific data
218+
if entry.entry_id in hass.data[DOMAIN]:
219+
del hass.data[DOMAIN][entry.entry_id]
220+
186221
# Check if there are any remaining entries
187222
if not hass.config_entries.async_entries(DOMAIN):
188-
# Perform additional cleanup if this was the last entry
189-
if "coordinator" in hass.data[DOMAIN]:
190-
# Perform any cleanup for the coordinator if necessary
191-
coordinator = hass.data[DOMAIN]["coordinator"]
192-
await coordinator.async_shutdown()
193-
hass.data[DOMAIN].pop("coordinator")
223+
# Clean up the forecast coordinator
224+
forecast_coordinator = hass.data[DOMAIN].get("forecast_coordinator")
225+
if forecast_coordinator:
226+
await forecast_coordinator.async_shutdown()
227+
hass.data[DOMAIN].pop("forecast_coordinator", None)
228+
229+
# Clean up the measurement coordinator
230+
measurement_coordinator = hass.data[DOMAIN].get("measurement_coordinator")
231+
if measurement_coordinator:
232+
await measurement_coordinator.async_shutdown()
233+
hass.data[DOMAIN].pop("measurement_coordinator", None)
194234

195235
# Clean up the API client if no locations are left
196-
if "api_client" in hass.data[DOMAIN]:
197-
api_client = hass.data[DOMAIN]["api_client"]
198-
if not api_client.locations:
199-
hass.data[DOMAIN].pop("api_client")
236+
api_client = hass.data[DOMAIN].get("api_client")
237+
if api_client:
238+
locations = (
239+
await api_client.locations()
240+
) # Ensure async method is awaited
241+
if not locations:
242+
await (
243+
api_client.async_shutdown()
244+
) # Ensure proper API client shutdown
245+
hass.data[DOMAIN].pop("api_client", None)
246+
247+
# Clean up the REST API setup
248+
if "rest_api_setup" in hass.data[DOMAIN]:
249+
hass.data[DOMAIN].pop("rest_api_setup", None)
200250

201251
return unload_ok
202252

@@ -207,14 +257,15 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
207257
name = entry.data["name"]
208258

209259
# Remove the location from the api_client
210-
api_client.remove_location(name)
260+
await api_client.remove_location(name)
211261

212262
# Clean up the entry data
213263
if entry.entry_id in hass.data[DOMAIN]:
214264
hass.data[DOMAIN].pop(entry.entry_id)
215265

216266
# Optionally, clean up the api_client if no locations are left
217-
if not api_client.locations:
267+
locations = await api_client.locations
268+
if not locations:
218269
hass.data[DOMAIN].pop("api_client")
219270
# Check and remove forecast and measurement coordinators if they exist
220271
forecast_coordinator = hass.data[DOMAIN].get("forecast_coordinator")
@@ -239,7 +290,7 @@ async def wait_for_api_server(api_client, retries=5, delay=10):
239290
api_client.get_status()
240291
) # Assuming get_status is a method to check the API server status
241292
return True
242-
except Exception as e:
293+
except Exception:
243294
_LOGGER.warning(
244295
f"API server not available, retrying in {delay} seconds... (Attempt {attempt+1}/{retries})"
245296
)

custom_components/dwd_global_rad/api_client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ async def remove_location(self, name: str):
7070
async with self.session.delete(url) as response:
7171
return await response.json()
7272

73+
async def get_forecast_animated_gif(self):
74+
self._log_debug_info("Fetching forecast animated GIF")
75+
url = f"{self.base_url}/process"
76+
async with self.session.post(url) as response:
77+
if response.status == 200:
78+
return await response.read()
79+
else:
80+
return None
81+
7382
async def get_status(self):
7483
self._log_debug_info("Checking API server status")
7584
url = f"{self.base_url}/status"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import logging
2+
import time
3+
4+
from homeassistant.components.camera import Camera
5+
from homeassistant.config_entries import ConfigEntry
6+
from homeassistant.core import HomeAssistant, callback
7+
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
8+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
9+
10+
from .const import DOMAIN, MANUFACTURER
11+
12+
_LOGGER = logging.getLogger(__name__)
13+
14+
15+
async def async_setup_entry(
16+
hass: HomeAssistant,
17+
config_entry: ConfigEntry,
18+
async_add_entities: AddEntitiesCallback,
19+
) -> None:
20+
"""Set up the camera platform from a config entry."""
21+
async_add_entities([DWDGlobalRadCamera(hass, config_entry)])
22+
23+
24+
class DWDGlobalRadCamera(Camera):
25+
"""Representation of a DWD Global Radiation Forecast Camera."""
26+
27+
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
28+
"""Initialize the camera."""
29+
super().__init__()
30+
self._name = config_entry.data.get(
31+
"name", "DWD Global Radiation Forecast Camera"
32+
)
33+
self._unique_id = config_entry.data.get("unique_id")
34+
self.api_client = hass.data[DOMAIN]["api_client"]
35+
self.forecast_coordinator = hass.data[DOMAIN]["forecast_coordinator"]
36+
self._last_image = None
37+
self._attr_is_streaming = False
38+
self._config_entry = config_entry
39+
self._last_update_time = 0
40+
41+
# Subscribe to forecast coordinator updates
42+
self.forecast_coordinator.async_add_listener(self._schedule_update)
43+
44+
@property
45+
def name(self):
46+
"""Return the name of the camera."""
47+
return self._name
48+
49+
@property
50+
def unique_id(self):
51+
"""Return the unique ID of the camera."""
52+
return self._unique_id
53+
54+
@property
55+
def device_info(self) -> DeviceInfo:
56+
"""Return device information about this camera."""
57+
return DeviceInfo(
58+
identifiers={(DOMAIN, self._config_entry.entry_id)},
59+
name=self._name,
60+
manufacturer=MANUFACTURER,
61+
entry_type=DeviceEntryType.SERVICE,
62+
)
63+
64+
async def async_camera_image(self, width=None, height=None):
65+
"""Return bytes of camera image."""
66+
_LOGGER.debug(
67+
"Returning camera image with width=%s and height=%s", width, height
68+
)
69+
return self._last_image
70+
71+
@callback
72+
def _schedule_update(self):
73+
"""Schedule an update for the camera image."""
74+
_LOGGER.debug("Scheduling update for forecast animation GIF.")
75+
self.hass.async_create_task(self.async_update())
76+
77+
async def async_update(self):
78+
"""Fetch new state data for the camera."""
79+
current_time = time.time()
80+
# Only update if the last update was more than 30 minutes ago
81+
if current_time - self._last_update_time < 1800: # 30 minutes = 1800 seconds
82+
_LOGGER.debug("Skipping update, last update was less than 30 minutes ago.")
83+
return
84+
self._last_update_time = current_time
85+
_LOGGER.debug("Fetching new forecast animation GIF")
86+
image = await self.api_client.get_forecast_animated_gif()
87+
if image:
88+
self._last_image = image
89+
self._attr_is_streaming = True
90+
else:
91+
self._attr_is_streaming = False
92+
self.async_write_ha_state()
93+
94+
@property
95+
def should_poll(self) -> bool:
96+
"""Return True if entity has to be polled for state."""
97+
return False

custom_components/dwd_global_rad/config_flow.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def async_step_user(
5454
) -> ConfigFlowResult:
5555
"""Handle the initial step."""
5656
errors: dict[str, str] = {}
57+
5758
if user_input is not None:
5859
name = user_input[CONF_NAME]
5960

@@ -65,18 +66,28 @@ async def async_step_user(
6566
else:
6667
await self.async_set_unique_id(name)
6768
self._abort_if_unique_id_configured()
69+
70+
# Add location entry
71+
user_input["category"] = "location"
6872
return self.async_create_entry(title=name, data=user_input)
6973

7074
user_input = user_input or {
7175
CONF_NAME: self.hass.config.location_name,
7276
CONF_LATITUDE: self.hass.config.latitude,
7377
CONF_LONGITUDE: self.hass.config.longitude,
7478
}
75-
# Display the form with default values
79+
80+
# Display the form with default values and validation schema
7681
return self.async_show_form(
7782
step_id="user", data_schema=get_user_data_schema(self.hass), errors=errors
7883
)
7984

85+
async def async_step_implicit_config_entry_create(
86+
self, data: dict[str, Any]
87+
) -> ConfigFlowResult:
88+
"""Handle the implicit creation of a config entry."""
89+
return self.async_create_entry(title=data["name"], data=data)
90+
8091

8192
class CannotConnect(HomeAssistantError):
8293
"""Error to indicate we cannot connect."""

custom_components/dwd_global_rad/manifest.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"iot_class": "cloud_polling",
1010
"requirements": [],
1111
"ssdp": [],
12-
"version": "0.2.9-beta",
12+
"version": "0.3.0-beta",
1313
"zeroconf": []
14-
}
14+
}

0 commit comments

Comments
 (0)