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

Add all changes for 1.0.0 to master #148

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
12 changes: 9 additions & 3 deletions PyTado/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,23 @@ def main():
show_config_parser = subparsers.add_parser("get_me", help="Get home information.")
show_config_parser.set_defaults(func=get_me)

start_activity_parser = subparsers.add_parser("get_state", help="Get state of zone.")
start_activity_parser = subparsers.add_parser(
"get_state", help="Get state of zone."
)
start_activity_parser.add_argument("--zone", help="Zone to get the state of.")
start_activity_parser.set_defaults(func=get_state)

start_activity_parser = subparsers.add_parser("get_states", help="Get states of all zones.")
start_activity_parser = subparsers.add_parser(
"get_states", help="Get states of all zones."
)
start_activity_parser.set_defaults(func=get_states)

start_activity_parser = subparsers.add_parser(
"get_capabilities", help="Get capabilities of zone."
)
start_activity_parser.add_argument("--zone", help="Zone to get the capabilities of.")
start_activity_parser.add_argument(
"--zone", help="Zone to get the capabilities of."
)
start_activity_parser.set_defaults(func=get_capabilities)

args = parser.parse_args()
Expand Down
94 changes: 25 additions & 69 deletions PyTado/const.py
Original file line number Diff line number Diff line change
@@ -1,53 +1,22 @@
"""Constant values for the Tado component."""

# Api credentials
import enum

from PyTado.types import HvacMode

CLIENT_ID = "tado-web-app" # nosec B105
CLIENT_SECRET = "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc" # nosec B105

# Types
TYPE_AIR_CONDITIONING = "AIR_CONDITIONING"
TYPE_HEATING = "HEATING"
TYPE_HOT_WATER = "HOT_WATER"

# Base modes
CONST_MODE_OFF = "OFF"
CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule
CONST_MODE_AUTO = "AUTO"
CONST_MODE_COOL = "COOL"
CONST_MODE_HEAT = "HEAT"
CONST_MODE_DRY = "DRY"
CONST_MODE_FAN = "FAN"

CONST_LINK_OFFLINE = "OFFLINE"
CONST_CONNECTION_OFFLINE = "OFFLINE"

CONST_FAN_OFF = "OFF"
CONST_FAN_AUTO = "AUTO"
CONST_FAN_LOW = "LOW"
CONST_FAN_MIDDLE = "MIDDLE"
CONST_FAN_HIGH = "HIGH"

CONST_FAN_SPEED_OFF = "OFF"
CONST_FAN_SPEED_AUTO = "AUTO"
CONST_FAN_SPEED_SILENT = "SILENT"
CONST_FAN_SPEED_LEVEL1 = "LEVEL1"
CONST_FAN_SPEED_LEVEL2 = "LEVEL1"
CONST_FAN_SPEED_LEVEL3 = "LEVEL3"
CONST_FAN_SPEED_LEVEL4 = "LEVEL4"

CONST_VERTICAL_SWING_OFF = "OFF"
CONST_VERTICAL_SWING_ON = "ON"

CONST_HORIZONTAL_SWING_OFF = "OFF"
CONST_HORIZONTAL_SWING_ON = "ON"
CONST_HORIZONTAL_SWING_LEFT = "LEFT"
CONST_HORIZONTAL_SWING_MID_LEFT = "MID_LEFT"
CONST_HORIZONTAL_SWING_MID = "MID"
CONST_HORIZONTAL_SWING_MID_RIGHT = "MID_RIGHT"
CONST_HORIZONTAL_SWING_RIGHT = "RIGHT"

# When we change the temperature setting, we need an overlay mode
CONST_OVERLAY_TADO_MODE = "NEXT_TIME_BLOCK" # wait until tado changes the mode automatic
CONST_OVERLAY_TADO_MODE = (
"NEXT_TIME_BLOCK" # wait until tado changes the mode automatic
)
CONST_OVERLAY_MANUAL = "MANUAL" # the user has changed the temperature or mode manually
CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan

Expand All @@ -56,45 +25,32 @@
# it.
# Heat is preferred as it generally has a lower minimum temperature
ORDERED_KNOWN_TADO_MODES = [
CONST_MODE_HEAT,
CONST_MODE_COOL,
CONST_MODE_AUTO,
CONST_MODE_DRY,
CONST_MODE_FAN,
HvacMode.HEAT,
HvacMode.COOL,
HvacMode.AUTO,
HvacMode.DRY,
HvacMode.FAN,
]

CONST_HVAC_HEAT = "HEATING"
CONST_HVAC_DRY = "DRYING"
CONST_HVAC_FAN = "FAN"
CONST_HVAC_COOL = "COOLING"
CONST_HVAC_IDLE = "IDLE"
CONST_HVAC_OFF = "OFF"
CONST_HVAC_HOT_WATER = TYPE_HOT_WATER

TADO_MODES_TO_HVAC_ACTION = {
CONST_MODE_HEAT: CONST_HVAC_HEAT,
CONST_MODE_DRY: CONST_HVAC_DRY,
CONST_MODE_FAN: CONST_HVAC_FAN,
CONST_MODE_COOL: CONST_HVAC_COOL,
}

TADO_HVAC_ACTION_TO_MODES = {
CONST_HVAC_HEAT: CONST_MODE_HEAT,
CONST_HVAC_HOT_WATER: CONST_HVAC_HEAT,
CONST_HVAC_DRY: CONST_MODE_DRY,
CONST_HVAC_FAN: CONST_MODE_FAN,
CONST_HVAC_COOL: CONST_MODE_COOL,
}

# These modes will not allow a temp to be set
TADO_MODES_WITH_NO_TEMP_SETTING = [
CONST_MODE_AUTO,
CONST_MODE_DRY,
CONST_MODE_FAN,
HvacMode.AUTO,
HvacMode.DRY,
HvacMode.FAN,
]

DEFAULT_TADO_PRECISION = 0.1
DEFAULT_TADOX_PRECISION = 0.01
DEFAULT_DATE_FORMAT = "%Y-%m-%d"

HOME_DOMAIN = "homes"
DEVICE_DOMAIN = "devices"

HTTP_CODES_OK = [200, 201, 202, 204]


class Unit(enum.Enum):
"""unit Enum"""

M3 = "m3"
KWH = "kWh"
125 changes: 71 additions & 54 deletions PyTado/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
import json
import logging
import pprint
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any
from urllib.parse import urlencode

import requests

from PyTado.const import CLIENT_ID, CLIENT_SECRET
from PyTado.const import CLIENT_ID, CLIENT_SECRET, HTTP_CODES_OK
from PyTado.exceptions import TadoException, TadoWrongCredentialsException
from PyTado.logger import Logger

Expand Down Expand Up @@ -56,57 +57,31 @@ class Mode(enum.Enum):
PLAIN = 2


@dataclass
class TadoRequest:
"""Data Container for my.tado.com API Requests"""

def __init__(
self,
endpoint: Endpoint = Endpoint.MY_API,
command: str | None = None,
action: Action | str = Action.GET,
payload: dict[str, Any] | None = None,
domain: Domain = Domain.HOME,
device: int | str | None = None,
mode: Mode = Mode.OBJECT,
params: dict[str, Any] | None = None,
) -> None:
self.endpoint = endpoint
self.command = command
self.action = action
self.payload = payload
self.domain = domain
self.device = device
self.mode = mode
self.params = params
endpoint: Endpoint = Endpoint.MY_API
command: str | None = None
action: Action | str = Action.GET
payload: dict[str, Any] | list[Any] | None = None
domain: Domain = Domain.HOME
device: int | str | None = None
mode: Mode = Mode.OBJECT
params: dict[str, Any] | None = None


@dataclass
class TadoXRequest(TadoRequest):
"""Data Container for hops.tado.com (Tado X) API Requests"""

def __init__(
self,
endpoint: Endpoint = Endpoint.HOPS_API,
command: str | None = None,
action: Action | str = Action.GET,
payload: dict[str, Any] | None = None,
domain: Domain = Domain.HOME,
device: int | str | None = None,
mode: Mode = Mode.OBJECT,
params: dict[str, Any] | None = None,
) -> None:
super().__init__(
endpoint=endpoint,
command=command,
action=action,
payload=payload,
domain=domain,
device=device,
mode=mode,
params=params,
)
self._action = action
endpoint: Endpoint = Endpoint.HOPS_API
_action: Action | str = Action.GET

@property
def __post_init__(self) -> None:
self._action = self.action

@property # type: ignore
def action(self) -> Action | str:
"""Get request action for Tado X"""
if self._action == Action.CHANGE:
Expand All @@ -133,6 +108,15 @@ class TadoResponse:
class Http:
"""API Request Class"""

_refresh_at: datetime
_session: requests.Session
_headers: dict[str, str]
_username: str
_password: str
_id: int
_token_refresh: str
_x_api: bool

def __init__(
self,
username: str,
Expand All @@ -159,7 +143,9 @@ def __init__(
def is_x_line(self) -> bool:
return self._x_api

def _log_response(self, response: requests.Response, *args, **kwargs) -> None:
def _log_response(
self, response: requests.Response, *args: Any, **kwargs: Any
) -> None:
og_request_method = response.request.method
og_request_url = response.request.url
og_request_headers = response.request.headers
Expand All @@ -178,15 +164,17 @@ def _log_response(self, response: requests.Response, *args, **kwargs) -> None:
f"\n\tData: {response_data}"
)

def request(self, request: TadoRequest) -> dict[str, Any]:
def request(self, request: TadoRequest) -> dict[str, Any] | list[Any]:
"""Request something from the API with a TadoRequest"""
self._refresh_token()

headers = self._headers
data = self._configure_payload(headers, request)
url = self._configure_url(request)

http_request = requests.Request(method=request.action, url=url, headers=headers, data=data)
http_request = requests.Request(
method=request.action, url=url, headers=headers, data=data
)
prepped = http_request.prepare()
prepped.hooks["response"].append(self._log_response)

Expand Down Expand Up @@ -217,13 +205,32 @@ def request(self, request: TadoRequest) -> dict[str, Any]:
if response.text is None or response.text == "":
return {}

return response.json()
if response.status_code not in HTTP_CODES_OK:
_LOGGER.error(
"Request %s failed with status code %d: %s",
url,
response.status_code,
response.json(),
)
raise TadoException(
f"Request failed with status code {response.status_code}"
)

response_json = response.json()
if isinstance(response_json, dict) or isinstance(response_json, list):
return response_json
else:
raise TadoException("Unexpected response type")

def _configure_url(self, request: TadoRequest) -> str:
if request.endpoint == Endpoint.MOBILE:
url = f"{request.endpoint}{request.command}"
elif request.domain == Domain.DEVICES or request.domain == Domain.HOME_BY_BRIDGE:
url = f"{request.endpoint}{request.domain}/{request.device}/{request.command}"
elif (
request.domain == Domain.DEVICES or request.domain == Domain.HOME_BY_BRIDGE
):
url = (
f"{request.endpoint}{request.domain}/{request.device}/{request.command}"
)
elif request.domain == Domain.ME:
url = f"{request.endpoint}{request.domain}"
else:
Expand All @@ -235,7 +242,9 @@ def _configure_url(self, request: TadoRequest) -> str:

return url

def _configure_payload(self, headers: dict[str, str], request: TadoRequest) -> bytes:
def _configure_payload(
self, headers: dict[str, str], request: TadoRequest
) -> bytes:
if request.payload is None:
return b""

Expand All @@ -261,7 +270,7 @@ def _set_oauth_header(self, data: dict[str, Any]) -> str:
self._refresh_at = self._refresh_at - timedelta(seconds=30)

self._headers["Authorization"] = f"Bearer {access_token}"
return refresh_token
return str(refresh_token)

def _refresh_token(self) -> None:
"""Refresh the token if it is about to expire"""
Expand Down Expand Up @@ -351,11 +360,16 @@ def _get_id(self) -> int:
request.action = Action.GET
request.domain = Domain.ME

homes_ = self.request(request)["homes"]
response = self.request(request)

if not isinstance(response, dict):
raise TadoException("Unexpected response type")

return homes_[0]["id"]
homes_ = response["homes"]

def _check_x_line_generation(self):
return int(homes_[0]["id"])

def _check_x_line_generation(self) -> bool:
# get home info
request = TadoRequest()
request.action = Action.GET
Expand All @@ -364,4 +378,7 @@ def _check_x_line_generation(self):

home_ = self.request(request)

if not isinstance(home_, dict):
raise TadoException("Unexpected response type")

return "generation" in home_ and home_["generation"] == "LINE_X"
3 changes: 2 additions & 1 deletion PyTado/interface/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Module for all API interfaces."""

from .base_tado import TadoBase
from .hops_tado import TadoX
from .my_tado import Tado

__all__ = ["Tado", "TadoX"]
__all__ = ["Tado", "TadoX", "TadoBase"]
Loading
Loading