File: mydevolo.py

package info (click to toggle)
devolo-home-control-api 0.19.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 804 kB
  • sloc: python: 3,167; makefile: 3
file content (168 lines) | stat: -rw-r--r-- 6,735 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""my devolo."""
from __future__ import annotations

import logging
from functools import lru_cache
from http import HTTPStatus
from typing import Any

import requests

from . import __version__
from .exceptions.gateway import GatewayOfflineError
from .exceptions.general import WrongCredentialsError, WrongUrlError


class Mydevolo:
    """
    The Mydevolo object handles calls to the my devolo API v1. It does not cover all API calls, just those requested up to now.
    All calls are done in a user context, so you need to provide credentials of that user.
    """

    def __init__(self) -> None:
        """Initialize my devolo communication."""
        self._logger = logging.getLogger(self.__class__.__name__)
        self._user = ""
        self._password = ""

        self.url = "https://www.mydevolo.com"

    @property
    def user(self) -> str:
        """The user (also known as my devolo ID) is used for basic authentication."""
        return self._user

    @user.setter
    def user(self, user: str) -> None:
        """Invalidate uuid and gateway IDs on user name change."""
        self._user = user
        self.uuid.cache_clear()

    @property
    def password(self) -> str:
        """The password is used for basic authentication."""
        return self._password

    @password.setter
    def password(self, password: str) -> None:
        """Invalidate uuid and gateway IDs on password change."""
        self._password = password
        self.uuid.cache_clear()

    def credentials_valid(self) -> bool:
        """
        Check if current credentials are valid. This is done by trying to get the UUID. If that fails, credentials must be
        wrong. If it succeeds, we can reuse the UUID for later usages.
        """
        try:
            self.uuid()
            return True
        except WrongCredentialsError:
            return False

    def get_gateway_ids(self) -> list[str]:
        """Get all gateway IDs attached to current account."""
        self._logger.debug("Getting list of gateways")
        items = self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/status")["items"]
        gateway_ids = [gateway["gatewayId"] for gateway in items]
        if not gateway_ids:
            self._logger.error("Could not get gateway list. No gateway attached to account?")
            raise IndexError("No gateways found.")  # noqa: TRY003
        return gateway_ids

    def get_gateway(self, gateway_id: str) -> dict[str, Any]:
        """
        Get gateway details like name, local passkey and other.

        :param gateway_id: Gateway ID
        :return: Gateway details
        """
        self._logger.debug("Getting details for gateway %s", gateway_id)
        try:
            details = self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/{gateway_id}")
        except WrongUrlError:
            self._logger.error("Could not get full URL. Wrong gateway ID used?")
            raise
        details["location"] = self._call(details["location"]) if details["location"] else {}
        return details

    def get_full_url(self, gateway_id: str) -> str:
        """
        Get gateway's portal URL.

        :param gateway_id: Gateway ID
        :return: URL to access the gateway's portal.
        """
        self._logger.debug("Getting full URL of gateway.")
        return self._call(f"{self.url}/v1/users/{self.uuid()}/hc/gateways/{gateway_id}/fullURL")["url"]

    def get_timezone(self) -> str:
        """
        Get user's standard timezone.

        :return: Standard timezone of the user based on his country settings.
        """
        self._logger.debug("Getting the user's standard timezone.")
        return self._call(f"{self.url}/v1/users/{self.uuid()}/standardTimezone")["timezone"]

    def get_zwave_products(self, manufacturer: str, product_type: str, product: str) -> dict[str, Any]:
        """
        Get information about a Z-Wave device.

        :param manufacturer: The manufacturer ID in hex.
        :param product_type: The product type ID in hex.
        :param product: The product ID in hex.
        :return: All known product information.
        """
        self._logger.debug("Getting information for %s/%s/%s", manufacturer, product_type, product)
        try:
            device_info = self._call(f"{self.url}/v1/zwave/products/{manufacturer}/{product_type}/{product}")
        except WrongUrlError:
            # At some devices no device information are returned
            self._logger.debug("No device info found")
            device_info = {
                "brand": "devolo" if manufacturer == "0x0175" else "Unknown",
                "deviceType": "Unknown",
                "genericDeviceClass": "Unknown",
                "href": f"{self.url}/v1/zwave/products/{manufacturer}/{product_type}/{product}",
                "identifier": "Unknown",
                "isZWavePlus": False,
                "manufacturerId": manufacturer,
                "name": "Unknown",
                "productId": product,
                "productTypeId": product_type,
                "specificDeviceClass": "Unknown",
                "zwaveVersion": "Unknown",
            }
        return device_info

    def maintenance(self) -> bool:
        """If devolo Home Control is in maintenance, there is not much we can do via cloud."""
        state = self._call(f"{self.url}/v1/hc/maintenance")["state"]
        if state == "on":
            return False
        self._logger.debug("devolo Home Control is in maintenance mode.")
        return True

    @lru_cache(maxsize=1)  # noqa: B019
    def uuid(self) -> str:
        """Get the uuid. The uuid is a central attribute in my devolo. Most URLs in the user's context contain it."""
        self._logger.debug("Getting UUID")
        return self._call(f"{self.url.rstrip('/')}/v1/users/uuid")["uuid"]

    def _call(self, url: str) -> dict[str, Any]:
        """Make a call to any entry point with the user's context."""
        headers = {"content-type": "application/json", "User-Agent": f"devolo_home_control_api/{__version__}"}
        responds = requests.get(url, auth=(self._user, self._password), headers=headers, timeout=60)

        if responds.status_code == HTTPStatus.FORBIDDEN:
            self._logger.error("Could not get full URL. Wrong username or password?")
            raise WrongCredentialsError
        if responds.status_code == HTTPStatus.NOT_FOUND:
            raise WrongUrlError(url)
        if responds.status_code == HTTPStatus.SERVICE_UNAVAILABLE:
            # mydevolo sends a 503, if the gateway is offline
            self._logger.debug("The requested gateway seems to be offline.")
            raise GatewayOfflineError

        return responds.json()