Skip to content

Commit 14aec5b

Browse files
Adding Theme Parks custom component v1.0.0
1 parent 3b10d69 commit 14aec5b

File tree

7 files changed

+375
-0
lines changed

7 files changed

+375
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""The Theme Park Wait Times integration."""
2+
from __future__ import annotations
3+
4+
import logging
5+
6+
from homeassistant.config_entries import ConfigEntry
7+
from homeassistant.const import Platform
8+
from homeassistant.core import HomeAssistant
9+
from homeassistant.helpers import device_registry as dr, entity_registry as er
10+
from homeassistant.helpers.httpx_client import get_async_client
11+
12+
from .const import (
13+
DOMAIN,
14+
ENTITY_BASE_URL,
15+
ENTITY_TYPE,
16+
ID,
17+
LIVE,
18+
LIVE_DATA,
19+
METHOD_GET,
20+
NAME,
21+
PARKNAME,
22+
PARKSLUG,
23+
QUEUE,
24+
STANDBY,
25+
TIME,
26+
TYPE_ATTRACTION,
27+
TYPE_SHOW,
28+
WAIT_TIME,
29+
)
30+
31+
_LOGGER = logging.getLogger(__name__)
32+
33+
PLATFORMS: list[Platform] = [Platform.SENSOR]
34+
35+
36+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
37+
"""Set up Theme Park Wait Times from a config entry."""
38+
data = hass.data.setdefault(DOMAIN, {})
39+
40+
api = ThemeParkAPI(hass, entry)
41+
await api.async_initialize()
42+
43+
data[entry.entry_id] = api
44+
45+
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
46+
47+
device_registry = dr.async_get(hass)
48+
device_registry.async_get_or_create(
49+
config_entry_id=entry.entry_id,
50+
identifiers={(DOMAIN, entry.entry_id)},
51+
connections=None,
52+
name=entry.title,
53+
)
54+
55+
return True
56+
57+
58+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
59+
"""Unload a config entry."""
60+
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
61+
hass.data[DOMAIN].pop(entry.entry_id)
62+
63+
return unload_ok
64+
65+
66+
class ThemeParkAPI:
67+
"""Wrapper for theme parks API."""
68+
69+
# -- Set in async_initialize --
70+
ha_device_registry: dr.DeviceRegistry
71+
ha_entity_registry: er.EntityRegistry
72+
73+
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
74+
"""Initialize the gateway."""
75+
self._hass = hass
76+
self._config_entry = config_entry
77+
self._parkslug = config_entry.data[PARKSLUG]
78+
self._parkname = config_entry.data[PARKNAME]
79+
80+
async def async_initialize(self) -> None:
81+
"""Initialize controller and connect radio."""
82+
self.ha_device_registry = dr.async_get(self._hass)
83+
self.ha_entity_registry = er.async_get(self._hass)
84+
85+
async def do_live_lookup(self):
86+
"""Do API lookup of the 'live' page of this park."""
87+
_LOGGER.debug("Running do_live_lookup in ThemeParkAPI")
88+
89+
items = await self.do_api_lookup()
90+
91+
def parse_live(item):
92+
"""Parse live data from API."""
93+
94+
_LOGGER.debug("Parsed API item for: %s", item[NAME])
95+
96+
name = item[NAME] + " (" + self._parkname + ")"
97+
98+
if "queue" not in item:
99+
_LOGGER.debug("No queue in item")
100+
return (item[ID], {ID: item[ID], NAME: name, TIME: None})
101+
102+
if "STANDBY" not in item[QUEUE]:
103+
_LOGGER.debug("No STANDBY in item['queue']")
104+
return (item[ID], {ID: item[ID], NAME: name, TIME: None})
105+
106+
_LOGGER.debug("Time found")
107+
return (
108+
item[ID],
109+
{
110+
ID: item[ID],
111+
NAME: name,
112+
TIME: item[QUEUE][STANDBY][WAIT_TIME],
113+
},
114+
)
115+
116+
return dict(map(parse_live, items))
117+
118+
async def do_api_lookup(self):
119+
"""Lookup the subpage and subfield in the API."""
120+
url = f"{ENTITY_BASE_URL}/{self._parkslug}/{LIVE}"
121+
122+
client = get_async_client(self._hass)
123+
response = await client.request(
124+
METHOD_GET,
125+
url,
126+
timeout=30,
127+
follow_redirects=True,
128+
)
129+
130+
items_data = response.json()
131+
132+
def filter_item(item):
133+
return (
134+
item[ENTITY_TYPE] == TYPE_SHOW or item[ENTITY_TYPE] == TYPE_ATTRACTION
135+
)
136+
137+
return filter(filter_item, items_data[LIVE_DATA])
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Config flow for Theme Park Wait Times integration."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
import voluptuous as vol
7+
8+
from homeassistant import config_entries
9+
10+
# from homeassistant.core import HomeAssistant
11+
from homeassistant.data_entry_flow import FlowResult
12+
from homeassistant.helpers.httpx_client import get_async_client
13+
14+
from .const import (
15+
DESTINATIONS,
16+
DESTINATIONS_URL,
17+
DOMAIN,
18+
METHOD_GET,
19+
NAME,
20+
PARKNAME,
21+
PARKSLUG,
22+
SLUG,
23+
STEP_USER,
24+
)
25+
26+
STEP_USER_DATA_SCHEMA = vol.Schema(
27+
{vol.Required(PARKSLUG): str, vol.Required(PARKNAME): str}
28+
)
29+
30+
31+
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
32+
"""Handle a config flow for Theme Park Wait Times."""
33+
34+
VERSION = 1
35+
_destinations: dict[str, Any] = {}
36+
37+
async def _async_update_data(self):
38+
"""Fetch list of parks."""
39+
40+
client = get_async_client(self.hass)
41+
response = await client.request(
42+
METHOD_GET,
43+
DESTINATIONS_URL,
44+
timeout=10,
45+
follow_redirects=True,
46+
)
47+
48+
parkdata = response.json()
49+
50+
def parse_dest(item):
51+
slug = item[SLUG]
52+
name = item[NAME]
53+
return (name, slug)
54+
55+
return dict(map(parse_dest, parkdata[DESTINATIONS]))
56+
57+
async def async_step_user(
58+
self, user_input: dict[str, Any] | None = None
59+
) -> FlowResult:
60+
"""Run the user config flow step."""
61+
62+
if user_input is not None:
63+
64+
return self.async_create_entry(
65+
title="Theme Park: %s" % user_input[PARKNAME],
66+
data={
67+
PARKSLUG: self._destinations[user_input[PARKNAME]],
68+
PARKNAME: user_input[PARKNAME],
69+
},
70+
)
71+
72+
if self._destinations == {}:
73+
self._destinations = await self._async_update_data()
74+
75+
schema = {vol.Required(PARKNAME): vol.In(sorted(self._destinations.keys()))}
76+
return self.async_show_form(
77+
step_id=STEP_USER, data_schema=vol.Schema(schema), last_step=True
78+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Constants for the Theme Park Wait Times integration."""
2+
3+
DOMAIN = "themeparks"
4+
5+
PARKSLUG = "parkslug"
6+
PARKNAME = "parkname"
7+
8+
BASE_URL = "https://api.themeparks.wiki/v1"
9+
DESTINATIONS_URL = "%s/destinations" % BASE_URL
10+
ENTITY_BASE_URL = "%s/entity" % BASE_URL
11+
12+
LIVE_DATA = "liveData"
13+
ENTITY_TYPE = "entityType"
14+
15+
TYPE_SHOW = "SHOW"
16+
TYPE_ATTRACTION = "ATTRACTION"
17+
18+
NAME = "name"
19+
TIME = "time"
20+
ID = "id"
21+
SLUG = "slug"
22+
DESTINATIONS = "destinations"
23+
QUEUE = "queue"
24+
STANDBY = "STANDBY"
25+
WAIT_TIME = "waitTime"
26+
LIVE = "live"
27+
28+
STEP_USER = "user"
29+
METHOD_GET = "GET"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"domain": "themeparks",
3+
"name": "Theme Park Wait Times",
4+
"config_flow": true,
5+
"documentation": "https://www.home-assistant.io/integrations/themeparks",
6+
"requirements": [],
7+
"ssdp": [],
8+
"zeroconf": [],
9+
"homekit": {},
10+
"dependencies": [],
11+
"codeowners": ["@danielsmith-eu"],
12+
"iot_class": "cloud_polling"
13+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""Platform for Theme Park sensor integration."""
2+
from __future__ import annotations
3+
4+
from datetime import timedelta
5+
import logging
6+
7+
from homeassistant.components.sensor import (
8+
SensorDeviceClass,
9+
SensorEntity,
10+
SensorStateClass,
11+
)
12+
from homeassistant.config_entries import ConfigEntry
13+
from homeassistant.const import TIME_MINUTES
14+
from homeassistant.core import HomeAssistant, callback
15+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
16+
from homeassistant.helpers.update_coordinator import (
17+
CoordinatorEntity,
18+
DataUpdateCoordinator,
19+
)
20+
21+
from .const import DOMAIN, NAME, TIME
22+
23+
_LOGGER = logging.getLogger(__name__)
24+
25+
26+
async def async_setup_entry(
27+
hass: HomeAssistant,
28+
config_entry: ConfigEntry,
29+
async_add_entities: AddEntitiesCallback,
30+
) -> None:
31+
"""Set up the sensor platform."""
32+
33+
my_api = hass.data[DOMAIN][config_entry.entry_id]
34+
coordinator = ThemeParksCoordinator(hass, my_api)
35+
36+
await coordinator.async_config_entry_first_refresh()
37+
38+
_LOGGER.info("Config entry first refresh completed, adding entities")
39+
entities = [AttractionSensor(coordinator, idx) for idx in coordinator.data.keys()]
40+
41+
_LOGGER.info(
42+
"Entities to add (count: %s): %s", str(entities.__len__), str(entities)
43+
)
44+
async_add_entities(entities)
45+
46+
47+
class AttractionSensor(SensorEntity, CoordinatorEntity):
48+
"""An entity using CoordinatorEntity."""
49+
50+
def __init__(self, coordinator, idx):
51+
"""Pass coordinator to CoordinatorEntity."""
52+
super().__init__(coordinator)
53+
self.idx = idx
54+
self._attr_name = coordinator.data[idx][NAME]
55+
self._attr_native_unit_of_measurement = TIME_MINUTES
56+
self._attr_device_class = SensorDeviceClass.DURATION
57+
self._attr_state_class = SensorStateClass.MEASUREMENT
58+
self._attr_native_value = self.coordinator.data[self.idx][TIME]
59+
60+
_LOGGER.debug("Adding AttractionSensor called %s", self._attr_name)
61+
62+
@callback
63+
def _handle_coordinator_update(self) -> None:
64+
"""Handle updated data from the coordinator."""
65+
newtime = self.coordinator.data[self.idx][TIME]
66+
_LOGGER.debug(
67+
"Setting updated time from coordinator for %s to %s",
68+
str(self._attr_name),
69+
str(newtime),
70+
)
71+
self._attr_native_value = newtime
72+
self.async_write_ha_state()
73+
74+
75+
class ThemeParksCoordinator(DataUpdateCoordinator):
76+
"""Theme parks coordinator."""
77+
78+
def __init__(self, hass, api):
79+
"""Initialize theme parks coordinator."""
80+
super().__init__(
81+
hass,
82+
_LOGGER,
83+
name="Theme Park Wait Time Sensor",
84+
update_interval=timedelta(minutes=5),
85+
)
86+
self.api = api
87+
88+
async def _async_update_data(self):
89+
"""Fetch data from API endpoint."""
90+
_LOGGER.debug("Calling do_live_lookup in ThemeParksCoordinator")
91+
return await self.api.do_live_lookup()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"config": {
3+
"step": {
4+
"user": {
5+
"title": "Theme Parks",
6+
"description": "Select a Theme Park to add to tracker",
7+
"data": {
8+
"parkname": "Theme Park"
9+
},
10+
"data_description": {
11+
"parkname": "The Theme Park to track"
12+
}
13+
}
14+
}
15+
}
16+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"config": {
3+
"step": {
4+
"user": {
5+
"data": {
6+
"parkname": "Theme Park"
7+
}
8+
}
9+
}
10+
}
11+
}

0 commit comments

Comments
 (0)