oocone 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oocone/__init__.py +11 -0
- oocone/_internal/__init__.py +10 -0
- oocone/_internal/api_types.py +0 -0
- oocone/enocoo.py +136 -0
- oocone/errors.py +10 -0
- oocone/py.typed +0 -0
- oocone/types.py +20 -0
- oocone-0.1.dist-info/METADATA +27 -0
- oocone-0.1.dist-info/RECORD +11 -0
- oocone-0.1.dist-info/WHEEL +4 -0
- oocone-0.1.dist-info/licenses/LICENSE +15 -0
oocone/__init__.py
ADDED
|
File without changes
|
oocone/enocoo.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
from oocone import errors
|
|
10
|
+
from oocone.types import UNKNOWN, TrafficLightColor, TrafficLightStatus
|
|
11
|
+
|
|
12
|
+
ROUTE_LOGIN = "/signinForm.php?mode=ok"
|
|
13
|
+
BEAUTIFULSOUP_PARSER = "html.parser"
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Auth:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
*,
|
|
22
|
+
websession: aiohttp.ClientSession | None = None,
|
|
23
|
+
base_url: str,
|
|
24
|
+
username: str,
|
|
25
|
+
password: str,
|
|
26
|
+
):
|
|
27
|
+
self._base_url = base_url.rstrip("/")
|
|
28
|
+
self.__username = username
|
|
29
|
+
self.__password = password
|
|
30
|
+
self._session = websession or aiohttp.ClientSession()
|
|
31
|
+
self._logged_in = False
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def response_indicates_not_logged_in(response: BeautifulSoup) -> bool:
|
|
35
|
+
return response.find("input", {"type": "password"}) is not None
|
|
36
|
+
|
|
37
|
+
async def _login(self) -> None:
|
|
38
|
+
async with self._session.post(
|
|
39
|
+
self._base_url + ROUTE_LOGIN,
|
|
40
|
+
data={"user": self.__username, "passwort": self.__password},
|
|
41
|
+
) as response:
|
|
42
|
+
response_text = await response.text()
|
|
43
|
+
|
|
44
|
+
soup = BeautifulSoup(response_text, features=BEAUTIFULSOUP_PARSER)
|
|
45
|
+
if self.response_indicates_not_logged_in(soup):
|
|
46
|
+
raise errors.AuthenticationFailed
|
|
47
|
+
|
|
48
|
+
async def _request(
|
|
49
|
+
self, method: str, path: str, retry_with_login: bool = True, **kwargs
|
|
50
|
+
) -> (aiohttp.ClientResponse, BeautifulSoup):
|
|
51
|
+
"""Make a request."""
|
|
52
|
+
|
|
53
|
+
response = await self._session.request(
|
|
54
|
+
method,
|
|
55
|
+
f"{self._base_url}/{path}",
|
|
56
|
+
**kwargs,
|
|
57
|
+
)
|
|
58
|
+
soup = BeautifulSoup(await response.text(), BEAUTIFULSOUP_PARSER)
|
|
59
|
+
|
|
60
|
+
if self.response_indicates_not_logged_in(soup):
|
|
61
|
+
if retry_with_login:
|
|
62
|
+
self._login()
|
|
63
|
+
return await self._request(method=method, retry_with_login=False, **kwargs)
|
|
64
|
+
else:
|
|
65
|
+
raise errors.AuthenticationFailed()
|
|
66
|
+
else:
|
|
67
|
+
return (response, soup)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Enocoo:
|
|
71
|
+
"""Provides access to the data accessible via the enocoo Web interface"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, auth: Auth):
|
|
74
|
+
"""Initialize the API and store the auth so we can make requests."""
|
|
75
|
+
self.auth = auth
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def __extract_key_from_response(response_data: dict[str, Any], key: str) -> Any:
|
|
79
|
+
try:
|
|
80
|
+
result = response_data[key]
|
|
81
|
+
except KeyError:
|
|
82
|
+
raise KeyError(
|
|
83
|
+
f'API response does not contain key "{key}".\n'
|
|
84
|
+
f"Response data:\n"
|
|
85
|
+
f"{response_data}"
|
|
86
|
+
) from None
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
async def get_traffic_light_status(self):
|
|
91
|
+
response, _ = await self.auth._request("GET", "php/getTrafficLightStatus.php")
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
# We parse the response as JSON, even though the Content-Type header might indicate
|
|
95
|
+
# otherwise.
|
|
96
|
+
response_data = await response.json(content_type=None)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
raise errors.UnexpectedResponse() from e
|
|
99
|
+
|
|
100
|
+
def parse_color(response_data: dict) -> TrafficLightColor | Literal[UNKNOWN]:
|
|
101
|
+
try:
|
|
102
|
+
raw = self.__extract_key_from_response(response_data, "color")
|
|
103
|
+
except KeyError as e:
|
|
104
|
+
logger.warning(e)
|
|
105
|
+
return UNKNOWN
|
|
106
|
+
|
|
107
|
+
if raw == "rot":
|
|
108
|
+
return TrafficLightColor.RED
|
|
109
|
+
if raw == "gelb":
|
|
110
|
+
return TrafficLightColor.YELLOW
|
|
111
|
+
if raw == r"grün":
|
|
112
|
+
return TrafficLightColor.GREEN
|
|
113
|
+
|
|
114
|
+
logger.warning(f'Got unexpected color: "{raw}"')
|
|
115
|
+
return UNKNOWN
|
|
116
|
+
|
|
117
|
+
def parse_current_energy_price(response_data: dict) -> float | Literal[UNKNOWN]:
|
|
118
|
+
try:
|
|
119
|
+
raw = self.__extract_key_from_response(response_data, "currentEnergyprice")
|
|
120
|
+
except KeyError as e:
|
|
121
|
+
logger.warning(e)
|
|
122
|
+
return UNKNOWN
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
result = float(raw)
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.warning("Could not parse energy price %s as a number")
|
|
128
|
+
return UNKNOWN
|
|
129
|
+
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
result = TrafficLightStatus(
|
|
133
|
+
color=parse_color(response_data),
|
|
134
|
+
current_energy_price=parse_current_energy_price(response_data),
|
|
135
|
+
)
|
|
136
|
+
return result
|
oocone/errors.py
ADDED
oocone/py.typed
ADDED
|
File without changes
|
oocone/types.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Types for library users"""
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TrafficLightColor(enum.StrEnum):
|
|
9
|
+
RED = enum.auto()
|
|
10
|
+
YELLOW = enum.auto()
|
|
11
|
+
GREEN = enum.auto()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
UNKNOWN = "UNKNOWN"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TrafficLightStatus:
|
|
19
|
+
color: TrafficLightColor | Literal[UNKNOWN]
|
|
20
|
+
current_energy_price: float | Literal[UNKNOWN]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: oocone
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Unofficial library for interacting with an enocoo energy management system
|
|
5
|
+
Author-Email: Simon Leiner <simon@leiner.me>
|
|
6
|
+
License: ISC
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Project-URL: Repository, https://github.com/sleiner/oocone
|
|
10
|
+
Project-URL: Homepage, https://github.com/sleiner/oocone
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: aiohttp>=3.9.4
|
|
13
|
+
Requires-Dist: beautifulsoup4>=4.12.3
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# oocone
|
|
17
|
+
|
|
18
|
+
[](https://github.com/sleiner/oocone/actions?query=workflow%3Aci)
|
|
19
|
+
[](https://pypi.org/project/oocone/)
|
|
20
|
+
[](https://astral.sh/ruff)
|
|
21
|
+
[](https://pdm.fming.dev)
|
|
22
|
+
|
|
23
|
+
Unofficial library for interacting with an enocoo energy management system
|
|
24
|
+
|
|
25
|
+
## Requirements
|
|
26
|
+
|
|
27
|
+
oocone requires Python >= 3.11.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
oocone-0.1.dist-info/METADATA,sha256=LJCfIGE_R7PGIBNcs-VRyV86VdVy7aFFcRB8vzSiWVM,1094
|
|
2
|
+
oocone-0.1.dist-info/WHEEL,sha256=SOP-4bEE0jbVaCHQGVvF08uWxk5rcSsfEybvoQVHlD8,90
|
|
3
|
+
oocone-0.1.dist-info/licenses/LICENSE,sha256=kyCZ7TTrgf3ZCI2_jqFIpG-t-NEWdnFeZ5Z6hUNLD4w,736
|
|
4
|
+
oocone/__init__.py,sha256=B0DC-jVCF3fy59FfQ7l86S6_-iWH3iXqxmU76hSEyKk,209
|
|
5
|
+
oocone/_internal/__init__.py,sha256=iQJYMCGQBBcftPVBoluljC3g-gFEUh7k5Zdx7VXRpwE,169
|
|
6
|
+
oocone/_internal/api_types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
oocone/enocoo.py,sha256=RD9XfRbgkJJOqTzHgGs2LCv_KyHwwI6-mLx6j850L4A,4452
|
|
8
|
+
oocone/errors.py,sha256=2cQvR5JIGgLC5Z8jGT4VeZt1f9Yxx708TvLW5Uc84nI,243
|
|
9
|
+
oocone/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
oocone/types.py,sha256=rlMCRC9T_AXROvaK40R74uCePI4OE4kaSvIqhDaIeU4,376
|
|
11
|
+
oocone-0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024, Simon Leiner
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|