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.
Files changed (39) hide show
  1. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/PKG-INFO +1 -1
  2. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/pyproject.toml +1 -1
  3. gridworks_admin-1.0.0.dev6/src/gwadmin/cli.py +441 -0
  4. gridworks_admin-1.0.0.dev6/src/gwadmin/config.py +89 -0
  5. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/admin_client.py +68 -12
  6. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/constrained_mqtt_client.py +11 -2
  7. gridworks_admin-1.0.0.dev6/src/gwadmin/watch/clients/dac_client.py +319 -0
  8. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/clients/relay_client.py +18 -0
  9. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/relay_app.py +77 -17
  10. gridworks_admin-1.0.0.dev6/src/gwadmin/watch/relay_app.tcss +107 -0
  11. gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets/dac_widget_info.py +86 -0
  12. gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets/dacs.py +166 -0
  13. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/keepalive.py +6 -4
  14. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_toggle_button.py +6 -4
  15. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relays.py +37 -9
  16. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/time_input.py +5 -3
  17. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/timer.py +9 -5
  18. gridworks_admin-1.0.0.dev4/src/gwadmin/cli.py +0 -167
  19. gridworks_admin-1.0.0.dev4/src/gwadmin/settings.py +0 -31
  20. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/actions.py +0 -46
  21. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/actions05.tcss +0 -20
  22. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/cli.py +0 -39
  23. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/stopwatch.py +0 -109
  24. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/stopwatch.tcss +0 -51
  25. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/switch.py +0 -42
  26. gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo/switch.tcss +0 -28
  27. gridworks_admin-1.0.0.dev4/src/gwadmin/watch/relay_app.tcss +0 -52
  28. gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/__init__.py +0 -0
  29. gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/watchex_app.py +0 -49
  30. gridworks_admin-1.0.0.dev4/src/gwadmin/watch/watchex/watchex_app.tcss +0 -0
  31. gridworks_admin-1.0.0.dev4/src/gwadmin/watch/widgets/__init__.py +0 -0
  32. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/README.md +0 -0
  33. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/__init__.py +0 -0
  34. {gridworks_admin-1.0.0.dev4/src/gwadmin/tdemo → gridworks_admin-1.0.0.dev6/src/gwadmin/watch}/__init__.py +0 -0
  35. {gridworks_admin-1.0.0.dev4/src/gwadmin/watch → gridworks_admin-1.0.0.dev6/src/gwadmin/watch/clients}/__init__.py +0 -0
  36. {gridworks_admin-1.0.0.dev4/src/gwadmin/watch/clients → gridworks_admin-1.0.0.dev6/src/gwadmin/watch/widgets}/__init__.py +0 -0
  37. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/mqtt.py +0 -0
  38. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_state_text.py +0 -0
  39. {gridworks_admin-1.0.0.dev4 → gridworks_admin-1.0.0.dev6}/src/gwadmin/watch/widgets/relay_widget_info.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gridworks-admin
3
- Version: 1.0.0.dev4
3
+ Version: 1.0.0.dev6
4
4
  Summary: CLI tool for monitoring gridworks-scada devices.
5
5
  Author: Andrew Schweitzer
6
6
  Author-email: Andrew Schweitzer <schweitz72@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "gridworks-admin"
3
- version = "1.0.0.dev4"
3
+ version = "1.0.0.dev6"
4
4
  description = "CLI tool for monitoring gridworks-scada devices."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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 ""