-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 5751eb2
Showing
6 changed files
with
292 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
custom: ["buymeacoffee.com/PiotrMachowski", "paypal.me/PiMachowski"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2020 Piotr Machowski | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
# MPK Łódź sensor | ||
|
||
[![buymeacoffee_badge](https://img.shields.io/badge/Donate-Buy%20Me%20a%20Coffee-ff813f?style=flat)](https://www.buymeacoffee.com/PiotrMachowski) | ||
[![paypalme_badge](https://img.shields.io/badge/Donate-PayPal-0070ba?style=flat)](https://paypal.me/PiMachowski) | ||
|
||
This sensor uses unofficial API provided by MPK Łódź. | ||
|
||
## Configuration options | ||
|
||
| Key | Type | Required | Default | Description | | ||
| --- | --- | --- | --- | --- | | ||
| `name` | `string` | `False` | `MPK Łódź` | Name of sensor | | ||
| `stops` | `list` | `True` | - | List of stop configurations | | ||
|
||
### Stop configuration | ||
|
||
| Key | Type | Required | Default | Description | | ||
| --- | --- | --- | --- | --- | | ||
| `id` | `positive integer` | `True` | - | ID of a stop | | ||
| `name` | `string` | `False` | id | Name of a stop | | ||
| `lines` | `list` | `False` | all available | List of monitored lines. | | ||
| `directions` | `list` | `False` | all available | List of monitored directions. | | ||
|
||
## Example usage | ||
|
||
``` | ||
sensor: | ||
- platform: mpk_lodz | ||
stops: | ||
- id: 2427 | ||
lines: | ||
- "o97A" | ||
- id: 2873 | ||
directions: | ||
- "DW. ŁÓDŹ KALISKA" | ||
``` | ||
|
||
## Installation | ||
|
||
Download [*sensor.py*](https://github.com/PiotrMachowski/Home-Assistant-custom-components-MPK-Lodz/raw/master/custom_components/mpk_lodz/sensor.py) and [*manifest.json*](https://github.com/PiotrMachowski/Home-Assistant-custom-components-MPK-Lodz/raw/master/custom_components/mpk_lodz/manifest.json) to `config/custom_components/mpk_lodz` directory: | ||
```bash | ||
mkdir -p custom_components/mpk_lodz | ||
cd custom_components/mpk_lodz | ||
wget https://github.com/PiotrMachowski/Home-Assistant-custom-components-MPK-Lodz/raw/master/custom_components/mpk_lodz/sensor.py | ||
wget https://github.com/PiotrMachowski/Home-Assistant-custom-components-MPK-Lodz/raw/master/custom_components/mpk_lodz/manifest.json | ||
``` | ||
|
||
## Hints | ||
|
||
* Value for `stop_id` can be retrieved from [*ITS Łódź*](http://rozklady.lodz.pl/). After choosing a desired stop open its electronical table. `stop_id` is a number visibile in URL. | ||
|
||
* These sensors provides attributes which can be used in [*HTML card*](https://github.com/PiotrMachowski/Home-Assistant-Lovelace-HTML-card) or [*HTML Template card*](https://github.com/PiotrMachowski/Home-Assistant-Lovelace-HTML-Template-card): `html_timetable`, `html_departures` | ||
* HTML card: | ||
```yaml | ||
- type: custom:html-card | ||
title: 'MPK' | ||
content: | | ||
<big><center>Timetable</center></big> | ||
[[ sensor.mpk_lodz_2427.attributes.html_timetable ]] | ||
<big><center>Departures</center></big> | ||
[[ sensor.mpk_lodz_2873.attributes.html_departures ]] | ||
``` | ||
* HTML Template card: | ||
```yaml | ||
- type: custom:html-template-card | ||
title: 'MPK' | ||
ignore_line_breaks: true | ||
content: | | ||
<big><center>Timetable</center></big></br> | ||
{{ state_attr('sensor.mpk_lodz_2427','html_timetable') }} | ||
</br><big><center>Departures</center></big></br> | ||
{{ state_attr('sensor.mpk_lodz_2873','html_departures') }} | ||
``` | ||
<a href="https://www.buymeacoffee.com/PiotrMachowski" target="_blank"><img src="https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: auto !important;width: auto !important;" ></a> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""MPK Łódź""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "mpk_lodz", | ||
"name": "MPK Łódź", | ||
"documentation": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-MPK-Lodz", | ||
"dependencies": [], | ||
"codeowners": ["@PiotrMachowski"], | ||
"requirements": ["requests"], | ||
"version": "v1.0.0", | ||
"iot_class": "cloud_polling" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
import requests | ||
from datetime import datetime, timedelta | ||
import xml.etree.ElementTree as ET | ||
import voluptuous as vol | ||
|
||
from homeassistant.components.sensor import PLATFORM_SCHEMA, ENTITY_ID_FORMAT | ||
from homeassistant.const import CONF_ID, CONF_NAME | ||
import homeassistant.helpers.config_validation as cv | ||
from homeassistant.helpers.entity import Entity | ||
from homeassistant.helpers.entity import async_generate_entity_id | ||
|
||
DEFAULT_NAME = 'MPK Łódź' | ||
|
||
CONF_STOPS = 'stops' | ||
CONF_LINES = 'lines' | ||
CONF_DIRECTIONS = 'directions' | ||
|
||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, | ||
vol.Required(CONF_STOPS): vol.All(cv.ensure_list, [ | ||
vol.Schema({ | ||
vol.Required(CONF_ID): cv.positive_int, | ||
vol.Optional(CONF_NAME): cv.string, | ||
vol.Optional(CONF_LINES, default=[]): cv.ensure_list, | ||
vol.Optional(CONF_DIRECTIONS, default=[]): cv.ensure_list | ||
})]) | ||
}) | ||
|
||
|
||
def setup_platform(hass, config, add_entities, discovery_info=None): | ||
name = config.get(CONF_NAME) | ||
stops = config.get(CONF_STOPS) | ||
dev = [] | ||
for stop in stops: | ||
stop_id = str(stop.get(CONF_ID)) | ||
lines = stop.get(CONF_LINES) | ||
directions = stop.get(CONF_DIRECTIONS) | ||
real_stop_name = MpkLodzSensor.get_stop_name(stop_id) | ||
if real_stop_name is None: | ||
raise Exception("Invalid stop id: {}".format(stop_id)) | ||
stop_name = stop.get(CONF_NAME) or stop_id | ||
uid = '{}_{}'.format(name, stop_name) | ||
entity_id = async_generate_entity_id(ENTITY_ID_FORMAT, uid, hass=hass) | ||
dev.append(MpkLodzSensor(entity_id, name, stop_id, stop_name, real_stop_name, lines, directions)) | ||
add_entities(dev, True) | ||
|
||
|
||
class MpkLodzSensor(Entity): | ||
def __init__(self, entity_id, name, stop_id, stop_name, real_stop_name, watched_lines, watched_directions): | ||
self.entity_id = entity_id | ||
self._name = name | ||
self._stop_id = stop_id | ||
self._watched_lines = watched_lines | ||
self._watched_directions = watched_directions | ||
self._stop_name = stop_name | ||
self._real_stop_name = real_stop_name | ||
self._departures = [] | ||
self._departures_number = 0 | ||
self._departures_by_line = dict() | ||
|
||
@property | ||
def name(self): | ||
return '{} - {}'.format(self._name, self._stop_name) | ||
|
||
@property | ||
def icon(self): | ||
return "mdi:bus-clock" | ||
|
||
@property | ||
def state(self): | ||
if self._departures_number is not None and self._departures_number > 0: | ||
dep = self._departures[0] | ||
return MpkLodzSensor.departure_to_str(dep) | ||
return None | ||
|
||
@property | ||
def unit_of_measurement(self): | ||
return None | ||
|
||
@property | ||
def device_state_attributes(self): | ||
attr = dict() | ||
attr['stop_name'] = self._real_stop_name | ||
if self._departures is not None: | ||
attr['list'] = self._departures | ||
attr['html_timetable'] = self.get_html_timetable() | ||
attr['html_departures'] = self.get_html_departures() | ||
if self._departures_number > 0: | ||
dep = self._departures[0] | ||
attr['line'] = dep["line"] | ||
attr['direction'] = dep["direction"] | ||
attr['departure'] = dep["departure"] | ||
attr['time_to_departure'] = dep["time_to_departure"] | ||
return attr | ||
|
||
def update(self): | ||
now = datetime.now() | ||
data = MpkLodzSensor.get_data(self._stop_id) | ||
if data is None: | ||
return | ||
departures = data[0][0] | ||
parsed_departures = [] | ||
for departure in departures: | ||
line = departure.attrib["nr"] | ||
direction = departure.attrib["dir"] | ||
if len(self._watched_lines) > 0 and line not in self._watched_lines \ | ||
or len(self._watched_directions) > 0 and direction not in self._watched_directions: | ||
continue | ||
time_in_seconds = int(departure[0].attrib["s"]) | ||
departure = now + timedelta(seconds=time_in_seconds) | ||
time_to_departure = time_in_seconds // 60 | ||
parsed_departures.append( | ||
{ | ||
"line": line, | ||
"direction": direction, | ||
"departure": "{:02}:{:02}".format(departure.hour, departure.minute), | ||
"time_to_departure": int(time_to_departure), | ||
}) | ||
self._departures = parsed_departures | ||
self._departures_number = len(parsed_departures) | ||
self._departures_by_line = MpkLodzSensor.group_by_line(self._departures) | ||
|
||
def get_html_timetable(self): | ||
html = '<table width="100%" border=1 style="border: 1px black solid; border-collapse: collapse;">\n' | ||
lines = list(self._departures_by_line.keys()) | ||
lines.sort() | ||
for line in lines: | ||
directions = list(self._departures_by_line[line].keys()) | ||
directions.sort() | ||
for direction in directions: | ||
if len(direction) == 0: | ||
continue | ||
html = html + '<tr><td style="text-align: center; padding: 4px"><big>{}, kier. {}</big></td>'.format( | ||
line, direction) | ||
departures = ', '.join(map(lambda x: x["departure"], self._departures_by_line[line][direction])) | ||
html = html + '<td style="text-align: right; padding: 4px">{}</td></tr>\n'.format(departures) | ||
if len(lines) == 0: | ||
html = html + '<tr><td style="text-align: center; padding: 4px">Brak połączeń</td>' | ||
html = html + '</table>' | ||
return html | ||
|
||
def get_html_departures(self): | ||
html = '<table width="100%" border=1 style="border: 1px black solid; border-collapse: collapse;">\n' | ||
for departure in self._departures: | ||
html = html + '<tr><td style="text-align: center; padding: 4px">{}</td></tr>\n'.format( | ||
MpkLodzSensor.departure_to_str(departure)) | ||
html = html + '</table>' | ||
return html | ||
|
||
@staticmethod | ||
def departure_to_str(dep): | ||
return '{}, kier. {}: {} ({}m)'.format(dep["line"], dep["direction"], dep["departure"], | ||
dep["time_to_departure"]) | ||
|
||
@staticmethod | ||
def group_by_line(departures): | ||
departures_by_line = dict() | ||
for departure in departures: | ||
line = departure["line"] | ||
direction = departure["direction"] | ||
if line not in departures_by_line: | ||
departures_by_line[line] = dict() | ||
if direction not in departures_by_line[line]: | ||
departures_by_line[line][direction] = [] | ||
departures_by_line[line][direction].append(departure) | ||
return departures_by_line | ||
|
||
@staticmethod | ||
def get_stop_name(stop_id): | ||
data = MpkLodzSensor.get_data(stop_id) | ||
if data is None: | ||
return None | ||
return data[0].attrib["name"] | ||
|
||
@staticmethod | ||
def get_data(stop_id): | ||
address = "http://rozklady.lodz.pl/Home/GetTimeTableReal?busStopId={}".format(stop_id) | ||
headers = { | ||
'referer': address, | ||
} | ||
response = requests.get(address, headers=headers) | ||
if response.status_code == 200 and response.content.__len__() > 0: | ||
return ET.fromstring(response.text) | ||
return None |