Skip to content

Commit

Permalink
Select lines from list and sensor reconfiguration
Browse files Browse the repository at this point in the history
  • Loading branch information
warrior25 committed Feb 21, 2023
1 parent c8fd921 commit 9779a00
Show file tree
Hide file tree
Showing 11 changed files with 303 additions and 123 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ title: Hervanan kampus A
ignore_line_breaks: true
content: >
{% for departure in
states.sensor.nysse_hervannan_kampus_a_0835.attributes.departures %}
states.sensor.hervannan_kampus_a_0835.attributes.departures %}
<div style="display:grid; grid-template-columns: 2fr 0.5fr; font-size: 20px">
<div><ha-icon style="padding: 10px 10px 10px 10px" icon={{ departure.icon
Expand Down
8 changes: 8 additions & 0 deletions custom_components/Nysse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@ async def async_setup_entry(
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
entry.async_on_unload(entry.add_update_listener(options_update_listener))
return True


async def options_update_listener(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)


async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
hass.data.setdefault(DOMAIN, {})
return True
Expand Down
179 changes: 144 additions & 35 deletions custom_components/Nysse/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from typing import Any, Optional
from homeassistant import config_entries
from .fetch_stop_points import fetch_stop_points
from .fetch_api import fetch_stop_points, fetch_lines
from homeassistant.helpers.selector import selector
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback
import voluptuous as vol
import logging

from .const import (
CONF_STOPS,
CONF_STATION,
CONF_MAX,
DEFAULT_MAX,
CONF_TIMELIMIT,
DEFAULT_TIMELIMIT,
CONF_LINES,
DEFAULT_LINES,
DOMAIN,
)

Expand All @@ -23,26 +24,56 @@ class NysseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):

def __init__(self) -> None:
"""Initialize."""
self.data: dict[str, Any] = {CONF_STOPS: []}
self.data: dict[str, Any] = {}
self.stations = []
self.title = "Nysse"

async def async_step_user(self, user_input: Optional[dict[str, Any]] = None):
errors: dict[str, str] = {}

stations = await fetch_stop_points(True)
if not stations:
return

self.stations = await fetch_stop_points(True)
data_schema = {
vol.Required(CONF_STATION): selector(
{
"select": {
"options": stations,
"options": self.stations,
"mode": "dropdown",
"custom_value": "true",
}
}
),
vol.Optional(CONF_LINES, default=DEFAULT_LINES): str,
)
}

if user_input is not None:
try:
await self.validate_stop(user_input[CONF_STATION])
except ValueError:
errors[CONF_STATION] = "invalid_station"

if not errors:
await self.async_set_unique_id(user_input[CONF_STATION])
self._abort_if_unique_id_configured()
self.data[CONF_STATION] = user_input[CONF_STATION]

for station in self.stations:
if station["value"] == user_input[CONF_STATION]:
self.title = station["label"]
break

return await self.async_step_options()

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(data_schema),
errors=errors,
)

async def async_step_options(self, user_input: Optional[dict[str, Any]] = None):
errors: dict[str, str] = {}

lines = await fetch_lines(self.data[CONF_STATION])
options_schema = {
vol.Required(CONF_LINES): cv.multi_select(lines),
vol.Optional(CONF_TIMELIMIT, default=DEFAULT_TIMELIMIT): selector(
{
"number": {
Expand All @@ -56,39 +87,117 @@ async def async_step_user(self, user_input: Optional[dict[str, Any]] = None):
{"number": {"min": 1, "max": 30}}
),
}

if user_input is not None:
try:
await self.validate_stop(user_input[CONF_STATION], stations)
await self.validate_lines(user_input[CONF_LINES])
except ValueError:
errors[CONF_STATION] = "invalid_station"

errors[CONF_LINES] = "invalid_lines"
if not errors:
self.data[CONF_STOPS].append(
{
"station": user_input[CONF_STATION],
"max": user_input[CONF_MAX],
"timelimit": user_input[CONF_TIMELIMIT],
"lines": user_input[CONF_LINES],
}
)
integration_title = "Nysse"

for station in stations:
if station["value"] == user_input[CONF_STATION]:
integration_title = station["label"]
break

return self.async_create_entry(title=integration_title, data=self.data)
self.data = {
"station": self.data[CONF_STATION],
"lines": user_input[CONF_LINES],
"timelimit": user_input[CONF_TIMELIMIT],
"max": user_input[CONF_MAX],
}
return self.async_create_entry(title=self.title, data=self.data)

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(data_schema),
step_id="options",
data_schema=vol.Schema(options_schema),
errors=errors,
)

async def validate_stop(self, stop_id, stations):
for station in stations:
async def validate_stop(self, stop_id):
for station in self.stations:
if station["value"] == stop_id:
return
raise ValueError

async def validate_lines(self, lines):
if len(lines) < 1:
raise ValueError

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Create the options flow."""
return OptionsFlowHandler(config_entry)


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handles options flow for the component."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self.config_entry = config_entry
self.data: dict[str, Any] = {}
self.title = ""

async def async_step_init(
self, user_input: dict[str, Any] = None
) -> dict[str, Any]:
errors: dict[str, str] = {}

if user_input is not None:
print(user_input)
self.stations = await fetch_stop_points(True)
for station in self.stations:
if station["value"] == self.config_entry.data[CONF_STATION]:
self.title = station["label"]
break

self.data = {
"station": self.config_entry.data[CONF_STATION],
"lines": self.config_entry.data[CONF_LINES],
"timelimit": user_input[CONF_TIMELIMIT],
"max": user_input[CONF_MAX],
}
return self.async_create_entry(title="", data=self.data)

if self.config_entry.options:
options_schema = vol.Schema(
{
vol.Optional(
CONF_TIMELIMIT,
default=self.config_entry.options[CONF_TIMELIMIT],
): selector(
{
"number": {
"min": 0,
"max": 60,
"unit_of_measurement": "min",
}
}
),
vol.Optional(
CONF_MAX, default=self.config_entry.options[CONF_MAX]
): selector({"number": {"min": 1, "max": 30}}),
}
)
else:
options_schema = vol.Schema(
{
vol.Optional(
CONF_TIMELIMIT,
default=self.config_entry.data[CONF_TIMELIMIT],
): selector(
{
"number": {
"min": 0,
"max": 60,
"unit_of_measurement": "min",
}
}
),
vol.Optional(
CONF_MAX, default=self.config_entry.data[CONF_MAX]
): selector({"number": {"min": 1, "max": 30}}),
}
)

return self.async_show_form(
step_id="init",
data_schema=options_schema,
errors=errors,
)
3 changes: 1 addition & 2 deletions custom_components/Nysse/const.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
DOMAIN = "nysse"

PLATFORM_NAME = "Nysse"
CONF_STOPS = "stops"

CONF_STATION = "station"
CONF_TIMELIMIT = "timelimit"
DEFAULT_TIMELIMIT = 0
CONF_MAX = "max"
DEFAULT_MAX = 3
CONF_LINES = "lines"
DEFAULT_LINES = "all"
DEFAULT_ICON = "mdi:bus-clock"
TRAM_LINES = ["1", "3"]

Expand All @@ -26,3 +24,4 @@
NYSSE_STOP_URL = "https://data.itsfactory.fi/journeys/api/1/stop-monitoring?stops={0}"
NYSSE_STOP_POINTS_URL = "https://data.itsfactory.fi/journeys/api/1/stop-points/"
NYSSE_JOURNEYS_URL = "https://data.itsfactory.fi/journeys/api/1/journeys?stopPointId={0}&dayTypes={1}&startIndex={2}"
NYSSE_LINES_URL = "https://data.itsfactory.fi/journeys/api/1/lines?stopPointId={0}"
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .network import request
from .const import NYSSE_STOP_POINTS_URL
from .const import NYSSE_STOP_POINTS_URL, NYSSE_LINES_URL
import logging
import json

Expand All @@ -15,7 +15,7 @@ async def fetch_stop_points(has_id):
try:
result = await request(NYSSE_STOP_POINTS_URL)
if not result:
_LOGGER.warning("Could not fetch stop points")
_LOGGER.error("Could not fetch stop points")
return
result = json.loads(result)
for stop in result["body"]:
Expand All @@ -34,5 +34,24 @@ async def fetch_stop_points(has_id):
return stations

except OSError:
_LOGGER.warning("Unknown exception. Check your internet connection")
_LOGGER.error("Unknown exception. Check your internet connection")
return


async def fetch_lines(stop):
"""Fetches stop point names"""
lines = []
try:
lines_url = NYSSE_LINES_URL.format(stop)
result = await request(lines_url)
if not result:
_LOGGER.error("Could not fetch lines points")
return
result = json.loads(result)
for line in result["body"]:
lines.append(line["name"])
return lines

except OSError:
_LOGGER.error("Unknown exception. Check your internet connection")
return
2 changes: 1 addition & 1 deletion custom_components/Nysse/manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"codeowners": ["@morosanmihail", "@warrior25"],
"codeowners": ["@warrior25"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/warrior25/HA-Nysse",
Expand Down
22 changes: 14 additions & 8 deletions custom_components/Nysse/nysse_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
_LOGGER = logging.getLogger(__name__)
LOCAL_TZ = pytz.timezone("Europe/Helsinki")


class NysseData:
def __init__(self):
# Last update timestamp for the sensor
Expand Down Expand Up @@ -38,12 +39,15 @@ def populate(self, departures, journeys, station_id, stops, max_items):
while len(self._json_data) < max_items:
if len(journeys[weekday_int]) <= i:
i = 0
if (weekday_int < 6):
if weekday_int < 6:
weekday_int += 1
else:
weekday_int = 0
if weekday_int == datetime.today().weekday():
_LOGGER.warning("%s: Not enough timetable data was found. Try decreasing the number of requested departures", station_id)
_LOGGER.warning(
"%s: Not enough timetable data was found. Try decreasing the number of requested departures",
station_id,
)
break
else:
self._json_data.append(journeys[weekday_int][i])
Expand Down Expand Up @@ -73,11 +77,11 @@ def get_departures(self):
if departure["time_to_station"] != "unavailable":
departures.append(departure)
else:
_LOGGER.info("Discarding departure with unavailable time_to_station")
_LOGGER.info(departure)
_LOGGER.debug("Discarding departure with unavailable time_to_station")
_LOGGER.debug(departure)

# Sort departures according to their departure times
departures = sorted(departures, key=lambda d: d['time_to_station'])
departures = sorted(departures, key=lambda d: d["time_to_station"])
return departures

def is_realtime(self, item):
Expand All @@ -86,13 +90,15 @@ def is_realtime(self, item):
return False
return True

def get_departure_time(self, item, stringify = False, time_type = "any"):
def get_departure_time(self, item, stringify=False, time_type="any"):
"""Get departure time from json data"""
if "expectedArrivalTime" in item["call"] and time_type == "any":
parsed = parser.parse(item["call"]["expectedArrivalTime"])
elif "expectedDepartureTime" in item["call"] and time_type == "any":
parsed = parser.parse(item["call"]["expectedDepartureTime"])
elif "aimedArrivalTime" in item["call"] and (time_type in ("any", "aimedArrival")):
elif "aimedArrivalTime" in item["call"] and (
time_type in ("any", "aimedArrival")
):
parsed = parser.parse(item["call"]["aimedArrivalTime"])
elif "aimedDepartureTime" in item["call"] and time_type == "any":
parsed = parser.parse(item["call"]["aimedDepartureTime"])
Expand Down Expand Up @@ -122,7 +128,7 @@ def get_destination_name(self, entry):
return self._stops[entry["destinationShortName"]]
return "unavailable"

def time_to_station(self, entry, seconds = False):
def time_to_station(self, entry, seconds=False):
"""Get time until departure in minutes"""
time = self.get_departure_time(entry, False)
if time != "unavailable":
Expand Down
Loading

0 comments on commit 9779a00

Please sign in to comment.