Skip to content

Commit c8fe0a2

Browse files
committed
fix: Updated bus filters to support buses with with spaces
1 parent 435db89 commit c8fe0a2

14 files changed

+222
-52
lines changed

custom_components/first_bus/api_client.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ class FirstBusApiClient:
55
def __init__(self):
66
self._base_url = 'https://www.firstbus.co.uk'
77

8-
async def async_get_buses(self, stop):
9-
"""Get the user's account"""
8+
async def async_get_bus_times(self, stop):
9+
"""Get the bus times for a given stop"""
1010
async with aiohttp.ClientSession() as client:
1111
url = f'{self._base_url}/getNextBus?stop={stop}'
1212
async with client.get(url) as response:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import re
2+
from ..const import CONFIG_BUSES, REGEX_BUSES
3+
4+
def validate_config(config: dict):
5+
new_config = dict(config)
6+
errors = {}
7+
if CONFIG_BUSES in new_config and new_config[CONFIG_BUSES] is not None and len(new_config[CONFIG_BUSES]) > 0:
8+
matches = re.search(REGEX_BUSES, new_config[CONFIG_BUSES])
9+
if (matches is None):
10+
errors[CONFIG_BUSES] = "invalid_buses"
11+
else:
12+
new_config[CONFIG_BUSES] = new_config[CONFIG_BUSES].split(",")
13+
else:
14+
new_config[CONFIG_BUSES] = []
15+
16+
return (errors, new_config)
17+

custom_components/first_bus/config_flow.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from custom_components.first_bus.config import validate_config
12
import voluptuous as vol
23
import re
34
import logging
@@ -25,22 +26,14 @@ class FirstBusConfigFlow(ConfigFlow, domain=DOMAIN):
2526
async def async_step_user(self, user_input):
2627
"""Setup based on user config"""
2728

28-
errors = {}
2929
if user_input is not None:
30-
if CONFIG_BUSES in user_input and user_input[CONFIG_BUSES] is not None:
31-
matches = re.search(REGEX_BUSES, user_input[CONFIG_BUSES])
32-
if (matches is None):
33-
errors[CONFIG_BUSES] = "invalid_buses"
34-
else:
35-
user_input[CONFIG_BUSES] = user_input[CONFIG_BUSES].split(",")
36-
else:
37-
user_input[CONFIG_BUSES] = []
30+
(errors, config) = validate_config(user_input)
3831

3932
# Setup our basic sensors
4033
if len(errors) < 1:
4134
return self.async_create_entry(
42-
title=f"Bus Stop {user_input[CONFIG_NAME]}",
43-
data=user_input
35+
title=f"Bus Stop {config[CONFIG_NAME]}",
36+
data=config
4437
)
4538

4639
return self.async_show_form(
@@ -85,14 +78,7 @@ async def async_step_user(self, user_input):
8578

8679
_LOGGER.debug(f"Update config {config}")
8780

88-
if CONFIG_BUSES in config and config[CONFIG_BUSES] is not None and len(config[CONFIG_BUSES]) > 0:
89-
matches = re.search(REGEX_BUSES, config[CONFIG_BUSES])
90-
if (matches is None):
91-
errors[CONFIG_BUSES] = "invalid_buses"
92-
else:
93-
config[CONFIG_BUSES] = config[CONFIG_BUSES].split(",")
94-
else:
95-
config[CONFIG_BUSES] = []
81+
(errors, config) = validate_config(config)
9682

9783
if len(errors) < 1:
9884
return self.async_create_entry(title="", data=config)

custom_components/first_bus/const.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
CONFIG_STOP = "Stop"
77
CONFIG_BUSES = "Buses"
88

9-
REGEX_BUSES="^[a-zA-Z0-9]+(,[a-zA-Z0-9]+)*$"
9+
REGEX_BUSES="^[a-zA-Z0-9 ]+(,[a-zA-Z0-9 ]+)*$"
1010
REGEX_TIME="^[0-9]{2}:[0-9]{2}$"
1111
REGEX_TIME_MINS="([0-9]+) mins"
1212

custom_components/first_bus/sensor.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .api_client import (FirstBusApiClient)
1515
from .utils import (
1616
get_next_bus,
17-
async_get_buses,
17+
get_buses,
1818
calculate_minutes_remaining
1919
)
2020

@@ -77,7 +77,8 @@ async def async_update(self):
7777

7878
# We only want to update every 5 minutes so we don't hammer the service
7979
if self._minsSinceLastUpdate <= 0:
80-
buses = await async_get_buses(self._client, self._data[CONFIG_STOP], now())
80+
bus_times = await self._client.async_get_bus_times(self._data[CONFIG_STOP])
81+
buses = get_buses(bus_times, now())
8182
self._buses = buses
8283
self._minsSinceLastUpdate = 5
8384

custom_components/first_bus/utils.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import logging
22
import re
3+
from datetime import (datetime, timedelta)
4+
from homeassistant.util.dt import (parse_datetime)
35

46
from .const import (
57
REGEX_TIME,
68
REGEX_TIME_MINS,
79
)
810

9-
from datetime import (timedelta)
10-
from homeassistant.util.dt import (parse_datetime)
11-
from .api_client import FirstBusApiClient
12-
1311
_LOGGER = logging.getLogger(__name__)
1412

15-
async def async_get_buses(api_client: FirstBusApiClient, stop, current_timestamp):
16-
bus_times = await api_client.async_get_buses(stop)
13+
def get_buses(bus_times: list, current_timestamp: datetime):
1714
_LOGGER.debug(f'buses: {bus_times}')
1815

1916
for bus_time in bus_times:

run_tests.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python -m pytest tests

run_unit_tests.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python -m pytest tests/unit

tests/integration/api_client/test_get_buses.py tests/integration/api_client/test_get_bus_times.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async def test_when_get_buses_is_called_then_next_bus_is_returned():
1414
for stop in stops:
1515
try:
1616
# Act
17-
buses = await client.async_get_buses(stop)
17+
buses = await client.async_get_bus_times(stop)
1818
assert buses is not None
1919
assert len(buses) > 0
2020

tests/integration/utils/test_get_next_bus.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from homeassistant.util.dt import (now)
66

77
from custom_components.first_bus.api_client import FirstBusApiClient
8-
from custom_components.first_bus.utils import (async_get_buses, get_next_bus)
8+
from custom_components.first_bus.utils import (get_buses, get_next_bus)
99

1010
stops = ["0170SGB20116", "3800C509801", "2200YEA00934"]
1111

@@ -24,7 +24,8 @@ async def test_when_get_next_bus_is_called_then_next_bus_is_returned(target_buse
2424
for stop in stops:
2525
try:
2626
# Act
27-
buses = await async_get_buses(client, stop, now())
27+
bus_times = await client.async_get_bus_times(stop)
28+
buses = get_buses(bus_times, now())
2829
assert buses is not None
2930
assert len(buses) > 0
3031

tests/unit/config/__init__.py

Whitespace-only changes.
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import pytest
2+
from custom_components.first_bus.config import (validate_config)
3+
from custom_components.first_bus.const import CONFIG_NAME, CONFIG_STOP, CONFIG_BUSES
4+
5+
@pytest.mark.asyncio
6+
async def test_when_data_valid_then_no_errors_returned():
7+
# Arrange
8+
original_config = {
9+
CONFIG_NAME: "test",
10+
CONFIG_STOP: "123",
11+
CONFIG_BUSES: "12 A,12B,12"
12+
}
13+
14+
# Act
15+
(errors, config) = validate_config(original_config)
16+
17+
# Assert
18+
assert CONFIG_NAME not in errors
19+
assert CONFIG_STOP not in errors
20+
assert CONFIG_BUSES not in errors
21+
22+
assert CONFIG_NAME in config
23+
assert config[CONFIG_NAME] == original_config[CONFIG_NAME]
24+
assert CONFIG_STOP in config
25+
assert config[CONFIG_STOP] == original_config[CONFIG_STOP]
26+
assert CONFIG_BUSES in config
27+
assert config[CONFIG_BUSES] == ["12 A", "12B", "12"]
28+
29+
@pytest.mark.asyncio
30+
async def test_when_buses_not_present_then_buses_empty_array():
31+
# Arrange
32+
original_config = {
33+
CONFIG_NAME: "test",
34+
CONFIG_STOP: "123"
35+
}
36+
37+
# Act
38+
(errors, config) = validate_config(original_config)
39+
40+
# Assert
41+
assert CONFIG_NAME not in errors
42+
assert CONFIG_STOP not in errors
43+
assert CONFIG_BUSES not in errors
44+
45+
assert CONFIG_NAME in config
46+
assert config[CONFIG_NAME] == original_config[CONFIG_NAME]
47+
assert CONFIG_STOP in config
48+
assert config[CONFIG_STOP] == original_config[CONFIG_STOP]
49+
assert CONFIG_BUSES in config
50+
assert config[CONFIG_BUSES] == []
51+
52+
@pytest.mark.asyncio
53+
async def test_when_buses_none_then_buses_empty_array():
54+
# Arrange
55+
original_config = {
56+
CONFIG_NAME: "test",
57+
CONFIG_STOP: "123",
58+
CONFIG_BUSES: None
59+
}
60+
61+
# Act
62+
(errors, config) = validate_config(original_config)
63+
64+
# Assert
65+
assert CONFIG_NAME not in errors
66+
assert CONFIG_STOP not in errors
67+
assert CONFIG_BUSES not in errors
68+
69+
assert CONFIG_NAME in config
70+
assert config[CONFIG_NAME] == original_config[CONFIG_NAME]
71+
assert CONFIG_STOP in config
72+
assert config[CONFIG_STOP] == original_config[CONFIG_STOP]
73+
assert CONFIG_BUSES in config
74+
assert config[CONFIG_BUSES] == []
75+
76+
@pytest.mark.asyncio
77+
async def test_when_buses_empty_then_buses_empty_array():
78+
# Arrange
79+
original_config = {
80+
CONFIG_NAME: "test",
81+
CONFIG_STOP: "123",
82+
CONFIG_BUSES: ""
83+
}
84+
85+
# Act
86+
(errors, config) = validate_config(original_config)
87+
88+
# Assert
89+
assert CONFIG_NAME not in errors
90+
assert CONFIG_STOP not in errors
91+
assert CONFIG_BUSES not in errors
92+
93+
assert CONFIG_NAME in config
94+
assert config[CONFIG_NAME] == original_config[CONFIG_NAME]
95+
assert CONFIG_STOP in config
96+
assert config[CONFIG_STOP] == original_config[CONFIG_STOP]
97+
assert CONFIG_BUSES in config
98+
assert config[CONFIG_BUSES] == []
99+
100+
@pytest.mark.asyncio
101+
@pytest.mark.parametrize("bus_value",[
102+
("A-B"),
103+
("12,12B,"),
104+
])
105+
async def test_when_buses_not_valid_then_buses_empty_array(bus_value: str):
106+
# Arrange
107+
original_config = {
108+
CONFIG_NAME: "test",
109+
CONFIG_STOP: "123",
110+
CONFIG_BUSES: bus_value
111+
}
112+
113+
# Act
114+
(errors, config) = validate_config(original_config)
115+
116+
# Assert
117+
assert CONFIG_NAME not in errors
118+
assert CONFIG_STOP not in errors
119+
assert CONFIG_BUSES in errors
120+
assert errors[CONFIG_BUSES] == "invalid_buses"
121+
122+
assert CONFIG_NAME in config
123+
assert config[CONFIG_NAME] == original_config[CONFIG_NAME]
124+
assert CONFIG_STOP in config
125+
assert config[CONFIG_STOP] == original_config[CONFIG_STOP]
126+
assert CONFIG_BUSES in config
127+
assert config[CONFIG_BUSES] == original_config[CONFIG_BUSES]

tests/unit/utils/test_get_buses.py

+4-12
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import pytest
2-
from custom_components.first_bus.utils import (async_get_buses)
32
from homeassistant.util.dt import (parse_datetime)
43

5-
class MockedFirstBusApiClient:
6-
def __init__(self, buses):
7-
self._buses = buses
8-
9-
async def async_get_buses(self, stop):
10-
return self._buses
4+
from custom_components.first_bus.utils import (get_buses)
115

126
now = parse_datetime('2022-01-01T10:23:15+01:00')
137

@@ -21,24 +15,22 @@ async def async_get_buses(self, stop):
2115
])
2216
async def test_when_get_buses_is_called_and_set_time_in_past_is_returned_then_due_timestamp_is_correct(raw_due, expected_due):
2317
# Arrange
24-
raw_buses = [{
18+
bus_times = [{
2519
'ServiceRef': '0',
2620
'ServiceNumber': '43',
2721
'Destination': 'Newton Road Shops',
2822
'Due': raw_due,
2923
'IsFG': 'N',
3024
'IsLive': 'Y'
3125
}]
32-
client = MockedFirstBusApiClient(raw_buses)
33-
stop = "TEST_STOP"
3426

3527
# Act
36-
buses = await async_get_buses(client, stop, now)
28+
buses = get_buses(bus_times, now)
3729
assert buses is not None
3830
assert len(buses) == 1
3931

4032
bus = buses[0]
41-
raw_bus = raw_buses[0]
33+
raw_bus = bus_times[0]
4234

4335
# Assert
4436
assert "Due" in bus

0 commit comments

Comments
 (0)