oocone 0.1__tar.gz
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-0.1/LICENSE +15 -0
- oocone-0.1/PKG-INFO +27 -0
- oocone-0.1/README.md +12 -0
- oocone-0.1/pyproject.toml +136 -0
- oocone-0.1/src/oocone/__init__.py +11 -0
- oocone-0.1/src/oocone/_internal/__init__.py +10 -0
- oocone-0.1/src/oocone/_internal/api_types.py +0 -0
- oocone-0.1/src/oocone/enocoo.py +136 -0
- oocone-0.1/src/oocone/errors.py +10 -0
- oocone-0.1/src/oocone/py.typed +0 -0
- oocone-0.1/src/oocone/types.py +20 -0
- oocone-0.1/tests/__init__.py +6 -0
- oocone-0.1/tests/conftest.py +79 -0
- oocone-0.1/tests/data/responses/getTrafficLightStatus.php +4 -0
- oocone-0.1/tests/data/responses/newMeterTable.notLoggedIn.php +23 -0
- oocone-0.1/tests/data/responses/newMeterTable.php +4 -0
- oocone-0.1/tests/data/responses/signinForm.failure.php +103 -0
- oocone-0.1/tests/data/responses/signinForm.success.php +80 -0
- oocone-0.1/tests/test_auth.py +29 -0
- oocone-0.1/tests/test_enocoo.py +13 -0
oocone-0.1/LICENSE
ADDED
|
@@ -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.
|
oocone-0.1/PKG-INFO
ADDED
|
@@ -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.
|
oocone-0.1/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# oocone
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sleiner/oocone/actions?query=workflow%3Aci)
|
|
4
|
+
[](https://pypi.org/project/oocone/)
|
|
5
|
+
[](https://astral.sh/ruff)
|
|
6
|
+
[](https://pdm.fming.dev)
|
|
7
|
+
|
|
8
|
+
Unofficial library for interacting with an enocoo energy management system
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
oocone requires Python >= 3.11.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "oocone"
|
|
3
|
+
description = "Unofficial library for interacting with an enocoo energy management system"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Simon Leiner", email = "simon@leiner.me" },
|
|
6
|
+
]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"aiohttp>=3.9.4",
|
|
11
|
+
"beautifulsoup4>=4.12.3",
|
|
12
|
+
]
|
|
13
|
+
dynamic = []
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
]
|
|
18
|
+
version = "0.1"
|
|
19
|
+
|
|
20
|
+
[project.license]
|
|
21
|
+
text = "ISC"
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Repository = "https://github.com/sleiner/oocone"
|
|
25
|
+
Homepage = "https://github.com/sleiner/oocone"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = [
|
|
29
|
+
"pdm-backend",
|
|
30
|
+
]
|
|
31
|
+
build-backend = "pdm.backend"
|
|
32
|
+
|
|
33
|
+
[tool.pdm.version]
|
|
34
|
+
source = "scm"
|
|
35
|
+
|
|
36
|
+
[tool.pdm.build]
|
|
37
|
+
package-dir = "src"
|
|
38
|
+
|
|
39
|
+
[tool.pdm.dev-dependencies]
|
|
40
|
+
test = [
|
|
41
|
+
"pytest>=8.1.1",
|
|
42
|
+
"pytest-aiohttp>=1.0.5",
|
|
43
|
+
"pytest-asyncio>=0.23.6",
|
|
44
|
+
"pytest-cov>=5.0.0",
|
|
45
|
+
"typeguard>=4.2.1",
|
|
46
|
+
]
|
|
47
|
+
dev = [
|
|
48
|
+
"towncrier>=23.11.0",
|
|
49
|
+
"parver>=0.5",
|
|
50
|
+
]
|
|
51
|
+
doc = [
|
|
52
|
+
"mkdocs>=1.5.3",
|
|
53
|
+
"mkdocs-material>=9.5.17",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.pdm.scripts]
|
|
57
|
+
release = "python -m tasks.release"
|
|
58
|
+
update_api_responses = "python -m tasks.update_api_responses"
|
|
59
|
+
test = "pytest tests/"
|
|
60
|
+
lint = "pre-commit run --all-files"
|
|
61
|
+
|
|
62
|
+
[tool.pdm.scripts.doc]
|
|
63
|
+
shell = "cd docs && mkdocs serve"
|
|
64
|
+
help = "Start the dev server for doc preview"
|
|
65
|
+
|
|
66
|
+
[tool.pytest.ini_options]
|
|
67
|
+
markers = [
|
|
68
|
+
"mocked_api: mark test as using a mock of the enocoo website",
|
|
69
|
+
]
|
|
70
|
+
addopts = "--typeguard-packages=oocone --cov=oocone --cov-report=html"
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
line-length = 100
|
|
74
|
+
src = [
|
|
75
|
+
"src",
|
|
76
|
+
]
|
|
77
|
+
target-version = "py312"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint]
|
|
80
|
+
select = [
|
|
81
|
+
"B",
|
|
82
|
+
"C4",
|
|
83
|
+
"E",
|
|
84
|
+
"F",
|
|
85
|
+
"I001",
|
|
86
|
+
"PGH",
|
|
87
|
+
"RUF",
|
|
88
|
+
"TCH",
|
|
89
|
+
"W",
|
|
90
|
+
"YTT",
|
|
91
|
+
]
|
|
92
|
+
extend-ignore = [
|
|
93
|
+
"B018",
|
|
94
|
+
"B019",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.lint.mccabe]
|
|
98
|
+
max-complexity = 10
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint.isort]
|
|
101
|
+
known-first-party = [
|
|
102
|
+
"oocone",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
[tool.mypy]
|
|
106
|
+
follow_imports = "silent"
|
|
107
|
+
disallow_incomplete_defs = true
|
|
108
|
+
disallow_untyped_defs = true
|
|
109
|
+
disallow_untyped_decorators = true
|
|
110
|
+
namespace_packages = true
|
|
111
|
+
mypy_path = "src"
|
|
112
|
+
explicit_package_bases = true
|
|
113
|
+
|
|
114
|
+
[[tool.mypy.overrides]]
|
|
115
|
+
module = [
|
|
116
|
+
"pytest_aiohttp.plugin",
|
|
117
|
+
]
|
|
118
|
+
ignore_missing_imports = true
|
|
119
|
+
|
|
120
|
+
[tool.towncrier]
|
|
121
|
+
package = "oocone"
|
|
122
|
+
filename = "CHANGELOG.md"
|
|
123
|
+
issue_format = "[#{issue}](https://github.com/sleiner/oocone/issues/{issue})"
|
|
124
|
+
directory = "news/"
|
|
125
|
+
start_string = "<!-- insertion marker -->"
|
|
126
|
+
title_format = "[v{version}](https://github.com/sleiner/oocone/releases/tag/{version}) ({project_date})"
|
|
127
|
+
template = "news/towncrier_template.md"
|
|
128
|
+
underlines = "-~^"
|
|
129
|
+
type = [
|
|
130
|
+
{ directory = "feature", name = "Features & Improvements", showcontent = true },
|
|
131
|
+
{ directory = "bugfix", name = "Bug Fixes", showcontent = true },
|
|
132
|
+
{ directory = "doc", name = "Improved Documentation", showcontent = true },
|
|
133
|
+
{ directory = "dep", name = "Dependencies", showcontent = true },
|
|
134
|
+
{ directory = "removal", name = "Removals and Deprecations", showcontent = true },
|
|
135
|
+
{ directory = "misc", name = "Miscellaneous", showcontent = true },
|
|
136
|
+
]
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|
|
@@ -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,79 @@
|
|
|
1
|
+
from asyncio import AbstractEventLoop
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from aiohttp import web
|
|
6
|
+
from aiohttp.test_utils import TestClient
|
|
7
|
+
from aiohttp.typedefs import Handler
|
|
8
|
+
from pytest_aiohttp.plugin import AiohttpClient
|
|
9
|
+
|
|
10
|
+
from oocone import Auth
|
|
11
|
+
|
|
12
|
+
from . import RESPONSES_DIR
|
|
13
|
+
|
|
14
|
+
pytest_plugins = ("pytest_asyncio",)
|
|
15
|
+
|
|
16
|
+
signed_in = web.AppKey("signed_in", bool)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _signin_form(request: web.Request) -> web.Response:
|
|
20
|
+
response_path = RESPONSES_DIR / "signinForm.failure.php"
|
|
21
|
+
response = web.Response()
|
|
22
|
+
|
|
23
|
+
if request.method == "POST":
|
|
24
|
+
data = await request.post()
|
|
25
|
+
username = data.get("user")
|
|
26
|
+
password = data.get("passwort")
|
|
27
|
+
credentials_correct = username == "correct" and password == "correct"
|
|
28
|
+
|
|
29
|
+
if credentials_correct and request.query.get("mode") == "ok":
|
|
30
|
+
response_path = RESPONSES_DIR / "signinForm.success.php"
|
|
31
|
+
response.set_cookie("logged_in", "true")
|
|
32
|
+
|
|
33
|
+
with open(response_path, "rb") as f:
|
|
34
|
+
response.body = f.read()
|
|
35
|
+
|
|
36
|
+
return response
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _response_from_file(response_path: Path, needs_login: bool = True) -> Handler:
|
|
40
|
+
original_response_path = response_path
|
|
41
|
+
|
|
42
|
+
async def handler(request: web.Request) -> web.Response:
|
|
43
|
+
response = web.Response()
|
|
44
|
+
|
|
45
|
+
if needs_login and request.cookies.get("logged_in") != "true":
|
|
46
|
+
response_path = RESPONSES_DIR / "newMeterTable.notLoggedIn.php"
|
|
47
|
+
else:
|
|
48
|
+
response_path = RESPONSES_DIR / original_response_path
|
|
49
|
+
|
|
50
|
+
with open(response_path, "rb") as f:
|
|
51
|
+
response.body = f.read()
|
|
52
|
+
|
|
53
|
+
return response
|
|
54
|
+
|
|
55
|
+
return handler
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def mock_api(event_loop: AbstractEventLoop, aiohttp_client: AiohttpClient) -> TestClient:
|
|
60
|
+
app = web.Application()
|
|
61
|
+
app[signed_in] = False
|
|
62
|
+
|
|
63
|
+
app.router.add_post("/signinForm.php", _signin_form)
|
|
64
|
+
app.router.add_get(
|
|
65
|
+
"/php/getTrafficLightStatus.php",
|
|
66
|
+
_response_from_file("getTrafficLightStatus.php", needs_login=False),
|
|
67
|
+
)
|
|
68
|
+
return event_loop.run_until_complete(aiohttp_client(app))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture
|
|
72
|
+
def mock_auth(mock_api: TestClient):
|
|
73
|
+
auth = Auth(
|
|
74
|
+
websession=mock_api.session,
|
|
75
|
+
base_url=str(mock_api.server.make_url("/")),
|
|
76
|
+
username="correct",
|
|
77
|
+
password="correct",
|
|
78
|
+
)
|
|
79
|
+
return auth
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="de">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<meta name="description" content="enocoo | Abgemeldet">
|
|
8
|
+
<meta name="author" content="enocoo">
|
|
9
|
+
<!--link rel="shortcut icon" href="../images/favicon.ico"-->
|
|
10
|
+
<link rel="apple-touch-icon" sizes="180x180" href="../apple-touch-icon.png">
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="../favicon-32x32.png">
|
|
12
|
+
<link rel="icon" type="image/png" sizes="16x16" href="../favicon-16x16.png">
|
|
13
|
+
<link rel="manifest" href="../site.webmanifest.json">
|
|
14
|
+
<link rel="mask-icon" href="../safari-pinned-tab.svg" color="#5bbad5">
|
|
15
|
+
<meta name="msapplication-TileColor" content="#2d89ef">
|
|
16
|
+
<meta name="theme-color" content="#ffffff">
|
|
17
|
+
|
|
18
|
+
<title>enocoo | Abgemeldet</title>
|
|
19
|
+
<link href="../css/signInStyle.css" rel="stylesheet">
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<p /><DIV ALIGN=CENTER class="loggedOut"><a id="logo"><img align="center" src="../images/ENOCOO_LOGO_300x111.png" alt="enocoo Logo" /></a><h3>Sie sind nicht angemeldet.</h3><strong>Möglicherweise ist Ihre Sitzung abgelaufen.<br />Bitte gehen Sie zur <a href="../signinForm.php">Anmelde-Seite</a><br />und melden Sie sich an.</strong></DIV> </body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<div class="row"><div class="col-md-12"><div class="kachelRund"><table class="table table-striped"><tr><th>Fläche</th><th>Bezeichnung</th><th>Zähler-Nr.</th><th>Zeitpunkt</th><th class="alignright">Zählerstand</th><th>Einheit</th></tr><tr><td>H12W34</td><td>Verbrauch Kaltwasser H12W34 Bad</td><td>00000001</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>m<sup>3</sup></td></tr><tr><td>H12W34</td><td>Verbrauch Kaltwasser H12W34 WC</td><td>00000002</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>m<sup>3</sup></td></tr><tr><td>H12W34</td><td>Verbrauch Strom H12W34</td><td>00000003</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>kWh</td></tr><tr><td>H12W34</td><td>Verbrauch Wärme H12W34</td><td>00000004</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>kWh</td></tr><tr><td>H12W34</td><td>Verbrauch Warmwasser H12W34 Bad</td><td>00000005</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>m<sup>3</sup></td></tr><tr><td>H12W34</td><td>Verbrauch Warmwasser H12W34 WC</td><td>00000006</td><td>01.01.2021 12:34:56</td><td class="alignright">1.234,56</td><td>m<sup>3</sup></td></tr></table></div></div></div>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="de">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<meta name="description" content="enocoo | Anmeldung">
|
|
12
|
+
<meta name="author" content="">
|
|
13
|
+
<!--link rel="shortcut icon" href="../images/favicon.ico"-->
|
|
14
|
+
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
|
15
|
+
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
|
16
|
+
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
|
17
|
+
<link rel="manifest" href="site.webmanifest.json">
|
|
18
|
+
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
|
|
19
|
+
<meta name="msapplication-TileColor" content="#2d89ef">
|
|
20
|
+
<meta name="theme-color" content="#ffffff">
|
|
21
|
+
|
|
22
|
+
<title>enocoo | Anmeldung</title>
|
|
23
|
+
|
|
24
|
+
<!-- enocoo custom styles. Bootstrap core CSS wird in diesem als erstes Nachgeladen -->
|
|
25
|
+
<link href="css/signInStyle.css" rel="stylesheet">
|
|
26
|
+
|
|
27
|
+
<!-- Just for debugging purposes. Don't actually copy this line! -->
|
|
28
|
+
<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
|
|
29
|
+
|
|
30
|
+
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
31
|
+
<!--[if lt IE 9]>
|
|
32
|
+
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
|
33
|
+
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
|
34
|
+
<![endif]-->
|
|
35
|
+
|
|
36
|
+
<script src="js/jquery-2.1.0.js" type="text/javascript"></script>
|
|
37
|
+
|
|
38
|
+
</head>
|
|
39
|
+
|
|
40
|
+
<body>
|
|
41
|
+
<div class="container">
|
|
42
|
+
|
|
43
|
+
<form class="form-signin" role="form" method="post" action="/signinForm.php?mode=ok" accept-charset="UTF-8">
|
|
44
|
+
<a id="logo"><img align="center" src="images/ENOCOO_LOGO_300x111.png" alt="enocoo Logo" /></a>
|
|
45
|
+
|
|
46
|
+
<!-- Bei nur einem Projekt nichts anzeigen, sonst Auswahlbox -->
|
|
47
|
+
|
|
48
|
+
<!-- Bei nur einer Sprache nichts anzeigen, sonst Auswahlbox -->
|
|
49
|
+
|
|
50
|
+
<!-- bei nur 1 Projekt/Sprache keine Auswahlbox anzeigen und die einzige ID in jeweiliges Übergabefeld schreiben. value statt placeholder! -->
|
|
51
|
+
<input type="text" class="form-control" id="idProject" name="idProject" style="display:none;" value="1">
|
|
52
|
+
<input type="text" class="form-control" id="idLanguage" name="idLanguage" style="display:none;" value="1">
|
|
53
|
+
<h2 class="form-signin-heading">Ihre Anmeldung:</h2>
|
|
54
|
+
<input type="user" class="form-control" placeholder="Benutzername" name="user" required autofocus >
|
|
55
|
+
<input type="password" class="form-control" placeholder="Passwort" name="passwort" required >
|
|
56
|
+
<!-- <label class="checkbox">
|
|
57
|
+
<input type="checkbox" value="remember-me"> Remember me
|
|
58
|
+
</label> -->
|
|
59
|
+
<button class="btn btn-lg btn-primary btn-block" type="submit" >Anmelden</button>
|
|
60
|
+
|
|
61
|
+
<!-- PW-Änderung im Maintenancemode nicht möglich -->
|
|
62
|
+
</br><a href="php/pwLost.php">Passwort vergessen?</a>
|
|
63
|
+
<div align='center'><p></p></div> <div align='center'><p><strong>Ihre Anmeldedaten waren nicht korrekt. Bitte melden Sie sich erneut an.</strong></p></div>
|
|
64
|
+
</form>
|
|
65
|
+
|
|
66
|
+
</div> <!-- /container -->
|
|
67
|
+
|
|
68
|
+
<script type="text/javascript">
|
|
69
|
+
var chosenProject = "1";
|
|
70
|
+
var chosenLanguage = "1";
|
|
71
|
+
|
|
72
|
+
function dropDownActionProj(menuEntry) {
|
|
73
|
+
chosenProject = menuEntry.id;
|
|
74
|
+
// document.getElementById("resultColorValue").innerHTML = document.getElementById("Color").value;
|
|
75
|
+
$('#idProject').val(menuEntry.id);
|
|
76
|
+
$('#project').html($(menuEntry).text() + ' <span class="caret"></span>');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function dropDownActionLang(menuEntry) {
|
|
80
|
+
chosenLanguage = menuEntry.id;
|
|
81
|
+
$('#idLanguage').val(menuEntry.id);
|
|
82
|
+
$('#language').html($(menuEntry).text() + ' <span class="caret"></span>');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// assign all dropdown-entries a callback-function. Without event_ref.preventDefault(); the page scrolls to the top after the change! (a often suggested "return false;" instead of it does not work.
|
|
86
|
+
$('.pdmProj').each(function( index ) {
|
|
87
|
+
$( this ).click(function(event_ref){ dropDownActionProj(this); event_ref.preventDefault(); });
|
|
88
|
+
});
|
|
89
|
+
$('.pdmLang').each(function( index ) {
|
|
90
|
+
$( this ).click(function(event_ref){ dropDownActionLang(this); event_ref.preventDefault(); });
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
<!-- Bootstrap core JavaScript
|
|
96
|
+
================================================== -->
|
|
97
|
+
<!-- Placed at the end of the document so the pages load faster -->
|
|
98
|
+
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
|
99
|
+
<script src="js/jquery-2.1.0.js" type="text/javascript"></script>
|
|
100
|
+
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
|
101
|
+
<script src="components/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="de">
|
|
7
|
+
<head>
|
|
8
|
+
<meta http-equiv="refresh" content="0; URL=php/mainPage.php" /> <meta charset="utf-8">
|
|
9
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
10
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
11
|
+
<meta name="description" content="enocoo | Anmeldung">
|
|
12
|
+
<meta name="author" content="">
|
|
13
|
+
<!--link rel="shortcut icon" href="../images/favicon.ico"-->
|
|
14
|
+
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
|
|
15
|
+
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
|
|
16
|
+
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
|
|
17
|
+
<link rel="manifest" href="site.webmanifest.json">
|
|
18
|
+
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
|
|
19
|
+
<meta name="msapplication-TileColor" content="#2d89ef">
|
|
20
|
+
<meta name="theme-color" content="#ffffff">
|
|
21
|
+
|
|
22
|
+
<title>enocoo | Anmeldung</title>
|
|
23
|
+
|
|
24
|
+
<!-- enocoo custom styles. Bootstrap core CSS wird in diesem als erstes Nachgeladen -->
|
|
25
|
+
<link href="css/signInStyle.css" rel="stylesheet">
|
|
26
|
+
|
|
27
|
+
<!-- Just for debugging purposes. Don't actually copy this line! -->
|
|
28
|
+
<!--[if lt IE 9]><script src="../../assets/js/ie8-responsive-file-warning.js"></script><![endif]-->
|
|
29
|
+
|
|
30
|
+
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
31
|
+
<!--[if lt IE 9]>
|
|
32
|
+
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
|
33
|
+
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
|
34
|
+
<![endif]-->
|
|
35
|
+
|
|
36
|
+
<script src="js/jquery-2.1.0.js" type="text/javascript"></script>
|
|
37
|
+
|
|
38
|
+
</head>
|
|
39
|
+
|
|
40
|
+
<body>
|
|
41
|
+
<div align="center">
|
|
42
|
+
<strong>Die Startseite wird geladen...</strong>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<script type="text/javascript">
|
|
46
|
+
var chosenProject = "1";
|
|
47
|
+
var chosenLanguage = "1";
|
|
48
|
+
|
|
49
|
+
function dropDownActionProj(menuEntry) {
|
|
50
|
+
chosenProject = menuEntry.id;
|
|
51
|
+
// document.getElementById("resultColorValue").innerHTML = document.getElementById("Color").value;
|
|
52
|
+
$('#idProject').val(menuEntry.id);
|
|
53
|
+
$('#project').html($(menuEntry).text() + ' <span class="caret"></span>');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function dropDownActionLang(menuEntry) {
|
|
57
|
+
chosenLanguage = menuEntry.id;
|
|
58
|
+
$('#idLanguage').val(menuEntry.id);
|
|
59
|
+
$('#language').html($(menuEntry).text() + ' <span class="caret"></span>');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// assign all dropdown-entries a callback-function. Without event_ref.preventDefault(); the page scrolls to the top after the change! (a often suggested "return false;" instead of it does not work.
|
|
63
|
+
$('.pdmProj').each(function( index ) {
|
|
64
|
+
$( this ).click(function(event_ref){ dropDownActionProj(this); event_ref.preventDefault(); });
|
|
65
|
+
});
|
|
66
|
+
$('.pdmLang').each(function( index ) {
|
|
67
|
+
$( this ).click(function(event_ref){ dropDownActionLang(this); event_ref.preventDefault(); });
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
<!-- Bootstrap core JavaScript
|
|
73
|
+
================================================== -->
|
|
74
|
+
<!-- Placed at the end of the document so the pages load faster -->
|
|
75
|
+
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
|
|
76
|
+
<script src="js/jquery-2.1.0.js" type="text/javascript"></script>
|
|
77
|
+
<!-- Include all compiled plugins (below), or include individual files as needed -->
|
|
78
|
+
<script src="components/bootstrap/js/bootstrap.min.js" type="text/javascript"></script>
|
|
79
|
+
</body>
|
|
80
|
+
</html>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from aiohttp.test_utils import TestClient
|
|
3
|
+
|
|
4
|
+
from oocone import Auth, errors
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
@pytest.mark.mocked_api
|
|
9
|
+
async def test_login_successful(mock_api: TestClient) -> None:
|
|
10
|
+
auth = Auth(
|
|
11
|
+
websession=mock_api.session,
|
|
12
|
+
base_url=str(mock_api.server.make_url("/")),
|
|
13
|
+
username="correct",
|
|
14
|
+
password="correct",
|
|
15
|
+
)
|
|
16
|
+
await auth._login()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
@pytest.mark.mocked_api
|
|
21
|
+
async def test_login_raise_on_failure(mock_api: TestClient) -> None:
|
|
22
|
+
auth = Auth(
|
|
23
|
+
websession=mock_api.session,
|
|
24
|
+
base_url=str(mock_api.server.make_url("/")),
|
|
25
|
+
username="correct",
|
|
26
|
+
password="incorrect",
|
|
27
|
+
)
|
|
28
|
+
with pytest.raises(errors.AuthenticationFailed):
|
|
29
|
+
await auth._login()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from oocone import Auth, Enocoo
|
|
4
|
+
from oocone.types import TrafficLightColor
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
@pytest.mark.mocked_api
|
|
9
|
+
async def test_get_traffic_light_status(mock_auth: Auth):
|
|
10
|
+
enocoo = Enocoo(mock_auth)
|
|
11
|
+
result = await enocoo.get_traffic_light_status()
|
|
12
|
+
assert isinstance(result.color, TrafficLightColor)
|
|
13
|
+
assert isinstance(result.current_energy_price, float)
|