Add transport data from maps.yandex.ru api (#26252)
* adding feature obtaining Moscow transport data from maps.yandex.ru api * extracting the YandexMapsRequester to pypi * fix code review comments * fix stop_name, state in datetime, logger formating * fix comments * add docstring to init * rename, because it works not only Moscow, but many another big cities in Russia * fix comments * Try to solve relative view in sensor timestamp * back to isoformat * add tests, update external library version * flake8 and black tests for sensor.py * fix manifest.json * update tests, migrate to pytest, async, Using MockDependency * move json to tests/fixtures * script/lint fixes * fix comments * removing check_filter function * fix typo
This commit is contained in:
parent
aac2c3e91c
commit
9e2cd5116a
9 changed files with 2341 additions and 0 deletions
|
@ -747,6 +747,7 @@ omit =
|
||||||
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
|
||||||
homeassistant/components/yamaha/media_player.py
|
homeassistant/components/yamaha/media_player.py
|
||||||
homeassistant/components/yamaha_musiccast/media_player.py
|
homeassistant/components/yamaha_musiccast/media_player.py
|
||||||
|
homeassistant/components/yandex_transport/*
|
||||||
homeassistant/components/yeelight/*
|
homeassistant/components/yeelight/*
|
||||||
homeassistant/components/yeelightsunflower/light.py
|
homeassistant/components/yeelightsunflower/light.py
|
||||||
homeassistant/components/yi/camera.py
|
homeassistant/components/yi/camera.py
|
||||||
|
|
|
@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi
|
||||||
homeassistant/components/xiaomi_tv/* @simse
|
homeassistant/components/xiaomi_tv/* @simse
|
||||||
homeassistant/components/xmpp/* @fabaff @flowolf
|
homeassistant/components/xmpp/* @fabaff @flowolf
|
||||||
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
homeassistant/components/yamaha_musiccast/* @jalmeroth
|
||||||
|
homeassistant/components/yandex_transport/* @rishatik92
|
||||||
homeassistant/components/yeelight/* @rytilahti @zewelor
|
homeassistant/components/yeelight/* @rytilahti @zewelor
|
||||||
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
homeassistant/components/yeelightsunflower/* @lindsaymarkward
|
||||||
homeassistant/components/yessssms/* @flowolf
|
homeassistant/components/yessssms/* @flowolf
|
||||||
|
|
1
homeassistant/components/yandex_transport/__init__.py
Normal file
1
homeassistant/components/yandex_transport/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Service for obtaining information about closer bus from Transport Yandex Service."""
|
12
homeassistant/components/yandex_transport/manifest.json
Normal file
12
homeassistant/components/yandex_transport/manifest.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"domain": "yandex_transport",
|
||||||
|
"name": "Yandex Transport",
|
||||||
|
"documentation": "https://www.home-assistant.io/components/yandex_transport",
|
||||||
|
"requirements": [
|
||||||
|
"ya_ma==0.3.4"
|
||||||
|
],
|
||||||
|
"dependencies": [],
|
||||||
|
"codeowners": [
|
||||||
|
"@rishatik92"
|
||||||
|
]
|
||||||
|
}
|
128
homeassistant/components/yandex_transport/sensor.py
Normal file
128
homeassistant/components/yandex_transport/sensor.py
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Service for obtaining information about closer bus from Transport Yandex Service."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from ya_ma import YandexMapsRequester
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STOP_NAME = "stop_name"
|
||||||
|
USER_AGENT = "Home Assistant"
|
||||||
|
ATTRIBUTION = "Data provided by maps.yandex.ru"
|
||||||
|
|
||||||
|
CONF_STOP_ID = "stop_id"
|
||||||
|
CONF_ROUTE = "routes"
|
||||||
|
|
||||||
|
DEFAULT_NAME = "Yandex Transport"
|
||||||
|
ICON = "mdi:bus"
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_STOP_ID): cv.string,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
|
"""Set up the Yandex transport sensor."""
|
||||||
|
stop_id = config[CONF_STOP_ID]
|
||||||
|
name = config[CONF_NAME]
|
||||||
|
routes = config[CONF_ROUTE]
|
||||||
|
|
||||||
|
data = YandexMapsRequester(user_agent=USER_AGENT)
|
||||||
|
add_entities([DiscoverMoscowYandexTransport(data, stop_id, routes, name)], True)
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverMoscowYandexTransport(Entity):
|
||||||
|
"""Implementation of yandex_transport sensor."""
|
||||||
|
|
||||||
|
def __init__(self, requester, stop_id, routes, name):
|
||||||
|
"""Initialize sensor."""
|
||||||
|
self.requester = requester
|
||||||
|
self._stop_id = stop_id
|
||||||
|
self._routes = []
|
||||||
|
self._routes = routes
|
||||||
|
self._state = None
|
||||||
|
self._name = name
|
||||||
|
self._attrs = None
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Get the latest data from maps.yandex.ru and update the states."""
|
||||||
|
attrs = {}
|
||||||
|
closer_time = None
|
||||||
|
try:
|
||||||
|
yandex_reply = self.requester.get_stop_info(self._stop_id)
|
||||||
|
data = yandex_reply["data"]
|
||||||
|
stop_metadata = data["properties"]["StopMetaData"]
|
||||||
|
except KeyError as key_error:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Exception KeyError was captured, missing key is %s. Yandex returned: %s",
|
||||||
|
key_error,
|
||||||
|
yandex_reply,
|
||||||
|
)
|
||||||
|
self.requester.set_new_session()
|
||||||
|
data = self.requester.get_stop_info(self._stop_id)["data"]
|
||||||
|
stop_metadata = data["properties"]["StopMetaData"]
|
||||||
|
stop_name = data["properties"]["name"]
|
||||||
|
transport_list = stop_metadata["Transport"]
|
||||||
|
for transport in transport_list:
|
||||||
|
route = transport["name"]
|
||||||
|
if self._routes and route not in self._routes:
|
||||||
|
# skip unnecessary route info
|
||||||
|
continue
|
||||||
|
if "Events" in transport["BriefSchedule"]:
|
||||||
|
for event in transport["BriefSchedule"]["Events"]:
|
||||||
|
if "Estimated" in event:
|
||||||
|
posix_time_next = int(event["Estimated"]["value"])
|
||||||
|
if closer_time is None or closer_time > posix_time_next:
|
||||||
|
closer_time = posix_time_next
|
||||||
|
if route not in attrs:
|
||||||
|
attrs[route] = []
|
||||||
|
attrs[route].append(event["Estimated"]["text"])
|
||||||
|
attrs[STOP_NAME] = stop_name
|
||||||
|
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
|
||||||
|
if closer_time is None:
|
||||||
|
self._state = None
|
||||||
|
else:
|
||||||
|
self._state = dt_util.utc_from_timestamp(closer_time).isoformat(
|
||||||
|
timespec="seconds"
|
||||||
|
)
|
||||||
|
self._attrs = attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self._state
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
"""Return the device class."""
|
||||||
|
return DEVICE_CLASS_TIMESTAMP
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self):
|
||||||
|
"""Return the state attributes."""
|
||||||
|
return self._attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self):
|
||||||
|
"""Icon to use in the frontend, if any."""
|
||||||
|
return ICON
|
|
@ -1994,6 +1994,9 @@ xmltodict==0.12.0
|
||||||
# homeassistant.components.xs1
|
# homeassistant.components.xs1
|
||||||
xs1-api-client==2.3.5
|
xs1-api-client==2.3.5
|
||||||
|
|
||||||
|
# homeassistant.components.yandex_transport
|
||||||
|
ya_ma==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.yweather
|
# homeassistant.components.yweather
|
||||||
yahooweather==0.10
|
yahooweather==0.10
|
||||||
|
|
||||||
|
|
1
tests/components/yandex_transport/__init__.py
Normal file
1
tests/components/yandex_transport/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the yandex transport platform."""
|
|
@ -0,0 +1,88 @@
|
||||||
|
"""Tests for the yandex transport platform."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import homeassistant.components.sensor as sensor
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
from homeassistant.const import CONF_NAME
|
||||||
|
from tests.common import (
|
||||||
|
assert_setup_component,
|
||||||
|
async_setup_component,
|
||||||
|
MockDependency,
|
||||||
|
load_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
REPLY = json.loads(load_fixture("yandex_transport_reply.json"))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_requester():
|
||||||
|
"""Create a mock ya_ma module and YandexMapsRequester."""
|
||||||
|
with MockDependency("ya_ma") as ya_ma:
|
||||||
|
instance = ya_ma.YandexMapsRequester.return_value
|
||||||
|
instance.get_stop_info.return_value = REPLY
|
||||||
|
yield instance
|
||||||
|
|
||||||
|
|
||||||
|
STOP_ID = 9639579
|
||||||
|
ROUTES = ["194", "т36", "т47", "м10"]
|
||||||
|
NAME = "test_name"
|
||||||
|
TEST_CONFIG = {
|
||||||
|
"sensor": {
|
||||||
|
"platform": "yandex_transport",
|
||||||
|
"stop_id": 9639579,
|
||||||
|
"routes": ROUTES,
|
||||||
|
"name": NAME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FILTERED_ATTRS = {
|
||||||
|
"т36": ["21:43", "21:47", "22:02"],
|
||||||
|
"т47": ["21:40", "22:01"],
|
||||||
|
"м10": ["21:48", "22:00"],
|
||||||
|
"stop_name": "7-й автобусный парк",
|
||||||
|
"attribution": "Data provided by maps.yandex.ru",
|
||||||
|
}
|
||||||
|
|
||||||
|
RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
async def assert_setup_sensor(hass, config, count=1):
|
||||||
|
"""Set up the sensor and assert it's been created."""
|
||||||
|
with assert_setup_component(count):
|
||||||
|
assert await async_setup_component(hass, sensor.DOMAIN, config)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_platform_valid_config(hass, mock_requester):
|
||||||
|
"""Test that sensor is set up properly with valid config."""
|
||||||
|
await assert_setup_sensor(hass, TEST_CONFIG)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_platform_invalid_config(hass, mock_requester):
|
||||||
|
"""Check an invalid configuration."""
|
||||||
|
await assert_setup_sensor(
|
||||||
|
hass, {"sensor": {"platform": "yandex_transport", "stopid": 1234}}, count=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_name(hass, mock_requester):
|
||||||
|
"""Return the name if set in the configuration."""
|
||||||
|
await assert_setup_sensor(hass, TEST_CONFIG)
|
||||||
|
state = hass.states.get("sensor.test_name")
|
||||||
|
assert state.name == TEST_CONFIG["sensor"][CONF_NAME]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_state(hass, mock_requester):
|
||||||
|
"""Return the contents of _state."""
|
||||||
|
await assert_setup_sensor(hass, TEST_CONFIG)
|
||||||
|
state = hass.states.get("sensor.test_name")
|
||||||
|
assert state.state == RESULT_STATE
|
||||||
|
|
||||||
|
|
||||||
|
async def test_filtered_attributes(hass, mock_requester):
|
||||||
|
"""Return the contents of attributes."""
|
||||||
|
await assert_setup_sensor(hass, TEST_CONFIG)
|
||||||
|
state = hass.states.get("sensor.test_name")
|
||||||
|
state_attrs = {key: state.attributes[key] for key in FILTERED_ATTRS}
|
||||||
|
assert state_attrs == FILTERED_ATTRS
|
2106
tests/fixtures/yandex_transport_reply.json
vendored
Normal file
2106
tests/fixtures/yandex_transport_reply.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue