Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
PiotrMachowski committed Apr 19, 2021
0 parents commit 6147a35
Show file tree
Hide file tree
Showing 13 changed files with 616 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.
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://hacs.xyz/docs/faq/custom_repositories)
[![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)
![GitHub All Releases](https://img.shields.io/github/downloads/PiotrMachowski/Home-Assistant-custom-components-Dom-5/total)

# Dom 5 Sensor

This custom integration retrieves data from [Dom 5](https://www.sacer.pl/dom5) - housing cooperative management system.

## Installation

### Using [HACS](https://hacs.xyz/) (recommended)

This integration can be added to HACS as a [custom repository](https://hacs.xyz/docs/faq/custom_repositories):
* URL: `https://github.com/PiotrMachowski/Home-Assistant-custom-components-Dom-5`
* Category: `Integration`

After adding a custom repository you can use HACS to install this integration using user interface.

### Manual

To install this integration manually you have to download [*dom_5.zip*](https://github.com/PiotrMachowski/Home-Assistant-custom-components-Dom-5/releases/latest/download/dom_5.zip) extract its contents to `config/custom_components/dom_5` directory:
```bash
mkdir -p custom_components/dom_5
cd custom_components/dom_5
wget https://github.com/PiotrMachowski/Home-Assistant-custom-components-Dom-5/releases/latest/download/dom_5.zip
unzip dom_5.zip
rm dom_5.zip
```

## Configuration

### Config flow (recommended)

To configure this integration go to: _Configuration_ -> _Integrations_ -> _Add integration_ -> _Dom 5_.

You can also use following [My Home Assistant](http://my.home-assistant.io/) link

[![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=dom_5)

### Manual - yaml
| Key | Type | Required | Value | Description |
|---|---|---|---|---|
| `platform` | string | true | `dom_5` | Name of a platform |
| `name` | string | false | | Desired name of a entity |
| `url` | string | true | | URL of system (in format: `https://dom5.pl` |
| `username` | string | true | | Username in Dom 5 system |
| `password` | string | true | | Password in Dom 5 system |

#### Example configuration
```yaml
sensor:
- platform: dom_5
url: "https://dom5.pl"
username: "123456"
password: "SecretPassword"
```
<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>
42 changes: 42 additions & 0 deletions custom_components/dom_5/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import asyncio

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .connector import Dom5Connector
from .const import *


async def async_setup(hass: HomeAssistant, config: dict):
hass.data.setdefault(DOMAIN, {})

return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
url = entry.data.get(CONF_URL)
username = entry.data.get(CONF_USERNAME)
password = entry.data.get(CONF_PASSWORD)
hass.data[DOMAIN][entry.entry_id] = Dom5Connector(url, username, password)

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok
60 changes: 60 additions & 0 deletions custom_components/dom_5/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import logging

import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.core import HomeAssistant

from .const import *
from .connector import test_connection

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema({
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str
})


async def validate_input(hass: HomeAssistant, data: dict):
if len(data.get(CONF_URL)) < 3:
raise InvalidUrl

url = data.get(CONF_URL)
username = data.get(CONF_USERNAME)
password = data.get(CONF_PASSWORD)

try:
result = await hass.async_add_executor_job(test_connection, url, username, password)
if not result:
raise InvalidCredentials
except:
raise InvalidUrl


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
errors = {}
if user_input is not None:
try:
await validate_input(self.hass, user_input)
title = f"{user_input.get(CONF_USERNAME)} ({user_input.get(CONF_URL)})"
return self.async_create_entry(title=title, data=user_input)
except InvalidCredentials:
errors[CONF_PASSWORD] = "invalid_auth"
except InvalidUrl:
errors[CONF_URL] = "cannot_connect"

return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors=errors)


class InvalidCredentials(exceptions.HomeAssistantError):
pass


class InvalidUrl(exceptions.HomeAssistantError):
pass
170 changes: 170 additions & 0 deletions custom_components/dom_5/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from datetime import timedelta

from homeassistant.util import Throttle
import logging
from typing import Any, Callable, List, Optional

import requests
import html2text

from requests import Response, Session

_LOGGER = logging.getLogger(__name__)

THROTTLE_INTERVAL = timedelta(minutes=30)


def test_connection(url: str, username: str, password: str) -> bool:
sensor = Dom5Connector(url, username, password)
return sensor.test_connection()


class Dom5Data:
messages_number: Optional[int]
last_messages_titles: Optional[List[str]]
last_message_id: Optional[str]
last_message_title: Optional[str]
last_message_body: Optional[str]
last_message_date: Optional[str]
announcements_number: Optional[int]
last_announcements_titles: Optional[List[str]]
last_announcement_id: Optional[str]
last_announcement_title: Optional[str]
last_announcement_body: Optional[str]
last_announcement_date: Optional[str]
arrear: Optional[float]
overpayment: Optional[float]
balance: Optional[float]

def __init__(self):
self.messages_number = None
self.last_messages_titles = None
self.last_message_id = None
self.last_message_title = None
self.last_message_body = None
self.last_message_date = None
self.announcements_number = None
self.last_announcements_titles = None
self.last_announcement_id = None
self.last_announcement_title = None
self.last_announcement_body = None
self.last_announcement_date = None
self.arrear = None
self.overpayment = None
self.balance = None

def set_messages(self, messages_response: Response):
if not Dom5Data.is_valid(messages_response):
return
self.messages_number, self.last_message_id, self.last_messages_titles = \
Dom5Data.parse_communications(messages_response.json())

def set_last_message(self, last_message_response: Response):
if not Dom5Data.is_valid(last_message_response):
return
self.last_message_title, self.last_message_body, self.last_message_date = \
Dom5Data.parse_specific_communication(last_message_response.json())

def set_announcements(self, announcements_response: Response):
if not Dom5Data.is_valid(announcements_response):
return
self.announcements_number, self.last_announcement_id, self.last_announcements_titles = \
Dom5Data.parse_communications(announcements_response.json())

def set_last_announcement(self, last_announcement_response: Response):
if not Dom5Data.is_valid(last_announcement_response):
return
self.last_announcement_title, self.last_announcement_body, self.last_announcement_date = \
Dom5Data.parse_specific_communication(last_announcement_response.json())

def set_finances(self, finances_response: Response):
if not Dom5Data.is_valid(finances_response):
return
json = finances_response.json()["data"]
self.arrear = json["Zaleglosci"]
self.overpayment = json["Nadplaty"]
self.balance = self.overpayment - self.arrear

@staticmethod
def is_valid(response: Response):
return response.status_code == 200 and "status" in response.json() and response.json()["status"] == "success"

@staticmethod
def parse_communications(json: Any):
communications_number = len(json["data"])
last_communication_id = None if communications_number == 0 else json["data"][0]["Ident"]
last_titles = list(map(lambda r: r["Tytul"], json["data"]))[0:10]
return communications_number, last_communication_id, last_titles

@staticmethod
def parse_specific_communication(json: Any):
title = json["data"]["Tytul"]
message = html2text.html2text(json["data"]["Tresc"])
date = json["data"]["Data"]
return title, message, date


class Dom5Connector:
data: Optional[Dom5Data]

def __init__(self, url: str, username: str, password: str):
self._base_url = url
self._username = username
self._password = password
self.data = Dom5Data()
self.update = Throttle(THROTTLE_INTERVAL)(self._update)

@property
def url(self) -> str:
return self._base_url

@property
def username(self) -> str:
return self._username

def _update(self):
session = self._login()
if session is None:
_LOGGER.error('Failed to login')
return
try:
data = Dom5Data()
messages_response = session.get(self._url('/iokEwid/DajKorespPoz'))
data.set_messages(messages_response)
if data.last_message_id is not None:
last_message_response = session.get(
self._url(f'/iokEwid/DajSzczegKorespPoz?Ident={data.last_message_id}'))
data.set_last_message(last_message_response)
announcements_response = session.get(self._url('/iok/DajOgloszenia'))
data.set_announcements(announcements_response)
if data.last_announcement_id is not None:
last_announcement_response = session.get(
self._url(f'/iok/DajOgloszenie?Ident={data.last_announcement_id}'))
data.set_last_announcement(last_announcement_response)
finances_response = session.get(self._url('/iokRozr/DajListeRozrachFin'))
data.set_finances(finances_response)
self.data = data
finally:
self._logout(session)

def _login(self) -> Optional[Session]:
session = requests.session()
login = session.post(url=self._url('/iok/Zaloguj'),
data={"Ident": self._username, "Haslo": self._password},
headers={"Referer": self._url("/content/InetObsKontr/#")})
if Dom5Data.is_valid(login):
return session
return None

def _logout(self, session: Session):
session.post(self._url('/iok/Wyloguj'))

def _url(self, path: str):
return f'{self._base_url}{path}'

def test_connection(self) -> bool:
session = self._login()
if session is not None:
self._logout(session)
return True
return False
6 changes: 6 additions & 0 deletions custom_components/dom_5/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_URL

DOMAIN = "dom_5"
DEFAULT_NAME = 'Dom 5'

PLATFORMS = ["sensor"]
11 changes: 11 additions & 0 deletions custom_components/dom_5/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"domain": "dom_5",
"name": "Dom 5",
"documentation": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Dom-5",
"issue_tracker": "https://github.com/PiotrMachowski/Home-Assistant-custom-components-Dom-5/issues",
"dependencies": [],
"codeowners": ["@PiotrMachowski"],
"requirements": ["requests", "html2text"],
"version": "v1.0.0",
"config_flow": true
}
Loading

0 comments on commit 6147a35

Please # to comment.