Skip to content

Commit

Permalink
Upload
Browse files Browse the repository at this point in the history
  • Loading branch information
PiotrMachowski committed Nov 23, 2021
0 parents commit 5751eb2
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
custom: ["buymeacoffee.com/PiotrMachowski", "paypal.me/PiMachowski"]
21 changes: 21 additions & 0 deletions LICENSE
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.
75 changes: 75 additions & 0 deletions README.md
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>
1 change: 1 addition & 0 deletions custom_components/mpk_lodz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""MPK Łódź"""
10 changes: 10 additions & 0 deletions custom_components/mpk_lodz/manifest.json
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"
}
184 changes: 184 additions & 0 deletions custom_components/mpk_lodz/sensor.py
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

0 comments on commit 5751eb2

Please sign in to comment.