Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added module type SENSE control #6

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "home-assistant/actions/hassfest@master"
58 changes: 58 additions & 0 deletions .github/workflows/quality.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Quality

on: [push]

jobs:
# pylint:
# runs-on: "ubuntu-latest"
# steps:
# - uses: "actions/checkout@v3"
# - name: Set up Python 3.11
# uses: actions/setup-python@v4
# with:
# python-version: "3.11"
# cache: 'pip'
# cache-dependency-path: |
# **/setup.cfg
# **/requirements*.txt
# - name: Install dependencies
# run: |
# pip install -r requirements-dev.txt
#
# - name: pytest
# run: |
# pylint $(git ls-files '*.py')
test:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: "3.11"
cache: 'pip'
cache-dependency-path: |
**/setup.cfg
**/requirements*.txt
- name: Install dependencies
run: |
pip install flake8 pytest pytest-cov
pip install -r requirements-test.txt

- name: pytest
run: |
pytest --junitxml=pytest.xml --cov-report="xml:coverage.xml" --cov=custom_components/alpha_innotec tests/

- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
pytest-xml-coverage-path: ./coverage.xml
junitxml-path: ./pytest.xml
title: HA Alpha Innotec
badge-title: HA Alpha Innotec Coverage
hide-badge: false
hide-report: false
create-new-comment: false
hide-comment: false
report-only-changed-files: false
remove-link-from-badge: false
18 changes: 18 additions & 0 deletions .github/workflows/validate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Validate

on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
__pycache__/
venv
.vscode
*.code-workspace
*.pyc
*.swp
__pycache__
env
.mypy_cache
.coverage
coverage.xml
.secrets
.pytest_cache
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Alpha Innotec Home Assistant integration

![Version](https://img.shields.io/github/v/release/arjenbos/ha_alpha_innotec)
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration)

A custom Home Assistant integration for Alpha Innotec heat pumps.

## Pre-install
1. Create a user for Home Assistant in the control box; it's **not recommended** to use a shared user.
2. Ensure Home Assistant is able to reach the control box via the local network.
3. Ensure Home Assistant is able to reach the gateway via the local network.
4. You need to have the password for the gateway.

## Install
1. Add this repository as a custom repository in HACS.
2. Install the integration.

## Disclaimer
- I cannot say that i'm a python developer. So, if you see code that's bad practice. Please, feel free to contribute to this integration via a pull request.
- I do not own an Alpha Innotec heat pump. If you have an issue, please provide sample data and information about your installation because testing without sample data is impossible.
7 changes: 0 additions & 7 deletions const.py

This file was deleted.

Empty file added custom_components/__init__.py
Empty file.
23 changes: 14 additions & 9 deletions __init__.py → custom_components/alpha_innotec/__init__.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import logging

from .api import ControllerApi
from .const import DOMAIN, PLATFORMS

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

from .const import DOMAIN, PLATFORMS
from .controller_api import ControllerAPI
from .gateway_api import GatewayAPI

_LOGGER = logging.getLogger(__name__)


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> True:
"""Set up Alpha Home from config entry."""
_LOGGER.info("Setting up Alpha Home component")

controller_api = ControllerApi(entry.data['controller_ip'], entry.data['username'], entry.data['password'])
_LOGGER.debug("Setting up Alpha Home component")

controller_api = ControllerAPI(entry.data['controller_ip'], entry.data['controller_username'], entry.data['controller_password'])
controller_api = await hass.async_add_executor_job(controller_api.login)
gateway_api = GatewayAPI(entry.data['gateway_ip'], entry.data['gateway_password'])
gateway_api = await hass.async_add_executor_job(gateway_api.login)

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = controller_api
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"controller_api": controller_api,
"gateway_api": gateway_api,
}

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

Expand Down
61 changes: 61 additions & 0 deletions custom_components/alpha_innotec/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import base64
import logging

from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from backports.pbkdf2 import pbkdf2_hmac

_LOGGER = logging.getLogger(__name__)


class BaseAPI:

def __init__(self, hostname: str, username: str, password: str) -> None:
self.api_host: str = hostname
self.username: str = username
self.password: str = password

self.user_id: int | None = None
self.device_token_encrypted: str | None = None
self.device_token_decrypted: str | None = None
self.request_count: int = 0
self.last_request_signature: str | None = None
self.udid: str = "homeassistant"

@staticmethod
def string_to_charcodes(data: str) -> str:
a = ""
if len(data) > 0:
for i in range(len(data)):
t = str(ord(data[i]))
while len(t) < 3:
t = "0" + t
a += t

return a

def encode_signature(self, value: str, salt: str) -> str:
value = self.string_to_charcodes(value)
salt = self.string_to_charcodes(salt)

original = pbkdf2_hmac("sha512", value.encode(), salt.encode(), 1)

return original

@staticmethod
def _prepare_request_body_for_hash(urlencoded_string: str) -> str:
"""Replace dots for comma in case of temperature being passed"""
urlencoded_string = urlencoded_string.replace('%2C', ',').replace('%5B', '[').replace('%5D', ']')
# if(urlencodedString.find("temperature") != -1):
# urlencodedString = urlencodedString.replace('.', ',')
return urlencoded_string

@staticmethod
def decrypt2(encrypted_data: str, key: str):
static_iv = 'D3GC5NQEFH13is04KD2tOg=='
crypt_key = SHA256.new()
crypt_key.update(bytes(key, 'utf-8'))
crypt_key = crypt_key.digest()
cipher = AES.new(crypt_key, AES.MODE_CBC, base64.b64decode(static_iv))

return cipher.decrypt(base64.b64decode(encrypted_data)).decode('ascii').strip('\x10')
67 changes: 67 additions & 0 deletions custom_components/alpha_innotec/base_coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Platform for sensor integration."""
from __future__ import annotations

import logging

from homeassistant.core import HomeAssistant

from . import GatewayAPI
from .const import MODULE_TYPE_SENSOR, MODULE_TYPE_SENSE_CONTROL
from .controller_api import ControllerAPI
from .structs.Thermostat import Thermostat

_LOGGER = logging.getLogger(__name__)


class BaseCoordinator:

@staticmethod
async def get_thermostats(hass: HomeAssistant, gateway_api: GatewayAPI, controller_api: ControllerAPI) -> list[Thermostat]:
try:
rooms: dict = await hass.async_add_executor_job(gateway_api.all_modules)

thermostats: list[Thermostat] = []

db_modules: dict = await hass.async_add_executor_job(gateway_api.db_modules)
room_list: dict = await hass.async_add_executor_job(controller_api.room_list)

try:
for room_id in rooms:
room_module = rooms[room_id]
room = await hass.async_add_executor_job(controller_api.room_details, room_id, room_list)

current_temperature = None
battery_percentage = None

for module_id in room_module['modules']:
if module_id not in db_modules['modules']:
continue

module_details = db_modules['modules'][module_id]

if module_details["type"] == MODULE_TYPE_SENSOR:
current_temperature = module_details["currentTemperature"]
battery_percentage = module_details["battery"]
elif module_details["type"] == MODULE_TYPE_SENSE_CONTROL:
current_temperature = module_details["currentTemperature"]
battery_percentage = module_details["battery"]

thermostat = Thermostat(
identifier=room_id,
name=room['name'],
current_temperature=current_temperature,
desired_temperature=room.get('desiredTemperature'),
minimum_temperature=room.get('minTemperature'),
maximum_temperature=room.get('maxTemperature'),
cooling=room.get('cooling'),
cooling_enabled=room.get('coolingEnabled'),
battery_percentage=battery_percentage
)

thermostats.append(thermostat)
except Exception as exception:
_LOGGER.exception("There is an exception: %s", exception)

return thermostats
except Exception as exception:
raise exception
Loading