arthexis 0.1.8__py3-none-any.whl → 0.1.9__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.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/METADATA +42 -4
- arthexis-0.1.9.dist-info/RECORD +92 -0
- arthexis-0.1.9.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +133 -16
- config/urls.py +65 -6
- core/admin.py +1226 -191
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +158 -3
- core/backends.py +46 -4
- core/entity.py +62 -48
- core/fields.py +6 -1
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1071 -264
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/release.py +27 -20
- core/sigil_builder.py +131 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +129 -10
- core/tasks.py +118 -19
- core/test_system_info.py +22 -0
- core/tests.py +358 -63
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +329 -167
- core/views.py +383 -57
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +159 -284
- nodes/apps.py +9 -15
- nodes/backends.py +53 -0
- nodes/lcd.py +24 -10
- nodes/models.py +375 -178
- nodes/tasks.py +1 -5
- nodes/tests.py +524 -129
- nodes/utils.py +13 -2
- nodes/views.py +66 -23
- ocpp/admin.py +150 -61
- ocpp/apps.py +1 -1
- ocpp/consumers.py +432 -69
- ocpp/evcs.py +25 -8
- ocpp/models.py +408 -68
- ocpp/simulator.py +13 -6
- ocpp/store.py +258 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1198 -135
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +654 -101
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +19 -6
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +759 -40
- pages/urls.py +3 -0
- pages/utils.py +0 -1
- pages/views.py +576 -25
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.9.dist-info}/top_level.txt +0 -0
nodes/apps.py
CHANGED
|
@@ -7,7 +7,6 @@ from pathlib import Path
|
|
|
7
7
|
|
|
8
8
|
from django.apps import AppConfig
|
|
9
9
|
from django.conf import settings
|
|
10
|
-
from django.core.signals import request_started
|
|
11
10
|
from django.db import connections
|
|
12
11
|
from django.db.utils import OperationalError
|
|
13
12
|
from utils import revision
|
|
@@ -17,13 +16,9 @@ logger = logging.getLogger(__name__)
|
|
|
17
16
|
|
|
18
17
|
|
|
19
18
|
def _startup_notification() -> None:
|
|
20
|
-
"""Queue a
|
|
19
|
+
"""Queue a Net Message with ``hostname:port`` and version info."""
|
|
21
20
|
|
|
22
21
|
host = socket.gethostname()
|
|
23
|
-
try:
|
|
24
|
-
address = socket.gethostbyname(host)
|
|
25
|
-
except socket.gaierror:
|
|
26
|
-
address = host
|
|
27
22
|
|
|
28
23
|
port = os.environ.get("PORT", "8000")
|
|
29
24
|
|
|
@@ -35,9 +30,9 @@ def _startup_notification() -> None:
|
|
|
35
30
|
revision_value = revision.get_revision()
|
|
36
31
|
rev_short = revision_value[-6:] if revision_value else ""
|
|
37
32
|
|
|
38
|
-
body =
|
|
33
|
+
body = version
|
|
39
34
|
if rev_short:
|
|
40
|
-
body
|
|
35
|
+
body = f"{body} r{rev_short}" if body else f"r{rev_short}"
|
|
41
36
|
|
|
42
37
|
def _worker() -> None: # pragma: no cover - background thread
|
|
43
38
|
# Allow the LCD a moment to become ready and retry a few times
|
|
@@ -45,7 +40,7 @@ def _startup_notification() -> None:
|
|
|
45
40
|
try:
|
|
46
41
|
from nodes.models import NetMessage
|
|
47
42
|
|
|
48
|
-
NetMessage.broadcast(subject=f"{
|
|
43
|
+
NetMessage.broadcast(subject=f"{host}:{port}", body=body)
|
|
49
44
|
break
|
|
50
45
|
except Exception:
|
|
51
46
|
time.sleep(1)
|
|
@@ -54,9 +49,8 @@ def _startup_notification() -> None:
|
|
|
54
49
|
|
|
55
50
|
|
|
56
51
|
def _trigger_startup_notification(**_: object) -> None:
|
|
57
|
-
"""
|
|
52
|
+
"""Attempt to send the startup notification in the background."""
|
|
58
53
|
|
|
59
|
-
request_started.disconnect(_trigger_startup_notification, dispatch_uid="nodes-startup")
|
|
60
54
|
try:
|
|
61
55
|
connections["default"].ensure_connection()
|
|
62
56
|
except OperationalError:
|
|
@@ -68,9 +62,9 @@ def _trigger_startup_notification(**_: object) -> None:
|
|
|
68
62
|
class NodesConfig(AppConfig):
|
|
69
63
|
default_auto_field = "django.db.models.BigAutoField"
|
|
70
64
|
name = "nodes"
|
|
71
|
-
verbose_name = "
|
|
65
|
+
verbose_name = "4. Infrastructure"
|
|
72
66
|
|
|
73
67
|
def ready(self): # pragma: no cover - exercised on app start
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
68
|
+
from django.db.models.signals import post_migrate
|
|
69
|
+
|
|
70
|
+
post_migrate.connect(_trigger_startup_notification, sender=self)
|
nodes/backends.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.core.mail.backends.base import BaseEmailBackend
|
|
4
|
+
from django.core.mail import get_connection
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.db.models import Q
|
|
7
|
+
|
|
8
|
+
from .models import EmailOutbox
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OutboxEmailBackend(BaseEmailBackend):
|
|
12
|
+
"""Email backend that selects an :class:`EmailOutbox` automatically.
|
|
13
|
+
|
|
14
|
+
If a matching outbox exists for the message's ``from_email`` (matching
|
|
15
|
+
either ``from_email`` or ``username``), that outbox's SMTP credentials are
|
|
16
|
+
used. Otherwise, the first available outbox is used. When no outboxes are
|
|
17
|
+
configured, the system falls back to Django's default SMTP settings.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def _select_outbox(self, from_email: str | None) -> EmailOutbox | None:
|
|
21
|
+
if from_email:
|
|
22
|
+
return (
|
|
23
|
+
EmailOutbox.objects.filter(
|
|
24
|
+
Q(from_email__iexact=from_email) | Q(username__iexact=from_email)
|
|
25
|
+
).first()
|
|
26
|
+
or EmailOutbox.objects.first()
|
|
27
|
+
)
|
|
28
|
+
return EmailOutbox.objects.first()
|
|
29
|
+
|
|
30
|
+
def send_messages(self, email_messages):
|
|
31
|
+
sent = 0
|
|
32
|
+
for message in email_messages:
|
|
33
|
+
outbox = self._select_outbox(message.from_email)
|
|
34
|
+
if outbox:
|
|
35
|
+
connection = outbox.get_connection()
|
|
36
|
+
if not message.from_email:
|
|
37
|
+
message.from_email = (
|
|
38
|
+
outbox.from_email or settings.DEFAULT_FROM_EMAIL
|
|
39
|
+
)
|
|
40
|
+
else:
|
|
41
|
+
connection = get_connection(
|
|
42
|
+
"django.core.mail.backends.smtp.EmailBackend"
|
|
43
|
+
)
|
|
44
|
+
if not message.from_email:
|
|
45
|
+
message.from_email = settings.DEFAULT_FROM_EMAIL
|
|
46
|
+
try:
|
|
47
|
+
sent += connection.send_messages([message]) or 0
|
|
48
|
+
finally:
|
|
49
|
+
try:
|
|
50
|
+
connection.close()
|
|
51
|
+
except Exception: # pragma: no cover - close errors shouldn't fail send
|
|
52
|
+
pass
|
|
53
|
+
return sent
|
nodes/lcd.py
CHANGED
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
import subprocess
|
|
12
12
|
import time
|
|
13
13
|
from dataclasses import dataclass
|
|
14
|
-
from typing import List
|
|
15
14
|
|
|
16
15
|
try: # pragma: no cover - hardware dependent
|
|
17
16
|
import smbus # type: ignore
|
|
@@ -21,6 +20,12 @@ except Exception: # pragma: no cover - missing dependency
|
|
|
21
20
|
except Exception: # pragma: no cover - missing dependency
|
|
22
21
|
smbus = None # type: ignore
|
|
23
22
|
|
|
23
|
+
SMBUS_HINT = (
|
|
24
|
+
"smbus module not found. Enable the I2C interface and install the dependencies.\n"
|
|
25
|
+
"For Debian/Ubuntu run: sudo apt-get install i2c-tools python3-smbus\n"
|
|
26
|
+
"Within the virtualenv: pip install smbus2"
|
|
27
|
+
)
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
class LCDUnavailableError(RuntimeError):
|
|
26
31
|
"""Raised when the LCD cannot be initialised."""
|
|
@@ -32,9 +37,11 @@ class _BusWrapper:
|
|
|
32
37
|
|
|
33
38
|
channel: int
|
|
34
39
|
|
|
35
|
-
def write_byte(
|
|
40
|
+
def write_byte(
|
|
41
|
+
self, addr: int, data: int
|
|
42
|
+
) -> None: # pragma: no cover - thin wrapper
|
|
36
43
|
if smbus is None:
|
|
37
|
-
raise LCDUnavailableError(
|
|
44
|
+
raise LCDUnavailableError(SMBUS_HINT)
|
|
38
45
|
bus = smbus.SMBus(self.channel)
|
|
39
46
|
bus.write_byte(addr, data)
|
|
40
47
|
bus.close()
|
|
@@ -45,7 +52,7 @@ class CharLCD1602:
|
|
|
45
52
|
|
|
46
53
|
def __init__(self, bus: _BusWrapper | None = None) -> None:
|
|
47
54
|
if smbus is None: # pragma: no cover - hardware dependent
|
|
48
|
-
raise LCDUnavailableError(
|
|
55
|
+
raise LCDUnavailableError(SMBUS_HINT)
|
|
49
56
|
self.bus = bus or _BusWrapper(1)
|
|
50
57
|
self.BLEN = 1
|
|
51
58
|
self.PCF8574_address = 0x27
|
|
@@ -85,7 +92,7 @@ class CharLCD1602:
|
|
|
85
92
|
# Allow the LCD controller to catch up between data writes.
|
|
86
93
|
time.sleep(0.001)
|
|
87
94
|
|
|
88
|
-
def i2c_scan(self) ->
|
|
95
|
+
def i2c_scan(self) -> list[str]: # pragma: no cover - requires hardware
|
|
89
96
|
"""Return a list of detected I2C addresses.
|
|
90
97
|
|
|
91
98
|
The implementation relies on the external ``i2cdetect`` command. On
|
|
@@ -94,13 +101,18 @@ class CharLCD1602:
|
|
|
94
101
|
list so callers can fall back to a sensible default address.
|
|
95
102
|
"""
|
|
96
103
|
|
|
97
|
-
cmd = "i2cdetect -y 1 | awk 'NR>1 {$1=\"\"; print}'"
|
|
98
104
|
try:
|
|
99
|
-
|
|
105
|
+
output = subprocess.check_output(["i2cdetect", "-y", "1"], text=True)
|
|
100
106
|
except Exception: # pragma: no cover - depends on environment
|
|
101
107
|
return []
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
|
|
109
|
+
addresses: list[str] = []
|
|
110
|
+
for line in output.splitlines()[1:]:
|
|
111
|
+
parts = line.split()
|
|
112
|
+
for token in parts[1:]:
|
|
113
|
+
if token != "--":
|
|
114
|
+
addresses.append(token)
|
|
115
|
+
return addresses
|
|
104
116
|
|
|
105
117
|
def init_lcd(self, addr: int | None = None, bl: int = 1) -> None:
|
|
106
118
|
self.BLEN = 1 if bl else 0
|
|
@@ -138,7 +150,9 @@ class CharLCD1602:
|
|
|
138
150
|
"""Re-run the initialisation sequence to recover the display."""
|
|
139
151
|
self.init_lcd(addr=self.LCD_ADDR, bl=self.BLEN)
|
|
140
152
|
|
|
141
|
-
def set_backlight(
|
|
153
|
+
def set_backlight(
|
|
154
|
+
self, on: bool = True
|
|
155
|
+
) -> None: # pragma: no cover - hardware dependent
|
|
142
156
|
self.BLEN = 1 if on else 0
|
|
143
157
|
self._write_word(self.LCD_ADDR, 0x00)
|
|
144
158
|
|