Skip to content

Commit

Permalink
removed saving auth data in file, used config_entry instead
Browse files Browse the repository at this point in the history
  • Loading branch information
vaproloff committed Aug 15, 2024
1 parent 7275fca commit b4e2a21
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 98 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
Службы Home Assistant для управления вашими устройствами:
### climate.set_fan_mode
`fan_mode` задает скорость бризера следующим образом (тип - строка):
- `off`, `0` - выключить
- `1`-`6` - включить в ручном режиме с заданной скоростью
- `auto` - автоматическое управление скоростью в зависимости от уровня CO2

Expand Down
28 changes: 16 additions & 12 deletions custom_components/tion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from .client import TionClient
from .const import (
AUTH_DATA,
DEFAULT_AUTH_FILENAME,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
Expand Down Expand Up @@ -45,11 +46,9 @@
)


def create_api(user, password, interval, auth_fname):
def create_api(user, password, interval, auth_data):
"""Return Tion Api."""
return TionClient(
user, password, min_update_interval_sec=interval, auth_fname=auth_fname
)
return TionClient(user, password, min_update_interval_sec=interval, auth=auth_data)


async def async_setup(hass: HomeAssistant, config: Config):
Expand All @@ -70,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: Config):
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Tion",
"integration_title": MANUFACTURER,
},
)
return True
Expand All @@ -93,7 +92,7 @@ async def async_setup(hass: HomeAssistant, config: Config):
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Tion",
"integration_title": MANUFACTURER,
},
)

Expand All @@ -105,22 +104,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})

tion_api = await hass.async_add_executor_job(
async def update_auth_data(**kwargs):
hass.config_entries.async_update_entry(entry, data=kwargs)

client = await hass.async_add_executor_job(
create_api,
entry.data.get(CONF_USERNAME),
entry.data.get(CONF_PASSWORD),
entry.data.get(CONF_SCAN_INTERVAL),
entry.data.get(CONF_FILE_PATH),
entry.data.get(AUTH_DATA),
)

assert tion_api.authorization, "Couldn't get authorisation data!"
_LOGGER.info("Api initialized with authorization %s", tion_api.authorization)
client.add_entry_data_updater(update_auth_data)

assert client.authorization, "Couldn't get authorisation data!"
_LOGGER.info("Api initialized with authorization %s", client.authorization)

hass.data[DOMAIN][entry.entry_id] = tion_api
hass.data[DOMAIN][entry.entry_id] = client

device_registry = dr.async_get(hass)

devices = await hass.async_add_executor_job(tion_api.get_devices)
devices = await hass.async_add_executor_job(client.get_devices)
for device in devices:
_LOGGER.info("Device type: %s", device.type)
if device.valid:
Expand Down
134 changes: 74 additions & 60 deletions custom_components/tion/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""The Tion API interaction module."""

import logging
from os import path
from time import sleep, time
from typing import Any

Expand Down Expand Up @@ -125,29 +124,29 @@ class TionClient:
_API_ENDPOINT = "https://api2.magicair.tion.ru/"
_AUTH_URL = "idsrv/oauth2/token"
_LOCATION_URL = "location"
_DEVICE_URL = "device"
_ZONE_URL = "zone"
_TASK_URL = "task"
_CLIENT_ID = "cd594955-f5ba-4c20-9583-5990bb29f4ef"
_CLIENT_SECRET = "syRxSrT77P"

def __init__(
self,
email: str,
password: str,
auth_fname="tion_auth",
min_update_interval_sec=10,
auth=None,
) -> None:
"""Tion API client initialization."""
self._email = email
self._password = password
self._auth_fname = auth_fname
self._min_update_interval = min_update_interval_sec
if self._auth_fname and path.exists(self._auth_fname):
with open(self._auth_fname, encoding="utf-8") as file:
self.authorization = file.read()
else:
self.authorization = None
self._get_authorization()
self._last_update = 0
self._authorization = auth

self._locations: list[TionLocation] = []
self._auth_update_listeners = []
self._last_update = 0

self.get_location_data()

@property
Expand All @@ -157,7 +156,7 @@ def headers(self):
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "ru-RU",
"Authorization": self.authorization,
"Authorization": self._authorization,
"Connection": "Keep-Alive",
"Content-Type": "application/json",
"Host": "api2.magicair.tion.ru",
Expand All @@ -166,6 +165,15 @@ def headers(self):
"User-Agent": "Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.10586",
}

@property
def authorization(self) -> str:
"""Return authorization data."""
return self._authorization

def add_entry_data_updater(self, coro):
"""Add entry data update listener function."""
self._auth_update_listeners.append(coro)

def _get_authorization(self):
data = {
"username": self._email,
Expand All @@ -181,32 +189,27 @@ def _get_authorization(self):
timeout=10,
)
except requests.exceptions.RequestException as e:
_LOGGER.error("Request exception while getting token:\n%s", e)
_LOGGER.error("TionClient: request exception while getting token:\n%s", e)
return False

if response.status_code == 200:
response = response.json()
self.authorization = f"{response['token_type']} {response['access_token']}"

if self._auth_fname:
try:
with open(self._auth_fname, "w", encoding="utf-8") as file:
try:
file.write(self.authorization)
except OSError as e:
_LOGGER.error(
"Unable to write auth data to %s: %s",
self._auth_fname,
e,
)
except (FileNotFoundError, PermissionError, OSError) as e:
_LOGGER.error("Error opening file %s: %s", self._auth_fname, e)

_LOGGER.info("Got new authorization token")
self._authorization = f"{response['token_type']} {response['access_token']}"

_LOGGER.info("TionClient: got new authorization token")

for coro in self._auth_update_listeners:
coro(
username=self._email,
password=self._password,
scan_interval=self._min_update_interval,
auth=self._authorization,
)

return True

_LOGGER.error(
"Response while getting token: status code: %s, content:\n%s",
"TionClient: response while getting token: status code: %s, content:\n%s",
response.status_code,
response.json(),
)
Expand All @@ -227,7 +230,9 @@ def get_location_data(self, force=False) -> bool:
timeout=10,
)
except requests.exceptions.RequestException as e:
_LOGGER.error("Request exception while getting location data:\n%s", e)
_LOGGER.error(
"TionClient: request exception while getting location data:\n%s", e
)
return False

if response.status_code == 200:
Expand All @@ -236,14 +241,14 @@ def get_location_data(self, force=False) -> bool:
return True

if response.status_code == 401:
_LOGGER.info("Need to get new authorization")
_LOGGER.info("TionClient: need to get new authorization")
if self._get_authorization():
return self.get_location_data(force=True)

_LOGGER.error("Authorization failed!")
_LOGGER.error("TionClient: authorization failed!")
else:
_LOGGER.error(
"Response while getting location data: status code: %s, content:\n%s",
"TionClient: response while getting location data: status code: %s, content:\n%s",
response.status_code,
response.json(),
)
Expand Down Expand Up @@ -301,6 +306,7 @@ def send_breezer(
gate: int | None = None,
):
"""Send new breezer data to API."""
url = f"{self._API_ENDPOINT}{self._DEVICE_URL}/{guid}/mode"
data = {
"is_on": is_on,
"heater_enabled": heater_enabled,
Expand All @@ -312,53 +318,59 @@ def send_breezer(
"gate": gate,
}

url = f"https://api2.magicair.tion.ru/device/{guid}/mode"
try:
response = requests.post(url, json=data, headers=self.headers, timeout=10)
except requests.exceptions.RequestException as e:
_LOGGER.error("Exception while sending new breezer data!:\n%s", e)
return False
response = response.json()
status = response["status"]
if status != "queued":
_LOGGER.error(
"TionApi parameters set %s: %s", status, response["description"]
)
return False

return self._wait_for_task(response["task_id"])
return self._send(url, data)

def send_zone(self, guid: str, mode: str, co2: int):
"""Send new zone data to API."""
url = f"{self._API_ENDPOINT}{self._ZONE_URL}/{guid}/mode"
data = {
"mode": mode,
"co2": co2,
}

url = f"https://api2.magicair.tion.ru/zone/{guid}/mode"
return self._send(url, data)

def _send(self, url: str, data: dict[str, Any]):
try:
response = requests.post(url, json=data, headers=self.headers, timeout=10)
response = requests.post(
url=url,
json=data,
headers=self.headers,
timeout=10,
)
except requests.exceptions.RequestException as e:
_LOGGER.error("Exception while sending new zone data!:\n%s", e)
_LOGGER.error(
"TionClient: request exception while sending new data:\n%s", e
)
return False

response = response.json()
status = response["status"]
if status != "queued":
_LOGGER.info("TionApi auto set %s: %s", status, response["description"])
if response["status"] != "queued":
_LOGGER.error(
"TionClient: parameters set %s: %s",
response["status"],
response["description"],
)
return False

return self._wait_for_task(response["task_id"])

def _wait_for_task(self, task_id: str, max_time: int = 5) -> bool:
"""Wait for task with defined task_id been completed."""
url = "https://api2.magicair.tion.ru/task/" + task_id
DELAY = 0.5
for _ in range(int(max_time / DELAY)):
try:
response = requests.get(url, headers=self.headers, timeout=max_time)
response = requests.get(
url=f"{self._API_ENDPOINT}{self._TASK_URL}/{task_id}",
headers=self.headers,
timeout=max_time,
)
except requests.exceptions.RequestException as e:
_LOGGER.error("Exception in wait_for_task:\n%s", e)
_LOGGER.error(
"TionClient: request exception while waiting for a task:\n%s", e
)
return False

if response.status_code == 200:
if response.json()["status"] == "completed":
self.get_location_data(force=True)
Expand All @@ -367,12 +379,14 @@ def _wait_for_task(self, task_id: str, max_time: int = 5) -> bool:
sleep(DELAY)
else:
_LOGGER.warning(
"Bad response code %s in wait_for_task, content:\n%s",
"TionClient: иad response code %s while waiting for a task, content:\n%s",
response.status_code,
response.text,
)
return False

_LOGGER.warning(
"Couldn't get completed status for %s sec in wait_for_task", response.text
"TionClient: сouldn't get completed status for %s sec while waiting for a task",
response.text,
)
return False
7 changes: 5 additions & 2 deletions custom_components/tion/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from homeassistant.helpers.device_registry import DeviceInfo

from .client import TionClient, TionZoneDevice
from .const import DOMAIN, MODELS_SUPPORTED, SwingMode, TionDeviceType
from .const import DOMAIN, SwingMode, TionDeviceType

_LOGGER = logging.getLogger(__name__)

Expand All @@ -41,7 +41,10 @@ async def async_setup_entry(
entities = []
devices = await hass.async_add_executor_job(tion_api.get_devices)
for device in devices:
if device.valid and device.type in MODELS_SUPPORTED:
if device.valid and device.type in [
TionDeviceType.BREEZER_3S,
TionDeviceType.BREEZER_4S,
]:
entities.append(TionClimate(tion_api, device))

else:
Expand Down
Loading

0 comments on commit b4e2a21

Please sign in to comment.