Add Trafikverket Camera integration (#79873)

This commit is contained in:
G Johansson
2023-08-24 10:39:22 +02:00
committed by GitHub
parent 7926c5cea9
commit 147351be6e
23 changed files with 1083 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
"domain": "trafikverket",
"name": "Trafikverket",
"integrations": [
"trafikverket_camera",
"trafikverket_ferry",
"trafikverket_train",
"trafikverket_weatherstation"

View File

@@ -0,0 +1,29 @@
"""The trafikverket_camera component."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN, PLATFORMS
from .coordinator import TVDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Trafikverket Camera from a config entry."""
coordinator = TVDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Trafikverket Camera config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -0,0 +1,84 @@
"""Camera for the Trafikverket Camera integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from homeassistant.components.camera import Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LOCATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_DESCRIPTION, ATTR_TYPE, DOMAIN
from .coordinator import TVDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Trafikverket Camera."""
coordinator: TVDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
TVCamera(
coordinator,
entry.title,
entry.entry_id,
)
],
)
class TVCamera(CoordinatorEntity[TVDataUpdateCoordinator], Camera):
"""Implement Trafikverket camera."""
_attr_has_entity_name = True
_attr_name = None
_attr_translation_key = "tv_camera"
coordinator: TVDataUpdateCoordinator
def __init__(
self,
coordinator: TVDataUpdateCoordinator,
name: str,
entry_id: str,
) -> None:
"""Initialize the camera."""
super().__init__(coordinator)
Camera.__init__(self)
self._attr_unique_id = entry_id
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, entry_id)},
manufacturer="Trafikverket",
model="v1.0",
name=name,
configuration_url="https://api.trafikinfo.trafikverket.se/",
)
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return camera picture."""
return self.coordinator.data.image
@property
def is_on(self) -> bool:
"""Return camera on."""
return self.coordinator.data.data.active is True
@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return additional attributes."""
return {
ATTR_DESCRIPTION: self.coordinator.data.data.description,
ATTR_LOCATION: self.coordinator.data.data.location,
ATTR_TYPE: self.coordinator.data.data.camera_type,
}

View File

@@ -0,0 +1,122 @@
"""Adds config flow for Trafikverket Camera integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from pytrafikverket.exceptions import (
InvalidAuthentication,
MultipleCamerasFound,
NoCameraFound,
UnknownError,
)
from pytrafikverket.trafikverket_camera import TrafikverketCamera
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import CONF_LOCATION, DOMAIN
class TVCameraConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Trafikverket Camera integration."""
VERSION = 1
entry: config_entries.ConfigEntry | None
async def validate_input(self, sensor_api: str, location: str) -> dict[str, str]:
"""Validate input from user input."""
errors: dict[str, str] = {}
web_session = async_get_clientsession(self.hass)
camera_api = TrafikverketCamera(web_session, sensor_api)
try:
await camera_api.async_get_camera(location)
except NoCameraFound:
errors["location"] = "invalid_location"
except MultipleCamerasFound:
errors["location"] = "more_locations"
except InvalidAuthentication:
errors["base"] = "invalid_auth"
except UnknownError:
errors["base"] = "cannot_connect"
return errors
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Handle re-authentication with Trafikverket."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm re-authentication with Trafikverket."""
errors = {}
if user_input:
api_key = user_input[CONF_API_KEY]
assert self.entry is not None
errors = await self.validate_input(api_key, self.entry.data[CONF_LOCATION])
if not errors:
self.hass.config_entries.async_update_entry(
self.entry,
data={
**self.entry.data,
CONF_API_KEY: api_key,
},
)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
}
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input:
api_key = user_input[CONF_API_KEY]
location = user_input[CONF_LOCATION]
errors = await self.validate_input(api_key, location)
if not errors:
await self.async_set_unique_id(f"{DOMAIN}-{location}")
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_LOCATION],
data={
CONF_API_KEY: api_key,
CONF_LOCATION: location,
},
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_LOCATION): cv.string,
}
),
errors=errors,
)

View File

@@ -0,0 +1,10 @@
"""Adds constants for Trafikverket Camera integration."""
from homeassistant.const import Platform
DOMAIN = "trafikverket_camera"
CONF_LOCATION = "location"
PLATFORMS = [Platform.CAMERA]
ATTRIBUTION = "Data provided by Trafikverket"
ATTR_DESCRIPTION = "description"
ATTR_TYPE = "type"

View File

@@ -0,0 +1,76 @@
"""DataUpdateCoordinator for the Trafikverket Camera integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from io import BytesIO
import logging
from pytrafikverket.exceptions import (
InvalidAuthentication,
MultipleCamerasFound,
NoCameraFound,
UnknownError,
)
from pytrafikverket.trafikverket_camera import CameraInfo, TrafikverketCamera
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_LOCATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
TIME_BETWEEN_UPDATES = timedelta(minutes=5)
@dataclass
class CameraData:
"""Dataclass for Camera data."""
data: CameraInfo
image: bytes | None
class TVDataUpdateCoordinator(DataUpdateCoordinator[CameraData]):
"""A Trafikverket Data Update Coordinator."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the Trafikverket coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=TIME_BETWEEN_UPDATES,
)
self.session = async_get_clientsession(hass)
self._camera_api = TrafikverketCamera(self.session, entry.data[CONF_API_KEY])
self._location = entry.data[CONF_LOCATION]
async def _async_update_data(self) -> CameraData:
"""Fetch data from Trafikverket."""
camera_data: CameraInfo
image: bytes | None = None
try:
camera_data = await self._camera_api.async_get_camera(self._location)
except (NoCameraFound, MultipleCamerasFound, UnknownError) as error:
raise UpdateFailed from error
except InvalidAuthentication as error:
raise ConfigEntryAuthFailed from error
if camera_data.photourl is None:
return CameraData(data=camera_data, image=None)
image_url = camera_data.photourl
if camera_data.fullsizephoto:
image_url = f"{camera_data.photourl}?type=fullsize"
async with self.session.get(image_url, timeout=10) as get_image:
if get_image.status not in range(200, 299):
raise UpdateFailed("Could not retrieve image")
image = BytesIO(await get_image.read()).getvalue()
return CameraData(data=camera_data, image=image)

View File

@@ -0,0 +1,10 @@
{
"domain": "trafikverket_camera",
"name": "Trafikverket Camera",
"codeowners": ["@gjohansson-ST"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/trafikverket_camera",
"iot_class": "cloud_polling",
"loggers": ["pytrafikverket"],
"requirements": ["pytrafikverket==0.3.5"]
}

View File

@@ -0,0 +1,13 @@
"""Integration platform for recorder."""
from __future__ import annotations
from homeassistant.const import ATTR_LOCATION
from homeassistant.core import HomeAssistant, callback
from .const import ATTR_DESCRIPTION
@callback
def exclude_attributes(hass: HomeAssistant) -> set[str]:
"""Exclude description and location from being recorded in the database."""
return {ATTR_DESCRIPTION, ATTR_LOCATION}

View File

@@ -0,0 +1,51 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_location": "Could not find a camera location with the specified name",
"more_locations": "Found multiple camera locations with the specified name"
},
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"location": "[%key:common::config_flow::data::location%]"
}
}
}
},
"entity": {
"camera": {
"tv_camera": {
"state_attributes": {
"description": {
"name": "Description"
},
"direction": {
"name": "Direction"
},
"full_size_photo": {
"name": "Full size photo"
},
"location": {
"name": "[%key:common::config_flow::data::location%]"
},
"photo_url": {
"name": "Photo url"
},
"status": {
"name": "Status"
},
"type": {
"name": "Camera type"
}
}
}
}
}
}

View File

@@ -480,6 +480,7 @@ FLOWS = {
"traccar",
"tractive",
"tradfri",
"trafikverket_camera",
"trafikverket_ferry",
"trafikverket_train",
"trafikverket_weatherstation",

View File

@@ -5878,6 +5878,12 @@
"trafikverket": {
"name": "Trafikverket",
"integrations": {
"trafikverket_camera": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling",
"name": "Trafikverket Camera"
},
"trafikverket_ferry": {
"integration_type": "hub",
"config_flow": true,