diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..9f65311
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+custom: ["buymeacoffee.com/PiotrMachowski", "paypal.me/PiMachowski"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6d56b8e
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..855d3a5
--- /dev/null
+++ b/README.md
@@ -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: |
+ Timetable
+ [[ sensor.mpk_lodz_2427.attributes.html_timetable ]]
+ Departures
+ [[ sensor.mpk_lodz_2873.attributes.html_departures ]]
+ ```
+ * HTML Template card:
+ ```yaml
+ - type: custom:html-template-card
+ title: 'MPK'
+ ignore_line_breaks: true
+ content: |
+ Timetable
+ {{ state_attr('sensor.mpk_lodz_2427','html_timetable') }}
+ Departures
+ {{ state_attr('sensor.mpk_lodz_2873','html_departures') }}
+ ```
+
+
diff --git a/custom_components/mpk_lodz/__init__.py b/custom_components/mpk_lodz/__init__.py
new file mode 100644
index 0000000..dbbad80
--- /dev/null
+++ b/custom_components/mpk_lodz/__init__.py
@@ -0,0 +1 @@
+"""MPK Łódź"""
\ No newline at end of file
diff --git a/custom_components/mpk_lodz/manifest.json b/custom_components/mpk_lodz/manifest.json
new file mode 100644
index 0000000..8f9c15c
--- /dev/null
+++ b/custom_components/mpk_lodz/manifest.json
@@ -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"
+}
\ No newline at end of file
diff --git a/custom_components/mpk_lodz/sensor.py b/custom_components/mpk_lodz/sensor.py
new file mode 100644
index 0000000..56bd918
--- /dev/null
+++ b/custom_components/mpk_lodz/sensor.py
@@ -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 = '
\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 + '{}, kier. {} | '.format(
+ line, direction)
+ departures = ', '.join(map(lambda x: x["departure"], self._departures_by_line[line][direction]))
+ html = html + '{} |
\n'.format(departures)
+ if len(lines) == 0:
+ html = html + 'Brak połączeń | '
+ html = html + '
'
+ return html
+
+ def get_html_departures(self):
+ html = '\n'
+ for departure in self._departures:
+ html = html + '{} |
\n'.format(
+ MpkLodzSensor.departure_to_str(departure))
+ html = html + '
'
+ 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