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 ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ oocone
3
+
4
+ Unofficial library for interacting with an enocoo energy management system
5
+ :author: Simon Leiner <simon@leiner.me>
6
+ :license: ISC
7
+ """
8
+
9
+ from .enocoo import Auth, Enocoo
10
+
11
+ __all__ = ["Auth", "Enocoo"]
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from pathlib import Path
7
+
8
+
9
+ def func(path: Path) -> str:
10
+ return str(path)
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
@@ -0,0 +1,10 @@
1
+ class OoconeError(Exception):
2
+ """Base class for oocone."""
3
+
4
+
5
+ class AuthenticationFailed(OoconeError):
6
+ """Authentication has failed."""
7
+
8
+
9
+ class UnexpectedResponse(OoconeError):
10
+ """Got unexpected data back from the enocoo website"""
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
+ [![Tests](https://github.com/sleiner/oocone/workflows/Tests/badge.svg)](https://github.com/sleiner/oocone/actions?query=workflow%3Aci)
19
+ [![pypi version](https://img.shields.io/pypi/v/oocone.svg)](https://pypi.org/project/oocone/)
20
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://astral.sh/ruff)
21
+ [![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: pdm-backend (2.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.