Skip to content

Commit

Permalink
Full camera implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
FL550 committed Jan 6, 2024
1 parent 6bb9330 commit a88b3ca
Show file tree
Hide file tree
Showing 14 changed files with 467 additions and 40 deletions.
Binary file added Precipitation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ This integration uses ['simple_dwd_weatherforecast'](https://github.com/FL550/si

![Screenshot Weather Details](./Weather-Details.png)

![Screenshot Precipitation Chart](./Precipitation.png)

# Installation

1. Install integration via HACS or use the button below.
Expand Down Expand Up @@ -108,9 +110,28 @@ You can enable the ones you like in HA UI under "Configuration" -> "Entities" ->

The sensor values will be set when the next update of dwd_weather is scheduled by Home Assistant. This is done every 15 minutes. You can skip the waiting time by reloading the component/integration or restarting HA.

Note:
Note:
If you activate the option for hourly updates during setup of a weather station, DWD does not provide data for precipitation duration and probability. If this or other data is not available for a certain weather station, this component does not create sensors for it. As a workaround you can create setup the same station without activating the hourly updates option and use the slightly less acurate sensor data that is refreshed twice daily.

## Weather charts

This integration provides also some weather charts as camera entities. They can be configured the same as a weather station.


### Precipitation Chart
![Screenshot Precipitation Chart](./Precipitation.png)

### Pollen Chart
![Screenshot Pollen Chart](./pollen.png)

### Alerts Chart
![Screenshot Alerts Chart](./warnungen.png)

### Custom Chart
The chart can be adjusted to every region you like, however as most data is only provided for Germany, it is best used within these region.

![Screenshot Alerts Chart](./warnungen_custom.png)

## Help and Contribution

Feel free to open an issue if you find one and I will do my best to help you. If you want to contribute, your help is appreciated! If you want to add a new feature, add a pull request first so we can chat about the details.
Expand Down
22 changes: 14 additions & 8 deletions custom_components/dwd_weather/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
update_method=dwd_weather_data.async_update,
update_interval=DEFAULT_MAP_INTERVAL,
)
await dwdweather_coordinator.async_refresh()
# Save the data
dwdweather_hass_data = hass.data.setdefault(DOMAIN, {})
dwdweather_hass_data[entry.entry_id] = {
Expand Down Expand Up @@ -173,14 +172,21 @@ async def async_update(self):

async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
if entry.data[CONF_ENTITY_TYPE] == CONF_ENTITY_TYPE_STATION:
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
elif entry.data[CONF_ENTITY_TYPE] == CONF_ENTITY_TYPE_MAP:
unload_ok = all(
await asyncio.gather(
hass.config_entries.async_forward_entry_unload(entry, "camera")
)
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
Expand Down
36 changes: 28 additions & 8 deletions custom_components/dwd_weather/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@
from custom_components.dwd_weather.connector import DWDMapData
from homeassistant.components.camera import Camera
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from simple_dwd_weatherforecast import dwdmap
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType

from .const import (
CONF_MAP_BACKGROUND_TYPE,
CONF_MAP_ID,
CONF_MAP_TYPE,
CONF_MAP_TYPE_CUSTOM,
CONF_MAP_WINDOW,
DOMAIN,
DWDWEATHER_COORDINATOR,
DWDWEATHER_DATA,
NAME,
CONF_MAP_FOREGROUND_TYPE,
conversion_table_map_foreground,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -36,23 +41,37 @@ def __init__(self, hass_data):
"""Class initialization."""
super().__init__()

dwd_data: DWDMapData = hass_data[DWDWEATHER_DATA]
self._dwd_data: DWDMapData = hass_data[DWDWEATHER_DATA]

self._map_type = "todo"
self._unique_id = f"map_{self._map_type}_{dwd_data._config[CONF_MAP_ID]}"
self._name = f"{self._map_type} {dwd_data._config[CONF_MAP_ID]}"
self._map_type = conversion_table_map_foreground[
self._dwd_data._config[CONF_MAP_FOREGROUND_TYPE]
]
self._unique_id = f"map_{self._map_type}_{self._dwd_data._config[CONF_MAP_ID]}"
self._name = f"{self._map_type}"

self._connector: DWDMapData = hass_data[DWDWEATHER_DATA]
self._dwd_data.set_type(self._dwd_data._config[CONF_MAP_TYPE])

if self._dwd_data._config[CONF_MAP_TYPE] == CONF_MAP_TYPE_CUSTOM:
self._dwd_data.set_location(
self._dwd_data._config[CONF_MAP_WINDOW]["latitude"],
self._dwd_data._config[CONF_MAP_WINDOW]["longitude"],
self._dwd_data._config[CONF_MAP_WINDOW]["radius"],
)

self._dwd_data.set_map_style(
self._dwd_data._config[CONF_MAP_FOREGROUND_TYPE],
self._dwd_data._config[CONF_MAP_BACKGROUND_TYPE],
)
self._coordinator = hass_data[DWDWEATHER_COORDINATOR]

async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return bytes of camera image."""
_LOGGER.debug("getting image")
# TODO respect options
self._dwd_data.set_size(width if width else 520, height if height else 580)
await self._coordinator.async_request_refresh()
image = self._connector.get_image()
image = self._dwd_data.get_image()
return image

@property
Expand All @@ -63,6 +82,7 @@ def name(self):
@property
def unique_id(self):
"""Return the unique of the sensor."""
_LOGGER.debug("camera unique id: {}".format(self._unique_id))
return self._unique_id

@property
Expand Down
163 changes: 154 additions & 9 deletions custom_components/dwd_weather/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,33 @@
CONF_INTERPOLATE,
CONF_LOCATION_COORDINATES,
CONF_CUSTOM_LOCATION,
CONF_MAP_BACKGROUND_TYPE,
CONF_MAP_FOREGROUND_MAXTEMP,
CONF_MAP_FOREGROUND_POLLENFLUG,
CONF_MAP_FOREGROUND_PRECIPITATION,
CONF_MAP_FOREGROUND_SATELLITE_IR,
CONF_MAP_FOREGROUND_SATELLITE_RGB,
CONF_MAP_FOREGROUND_TYPE,
CONF_MAP_FOREGROUND_UVINDEX,
CONF_MAP_FOREGROUND_WARNUNGEN_GEMEINDEN,
CONF_MAP_FOREGROUND_WARNUNGEN_KREISE,
CONF_MAP_BACKGROUND_LAENDER,
CONF_MAP_BACKGROUND_BUNDESLAENDER,
CONF_MAP_BACKGROUND_KREISE,
CONF_MAP_BACKGROUND_GEMEINDEN,
CONF_MAP_BACKGROUND_SATELLIT,
CONF_MAP_ID,
CONF_MAP_TYPE,
CONF_MAP_TYPE_CUSTOM,
CONF_MAP_TYPE_GERMANY,
CONF_MAP_WINDOW,
CONF_OPTION_MAP_MESSAGE,
CONF_STATION_ID,
CONF_STATION_NAME,
DOMAIN,
CONF_VERSION,
CONF_WIND_DIRECTION_TYPE,
conversion_table_map_foreground,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -62,13 +83,8 @@ async def async_step_user(self, user_input=None):
_LOGGER.debug("Selected weather_station")
return await self.async_step_station_select()
elif user_input[CONF_ENTITY_TYPE] == CONF_ENTITY_TYPE_MAP:
# TODO config flow steps for map

# TODO add map type to id
self.config_data[CONF_MAP_ID] = str(uuid.uuid4()).upper()[:4]
return self.async_create_entry(
title=f"Weathermaps", data=self.config_data
)
return await self.async_step_select_map_type()

data_schema = vol.Schema(
{
Expand Down Expand Up @@ -247,6 +263,129 @@ async def async_step_station_configure(self, user_input=None):
step_id="station_configure", data_schema=data_schema, errors=errors
)

async def async_step_select_map_type(self, user_input=None):
errors = {}
_LOGGER.debug("Map:user_input: {}".format(user_input))
if user_input is not None:
self.config_data.update(user_input)
if user_input[CONF_MAP_TYPE] == CONF_MAP_TYPE_CUSTOM:
return await self.async_step_select_map_window()
elif user_input[CONF_MAP_TYPE] == CONF_MAP_TYPE_GERMANY:
return await self.async_step_select_map_content()

data_schema = vol.Schema(
{
vol.Required(
CONF_MAP_TYPE,
default=CONF_MAP_TYPE_GERMANY,
): SelectSelector(
{
"options": list(
[
CONF_MAP_TYPE_GERMANY,
CONF_MAP_TYPE_CUSTOM,
]
),
"custom_value": False,
"mode": "list",
"translation_key": CONF_MAP_TYPE,
}
)
}
)

return self.async_show_form(
step_id="select_map_type", data_schema=data_schema, errors=errors
)

async def async_step_select_map_window(self, user_input=None):
errors = {}
_LOGGER.debug("Map_window:user_input: {}".format(user_input))
if user_input is not None:
user_input["map_window"]["latitude"] = round(
user_input["map_window"]["latitude"], 2
)
user_input["map_window"]["longitude"] = round(
user_input["map_window"]["longitude"], 2
)
if "radius" in user_input["map_window"]:
user_input["map_window"]["radius"] = round(
user_input["map_window"]["radius"] / 1000, 0
)
else:
user_input["map_window"]["radius"] = 100
self.config_data.update(user_input)
return await self.async_step_select_map_content()

data_schema = vol.Schema(
{vol.Optional(CONF_MAP_WINDOW): LocationSelector({"radius": True})}
)
_LOGGER.debug("Map_window:user_input:error {}".format(errors))
return self.async_show_form(
step_id="select_map_window", data_schema=data_schema, errors=errors
)

async def async_step_select_map_content(self, user_input=None):
errors = {}
_LOGGER.debug("Map_content:user_input: {}".format(user_input))
if user_input is not None:
self.config_data.update(user_input)

return self.async_create_entry(
title=f"Weathermap {conversion_table_map_foreground[self.config_data[CONF_MAP_FOREGROUND_TYPE]]}",
data=self.config_data,
)

data_schema = vol.Schema(
{
vol.Required(
CONF_MAP_FOREGROUND_TYPE,
default=CONF_MAP_FOREGROUND_PRECIPITATION,
): SelectSelector(
{
"options": list(
[
CONF_MAP_FOREGROUND_PRECIPITATION,
CONF_MAP_FOREGROUND_MAXTEMP,
CONF_MAP_FOREGROUND_UVINDEX,
CONF_MAP_FOREGROUND_POLLENFLUG,
CONF_MAP_FOREGROUND_SATELLITE_RGB,
CONF_MAP_FOREGROUND_SATELLITE_IR,
CONF_MAP_FOREGROUND_WARNUNGEN_GEMEINDEN,
CONF_MAP_FOREGROUND_WARNUNGEN_KREISE,
]
),
"custom_value": False,
"mode": "dropdown",
"translation_key": CONF_MAP_FOREGROUND_TYPE,
}
),
vol.Required(
CONF_MAP_BACKGROUND_TYPE,
default=CONF_MAP_BACKGROUND_BUNDESLAENDER,
): SelectSelector(
{
"options": list(
[
CONF_MAP_BACKGROUND_LAENDER,
CONF_MAP_BACKGROUND_BUNDESLAENDER,
CONF_MAP_BACKGROUND_KREISE,
CONF_MAP_BACKGROUND_GEMEINDEN,
CONF_MAP_BACKGROUND_SATELLIT,
]
),
"custom_value": False,
"mode": "dropdown",
"translation_key": CONF_MAP_BACKGROUND_TYPE,
}
),
}
)

return self.async_show_form(
step_id="select_map_content", data_schema=data_schema, errors=errors
)

@staticmethod
@callback
def async_get_options_flow(
Expand All @@ -270,7 +409,9 @@ async def async_step_init(self, user_input: dict[str] | None = None) -> FlowResu
"""Manage the options."""
if self.config_entry.data[CONF_ENTITY_TYPE] == CONF_ENTITY_TYPE_STATION:
if user_input is not None:
_LOGGER.debug("OptionsFlowHandler: user_input {}".format(user_input))
_LOGGER.debug(
"OptionsFlowHandler station: user_input {}".format(user_input)
)

user_input[CONF_ENTITY_TYPE] = self.config_entry.data[CONF_ENTITY_TYPE]
user_input[CONF_STATION_ID] = self.config_entry.data[CONF_STATION_ID]
Expand Down Expand Up @@ -331,5 +472,9 @@ async def async_step_init(self, user_input: dict[str] | None = None) -> FlowResu
),
)
elif self.config_entry.data[CONF_ENTITY_TYPE] == CONF_ENTITY_TYPE_MAP:
# TODO
pass
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{vol.Optional(CONF_OPTION_MAP_MESSAGE): TextSelector({})}
),
)
Loading

0 comments on commit a88b3ca

Please sign in to comment.