arthexis 0.1.24__py3-none-any.whl → 0.1.25__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.24.dist-info → arthexis-0.1.25.dist-info}/METADATA +35 -14
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/RECORD +30 -29
- config/settings.py +6 -3
- config/urls.py +2 -0
- core/admin.py +1 -186
- core/backends.py +3 -1
- core/models.py +74 -8
- core/system.py +67 -2
- core/views.py +0 -3
- nodes/admin.py +444 -251
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +211 -1
- nodes/urls.py +5 -0
- nodes/utils.py +9 -2
- nodes/views.py +128 -80
- ocpp/admin.py +190 -2
- ocpp/consumers.py +98 -0
- ocpp/models.py +271 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +108 -267
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +4 -2
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.24.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arthexis
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.25
|
|
4
4
|
Summary: Power & Energy Infrastructure
|
|
5
5
|
Author-email: "Rafael J. Guillén-Osorio" <tecnologia@gelectriic.com>
|
|
6
6
|
License-Expression: GPL-3.0-only
|
|
@@ -48,6 +48,7 @@ Requires-Dist: django-timezone-field==7.1
|
|
|
48
48
|
Requires-Dist: dnspython==2.7.0
|
|
49
49
|
Requires-Dist: docutils==0.22.2
|
|
50
50
|
Requires-Dist: gpiozero==2.0.1; sys_platform == "linux"
|
|
51
|
+
Requires-Dist: graphene-django==3.2.2
|
|
51
52
|
Requires-Dist: graphviz==0.21
|
|
52
53
|
Requires-Dist: h11==0.16.0
|
|
53
54
|
Requires-Dist: httpcore==1.0.9
|
|
@@ -124,19 +125,39 @@ Arthexis Constellation is a [narrative-driven](https://en.wikipedia.org/wiki/Nar
|
|
|
124
125
|
|
|
125
126
|
## Current Features
|
|
126
127
|
|
|
127
|
-
- Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
128
|
+
- Compatible with the [Open Charge Point Protocol (OCPP) 1.6](https://www.openchargealliance.org/protocols/ocpp-16/) central system. Supported actions are summarized below.
|
|
129
|
+
|
|
130
|
+
**Charge point → CSMS**
|
|
131
|
+
|
|
132
|
+
| Action | What we do |
|
|
133
|
+
| --- | --- |
|
|
134
|
+
| `Authorize` | Validate RFID or token authorization requests before a session starts. |
|
|
135
|
+
| `BootNotification` | Register the charge point and update identity, firmware, and status details. |
|
|
136
|
+
| `DataTransfer` | Accept vendor-specific payloads and record the results. |
|
|
137
|
+
| `DiagnosticsStatusNotification` | Track the progress of diagnostic uploads kicked off from the back office. |
|
|
138
|
+
| `FirmwareStatusNotification` | Track firmware update lifecycle events from charge points. |
|
|
139
|
+
| `Heartbeat` | Keep the websocket session alive and update last-seen timestamps. |
|
|
140
|
+
| `MeterValues` | Persist periodic energy and power readings while a transaction is active. |
|
|
141
|
+
| `StartTransaction` | Create charging sessions with initial meter values and identification data. |
|
|
142
|
+
| `StatusNotification` | Reflect connector availability and fault states in real time. |
|
|
143
|
+
| `StopTransaction` | Close charging sessions, capturing closing meter values and stop reasons. |
|
|
144
|
+
|
|
145
|
+
**CSMS → Charge point**
|
|
146
|
+
|
|
147
|
+
| Action | What we do |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `ChangeAvailability` | Switch connectors or the whole station between operative and inoperative states. |
|
|
150
|
+
| `DataTransfer` | Send vendor-specific commands and log the charge point response. |
|
|
151
|
+
| `GetConfiguration` | Poll the device for the current values of tracked configuration keys. |
|
|
152
|
+
| `RemoteStartTransaction` | Initiate a charging session remotely for an identified customer or token. |
|
|
153
|
+
| `RemoteStopTransaction` | Terminate active charging sessions from the control center. |
|
|
154
|
+
| `ReserveNow` | Reserve connectors for upcoming sessions with automatic connector selection and confirmation tracking. |
|
|
155
|
+
| `Reset` | Request a soft or hard reboot to recover from faults. |
|
|
156
|
+
| `TriggerMessage` | Ask the device to send an immediate update (for example status or diagnostics). |
|
|
157
|
+
|
|
158
|
+
**OCPP 1.6 roadmap.** The following catalogue actions are in our backlog: `CancelReservation`, `ChangeConfiguration`, `ClearCache`, `ClearChargingProfile`, `GetCompositeSchedule`, `GetDiagnostics`, `GetLocalListVersion`, `SendLocalList`, `SetChargingProfile`, `UnlockConnector`, `UpdateFirmware`.
|
|
159
|
+
|
|
160
|
+
- Charge point reservations with automated connector assignment, energy account and RFID linkage, and EVCS confirmation tracking.
|
|
140
161
|
- [API](https://en.wikipedia.org/wiki/API) integration with [Odoo](https://www.odoo.com/), syncing:
|
|
141
162
|
- Employee credentials via `res.users`
|
|
142
163
|
- Product catalog lookups via `product.product`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
arthexis-0.1.
|
|
1
|
+
arthexis-0.1.25.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
config/__init__.py,sha256=AwpOX7il-DAOmkdJ5dVfVJ3CWWebn1lHyQNmkw1EkDw,103
|
|
3
3
|
config/active_app.py,sha256=KJqYh-o91nPQjVXPEdbiJHzsI6cN9IZsBZ9O3iZ6Hyc,373
|
|
4
4
|
config/asgi.py,sha256=Z2HjWrxOxVU9BXcqS7dMEfOGJC48H-WPwFwokRdermY,774
|
|
@@ -10,17 +10,17 @@ config/loadenv.py,sha256=CjXx-wBaTt1wixub4GJ5CMSMFqtiK5JURc7cPXpqO7s,287
|
|
|
10
10
|
config/logging.py,sha256=1cIbPgRshHuMKnVEEH0jKpRAlJSpewvLFbYDz7sCBG4,2104
|
|
11
11
|
config/middleware.py,sha256=zF8Cma0n5G8NNdh2LVeNJi7Hgl1G4mF9msRE2eRi1RU,2328
|
|
12
12
|
config/offline.py,sha256=X-yDcyoI4C44Y27lpkUwszY_09GwwFfazEsthKJpQ70,1382
|
|
13
|
-
config/settings.py,sha256=
|
|
13
|
+
config/settings.py,sha256=1vKOC0VwPOMXXnpUr4OIDRS1C_l_PXY-yyiDo7jmflM,20970
|
|
14
14
|
config/settings_helpers.py,sha256=0BdBciUHIkwsWa0vV_RKAd4wDuEzgE7G-42XYiES4YQ,3127
|
|
15
|
-
config/urls.py,sha256=
|
|
15
|
+
config/urls.py,sha256=wiSmVWpLhPJgis4BPDoeRtGmuOM5A9TIZApf5iadE5k,5642
|
|
16
16
|
config/wsgi.py,sha256=zU_mKlya6hejQ21PxKacTui3dUWd4ca_-YJNSYAoMX0,433
|
|
17
17
|
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
core/admin.py,sha256=
|
|
18
|
+
core/admin.py,sha256=Mum2MQsVOnTtUTuRoy8wfC7POJGXODMdoZoDWhcbgoM,145179
|
|
19
19
|
core/admin_history.py,sha256=XZ4b0ryufIka-xcwboK3DzmOL-INSx5Y2fJO-aJdV70,1783
|
|
20
20
|
core/admindocs.py,sha256=ycD0bJ_VE6rTGf9ebXTiKdYkD8Y8hD2oQ4HxxoBURCM,6756
|
|
21
21
|
core/apps.py,sha256=S6fySxtxUzfvz8FI9dii0KI4wSyLhh5API_oeERLIsc,14084
|
|
22
22
|
core/auto_upgrade.py,sha256=1EffHHFylgydWdZM_id6CppV0QqBtdNw7cwBYVdbNdk,1715
|
|
23
|
-
core/backends.py,sha256=
|
|
23
|
+
core/backends.py,sha256=O6QzNsX3OXi0QeIO7PCXI57VLz7HoY72ouBGTpzsSPM,10793
|
|
24
24
|
core/changelog.py,sha256=SRn37i5N-qb-RYV4Gpu9fg7Kv8gu4TH8ZwEmDRgN-Vo,12594
|
|
25
25
|
core/entity.py,sha256=o4VteOXePGEsIWJFZ3fpq3DZsdWr3hpQ9A6kFbKosSE,4844
|
|
26
26
|
core/environment.py,sha256=4beGOYE1BC8q9vBmTLZzFo75nj_3tBekzMqhsNEZVbU,1653
|
|
@@ -34,7 +34,7 @@ core/liveupdate.py,sha256=22m0ueQ10-6b-9pQJHY0_5WRYA98fysXKEXOWzIr550,691
|
|
|
34
34
|
core/log_paths.py,sha256=lxvgXPgJtVNZ-kYrqV8VFle4GFQrSxG-yRTglqvclmU,3318
|
|
35
35
|
core/mailer.py,sha256=JpW0RnD9uZ4O-wvlqeW7CMw95IFeCSkdvbankJDwHq0,2886
|
|
36
36
|
core/middleware.py,sha256=j19K9SX-Emkv7BDDtAacR9g6RWsxhKHwCc8w23JFvMM,3388
|
|
37
|
-
core/models.py,sha256=
|
|
37
|
+
core/models.py,sha256=4fZTlc1ZaVFljnlNoE9e6QPvVfOrSdTjZaYATJUTSBw,174371
|
|
38
38
|
core/notifications.py,sha256=jNLSuSCrhb8x5cDu_APeDlkrmbMejufk5eJOhssAC4I,3917
|
|
39
39
|
core/public_wifi.py,sha256=yydLgxOo9DmJJbM4X_23wGR3gxL3YzHno54v9GssuFA,7213
|
|
40
40
|
core/reference_utils.py,sha256=tffCoyE1w4_SmYzXVWOsW8aR_ZVVTSPzrGhBq8K2xzA,3631
|
|
@@ -43,7 +43,7 @@ core/rfid_import_export.py,sha256=petyhPvL0WUpehc6uGUDUhjYQ9AVvc6O49zuhDs6YFw,35
|
|
|
43
43
|
core/sigil_builder.py,sha256=nMuhYlw3j3LosrK85Q0pYsMcfGWCmrmdnv8UG7GTq_o,4856
|
|
44
44
|
core/sigil_context.py,sha256=GCzjfM6fcVvBtSbVNfmE6sx3HU8QnxnXrCIytnNpQzM,439
|
|
45
45
|
core/sigil_resolver.py,sha256=rCsypuX-0oWNfKyM1T9ZLWHY0Ezwhtk4VmI0L3krnsE,11098
|
|
46
|
-
core/system.py,sha256=
|
|
46
|
+
core/system.py,sha256=RVQA66t43Vt-jn5jIZUzYZ63FANs4onk8bXNJRl-rWo,44104
|
|
47
47
|
core/tasks.py,sha256=ptO44VTBAoTwf7Y3pI6TnniIs4lTUgN4MKCgNAUjhm4,13135
|
|
48
48
|
core/temp_passwords.py,sha256=FieUnIUeQHmA1DoXvfJ5U6-Ayv3oDz-hSln5s_vNbA4,5271
|
|
49
49
|
core/test_system_info.py,sha256=IMPz21KEs6OC5YbL7YaIBdmJVLjRY6MgPuZpldJB5OI,6935
|
|
@@ -51,43 +51,44 @@ core/tests.py,sha256=PuxoarDS4reHNV4EDIyVRW7xIOFxZJYou1K_LI9ZNHY,105265
|
|
|
51
51
|
core/tests_liveupdate.py,sha256=IquU8ztk6zbzC1bQu3Nrr3RzGzuujtPwDkANJHbxg98,510
|
|
52
52
|
core/urls.py,sha256=YPippON1MAP2KeZZ8jHpcLO6mvbnKn1q7fdMv5Vm9dY,425
|
|
53
53
|
core/user_data.py,sha256=4pheHB5RqLJtmWMql30CLaCpuVqSyShXb7Sy-crRk_4,22400
|
|
54
|
-
core/views.py,sha256=
|
|
54
|
+
core/views.py,sha256=s_a2tu9xwOpVSo7vo0iDDV4RN23jxGZNR-o7JNHs1Wk,88256
|
|
55
55
|
core/widgets.py,sha256=vlR9PlFfZGlkHm5X2cqNXuEBZSj8gmWaR6MO1mMy6kg,6904
|
|
56
56
|
nodes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
|
-
nodes/admin.py,sha256=
|
|
57
|
+
nodes/admin.py,sha256=vTreqm-G0IWHuYagyKj0RXuXFuWbwbsvzNHEIFuwhHA,79511
|
|
58
58
|
nodes/apps.py,sha256=oi_M2Ya8CAR8N_MoYU68u7_9u-9SlIMelzLOgYM9tDs,3059
|
|
59
59
|
nodes/backends.py,sha256=dmmbS0X2YIlCDz2KjoDf_L62dy--nuqZF1rEDoi2JHM,5921
|
|
60
60
|
nodes/dns.py,sha256=D5smXD7Rkh6E4MdL6TBL2WY8GgJg7Rx9z88LZrcMbTw,7048
|
|
61
61
|
nodes/feature_checks.py,sha256=27e4PCkZ8BGWnJCOwMcY2Bo9z7LoeZWiTZuISWGnrzk,3996
|
|
62
62
|
nodes/lcd.py,sha256=iKA8Wmq85KZD52aTzAU8ZmS144_gbdGMOXcE8yuECps,5758
|
|
63
|
-
nodes/models.py,sha256=
|
|
63
|
+
nodes/models.py,sha256=PdvBLFqSsAThD6ZJyrJMzhiwOH42rtwdUpSz6H4KCH8,83726
|
|
64
64
|
nodes/reports.py,sha256=NRYh3Y0SlZFhx31Zh2K03yO12ZrpxEHEY6T-dODA6WE,12059
|
|
65
65
|
nodes/rfid_sync.py,sha256=oeblawcp6xeLApdIuhsJS83OAk58Eu7pVVmgpAc0Nt8,6953
|
|
66
66
|
nodes/signals.py,sha256=PtOKdQfb08mV1LgSZvn7ZAcfOyy2c3Xkq4AOpBQyUdE,622
|
|
67
|
-
nodes/tasks.py,sha256=
|
|
68
|
-
nodes/tests.py,sha256=
|
|
69
|
-
nodes/urls.py,sha256=
|
|
70
|
-
nodes/utils.py,sha256=
|
|
71
|
-
nodes/views.py,sha256=
|
|
67
|
+
nodes/tasks.py,sha256=TKSUE4eILV644iPtn2Xr_UL3ZFYgzjzSAIGUYmhg3Sk,5111
|
|
68
|
+
nodes/tests.py,sha256=0j1vcHoSpkR5e0aYSHffJi-CASFBKTW0qgK3sNStHhI,196800
|
|
69
|
+
nodes/urls.py,sha256=9uMhDq-b-EWZz0u-NvPRVSPXJeXfuS-BAACvsCs6gaE,1267
|
|
70
|
+
nodes/utils.py,sha256=tsVoXAC5QKXcUWrKxhIMLUdRPDtTUXJiG-r2Lmf28lg,4792
|
|
71
|
+
nodes/views.py,sha256=xWTaiUSEh5Paj6TZB780YcT6kU-HOar2QpcOamAKRok,59620
|
|
72
72
|
ocpp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
ocpp/admin.py,sha256=
|
|
73
|
+
ocpp/admin.py,sha256=BSfS4bwqxcC0a57xXOyIw9hFRxB0y7bLsF2gP5rRMfg,60175
|
|
74
74
|
ocpp/apps.py,sha256=i3NqrmIamNEQBT33CIqh7HOSOPmJXCMKrZ-DUd3whqg,842
|
|
75
|
-
ocpp/consumers.py,sha256=
|
|
75
|
+
ocpp/consumers.py,sha256=XfrtwJR8OLvYzxj0NIIEa_ky36UBjHROUtoEFqFK39I,75577
|
|
76
76
|
ocpp/evcs.py,sha256=q1mZrCVSZxXTrtYsDqH6lkeEcJ6tfSC7p9YxkDmpSCw,28883
|
|
77
77
|
ocpp/evcs_discovery.py,sha256=OmrzgaOHwveDRJs8AIhrM3apX8_k2PPXh_oYaYpNW3c,3876
|
|
78
|
-
ocpp/models.py,sha256=
|
|
78
|
+
ocpp/models.py,sha256=z5uy3zuTCICkrIg7rvBdnnYJzwHUyFrUP1AE-8LX8mk,47889
|
|
79
|
+
ocpp/network.py,sha256=N3je0wXckSqlHLJNQazpxrBvv0yAR7DdjfAR-hTcWDk,14149
|
|
79
80
|
ocpp/reference_utils.py,sha256=_UR82GfE93kv4766mHyVIfdhhyYvrT59660r3H6W55M,1072
|
|
80
81
|
ocpp/routing.py,sha256=3kQya-MdJ00778xDmX0esQLBP05P200V45asg-CGNoo,438
|
|
81
82
|
ocpp/simulator.py,sha256=vnyd59QffT79AaPhmfM_jipni_nqfG57X5tXyx1rBoc,28016
|
|
82
83
|
ocpp/status_display.py,sha256=YGFosd5HJETA0DcLdsjvx6EfhZSnI8Aa3cMnHG2WsBE,939
|
|
83
84
|
ocpp/store.py,sha256=gLCSaP9KKF7li2ALlE3O3RW5eVJtoe-_YHfKhdf0VOM,18943
|
|
84
|
-
ocpp/tasks.py,sha256=
|
|
85
|
+
ocpp/tasks.py,sha256=WCN1k4X8BGp8Yc0klhfivBskDCtb04jNxtvNNr7eWHQ,14751
|
|
85
86
|
ocpp/test_export_import.py,sha256=ouQbTCp4mxfqoK6gondlu3PPcyrT9jSbWAX5gqqgaNk,4561
|
|
86
87
|
ocpp/test_rfid.py,sha256=IhFSlvsI8A8D3S32sRE298nYfrmqxbv7GfVErtNU3DQ,39137
|
|
87
|
-
ocpp/tests.py,sha256=
|
|
88
|
+
ocpp/tests.py,sha256=pEVega4N2gJctjOiO8CSvMqGKrcrUtZrLSLBNSigRGU,214558
|
|
88
89
|
ocpp/transactions_io.py,sha256=p2aUsKlCDYnZ4ZBrOM7pxXoW_w3Tbm-tvRFSjnR3x24,7738
|
|
89
90
|
ocpp/urls.py,sha256=5ZomUtznJe3kfs8E-DtVp12eFva5jUuJdpTEczIsQ5w,1730
|
|
90
|
-
ocpp/views.py,sha256=
|
|
91
|
+
ocpp/views.py,sha256=9mywwxlc_-jXBftomOxRYLFiS3DvzmV0KDn_6q8I824,77165
|
|
91
92
|
pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
93
|
pages/admin.py,sha256=VbxkwgjrFx7lXVhwbZuPSvCkS7EC-flBpwOztHejWtE,35834
|
|
93
94
|
pages/apps.py,sha256=0qcTFKVX9_QgqexJtGeph1sHRqq7khJf4x5ZtkWwblg,1424
|
|
@@ -95,16 +96,16 @@ pages/checks.py,sha256=sM8_hUVM_HOIocvtTb2sY3AaSEvbTnOlO46UchGVd-0,1527
|
|
|
95
96
|
pages/context_processors.py,sha256=vrgMu4vYCOonZ8eZ27gQvGU74PBpMi47T512Lu1__sA,5297
|
|
96
97
|
pages/defaults.py,sha256=3tjv3nFPxwpFu6poJ1Ez1MP92Q6ZvyRluftKHlU-zeI,522
|
|
97
98
|
pages/forms.py,sha256=r3JM5qp3_4RR01-u6XV8WDOaeiRe4OvCN8Y52FcsAwI,7909
|
|
98
|
-
pages/middleware.py,sha256
|
|
99
|
+
pages/middleware.py,sha256=-tXFju1siXvzVsHcgjClfTtryw-5-PwW0171DQQxKu4,7115
|
|
99
100
|
pages/models.py,sha256=9LdIoIK2Epp3YDUk8LUWyhLW5pJ-NiuYTzO_-xKjg0c,23636
|
|
100
101
|
pages/module_defaults.py,sha256=rCAY8aTyxYNL0M5zDr393rX-Gi-svXqKtuLXm0rILrQ,5444
|
|
101
102
|
pages/site_config.py,sha256=f1Me0GFdHeGbIeyMlQNzD2e6hym59YHqbz92U_ppffY,4057
|
|
102
103
|
pages/tasks.py,sha256=ivcba_3wSQ1-cku0oDplzw6vLeQ9hBq3R4TG-LmR5gs,1913
|
|
103
|
-
pages/tests.py,sha256=
|
|
104
|
+
pages/tests.py,sha256=_bVEijMfjVq46hNeGRDZprbQXAAUyS2LhXqZ5_Tkryg,155739
|
|
104
105
|
pages/urls.py,sha256=Oe88tm67iVHRFcGJLSBidZ0rkRQPRZ_vRt6ahxNqPek,1499
|
|
105
|
-
pages/utils.py,sha256=
|
|
106
|
-
pages/views.py,sha256=
|
|
107
|
-
arthexis-0.1.
|
|
108
|
-
arthexis-0.1.
|
|
109
|
-
arthexis-0.1.
|
|
110
|
-
arthexis-0.1.
|
|
106
|
+
pages/utils.py,sha256=vEFrXSzN-3wsK2H687_oVKSwsSOP_NB7DXg1hHwHink,2471
|
|
107
|
+
pages/views.py,sha256=Yd7JRD0OQhhvYsYZLVDUxJz9zjba84jLmyhZ1K1RE0w,65286
|
|
108
|
+
arthexis-0.1.25.dist-info/METADATA,sha256=JNWsyBTUNjpgTb39j2uq8Dcta-N6V0Ntb3TlXglnxak,13987
|
|
109
|
+
arthexis-0.1.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
110
|
+
arthexis-0.1.25.dist-info/top_level.txt,sha256=J2a2q8_BWrCZ8H2WFUNMBfO2jz8j2gax6zZh-_1QDac,29
|
|
111
|
+
arthexis-0.1.25.dist-info/RECORD,,
|
config/settings.py
CHANGED
|
@@ -18,6 +18,8 @@ import ipaddress
|
|
|
18
18
|
import socket
|
|
19
19
|
from core.log_paths import select_log_dir
|
|
20
20
|
from django.utils.translation import gettext_lazy as _
|
|
21
|
+
from datetime import timedelta
|
|
22
|
+
|
|
21
23
|
from celery.schedules import crontab
|
|
22
24
|
from django.http import request as http_request
|
|
23
25
|
from django.http.request import split_domain_port
|
|
@@ -333,6 +335,7 @@ CsrfViewMiddleware._check_referer = _check_referer_with_forwarded
|
|
|
333
335
|
# Application definition
|
|
334
336
|
|
|
335
337
|
LOCAL_APPS = [
|
|
338
|
+
"api",
|
|
336
339
|
"nodes",
|
|
337
340
|
"core",
|
|
338
341
|
"ocpp",
|
|
@@ -666,8 +669,8 @@ CELERY_BEAT_SCHEDULE = {
|
|
|
666
669
|
"task": "ocpp.tasks.schedule_daily_charge_point_configuration_checks",
|
|
667
670
|
"schedule": crontab(minute=0, hour=0),
|
|
668
671
|
},
|
|
669
|
-
"
|
|
670
|
-
"task": "ocpp.tasks.
|
|
671
|
-
"schedule":
|
|
672
|
+
"ocpp_forwarding_push": {
|
|
673
|
+
"task": "ocpp.tasks.push_forwarded_charge_points",
|
|
674
|
+
"schedule": timedelta(seconds=5),
|
|
672
675
|
},
|
|
673
676
|
}
|
config/urls.py
CHANGED
|
@@ -20,6 +20,7 @@ from django.views.decorators.csrf import csrf_exempt
|
|
|
20
20
|
from django.views.generic import RedirectView
|
|
21
21
|
from django.views.i18n import set_language
|
|
22
22
|
from django.utils.translation import gettext_lazy as _
|
|
23
|
+
from api.views import EnergyGraphQLView
|
|
23
24
|
from core import views as core_views
|
|
24
25
|
from core.admindocs import (
|
|
25
26
|
CommandsView,
|
|
@@ -119,6 +120,7 @@ urlpatterns = [
|
|
|
119
120
|
name="admin-model-graph",
|
|
120
121
|
),
|
|
121
122
|
path("version/", core_views.version_info, name="version-info"),
|
|
123
|
+
path("graphql/", EnergyGraphQLView.as_view(), name="graphql"),
|
|
122
124
|
path(
|
|
123
125
|
"admin/core/releases/<int:pk>/<str:action>/",
|
|
124
126
|
core_views.release_progress,
|
core/admin.py
CHANGED
|
@@ -2266,199 +2266,15 @@ class ProductAdminForm(forms.ModelForm):
|
|
|
2266
2266
|
widgets = {"odoo_product": OdooProductWidget}
|
|
2267
2267
|
|
|
2268
2268
|
|
|
2269
|
-
class ProductFetchWizardForm(forms.Form):
|
|
2270
|
-
name = forms.CharField(label="Name", required=False)
|
|
2271
|
-
default_code = forms.CharField(label="Internal reference", required=False)
|
|
2272
|
-
barcode = forms.CharField(label="Barcode", required=False)
|
|
2273
|
-
renewal_period = forms.IntegerField(
|
|
2274
|
-
label="Renewal period (days)", min_value=1, initial=30
|
|
2275
|
-
)
|
|
2276
|
-
|
|
2277
|
-
def __init__(self, *args, require_search_terms=True, **kwargs):
|
|
2278
|
-
self.require_search_terms = require_search_terms
|
|
2279
|
-
super().__init__(*args, **kwargs)
|
|
2280
|
-
|
|
2281
|
-
def clean(self):
|
|
2282
|
-
cleaned = super().clean()
|
|
2283
|
-
if self.require_search_terms:
|
|
2284
|
-
if not any(
|
|
2285
|
-
cleaned.get(field) for field in ("name", "default_code", "barcode")
|
|
2286
|
-
):
|
|
2287
|
-
raise forms.ValidationError(
|
|
2288
|
-
_("Enter at least one field to search for a product.")
|
|
2289
|
-
)
|
|
2290
|
-
return cleaned
|
|
2291
|
-
|
|
2292
|
-
def build_domain(self):
|
|
2293
|
-
domain = []
|
|
2294
|
-
if self.cleaned_data.get("name"):
|
|
2295
|
-
domain.append(("name", "ilike", self.cleaned_data["name"]))
|
|
2296
|
-
if self.cleaned_data.get("default_code"):
|
|
2297
|
-
domain.append(("default_code", "ilike", self.cleaned_data["default_code"]))
|
|
2298
|
-
if self.cleaned_data.get("barcode"):
|
|
2299
|
-
domain.append(("barcode", "ilike", self.cleaned_data["barcode"]))
|
|
2300
|
-
return domain
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
2269
|
@admin.register(Product)
|
|
2304
2270
|
class ProductAdmin(EntityModelAdmin):
|
|
2305
2271
|
form = ProductAdminForm
|
|
2306
|
-
actions = ["
|
|
2272
|
+
actions = ["register_from_odoo"]
|
|
2307
2273
|
change_list_template = "admin/core/product/change_list.html"
|
|
2308
2274
|
|
|
2309
2275
|
def _odoo_profile_admin(self):
|
|
2310
2276
|
return self.admin_site._registry.get(OdooProfile)
|
|
2311
2277
|
|
|
2312
|
-
def _search_odoo_products(self, profile, form):
|
|
2313
|
-
domain = form.build_domain()
|
|
2314
|
-
return profile.execute(
|
|
2315
|
-
"product.product",
|
|
2316
|
-
"search_read",
|
|
2317
|
-
[domain],
|
|
2318
|
-
fields=[
|
|
2319
|
-
"name",
|
|
2320
|
-
"default_code",
|
|
2321
|
-
"barcode",
|
|
2322
|
-
"description_sale",
|
|
2323
|
-
],
|
|
2324
|
-
limit=50,
|
|
2325
|
-
)
|
|
2326
|
-
|
|
2327
|
-
@admin.action(description="Fetch Odoo Product")
|
|
2328
|
-
def fetch_odoo_product(self, request, queryset):
|
|
2329
|
-
profile = getattr(request.user, "odoo_profile", None)
|
|
2330
|
-
has_credentials = bool(profile and profile.is_verified)
|
|
2331
|
-
profile_admin = self._odoo_profile_admin()
|
|
2332
|
-
profile_url = None
|
|
2333
|
-
if profile_admin is not None:
|
|
2334
|
-
profile_url = profile_admin.get_my_profile_url(request)
|
|
2335
|
-
|
|
2336
|
-
context = {
|
|
2337
|
-
"opts": self.model._meta,
|
|
2338
|
-
"queryset": queryset,
|
|
2339
|
-
"action": "fetch_odoo_product",
|
|
2340
|
-
"has_credentials": has_credentials,
|
|
2341
|
-
"profile_url": profile_url,
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
if not has_credentials:
|
|
2345
|
-
context["credential_error"] = _(
|
|
2346
|
-
"Configure your Odoo employee credentials before fetching products."
|
|
2347
|
-
)
|
|
2348
|
-
return TemplateResponse(
|
|
2349
|
-
request, "admin/core/product/fetch_odoo.html", context
|
|
2350
|
-
)
|
|
2351
|
-
|
|
2352
|
-
is_import = "import" in request.POST
|
|
2353
|
-
form_kwargs = {"require_search_terms": not is_import}
|
|
2354
|
-
if request.method == "POST":
|
|
2355
|
-
form = ProductFetchWizardForm(request.POST, **form_kwargs)
|
|
2356
|
-
else:
|
|
2357
|
-
form = ProductFetchWizardForm()
|
|
2358
|
-
|
|
2359
|
-
results = None
|
|
2360
|
-
selected_product_id = request.POST.get("product_id", "")
|
|
2361
|
-
|
|
2362
|
-
if request.method == "POST" and form.is_valid():
|
|
2363
|
-
try:
|
|
2364
|
-
results = self._search_odoo_products(profile, form)
|
|
2365
|
-
except Exception:
|
|
2366
|
-
logger.exception(
|
|
2367
|
-
"Failed to fetch Odoo products for user %s (profile_id=%s, host=%s, database=%s)",
|
|
2368
|
-
getattr(getattr(request, "user", None), "pk", None),
|
|
2369
|
-
getattr(profile, "pk", None),
|
|
2370
|
-
getattr(profile, "host", None),
|
|
2371
|
-
getattr(profile, "database", None),
|
|
2372
|
-
)
|
|
2373
|
-
form.add_error(None, _("Unable to fetch products from Odoo."))
|
|
2374
|
-
results = []
|
|
2375
|
-
else:
|
|
2376
|
-
if is_import:
|
|
2377
|
-
if not self.has_add_permission(request):
|
|
2378
|
-
form.add_error(
|
|
2379
|
-
None, _("You do not have permission to add products.")
|
|
2380
|
-
)
|
|
2381
|
-
else:
|
|
2382
|
-
product_id = request.POST.get("product_id")
|
|
2383
|
-
if not product_id:
|
|
2384
|
-
form.add_error(None, _("Select a product to import."))
|
|
2385
|
-
else:
|
|
2386
|
-
try:
|
|
2387
|
-
odoo_id = int(product_id)
|
|
2388
|
-
except (TypeError, ValueError):
|
|
2389
|
-
form.add_error(None, _("Invalid product selection."))
|
|
2390
|
-
else:
|
|
2391
|
-
match = next(
|
|
2392
|
-
(item for item in results if item.get("id") == odoo_id),
|
|
2393
|
-
None,
|
|
2394
|
-
)
|
|
2395
|
-
if not match:
|
|
2396
|
-
form.add_error(
|
|
2397
|
-
None,
|
|
2398
|
-
_(
|
|
2399
|
-
"The selected product was not found. Run the search again."
|
|
2400
|
-
),
|
|
2401
|
-
)
|
|
2402
|
-
else:
|
|
2403
|
-
existing = self.model.objects.filter(
|
|
2404
|
-
odoo_product__id=odoo_id
|
|
2405
|
-
).first()
|
|
2406
|
-
if existing:
|
|
2407
|
-
self.message_user(
|
|
2408
|
-
request,
|
|
2409
|
-
_(
|
|
2410
|
-
"Product %(name)s already imported; opening existing record."
|
|
2411
|
-
)
|
|
2412
|
-
% {"name": existing.name},
|
|
2413
|
-
level=messages.WARNING,
|
|
2414
|
-
)
|
|
2415
|
-
return HttpResponseRedirect(
|
|
2416
|
-
reverse(
|
|
2417
|
-
"admin:%s_%s_change"
|
|
2418
|
-
% (
|
|
2419
|
-
existing._meta.app_label,
|
|
2420
|
-
existing._meta.model_name,
|
|
2421
|
-
),
|
|
2422
|
-
args=[existing.pk],
|
|
2423
|
-
)
|
|
2424
|
-
)
|
|
2425
|
-
product = self.model.objects.create(
|
|
2426
|
-
name=match.get("name") or f"Odoo Product {odoo_id}",
|
|
2427
|
-
description=match.get("description_sale", "") or "",
|
|
2428
|
-
renewal_period=form.cleaned_data["renewal_period"],
|
|
2429
|
-
odoo_product={
|
|
2430
|
-
"id": odoo_id,
|
|
2431
|
-
"name": match.get("name", ""),
|
|
2432
|
-
},
|
|
2433
|
-
)
|
|
2434
|
-
self.log_addition(
|
|
2435
|
-
request, product, "Imported product from Odoo"
|
|
2436
|
-
)
|
|
2437
|
-
self.message_user(
|
|
2438
|
-
request,
|
|
2439
|
-
_("Imported %(name)s from Odoo.")
|
|
2440
|
-
% {"name": product.name},
|
|
2441
|
-
)
|
|
2442
|
-
return HttpResponseRedirect(
|
|
2443
|
-
reverse(
|
|
2444
|
-
"admin:%s_%s_change"
|
|
2445
|
-
% (
|
|
2446
|
-
product._meta.app_label,
|
|
2447
|
-
product._meta.model_name,
|
|
2448
|
-
),
|
|
2449
|
-
args=[product.pk],
|
|
2450
|
-
)
|
|
2451
|
-
)
|
|
2452
|
-
context.update(
|
|
2453
|
-
{
|
|
2454
|
-
"form": form,
|
|
2455
|
-
"results": results,
|
|
2456
|
-
"selected_product_id": selected_product_id,
|
|
2457
|
-
}
|
|
2458
|
-
)
|
|
2459
|
-
context["media"] = self.media + form.media
|
|
2460
|
-
return TemplateResponse(request, "admin/core/product/fetch_odoo.html", context)
|
|
2461
|
-
|
|
2462
2278
|
def get_urls(self):
|
|
2463
2279
|
urls = super().get_urls()
|
|
2464
2280
|
custom = [
|
|
@@ -2507,7 +2323,6 @@ class ProductAdmin(EntityModelAdmin):
|
|
|
2507
2323
|
products = profile.execute(
|
|
2508
2324
|
"product.product",
|
|
2509
2325
|
"search_read",
|
|
2510
|
-
[[]],
|
|
2511
2326
|
fields=[
|
|
2512
2327
|
"name",
|
|
2513
2328
|
"description_sale",
|
core/backends.py
CHANGED
|
@@ -217,7 +217,9 @@ class LocalhostAdminBackend(ModelBackend):
|
|
|
217
217
|
try:
|
|
218
218
|
ipaddress.ip_address(host)
|
|
219
219
|
except ValueError:
|
|
220
|
-
if
|
|
220
|
+
if host.lower() == "localhost":
|
|
221
|
+
host = "127.0.0.1"
|
|
222
|
+
elif not self._is_test_environment(request):
|
|
221
223
|
return None
|
|
222
224
|
forwarded = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
223
225
|
if forwarded:
|
core/models.py
CHANGED
|
@@ -634,11 +634,21 @@ class OdooProfile(Profile):
|
|
|
634
634
|
"""Return the display label for this profile."""
|
|
635
635
|
|
|
636
636
|
username = self._resolved_field_value("username")
|
|
637
|
+
database = self._resolved_field_value("database")
|
|
638
|
+
if username and database:
|
|
639
|
+
return f"{username}@{database}"
|
|
637
640
|
if username:
|
|
638
641
|
return username
|
|
639
|
-
database = self._resolved_field_value("database")
|
|
640
642
|
return database or ""
|
|
641
643
|
|
|
644
|
+
def _profile_name(self) -> str:
|
|
645
|
+
"""Return the stored name for this profile without database suffix."""
|
|
646
|
+
|
|
647
|
+
username = self._resolved_field_value("username")
|
|
648
|
+
if username:
|
|
649
|
+
return username
|
|
650
|
+
return self._resolved_field_value("database")
|
|
651
|
+
|
|
642
652
|
def save(self, *args, **kwargs):
|
|
643
653
|
if self.pk:
|
|
644
654
|
old = type(self).all_objects.get(pk=self.pk)
|
|
@@ -649,7 +659,7 @@ class OdooProfile(Profile):
|
|
|
649
659
|
or old.host != self.host
|
|
650
660
|
):
|
|
651
661
|
self._clear_verification()
|
|
652
|
-
computed_name = self.
|
|
662
|
+
computed_name = self._profile_name()
|
|
653
663
|
update_fields = kwargs.get("update_fields")
|
|
654
664
|
update_fields_set = set(update_fields) if update_fields is not None else None
|
|
655
665
|
if computed_name != self.name:
|
|
@@ -684,6 +694,7 @@ class OdooProfile(Profile):
|
|
|
684
694
|
self.odoo_uid = uid
|
|
685
695
|
self.email = info.get("email", "")
|
|
686
696
|
self.verified_on = timezone.now()
|
|
697
|
+
self.name = self._profile_name()
|
|
687
698
|
self.save(update_fields=["odoo_uid", "name", "email", "verified_on"])
|
|
688
699
|
return True
|
|
689
700
|
|
|
@@ -3541,6 +3552,31 @@ class ClientReport(Entity):
|
|
|
3541
3552
|
|
|
3542
3553
|
return start_value, end_value
|
|
3543
3554
|
|
|
3555
|
+
@staticmethod
|
|
3556
|
+
def _format_session_datetime(value):
|
|
3557
|
+
if not value:
|
|
3558
|
+
return None
|
|
3559
|
+
localized = timezone.localtime(value)
|
|
3560
|
+
date_part = formats.date_format(
|
|
3561
|
+
localized, format="MONTH_DAY_FORMAT", use_l10n=True
|
|
3562
|
+
)
|
|
3563
|
+
time_part = formats.time_format(
|
|
3564
|
+
localized, format="TIME_FORMAT", use_l10n=True
|
|
3565
|
+
)
|
|
3566
|
+
return gettext("%(date)s, %(time)s") % {
|
|
3567
|
+
"date": date_part,
|
|
3568
|
+
"time": time_part,
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
@staticmethod
|
|
3572
|
+
def _calculate_duration_minutes(start, end):
|
|
3573
|
+
if not start or not end:
|
|
3574
|
+
return None
|
|
3575
|
+
total_seconds = (end - start).total_seconds()
|
|
3576
|
+
if total_seconds < 0:
|
|
3577
|
+
return None
|
|
3578
|
+
return int(round(total_seconds / 60.0))
|
|
3579
|
+
|
|
3544
3580
|
@staticmethod
|
|
3545
3581
|
def _normalize_dataset_for_display(dataset: dict[str, Any]):
|
|
3546
3582
|
schema = dataset.get("schema")
|
|
@@ -3576,6 +3612,15 @@ class ClientReport(Entity):
|
|
|
3576
3612
|
"session_kwh": row.get("session_kwh"),
|
|
3577
3613
|
"start": start_dt,
|
|
3578
3614
|
"end": end_dt,
|
|
3615
|
+
"start_display": ClientReport._format_session_datetime(
|
|
3616
|
+
start_dt
|
|
3617
|
+
),
|
|
3618
|
+
"end_display": ClientReport._format_session_datetime(
|
|
3619
|
+
end_dt
|
|
3620
|
+
),
|
|
3621
|
+
"duration_minutes": ClientReport._calculate_duration_minutes(
|
|
3622
|
+
start_dt, end_dt
|
|
3623
|
+
),
|
|
3579
3624
|
}
|
|
3580
3625
|
)
|
|
3581
3626
|
|
|
@@ -3624,6 +3669,7 @@ class ClientReport(Entity):
|
|
|
3624
3669
|
start_dt = timezone.make_aware(start_dt, timezone.utc)
|
|
3625
3670
|
item["start"] = start_dt
|
|
3626
3671
|
else:
|
|
3672
|
+
start_dt = None
|
|
3627
3673
|
item["start"] = None
|
|
3628
3674
|
|
|
3629
3675
|
if end_val:
|
|
@@ -3632,8 +3678,15 @@ class ClientReport(Entity):
|
|
|
3632
3678
|
end_dt = timezone.make_aware(end_dt, timezone.utc)
|
|
3633
3679
|
item["end"] = end_dt
|
|
3634
3680
|
else:
|
|
3681
|
+
end_dt = None
|
|
3635
3682
|
item["end"] = None
|
|
3636
3683
|
|
|
3684
|
+
item["start_display"] = ClientReport._format_session_datetime(start_dt)
|
|
3685
|
+
item["end_display"] = ClientReport._format_session_datetime(end_dt)
|
|
3686
|
+
item["duration_minutes"] = ClientReport._calculate_duration_minutes(
|
|
3687
|
+
start_dt, end_dt
|
|
3688
|
+
)
|
|
3689
|
+
|
|
3637
3690
|
parsed.append(item)
|
|
3638
3691
|
|
|
3639
3692
|
return {"schema": schema, "rows": parsed}
|
|
@@ -3749,7 +3802,8 @@ class ClientReport(Entity):
|
|
|
3749
3802
|
total_kw_period_label = gettext("Total kW (period)")
|
|
3750
3803
|
connector_label = gettext("Connector")
|
|
3751
3804
|
account_label = gettext("Account")
|
|
3752
|
-
session_kwh_label = gettext("Session
|
|
3805
|
+
session_kwh_label = gettext("Session kW")
|
|
3806
|
+
time_label = gettext("Time")
|
|
3753
3807
|
no_sessions_period = gettext(
|
|
3754
3808
|
"No charging sessions recorded for the selected period."
|
|
3755
3809
|
)
|
|
@@ -3765,16 +3819,18 @@ class ClientReport(Entity):
|
|
|
3765
3819
|
def format_datetime(value):
|
|
3766
3820
|
if not value:
|
|
3767
3821
|
return "—"
|
|
3768
|
-
|
|
3769
|
-
return formats.date_format(
|
|
3770
|
-
localized, format="DATETIME_FORMAT", use_l10n=True
|
|
3771
|
-
)
|
|
3822
|
+
return ClientReport._format_session_datetime(value) or "—"
|
|
3772
3823
|
|
|
3773
3824
|
def format_decimal(value):
|
|
3774
3825
|
if value is None:
|
|
3775
3826
|
return "—"
|
|
3776
3827
|
return formats.number_format(value, decimal_pos=2, use_l10n=True)
|
|
3777
3828
|
|
|
3829
|
+
def format_duration(value):
|
|
3830
|
+
if value is None:
|
|
3831
|
+
return "—"
|
|
3832
|
+
return formats.number_format(value, decimal_pos=0, use_l10n=True)
|
|
3833
|
+
|
|
3778
3834
|
if schema == "evcs-session/v1":
|
|
3779
3835
|
evcs_entries = dataset.get("evcs", [])
|
|
3780
3836
|
if not evcs_entries:
|
|
@@ -3810,6 +3866,7 @@ class ClientReport(Entity):
|
|
|
3810
3866
|
session_kwh_label,
|
|
3811
3867
|
gettext("Session start"),
|
|
3812
3868
|
gettext("Session end"),
|
|
3869
|
+
time_label,
|
|
3813
3870
|
connector_label,
|
|
3814
3871
|
gettext("RFID label"),
|
|
3815
3872
|
account_label,
|
|
@@ -3819,11 +3876,13 @@ class ClientReport(Entity):
|
|
|
3819
3876
|
for row in transactions:
|
|
3820
3877
|
start_dt = row.get("start")
|
|
3821
3878
|
end_dt = row.get("end")
|
|
3879
|
+
duration_value = row.get("duration_minutes")
|
|
3822
3880
|
table_data.append(
|
|
3823
3881
|
[
|
|
3824
3882
|
format_decimal(row.get("session_kwh")),
|
|
3825
3883
|
format_datetime(start_dt),
|
|
3826
3884
|
format_datetime(end_dt),
|
|
3885
|
+
format_duration(duration_value),
|
|
3827
3886
|
row.get("connector")
|
|
3828
3887
|
if row.get("connector") is not None
|
|
3829
3888
|
else "—",
|
|
@@ -3832,7 +3891,14 @@ class ClientReport(Entity):
|
|
|
3832
3891
|
]
|
|
3833
3892
|
)
|
|
3834
3893
|
|
|
3835
|
-
|
|
3894
|
+
column_count = len(table_data[0])
|
|
3895
|
+
col_width = document.width / column_count if column_count else None
|
|
3896
|
+
table = Table(
|
|
3897
|
+
table_data,
|
|
3898
|
+
repeatRows=1,
|
|
3899
|
+
colWidths=[col_width] * column_count if col_width else None,
|
|
3900
|
+
hAlign="LEFT",
|
|
3901
|
+
)
|
|
3836
3902
|
table.setStyle(
|
|
3837
3903
|
TableStyle(
|
|
3838
3904
|
[
|