gridworks-admin 1.0.0.dev4__tar.gz → 1.0.0.dev6__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.
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/PKG-INFO +1 -1
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/pyproject.toml +1 -1
- gridworks_admin-1.0.0.dev6/src/gwadmin/cli.py +441 -0
- gridworks_admin-1.0.0.dev6/src/gwadmin/config.py +89 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/admin_client.py +68 -12
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/constrained_mqtt_client.py +11 -2
- gridworks_admin-1.0.0.dev6/src/gwadmin/watch/clients/dac_client.py +319 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/relay_client.py +18 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/relay_app.py +77 -17
- gridworks_admin-1.0.0.dev6/src/gwadmin/watch/relay_app.tcss +107 -0
- gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets/dac_widget_info.py +86 -0
- gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets/dacs.py +166 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/keepalive.py +6 -4
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_toggle_button.py +6 -4
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relays.py +37 -9
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/time_input.py +5 -3
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/timer.py +9 -5
- gridworks_admin-1.0.0.dev4/src/gwadmin/cli.py +0 -167
- gridworks_admin-1.0.0.dev4/src/gwadmin/settings.py +0 -31
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/actions.py +0 -46
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/actions05.tcss +0 -20
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/cli.py +0 -39
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/stopwatch.py +0 -109
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/stopwatch.tcss +0 -51
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/switch.py +0 -42
- gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/switch.tcss +0 -28
- gridworks_admin-1.0.0.dev4/src/gwadmin/watch/relay_app.tcss +0 -52
- gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/__init__.py +0 -0
- gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/watchex_app.py +0 -49
- gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/watchex_app.tcss +0 -0
- gridworks_admin-1.0.0.dev4/src/gwadmin/watch/widgets/__init__.py +0 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/README.md +0 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/__init__.py +0 -0
- {gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo → gridworks_admin-1.0.0.dev6/src/gwadmin/watch}/__init__.py +0 -0
- {gridworks_admin-1.0.0.dev4/src/gwadmin/watch → gridworks_admin-1.0.0.dev6/src/gwadmin/watch/clients}/__init__.py +0 -0
- {gridworks_admin-1.0.0.dev4/src/gwadmin/watch/clients → gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets}/__init__.py +0 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/mqtt.py +0 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_state_text.py +0 -0
- {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_widget_info.py +0 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import rich
|
|
9
|
+
import typer
|
|
10
|
+
from dotenv import dotenv_values
|
|
11
|
+
from gwproactor.config.mqtt import TLSInfo
|
|
12
|
+
from pydantic import SecretStr
|
|
13
|
+
|
|
14
|
+
from gwadmin.config import AdminConfig
|
|
15
|
+
from gwadmin.config import AdminPaths
|
|
16
|
+
from gwadmin.config import CurrentAdminConfig
|
|
17
|
+
from gwadmin.config import AdminMQTTClient
|
|
18
|
+
from gwadmin.watch.relay_app import RelaysApp, __version__
|
|
19
|
+
from gwsproto.data_classes.house_0_names import H0N
|
|
20
|
+
|
|
21
|
+
CONFIG_ENV_VAR = "GWADMIN_CONFIG_NAME"
|
|
22
|
+
|
|
23
|
+
DEFAULT_ADMIN_NAME = H0N.admin
|
|
24
|
+
|
|
25
|
+
ENV_FILE_HELP_TEXT = "Optional path to a .env file used to control configuration name."
|
|
26
|
+
CONFIG_NAME_HELP_TEXT = (
|
|
27
|
+
"The subdirectory in $HOME/.config, $HOME/.local/share and "
|
|
28
|
+
"$HOME/.local/state used to store configuration and other Admin "
|
|
29
|
+
"information. The value is read from the first of these sources found, "
|
|
30
|
+
"in order: 1) The --config-name command line option; "
|
|
31
|
+
f"2) The environment variable {CONFIG_ENV_VAR}; "
|
|
32
|
+
f"3) The default value, '{DEFAULT_ADMIN_NAME}'."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
app = typer.Typer(
|
|
36
|
+
no_args_is_help=True,
|
|
37
|
+
pretty_exceptions_enable=False,
|
|
38
|
+
rich_markup_mode="rich",
|
|
39
|
+
help=f"GridWorks Scada Admin Client, version {__version__}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_config_name(env_file: str = "", config_name: Optional[str] = None) -> str:
|
|
43
|
+
if config_name is None:
|
|
44
|
+
if CONFIG_ENV_VAR in os.environ and os.environ[CONFIG_ENV_VAR]:
|
|
45
|
+
config_name = os.environ[CONFIG_ENV_VAR]
|
|
46
|
+
elif env_file and Path(env_file).exists():
|
|
47
|
+
config_name = dotenv_values(Path(env_file).resolve()).get(CONFIG_ENV_VAR, DEFAULT_ADMIN_NAME)
|
|
48
|
+
else:
|
|
49
|
+
config_name = DEFAULT_ADMIN_NAME
|
|
50
|
+
return config_name
|
|
51
|
+
|
|
52
|
+
def available_scadas(admin_config: AdminConfig) -> str:
|
|
53
|
+
available_scadas_str = ""
|
|
54
|
+
for i, existing_scada in enumerate(admin_config.scadas):
|
|
55
|
+
available_scadas_str += f"'{existing_scada}'"
|
|
56
|
+
if i < len(admin_config.scadas) - 1:
|
|
57
|
+
available_scadas_str += ", "
|
|
58
|
+
return available_scadas_str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_admin_config(
|
|
62
|
+
*,
|
|
63
|
+
config_name: Optional[str] = None,
|
|
64
|
+
env_file: str = "",
|
|
65
|
+
verbose: int = 0,
|
|
66
|
+
paho_verbose: int = 0,
|
|
67
|
+
show_clock: Optional[bool] = None,
|
|
68
|
+
show_footer: Optional[bool] = None,
|
|
69
|
+
default_scada: Optional[str] = None,
|
|
70
|
+
use_last_scada: Optional[bool] = None,
|
|
71
|
+
default_timeout_seconds: Optional[int] = None,
|
|
72
|
+
) -> CurrentAdminConfig:
|
|
73
|
+
paths = AdminPaths(name=get_config_name(env_file=env_file, config_name=config_name))
|
|
74
|
+
if not paths.admin_config_path.exists():
|
|
75
|
+
admin_config = AdminConfig()
|
|
76
|
+
else:
|
|
77
|
+
with paths.admin_config_path.open() as f:
|
|
78
|
+
json_data = f.read()
|
|
79
|
+
admin_config = AdminConfig.model_validate_json(json_data)
|
|
80
|
+
if verbose:
|
|
81
|
+
if verbose == 0:
|
|
82
|
+
verbosity = logging.INFO
|
|
83
|
+
else:
|
|
84
|
+
verbosity = logging.DEBUG
|
|
85
|
+
admin_config.verbosity = verbosity
|
|
86
|
+
if paho_verbose:
|
|
87
|
+
if paho_verbose == 0:
|
|
88
|
+
paho_verbosity = logging.INFO
|
|
89
|
+
else:
|
|
90
|
+
paho_verbosity = logging.DEBUG
|
|
91
|
+
admin_config.paho_verbosity = paho_verbosity
|
|
92
|
+
if show_footer is not None:
|
|
93
|
+
admin_config.show_footer = show_footer
|
|
94
|
+
if show_clock is not None:
|
|
95
|
+
admin_config.show_clock = show_clock
|
|
96
|
+
if default_scada is not None:
|
|
97
|
+
admin_config.default_scada = default_scada
|
|
98
|
+
# if not admin_config.default_scada in admin_config.scadas:
|
|
99
|
+
# rich.print(
|
|
100
|
+
# f"[yellow][bold]Default scada '{admin_config.default_scada}' does not exist[/yellow][/bold] "
|
|
101
|
+
# f"in config. Available scadas: {available_scadas(admin_config)}""."
|
|
102
|
+
# )
|
|
103
|
+
# raise typer.Exit(-1)
|
|
104
|
+
#
|
|
105
|
+
if use_last_scada is not None:
|
|
106
|
+
admin_config.use_last_scada = use_last_scada
|
|
107
|
+
if default_timeout_seconds is not None:
|
|
108
|
+
admin_config.default_timeout_seconds = default_timeout_seconds
|
|
109
|
+
return CurrentAdminConfig(
|
|
110
|
+
paths=paths,
|
|
111
|
+
config=admin_config,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class RelayState(StrEnum):
|
|
116
|
+
open = "0"
|
|
117
|
+
closed = "1"
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def watch(
|
|
121
|
+
scada: Annotated[
|
|
122
|
+
str,
|
|
123
|
+
typer.Argument(
|
|
124
|
+
help="Short, human-friendly name of the scada configuration to use.",
|
|
125
|
+
),
|
|
126
|
+
] = "",
|
|
127
|
+
*,
|
|
128
|
+
verbose: Annotated[
|
|
129
|
+
int,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--verbose", "-v", count=True, help=(
|
|
132
|
+
"Increase logging verbosity. Maybe specified more than once"
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
] = 0,
|
|
136
|
+
paho_verbose: Annotated[
|
|
137
|
+
int,
|
|
138
|
+
typer.Option(
|
|
139
|
+
"--paho-verbose", count=True,
|
|
140
|
+
help="Enable raw paho.mqtt logging",
|
|
141
|
+
)
|
|
142
|
+
] = 0,
|
|
143
|
+
show_clock: Annotated[
|
|
144
|
+
Optional[bool],
|
|
145
|
+
typer.Option(
|
|
146
|
+
"--show-clock",
|
|
147
|
+
show_default=False,
|
|
148
|
+
help="Show the clock in the title bar."
|
|
149
|
+
),
|
|
150
|
+
] = None,
|
|
151
|
+
show_footer: Annotated[
|
|
152
|
+
Optional[bool],
|
|
153
|
+
typer.Option(
|
|
154
|
+
"--show-footer",
|
|
155
|
+
show_default=False,
|
|
156
|
+
help="Show the footer with shortcut keys."
|
|
157
|
+
),
|
|
158
|
+
] = None,
|
|
159
|
+
default_scada: Annotated[
|
|
160
|
+
Optional[str],
|
|
161
|
+
typer.Option(
|
|
162
|
+
"--default-scada",
|
|
163
|
+
show_default=False,
|
|
164
|
+
help="Specify the default scada."
|
|
165
|
+
)
|
|
166
|
+
] = None,
|
|
167
|
+
use_last_scada: Annotated[
|
|
168
|
+
Optional[bool],
|
|
169
|
+
typer.Option(
|
|
170
|
+
"--use-last-scada",
|
|
171
|
+
show_default=False,
|
|
172
|
+
help="Use the scada last selected when watch was run."
|
|
173
|
+
)
|
|
174
|
+
] = None,
|
|
175
|
+
default_timeout_seconds: Annotated[
|
|
176
|
+
Optional[int],
|
|
177
|
+
typer.Option(
|
|
178
|
+
"--default-timeout-seconds",
|
|
179
|
+
show_default=False,
|
|
180
|
+
)
|
|
181
|
+
] = None,
|
|
182
|
+
save: Annotated[
|
|
183
|
+
bool,
|
|
184
|
+
typer.Option(
|
|
185
|
+
"--save",
|
|
186
|
+
help="Save any changes to the configuration produced by command line options."
|
|
187
|
+
)
|
|
188
|
+
] = False,
|
|
189
|
+
config_name: Annotated[
|
|
190
|
+
Optional[str], typer.Option(help=CONFIG_NAME_HELP_TEXT, show_default=False)
|
|
191
|
+
] = None,
|
|
192
|
+
env_file: Annotated[str, typer.Option(help=ENV_FILE_HELP_TEXT)] = "",
|
|
193
|
+
) -> None:
|
|
194
|
+
"""Connect to a GridWorks Scada and watch state information live."""
|
|
195
|
+
current_config = get_admin_config(
|
|
196
|
+
verbose=verbose,
|
|
197
|
+
paho_verbose=paho_verbose,
|
|
198
|
+
show_clock=show_clock,
|
|
199
|
+
show_footer=show_footer,
|
|
200
|
+
default_scada=default_scada,
|
|
201
|
+
use_last_scada=use_last_scada,
|
|
202
|
+
default_timeout_seconds=default_timeout_seconds,
|
|
203
|
+
config_name=config_name,
|
|
204
|
+
env_file=env_file,
|
|
205
|
+
)
|
|
206
|
+
if not scada and current_config.config.use_last_scada:
|
|
207
|
+
scada = current_config.last_scada()
|
|
208
|
+
if not scada:
|
|
209
|
+
scada = current_config.config.default_scada
|
|
210
|
+
if not scada:
|
|
211
|
+
rich.print(
|
|
212
|
+
"[yellow][bold]No scada specified[/yellow][/bold] on command line, "
|
|
213
|
+
"via last-scada-used or in default. "
|
|
214
|
+
"[yellow][bold]Doing nothing.[/yellow][/bold]"
|
|
215
|
+
)
|
|
216
|
+
if not current_config.paths.admin_config_path.exists():
|
|
217
|
+
rich.print(
|
|
218
|
+
f"\nConfig file {current_config.paths.admin_config_path} "
|
|
219
|
+
"does not exist. To create a default configuration run:"
|
|
220
|
+
)
|
|
221
|
+
rich.print("\n [green][bold]gwa mkconfig[/green]")
|
|
222
|
+
rich.print("\nThen, to add configuration for your scada, run:")
|
|
223
|
+
rich.print("\n [green][bold]gwa add-scada[/green]\n")
|
|
224
|
+
raise typer.Exit(2)
|
|
225
|
+
if not scada in current_config.config.scadas:
|
|
226
|
+
rich.print(
|
|
227
|
+
f"[yellow][bold]Specified scada '{scada}' does not exist[/yellow][/bold] "
|
|
228
|
+
f"in config. Available scadas: {available_scadas(current_config.config)}""."
|
|
229
|
+
)
|
|
230
|
+
raise typer.Exit(3)
|
|
231
|
+
current_config.curr_scada = scada
|
|
232
|
+
rich.print(f"Using scada '{scada}'.")
|
|
233
|
+
if current_config.config.use_last_scada:
|
|
234
|
+
current_config.save_curr_scada(scada)
|
|
235
|
+
if save:
|
|
236
|
+
rich.print(f"Saving configuration in {current_config.paths.admin_config_path}")
|
|
237
|
+
current_config.save_config()
|
|
238
|
+
watch_app = RelaysApp(settings=current_config)
|
|
239
|
+
watch_app.run()
|
|
240
|
+
|
|
241
|
+
@app.command()
|
|
242
|
+
def config_file(
|
|
243
|
+
config_name: Annotated[
|
|
244
|
+
Optional[str], typer.Option(help=CONFIG_NAME_HELP_TEXT, show_default=False)
|
|
245
|
+
] = None,
|
|
246
|
+
env_file: Annotated[str, typer.Option(help=ENV_FILE_HELP_TEXT)] = "",
|
|
247
|
+
) -> None:
|
|
248
|
+
"""Show path to admin config file."""
|
|
249
|
+
paths = AdminPaths(name=get_config_name(env_file=env_file, config_name=config_name))
|
|
250
|
+
rich.print(paths.admin_config_path)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@app.command()
|
|
255
|
+
def config(
|
|
256
|
+
verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0,
|
|
257
|
+
paho_verbose: Annotated[int, typer.Option("--paho-verbose", count=True)] = 0,
|
|
258
|
+
show_clock: Annotated[
|
|
259
|
+
Optional[bool],
|
|
260
|
+
typer.Option(
|
|
261
|
+
"--show-clock",
|
|
262
|
+
show_default=False,
|
|
263
|
+
help="Show the clock in the title bar."
|
|
264
|
+
),
|
|
265
|
+
] = None,
|
|
266
|
+
show_footer: Annotated[
|
|
267
|
+
Optional[bool],
|
|
268
|
+
typer.Option(
|
|
269
|
+
"--show-footer",
|
|
270
|
+
show_default=False,
|
|
271
|
+
help="Show the footer with shortcut keys."
|
|
272
|
+
),
|
|
273
|
+
] = None,
|
|
274
|
+
default_scada: Annotated[
|
|
275
|
+
Optional[str],
|
|
276
|
+
typer.Option(
|
|
277
|
+
"--default-scada",
|
|
278
|
+
show_default=False,
|
|
279
|
+
help="Specify the default scada."
|
|
280
|
+
)
|
|
281
|
+
] = None,
|
|
282
|
+
use_last_scada: Annotated[
|
|
283
|
+
Optional[bool],
|
|
284
|
+
typer.Option(
|
|
285
|
+
"--use-last-scada",
|
|
286
|
+
show_default=False,
|
|
287
|
+
help="Use the scada last selected when watch was run."
|
|
288
|
+
)
|
|
289
|
+
] = None,
|
|
290
|
+
default_timeout_seconds: Annotated[
|
|
291
|
+
Optional[int],
|
|
292
|
+
typer.Option(
|
|
293
|
+
"--default-timeout-seconds",
|
|
294
|
+
show_default=False,
|
|
295
|
+
)
|
|
296
|
+
] = None,
|
|
297
|
+
save: Annotated[
|
|
298
|
+
bool,
|
|
299
|
+
typer.Option(
|
|
300
|
+
"--save",
|
|
301
|
+
help="Save any changes to the configuration produced by command line options."
|
|
302
|
+
)
|
|
303
|
+
] = False,
|
|
304
|
+
config_name: Annotated[
|
|
305
|
+
Optional[str], typer.Option(help=CONFIG_NAME_HELP_TEXT, show_default=False)
|
|
306
|
+
] = None,
|
|
307
|
+
env_file: Annotated[str, typer.Option(help=ENV_FILE_HELP_TEXT)] = "",
|
|
308
|
+
) -> None:
|
|
309
|
+
"""Show and, optionally, change the admin configuration."""
|
|
310
|
+
current_config = get_admin_config(
|
|
311
|
+
verbose=verbose,
|
|
312
|
+
paho_verbose=paho_verbose,
|
|
313
|
+
show_clock=show_clock,
|
|
314
|
+
show_footer=show_footer,
|
|
315
|
+
default_scada=default_scada,
|
|
316
|
+
use_last_scada=use_last_scada,
|
|
317
|
+
default_timeout_seconds=default_timeout_seconds,
|
|
318
|
+
config_name=config_name,
|
|
319
|
+
env_file=env_file,
|
|
320
|
+
)
|
|
321
|
+
rich.print(current_config.config)
|
|
322
|
+
if save:
|
|
323
|
+
rich.print(f"Saving configuration in {current_config.paths.admin_config_path}")
|
|
324
|
+
current_config.save_config()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command()
|
|
328
|
+
def mkconfig(
|
|
329
|
+
*,
|
|
330
|
+
config_name: Annotated[
|
|
331
|
+
Optional[str], typer.Option(help=CONFIG_NAME_HELP_TEXT, show_default=False)
|
|
332
|
+
] = None,
|
|
333
|
+
env_file: Annotated[str, typer.Option(help=ENV_FILE_HELP_TEXT)] = "",
|
|
334
|
+
force: Annotated[
|
|
335
|
+
bool,
|
|
336
|
+
typer.Option(
|
|
337
|
+
"--force",
|
|
338
|
+
help="""Overwrites existing configuration file.
|
|
339
|
+
[yellow][bold]WARNING: [/yellow][/bold]--force will [red][bold]PERMANENTLY DELETE[/red][/bold]
|
|
340
|
+
this Admin configuration.""",
|
|
341
|
+
),
|
|
342
|
+
] = False,
|
|
343
|
+
) -> None:
|
|
344
|
+
"""Create a default configuration file."""
|
|
345
|
+
paths = AdminPaths(name=get_config_name(env_file=env_file, config_name=config_name))
|
|
346
|
+
if paths.admin_config_path.exists():
|
|
347
|
+
if not force:
|
|
348
|
+
rich.print(
|
|
349
|
+
f"Configuartion file {paths.admin_config_path} [yellow][bold]already exists. Doing nothing.[/yellow][/bold]"
|
|
350
|
+
)
|
|
351
|
+
rich.print(f"Use --force to overwrite existing configuration.")
|
|
352
|
+
return
|
|
353
|
+
else:
|
|
354
|
+
rich.print(
|
|
355
|
+
f"[yellow][bold]DELETING existing configuration[/yellow][/bold]."
|
|
356
|
+
)
|
|
357
|
+
paths.admin_config_path.unlink()
|
|
358
|
+
rich.print(f"Creating {paths.admin_config_path}")
|
|
359
|
+
paths.mkdirs(parents=True, exist_ok=True)
|
|
360
|
+
with paths.admin_config_path.open(mode="w") as file:
|
|
361
|
+
file.write(AdminConfig().model_dump_json(indent=2))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@app.command()
|
|
365
|
+
def add_scada(
|
|
366
|
+
name: Annotated[
|
|
367
|
+
str, typer.Argument(
|
|
368
|
+
help=(
|
|
369
|
+
"The short, human-friendly name of the scada to add."
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
],
|
|
373
|
+
*,
|
|
374
|
+
long_name: str = "",
|
|
375
|
+
host = "localhost",
|
|
376
|
+
port = 1883,
|
|
377
|
+
username: Optional[str] = None,
|
|
378
|
+
password: Optional[str] = None,
|
|
379
|
+
use_tls: bool = False,
|
|
380
|
+
default: Annotated[
|
|
381
|
+
bool, typer.Option(
|
|
382
|
+
help=(
|
|
383
|
+
"Whether to set this scada as the default scada."
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
] = False,
|
|
387
|
+
config_name: Annotated[
|
|
388
|
+
Optional[str], typer.Option(help=CONFIG_NAME_HELP_TEXT, show_default=False)
|
|
389
|
+
] = None,
|
|
390
|
+
env_file: Annotated[str, typer.Option(help=ENV_FILE_HELP_TEXT)] = "",
|
|
391
|
+
) -> None:
|
|
392
|
+
"""Add configuration to connect to a particular Scada."""
|
|
393
|
+
current_config = get_admin_config(config_name=config_name, env_file=env_file)
|
|
394
|
+
if current_config.add_scada(
|
|
395
|
+
name,
|
|
396
|
+
long_name=long_name,
|
|
397
|
+
mqtt_client_config=AdminMQTTClient(
|
|
398
|
+
host=host,
|
|
399
|
+
port=port,
|
|
400
|
+
username=username,
|
|
401
|
+
password=SecretStr(password),
|
|
402
|
+
tls=TLSInfo(use_tls=use_tls),
|
|
403
|
+
)
|
|
404
|
+
):
|
|
405
|
+
rich.print(f"Adding default configuration for scada '{name}'")
|
|
406
|
+
if len(current_config.config.scadas) == 1 or default:
|
|
407
|
+
current_config.config.default_scada = name
|
|
408
|
+
rich.print(f"Updating config file {current_config.paths.admin_config_path}")
|
|
409
|
+
with current_config.paths.admin_config_path.open(mode="w") as f:
|
|
410
|
+
f.write(current_config.config.model_dump_json(indent=2))
|
|
411
|
+
else:
|
|
412
|
+
rich.print(
|
|
413
|
+
f"Scada with name {name} [yellow][bold]already exists. Doing nothing.[/yellow][/bold]"
|
|
414
|
+
)
|
|
415
|
+
rich.print(
|
|
416
|
+
"Use --force to overwrite existing configuration[/yellow][/bold] or modify config file."
|
|
417
|
+
)
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def version_callback(value: bool):
|
|
422
|
+
if value:
|
|
423
|
+
print(f"gws admin {__version__}")
|
|
424
|
+
raise typer.Exit()
|
|
425
|
+
|
|
426
|
+
@app.callback()
|
|
427
|
+
def _main(
|
|
428
|
+
_version: Annotated[
|
|
429
|
+
Optional[bool],
|
|
430
|
+
typer.Option(
|
|
431
|
+
"--version",
|
|
432
|
+
callback=version_callback,
|
|
433
|
+
is_eager=True,
|
|
434
|
+
help="Show version and exit."
|
|
435
|
+
),
|
|
436
|
+
] = None,
|
|
437
|
+
) -> None: ...
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
app()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from gwproactor.config import MQTTClient
|
|
7
|
+
from gwproactor.config import Paths
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic import field_serializer
|
|
10
|
+
from pydantic_settings import BaseSettings
|
|
11
|
+
from pydantic_settings import SettingsConfigDict
|
|
12
|
+
|
|
13
|
+
MAX_ADMIN_TIMEOUT = 60 * 60 * 24
|
|
14
|
+
DEFAULT_ADMIN_TIMEOUT = 5 * 60
|
|
15
|
+
|
|
16
|
+
class AdminMQTTClient(MQTTClient):
|
|
17
|
+
|
|
18
|
+
@field_serializer("password", when_used="json")
|
|
19
|
+
def dump_secret(self, v):
|
|
20
|
+
return v.get_secret_value()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ScadaConfig(BaseSettings):
|
|
24
|
+
enabled: bool = True
|
|
25
|
+
long_name: str = ""
|
|
26
|
+
mqtt: AdminMQTTClient = AdminMQTTClient()
|
|
27
|
+
|
|
28
|
+
class AdminConfig(BaseModel):
|
|
29
|
+
scadas: dict[str, ScadaConfig] = {}
|
|
30
|
+
default_scada: str = ""
|
|
31
|
+
use_last_scada: bool = False
|
|
32
|
+
verbosity: int = logging.WARN
|
|
33
|
+
paho_verbosity: Optional[int] = None
|
|
34
|
+
show_clock: bool = False
|
|
35
|
+
show_footer: bool = False
|
|
36
|
+
default_timeout_seconds: int = DEFAULT_ADMIN_TIMEOUT
|
|
37
|
+
|
|
38
|
+
class AdminPaths(Paths):
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def admin_config_path(self) -> Path:
|
|
42
|
+
return Path(self.config_dir) / "admin-config.json"
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def last_scada_path(self) -> Path:
|
|
46
|
+
return Path(self.config_dir) / "last-scada.txt"
|
|
47
|
+
|
|
48
|
+
def duplicate(
|
|
49
|
+
self,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> "AdminPaths":
|
|
52
|
+
return AdminPaths(**super().duplicate(**kwargs).model_dump())
|
|
53
|
+
|
|
54
|
+
class AdminSettings(BaseSettings):
|
|
55
|
+
config_name: str = "admin"
|
|
56
|
+
|
|
57
|
+
model_config = SettingsConfigDict(
|
|
58
|
+
env_prefix="GWADMIN_",
|
|
59
|
+
env_nested_delimiter="__",
|
|
60
|
+
extra="ignore",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
class CurrentAdminConfig(BaseModel):
|
|
64
|
+
paths: AdminPaths = AdminPaths()
|
|
65
|
+
config: AdminConfig = AdminConfig()
|
|
66
|
+
curr_scada: str = ""
|
|
67
|
+
|
|
68
|
+
def save_curr_scada(self, scada: str) -> None:
|
|
69
|
+
with self.paths.last_scada_path.open(mode="w") as file:
|
|
70
|
+
file.write(scada)
|
|
71
|
+
|
|
72
|
+
def save_config(self) -> None:
|
|
73
|
+
with self.paths.admin_config_path.open(mode="w") as file:
|
|
74
|
+
file.write(self.config.model_dump_json(indent=2))
|
|
75
|
+
|
|
76
|
+
def add_scada(self, short_name: str, long_name: str, mqtt_client_config: AdminMQTTClient) -> Optional[ScadaConfig]:
|
|
77
|
+
if short_name not in self.config.scadas:
|
|
78
|
+
self.config.scadas[short_name] = ScadaConfig(
|
|
79
|
+
long_name=long_name,
|
|
80
|
+
mqtt=mqtt_client_config,
|
|
81
|
+
)
|
|
82
|
+
self.config.scadas[short_name].mqtt.update_tls_paths(self.paths.certs_dir, short_name)
|
|
83
|
+
return self.config.scadas[short_name]
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def last_scada(self) -> str:
|
|
87
|
+
if self.paths.last_scada_path.exists():
|
|
88
|
+
return self.paths.last_scada_path.read_text()
|
|
89
|
+
return ""
|