Add transport data from maps.yandex.ru api (#26766)

* 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

* up version on manifest.json

* up version to 0.3.7 in requirements_all.txt
This commit is contained in:
Askarov Rishat 2019-09-20 19:12:36 +03:00 committed by Pascal Vizeli
parent 54242cd65c
commit aaf0f9890d
9 changed files with 2341 additions and 0 deletions

View File

@ -747,6 +747,7 @@ omit =
homeassistant/components/yale_smart_alarm/alarm_control_panel.py
homeassistant/components/yamaha/media_player.py
homeassistant/components/yamaha_musiccast/media_player.py
homeassistant/components/yandex_transport/*
homeassistant/components/yeelight/*
homeassistant/components/yeelightsunflower/light.py
homeassistant/components/yi/camera.py

View File

@ -318,6 +318,7 @@ homeassistant/components/xiaomi_miio/* @rytilahti @syssi
homeassistant/components/xiaomi_tv/* @simse
homeassistant/components/xmpp/* @fabaff @flowolf
homeassistant/components/yamaha_musiccast/* @jalmeroth
homeassistant/components/yandex_transport/* @rishatik92
homeassistant/components/yeelight/* @rytilahti @zewelor
homeassistant/components/yeelightsunflower/* @lindsaymarkward
homeassistant/components/yessssms/* @flowolf

View File

@ -0,0 +1 @@
"""Service for obtaining information about closer bus from Transport Yandex Service."""

View 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.7"
],
"dependencies": [],
"codeowners": [
"@rishatik92"
]
}

View 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

View File

@ -1991,6 +1991,9 @@ xmltodict==0.12.0
# homeassistant.components.xs1
xs1-api-client==2.3.5
# homeassistant.components.yandex_transport
ya_ma==0.3.7
# homeassistant.components.yweather
yahooweather==0.10

View File

@ -0,0 +1 @@
"""Tests for the yandex transport platform."""

View File

@ -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

File diff suppressed because it is too large Load Diff