first version
This commit is contained in:
commit
71893eef75
|
@ -0,0 +1,6 @@
|
||||||
|
/*
|
||||||
|
!.gitignore
|
||||||
|
!custom_components/
|
||||||
|
!custom_components/reos_integration/**
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
|
@ -0,0 +1,43 @@
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .reos_api import ReosApi
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
logging.info(msg="Reos async_setup")
|
||||||
|
hass.data[DOMAIN] = {}
|
||||||
|
# Return boolean to indicate that initialization was successful.
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
logging.info(msg="Reos async_setup_entry")
|
||||||
|
host = entry.data["host"]
|
||||||
|
username = entry.data["username"]
|
||||||
|
password = entry.data["password"]
|
||||||
|
client_id = entry.data["client_id"]
|
||||||
|
client_secret = entry.data["client_secret"]
|
||||||
|
access_token = None
|
||||||
|
if "access_token" in entry.data:
|
||||||
|
access_token = entry.data["access_token"]
|
||||||
|
refresh_token = None
|
||||||
|
if "refresh_token" in entry.data:
|
||||||
|
refresh_token = entry.data["refresh_token"]
|
||||||
|
|
||||||
|
api = ReosApi(host, username, password, client_id, client_secret, access_token, refresh_token)
|
||||||
|
|
||||||
|
async def __update_entry(access, refresh):
|
||||||
|
new = {**entry.data}
|
||||||
|
new["access_token"] = access
|
||||||
|
new["refresh_token"] = refresh
|
||||||
|
hass.config_entries.async_update_entry(entry, data=new)
|
||||||
|
|
||||||
|
api.set_update_token_callback(__update_entry)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][username] = api
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setup(entry, "lock")
|
||||||
|
return True
|
|
@ -0,0 +1,23 @@
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
class ReosConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(self, info):
|
||||||
|
if info is not None:
|
||||||
|
return self.async_create_entry(title=info["username"], data=info)
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="user",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Optional("host", default="feapi.reos.software"): str,
|
||||||
|
vol.Required("client_id"): str,
|
||||||
|
vol.Required("client_secret"): str,
|
||||||
|
vol.Required("username"): str,
|
||||||
|
vol.Required("password"): str,
|
||||||
|
}))
|
|
@ -0,0 +1 @@
|
||||||
|
DOMAIN = "reos_integration"
|
|
@ -0,0 +1,46 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.lock import LockEntity, LockEntityFeature
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .reos_api import ReosApi, ReosLockModel
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: AddEntitiesCallback) -> None:
|
||||||
|
api: ReosApi = hass.data[DOMAIN][config_entry.data["username"]]
|
||||||
|
|
||||||
|
async_add_entities(ReosLock(lock) for lock in await api.get_locks())
|
||||||
|
|
||||||
|
class ReosLock(LockEntity):
|
||||||
|
|
||||||
|
def __init__(self, model: ReosLockModel) -> None:
|
||||||
|
self._model = model
|
||||||
|
self._attr_unique_id = f"reos_lock_{self._model.lock_id}"
|
||||||
|
self._attr_name = self._model.display
|
||||||
|
self._attr_is_locked = False
|
||||||
|
self._attr_device_class = "door"
|
||||||
|
self._attr_icon = "mdi:door"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Flag supported features."""
|
||||||
|
return LockEntityFeature.OPEN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def async_open(self, **kwargs) -> None:
|
||||||
|
self.hass.async_create_task(self._model.open())
|
||||||
|
|
||||||
|
def unlock(self, **kwargs: cv.Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lock(self, **kwargs: cv.Any) -> None:
|
||||||
|
pass
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"domain": "reos_integration",
|
||||||
|
"name": "Reos Integration",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"config_flow": true,
|
||||||
|
"codeowners": ["@obittner"]
|
||||||
|
}
|
|
@ -0,0 +1,126 @@
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import aiohttp
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import jwt
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TOKEN_ENDPOINT = "/oauth/token"
|
||||||
|
USER_ENDPOINT = "/feapi/v2/user"
|
||||||
|
LOCK_ENDPOINT = "/feapi/v2/lock"
|
||||||
|
LOCK_OPEN_ENDPOINT = "/feapi/v2/lock/open"
|
||||||
|
PROTOCOL = "https"
|
||||||
|
|
||||||
|
class ReosApi:
|
||||||
|
|
||||||
|
def __init__(self, host: str, user: str, password: str, client_id: str, client_secret: str, access_token: str, refresh_token: str) -> None:
|
||||||
|
self.host = host
|
||||||
|
self.user = user
|
||||||
|
self._id = user
|
||||||
|
self.password = password
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self._access_token = access_token
|
||||||
|
self._refresh_token = refresh_token
|
||||||
|
self._update_token_callback = None
|
||||||
|
|
||||||
|
def set_update_token_callback(self, callback) -> None:
|
||||||
|
self._update_token_callback = callback
|
||||||
|
|
||||||
|
async def __call_update_token_callback(self) -> None:
|
||||||
|
if self._update_token_callback:
|
||||||
|
await self._update_token_callback(self._access_token, self._refresh_token)
|
||||||
|
|
||||||
|
async def get_access_token(self) -> str:
|
||||||
|
if self._access_token is not None and self.__is_token_valid(self._access_token):
|
||||||
|
return self._access_token
|
||||||
|
elif self._refresh_token is not None:
|
||||||
|
access, refresh = await self.__get_access_and_refresh_token_by_refresh_token()
|
||||||
|
await self.__store_tokens(access, refresh)
|
||||||
|
return self._access_token
|
||||||
|
else:
|
||||||
|
access, refresh = await self.__get_access_and_refresh_token_by_password()
|
||||||
|
await self.__store_tokens(access, refresh)
|
||||||
|
return self._access_token
|
||||||
|
|
||||||
|
async def __store_tokens(self, access, refresh):
|
||||||
|
self._access_token = access
|
||||||
|
self._refresh_token = refresh
|
||||||
|
await self.__call_update_token_callback()
|
||||||
|
|
||||||
|
async def __get_access_and_refresh_token_by_password(self) -> tuple[str, str]:
|
||||||
|
async with aiohttp.ClientSession() as session, session.post(f"{PROTOCOL}://{self.host}{TOKEN_ENDPOINT}", json={
|
||||||
|
"username": self.user,
|
||||||
|
"password": self.password,
|
||||||
|
"grant_type": "password",
|
||||||
|
"client_id": self.client_id,
|
||||||
|
"client_secret": self.client_secret,
|
||||||
|
"scope": "tss"
|
||||||
|
}) as response:
|
||||||
|
_json = await response.json()
|
||||||
|
return _json["access_token"], _json["refresh_token"]
|
||||||
|
|
||||||
|
async def __get_access_and_refresh_token_by_refresh_token(self) -> tuple[str, str]:
|
||||||
|
async with aiohttp.ClientSession() as session, session.post(f"{PROTOCOL}://{self.host}{TOKEN_ENDPOINT}", json={
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"client_id": self.client_id,
|
||||||
|
"client_secret": self.client_secret,
|
||||||
|
"scope": "tss",
|
||||||
|
"refresh_token": self._refresh_token
|
||||||
|
}) as response:
|
||||||
|
_json = await response.json()
|
||||||
|
return _json["access_token"], _json["refresh_token"]
|
||||||
|
|
||||||
|
async def open_lock(self, lock_id: int) -> None:
|
||||||
|
async with await self.__get_session() as session:
|
||||||
|
await session.post(f"{PROTOCOL}://{self.host}{LOCK_OPEN_ENDPOINT}", json={"lockId": lock_id})
|
||||||
|
|
||||||
|
def __is_token_valid(self, token: str) -> bool:
|
||||||
|
try:
|
||||||
|
jwt.decode(token, options={"verify_signature": False})
|
||||||
|
return True
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def __get_session(self) -> aiohttp.ClientSession:
|
||||||
|
token = await self.get_access_token()
|
||||||
|
return aiohttp.ClientSession(headers={"Authorization": f"Bearer {token}"})
|
||||||
|
|
||||||
|
async def test_connection(self) -> bool:
|
||||||
|
async with await self.__get_session() as session, session.get(f"{PROTOCOL}://{self.host}{USER_ENDPOINT}") as response:
|
||||||
|
return response.status == 200
|
||||||
|
|
||||||
|
async def get_locks(self) -> list["ReosLockModel"]:
|
||||||
|
async with await self.__get_session() as session, session.get(f"{PROTOCOL}://{self.host}{LOCK_ENDPOINT}") as response:
|
||||||
|
_json = await response.json()
|
||||||
|
_api_locks = _json["data"]
|
||||||
|
locks: list[ReosLockModel] = []
|
||||||
|
for _api_lock in _api_locks:
|
||||||
|
if str(_api_lock["type"]).lower() == "lock":
|
||||||
|
_attr = _api_lock["attributes"]
|
||||||
|
_lock = ReosLockModel(
|
||||||
|
_attr["id"],
|
||||||
|
_attr["title"],
|
||||||
|
_attr["display_title"],
|
||||||
|
_attr["is_favorite"],
|
||||||
|
self
|
||||||
|
)
|
||||||
|
locks.append(_lock)
|
||||||
|
return locks
|
||||||
|
|
||||||
|
|
||||||
|
class ReosLockModel:
|
||||||
|
def __init__(self, id: int, name: str, display: str, is_favorite: bool, api: ReosApi) -> None:
|
||||||
|
self._id = id
|
||||||
|
self.name = name
|
||||||
|
self.display = display
|
||||||
|
self._api = api
|
||||||
|
self.is_favorite = is_favorite
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lock_id(self) -> int:
|
||||||
|
return self._id
|
||||||
|
|
||||||
|
async def open(self) -> None:
|
||||||
|
await self._api.open_lock(self.lock_id)
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"title": "Reos Integraion",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Setup Reos API",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"username": "User",
|
||||||
|
"password": "Password",
|
||||||
|
"client_id": "Client Id",
|
||||||
|
"client_secret": "Client Secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"title": "Reos Integraion",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Setup Reos API",
|
||||||
|
"data": {
|
||||||
|
"host": "Host",
|
||||||
|
"username": "User",
|
||||||
|
"password": "Password",
|
||||||
|
"client_id": "Client Id",
|
||||||
|
"client_secret": "Client Secret"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"link": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue