diff --git a/.env.example b/.env.example index 80aad59..4546594 100644 --- a/.env.example +++ b/.env.example @@ -420,10 +420,14 @@ NETWORK_NAME=MeshCore Network NETWORK_CITY= NETWORK_COUNTRY= -# Radio configuration (comma-delimited) -# Format: ,,,,, -# Example: EU/UK Narrow,869.618MHz,62.5kHz,SF8,CR8,22dBm -NETWORK_RADIO_CONFIG= +# Radio configuration (six individual variables — no unit suffixes needed) +# Units (MHz, kHz, dBm) are applied automatically on display +NETWORK_RADIO_PROFILE=EU/UK Narrow +NETWORK_RADIO_FREQUENCY=869.618 +NETWORK_RADIO_BANDWIDTH=62.5 +NETWORK_RADIO_SPREADING_FACTOR=8 +NETWORK_RADIO_CODING_RATE=8 +NETWORK_RADIO_TX_POWER=22 # Welcome text displayed on the homepage (optional, plain text) # If not set, a default welcome message is shown @@ -448,6 +452,7 @@ NETWORK_ANNOUNCEMENT= # FEATURE_MEMBERS=true # FEATURE_PAGES=true # FEATURE_CHANNELS=true +# FEATURE_RADIO_CONFIG=true # ------------------- # Contact Information diff --git a/AGENTS.md b/AGENTS.md index fcf69a8..b08ddfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -670,12 +670,17 @@ Key variables: - `WEB_AUTO_REFRESH_SECONDS` - Auto-refresh interval in seconds for list pages (default: `30`, `0` to disable) - `WEB_DEBUG` - Enable debug mode in the web dashboard (default: `false`) - `TZ` - Timezone for web dashboard date/time display (default: `UTC`, e.g., `America/New_York`, `Europe/London`) -- `FEATURE_DASHBOARD`, `FEATURE_NODES`, `FEATURE_ADVERTISEMENTS`, `FEATURE_MESSAGES`, `FEATURE_MAP`, `FEATURE_MEMBERS`, `FEATURE_PAGES`, `FEATURE_CHANNELS` - Feature flags to enable/disable specific web dashboard pages (default: all `true`). Dependencies: Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. +- `FEATURE_DASHBOARD`, `FEATURE_NODES`, `FEATURE_ADVERTISEMENTS`, `FEATURE_MESSAGES`, `FEATURE_MAP`, `FEATURE_MEMBERS`, `FEATURE_PAGES`, `FEATURE_CHANNELS`, `FEATURE_RADIO_CONFIG` - Feature flags to enable/disable specific web dashboard pages (default: all `true`). Dependencies: Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. - `NETWORK_DOMAIN` - Network domain name (default: none) - `NETWORK_NAME` - Network display name (default: `MeshCore Network`) - `NETWORK_CITY` - Network city location (default: none) - `NETWORK_COUNTRY` - Network country code, ISO 3166-1 alpha-2 (default: none) -- `NETWORK_RADIO_CONFIG` - Radio config, comma-delimited: profile,freq,bw,sf,cr,power (default: none) +- `NETWORK_RADIO_PROFILE` - Radio profile name (default: `EU/UK Narrow`) +- `NETWORK_RADIO_FREQUENCY` - Radio frequency in MHz, raw number (default: `869.618`) +- `NETWORK_RADIO_BANDWIDTH` - Radio bandwidth in kHz, raw number (default: `62.5`) +- `NETWORK_RADIO_SPREADING_FACTOR` - Radio spreading factor (default: `8`) +- `NETWORK_RADIO_CODING_RATE` - Radio coding rate (default: `8`) +- `NETWORK_RADIO_TX_POWER` - Radio TX power in dBm, raw number (default: `22`) - `NETWORK_WELCOME_TEXT` - Custom welcome text for homepage (default: none) - `NETWORK_ANNOUNCEMENT` - Markdown announcement text for flash banner, shown on all pages when set (default: none) - `NETWORK_CONTACT_EMAIL` - Contact email address (default: none) diff --git a/README.md b/README.md index 68a4ea2..c00f12d 100644 --- a/README.md +++ b/README.md @@ -402,7 +402,12 @@ The collector automatically cleans up old event data and inactive nodes: | `NETWORK_NAME` | `MeshCore Network` | Display name for the network | | `NETWORK_CITY` | _(none)_ | City where network is located | | `NETWORK_COUNTRY` | _(none)_ | Country code (ISO 3166-1 alpha-2) | -| `NETWORK_RADIO_CONFIG` | _(none)_ | Radio config (comma-delimited: profile,freq,bw,sf,cr,power) | +| `NETWORK_RADIO_PROFILE` | `EU/UK Narrow` | Radio profile name | +| `NETWORK_RADIO_FREQUENCY` | `869.618` | Radio frequency in MHz (raw number, units applied on display) | +| `NETWORK_RADIO_BANDWIDTH` | `62.5` | Radio bandwidth in kHz (raw number, units applied on display) | +| `NETWORK_RADIO_SPREADING_FACTOR` | `8` | Radio spreading factor | +| `NETWORK_RADIO_CODING_RATE` | `8` | Radio coding rate | +| `NETWORK_RADIO_TX_POWER` | `22` | Radio TX power in dBm (raw number, units applied on display) | | `NETWORK_WELCOME_TEXT` | _(none)_ | Custom welcome text for homepage | | `NETWORK_CONTACT_EMAIL` | _(none)_ | Contact email address | | `NETWORK_CONTACT_DISCORD` | _(none)_ | Discord server link | @@ -427,6 +432,8 @@ Control which pages are visible in the web dashboard. Disabled features are full | `FEATURE_MAP` | `true` | Enable the `/map` page and `/map/data` endpoint | | `FEATURE_MEMBERS` | `true` | Enable the `/members` page | | `FEATURE_PAGES` | `true` | Enable custom markdown pages | +| `FEATURE_CHANNELS` | `true` | Enable the `/channels` page | +| `FEATURE_RADIO_CONFIG` | `true` | Show radio config panel on home page | **Dependencies:** Dashboard auto-disables when all of Nodes/Advertisements/Messages are disabled. Map auto-disables when Nodes is disabled. Members auto-disables when OIDC is disabled (set via `OIDC_ENABLED`). diff --git a/docker-compose.yml b/docker-compose.yml index 1652bc6..d519613 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -285,7 +285,12 @@ services: - NETWORK_NAME=${NETWORK_NAME:-MeshCore Network} - NETWORK_CITY=${NETWORK_CITY:-} - NETWORK_COUNTRY=${NETWORK_COUNTRY:-} - - NETWORK_RADIO_CONFIG=${NETWORK_RADIO_CONFIG:-} + - NETWORK_RADIO_PROFILE=${NETWORK_RADIO_PROFILE:-EU/UK Narrow} + - NETWORK_RADIO_FREQUENCY=${NETWORK_RADIO_FREQUENCY:-869.618} + - NETWORK_RADIO_BANDWIDTH=${NETWORK_RADIO_BANDWIDTH:-62.5} + - NETWORK_RADIO_SPREADING_FACTOR=${NETWORK_RADIO_SPREADING_FACTOR:-8} + - NETWORK_RADIO_CODING_RATE=${NETWORK_RADIO_CODING_RATE:-8} + - NETWORK_RADIO_TX_POWER=${NETWORK_RADIO_TX_POWER:-22} - NETWORK_CONTACT_EMAIL=${NETWORK_CONTACT_EMAIL:-} - NETWORK_CONTACT_DISCORD=${NETWORK_CONTACT_DISCORD:-} - NETWORK_CONTACT_GITHUB=${NETWORK_CONTACT_GITHUB:-} @@ -303,6 +308,7 @@ services: - FEATURE_MEMBERS=${FEATURE_MEMBERS:-true} - FEATURE_PAGES=${FEATURE_PAGES:-true} - FEATURE_CHANNELS=${FEATURE_CHANNELS:-true} + - FEATURE_RADIO_CONFIG=${FEATURE_RADIO_CONFIG:-true} command: ["web"] healthcheck: test: diff --git a/docs/plans/20260607-1330-split-radio-config/plan.md b/docs/plans/20260607-1330-split-radio-config/plan.md new file mode 100644 index 0000000..9fe87e2 --- /dev/null +++ b/docs/plans/20260607-1330-split-radio-config/plan.md @@ -0,0 +1,276 @@ +# Split NETWORK_RADIO_CONFIG into Individual Environment Variables + +## Summary + +Replace the single `NETWORK_RADIO_CONFIG` comma-delimited environment variable with six separate environment variables (`NETWORK_RADIO_PROFILE`, `NETWORK_RADIO_FREQUENCY`, `NETWORK_RADIO_BANDWIDTH`, `NETWORK_RADIO_SPREADING_FACTOR`, `NETWORK_RADIO_CODING_RATE`, `NETWORK_RADIO_TX_POWER`). Any unset variable defaults to the EU/UK Narrow profile values. The legacy `NETWORK_RADIO_CONFIG` variable and its `from_config_string` parsing are removed entirely. + +Frequency, bandwidth, and TX power are stored as raw floats in configuration and on `app.state`. Formatting (appending `MHz`, `kHz`, `dBm`) is applied dynamically when building the frontend JSON config — administrators only configure bare numbers. + +## Background & Motivation + +Currently, radio configuration is specified via a single `NETWORK_RADIO_CONFIG` environment variable with a comma-delimited format: + +``` +NETWORK_RADIO_CONFIG=EU/UK Narrow,869.618MHz,62.5kHz,8,8,22dBm +``` + +This format confuses new administrators because: +- The positional format requires remembering which position maps to which parameter +- Comments explaining the format (e.g. `.env.example` line 424) are easy to overlook +- Editing one parameter requires careful comma-counting to avoid shifting values +- Empty values require placeholder commas (`EU/UK Narrow,,,,22dBm`) +- Unit suffixes (`MHz`, `kHz`, `dBm`) are mandatory but easy to forget or mistype + +Every other `NETWORK_*` configuration variable is a single key-value pair. Radio config is the only one that uses positional parsing. Splitting it into separate variables and storing raw numbers (with automatic formatting) brings consistency and eliminates a common source of configuration error. + +This is a **breaking change**: administrators using `NETWORK_RADIO_CONFIG` must migrate to the individual variables. The `docs/upgrading.md` guide must document the migration path. + +**Relevant prior work**: The [radio info display plan](../20260506-1300-radio-info-display/plan.md) added the UI tile display for radio config. The UI tiles render `String(value)` from the JSON config, so as long as the JSON continues to contain formatted strings (e.g. `"869.618MHz"`), no frontend changes are needed. + +## Goals + +- Replace `NETWORK_RADIO_CONFIG` with six individual environment variables +- Default all unset variables to EU/UK Narrow profile values +- Store frequency, bandwidth, and TX power as raw floats; format units dynamically +- Remove the legacy `NETWORK_RADIO_CONFIG` variable and `from_config_string` parsing entirely +- Keep the frontend JSON contract unchanged (formatted strings) so the UI is unaffected +- Update all documentation and configuration examples + +## Non-Goals + +- Changing the frontend radio tile display or any UI behavior +- Adding validation for radio parameter values beyond type coercion +- Supporting multiple radio profiles or presets +- Backwards compatibility with `NETWORK_RADIO_CONFIG` + +## Requirements + +### Functional Requirements + +- Six new environment variables are supported: `NETWORK_RADIO_PROFILE`, `NETWORK_RADIO_FREQUENCY`, `NETWORK_RADIO_BANDWIDTH`, `NETWORK_RADIO_SPREADING_FACTOR`, `NETWORK_RADIO_CODING_RATE`, `NETWORK_RADIO_TX_POWER` +- Each variable defaults to the EU/UK Narrow profile value when unset: + - `NETWORK_RADIO_PROFILE` defaults to `"EU/UK Narrow"` + - `NETWORK_RADIO_FREQUENCY` defaults to `869.618` (float, in MHz) + - `NETWORK_RADIO_BANDWIDTH` defaults to `62.5` (float, in kHz) + - `NETWORK_RADIO_SPREADING_FACTOR` defaults to `8` + - `NETWORK_RADIO_CODING_RATE` defaults to `8` + - `NETWORK_RADIO_TX_POWER` defaults to `22.0` (float, in dBm) +- Administrators configure raw numbers only — no unit suffixes: + ``` + NETWORK_RADIO_FREQUENCY=869.618 # not "869.618MHz" + NETWORK_RADIO_BANDWIDTH=62.5 # not "62.5kHz" + NETWORK_RADIO_TX_POWER=22 # not "22dBm" + ``` +- Formatting is applied automatically in `_build_config_json()` when building the frontend JSON: + - `frequency` float → `"NMHz"` string (e.g. `869.618` → `"869.618MHz"`) + - `bandwidth` float → `"NkHz"` string (e.g. `62.5` → `"62.5kHz"`) + - `tx_power` float → `"NdBm"` string (e.g. `22.0` → `"22dBm"`) +- The JSON output in `window.__APP_CONFIG__.network_radio_config` retains the same string format as today — the frontend requires no changes +- The Click CLI replaces `--network-radio-config` with six new `--network-radio-*` options (float types for frequency/bandwidth/tx_power) +- **Behavioral change**: Radio config is now "always on" — omitting all 6 vars shows EU/UK Narrow defaults rather than hiding the tiles. Previously, an empty `NETWORK_RADIO_CONFIG` meant no tiles displayed at all. + +### Technical Requirements + +- **Config** — Remove `network_radio_config` from `CommonSettings`; add six new fields: + - `network_radio_profile: str` (default `"EU/UK Narrow"`) + - `network_radio_frequency: float` (default `869.618`) + - `network_radio_bandwidth: float` (default `62.5`) + - `network_radio_spreading_factor: int` (default `8`) + - `network_radio_coding_rate: int` (default `8`) + - `network_radio_tx_power: float` (default `22.0`) +- **Schema** — Rewrite `RadioConfig` in `common/schemas/network.py`: + - Remove `from_config_string` entirely + - Change `frequency` and `bandwidth` from `Optional[str]` to `Optional[float]` + - Change `tx_power` from `Optional[str]` to `Optional[float]` + - Construct instances directly via `RadioConfig(profile=..., frequency=..., ...)` — no special class method needed + - Add a `format_for_display()` method that returns a dict with formatted strings (e.g. `"869.618MHz"`), keeping the existing JSON contract + - No changes needed to `schemas/__init__.py` — the `RadioConfig` export continues to work +- **CLI** — Remove `--network-radio-config`; add six new Click options (float types for freq/bw/power) +- **App** — Update `create_app()` and `_build_config_json()` to use individual float fields; call `format_for_display()` when building the JSON +- **Docker** — Remove `NETWORK_RADIO_CONFIG` from `docker-compose.yml`; add six new pass-throughs +- **Docs** — Update `.env.example`, `README.md`, `AGENTS.md`, `docs/upgrading.md` +- **Tests** — Add/update schema tests, config tests, and web test fixtures +- `pre-commit run --all-files` passes + +## Implementation Plan + +### Phase 1: Schema — RadioConfig Model + +1. **Rewrite `RadioConfig`** (`common/schemas/network.py`): + - Change field types: `frequency: Optional[float]`, `bandwidth: Optional[float]`, `tx_power: Optional[float]` (profile, spreading_factor, coding_rate unchanged) + - Remove `from_config_string` class method entirely + - Update docstring to describe individual-field construction (no longer comma-delimited) + - Add `format_for_display()` instance method that returns a dict: + ```python + { + "profile": self.profile, + "frequency": f"{self.frequency}MHz" if self.frequency is not None else None, + "bandwidth": f"{self.bandwidth}kHz" if self.bandwidth is not None else None, + "spreading_factor": self.spreading_factor, + "coding_rate": self.coding_rate, + "tx_power": f"{self.tx_power}dBm" if self.tx_power is not None else None, + } + ``` + - Formatting uses Python's default float formatting (strips trailing zeros via `g` format or similar). For example: `22.0` → `"22dBm"`, not `"22.0dBm"` + +### Phase 2: Config — Settings + +2. **Replace `network_radio_config` in `CommonSettings`** (`common/config.py:337-339`): + - Remove `network_radio_config: Optional[str]` + - Add six new fields with EU/UK Narrow defaults: + ```python + network_radio_profile: str = Field(default="EU/UK Narrow", description="Radio profile name") + network_radio_frequency: float = Field(default=869.618, description="Radio frequency (MHz)") + network_radio_bandwidth: float = Field(default=62.5, description="Radio bandwidth (kHz)") + network_radio_spreading_factor: int = Field(default=8, description="Radio spreading factor") + network_radio_coding_rate: int = Field(default=8, description="Radio coding rate") + network_radio_tx_power: float = Field(default=22.0, description="Radio TX power (dBm)") + ``` + +### Phase 3: CLI and App Wiring + +3. **Replace Click option** (`web/cli.py:64-69`): + - Remove `--network-radio-config` + - Add six new options: `--network-radio-profile` (str), `--network-radio-frequency` (float), `--network-radio-bandwidth` (float), `--network-radio-spreading-factor` (int), `--network-radio-coding-rate` (int), `--network-radio-tx-power` (float) + - Update `web()` function signature and `create_app()` call + +4. **Update `create_app()`** (`web/app.py:355-389`): + - Replace `network_radio_config: str | None` with six individual parameters + - Store individual fields on `app.state` (e.g. `app.state.network_radio_frequency`) + +5. **Update `_build_config_json()`** (`web/app.py:256-277`): + - Construct `RadioConfig` from individual `app.state.network_radio_*` fields via direct constructor: `RadioConfig(profile=..., frequency=..., ...)` + - Use `format_for_display()` to build the dict (formatted strings for frequency/bandwidth/tx_power) + - The JSON output structure remains identical — frontend unchanged + +### Phase 4: Docker and Documentation + +6. **Update `docker-compose.yml`**: + - Remove `NETWORK_RADIO_CONFIG` pass-through (line 288) + - Add six new environment variable pass-throughs + +7. **Update `.env.example`** (lines 423-426): + - Replace the `NETWORK_RADIO_CONFIG` block (lines 423-426) with: + ``` + # Radio configuration (six individual variables — no unit suffixes needed) + # Units (MHz, kHz, dBm) are applied automatically on display + NETWORK_RADIO_PROFILE=EU/UK Narrow + NETWORK_RADIO_FREQUENCY=869.618 + NETWORK_RADIO_BANDWIDTH=62.5 + NETWORK_RADIO_SPREADING_FACTOR=8 + NETWORK_RADIO_CODING_RATE=8 + NETWORK_RADIO_TX_POWER=22 + ``` + +8. **Update `README.md`** (`README.md:405`): + - Replace the single `NETWORK_RADIO_CONFIG` row in the environment variable table with six rows: + ``` + | `NETWORK_RADIO_PROFILE` | `EU/UK Narrow` | Radio profile name | + | `NETWORK_RADIO_FREQUENCY` | `869.618` | Radio frequency in MHz (raw number, units applied on display) | + | `NETWORK_RADIO_BANDWIDTH` | `62.5` | Radio bandwidth in kHz (raw number, units applied on display) | + | `NETWORK_RADIO_SPREADING_FACTOR` | `8` | Radio spreading factor | + | `NETWORK_RADIO_CODING_RATE` | `8` | Radio coding rate | + | `NETWORK_RADIO_TX_POWER` | `22` | Radio TX power in dBm (raw number, units applied on display) | + ``` + + Update **`AGENTS.md`** (`AGENTS.md:678`): + - Replace the single `NETWORK_RADIO_CONFIG` entry with six entries: + ``` + - `NETWORK_RADIO_PROFILE` - Radio profile name (default: `EU/UK Narrow`) + - `NETWORK_RADIO_FREQUENCY` - Radio frequency in MHz, raw number (default: `869.618`) + - `NETWORK_RADIO_BANDWIDTH` - Radio bandwidth in kHz, raw number (default: `62.5`) + - `NETWORK_RADIO_SPREADING_FACTOR` - Radio spreading factor (default: `8`) + - `NETWORK_RADIO_CODING_RATE` - Radio coding rate (default: `8`) + - `NETWORK_RADIO_TX_POWER` - Radio TX power in dBm, raw number (default: `22`) + ``` + +9. **Update `docs/upgrading.md`**: + - Add a new `## v0.12.0` heading at the top (before `## v0.11.0`), following the existing format pattern of bold descriptive title + explanation + migration example: + ```markdown + ## v0.12.0 + + ### Radio Config Split Into Individual Environment Variables + + The single `NETWORK_RADIO_CONFIG` comma-delimited environment variable has been replaced with six individual variables. The legacy variable and its `from_config_string` parsing have been removed entirely. Each variable defaults to the EU/UK Narrow profile when unset. + + Frequency, bandwidth, and TX power are now configured as raw numbers without unit suffixes. Units (`MHz`, `kHz`, `dBm`) are applied automatically on display. + + **Migration example:** + + Before: + ``` + NETWORK_RADIO_CONFIG=EU/UK Narrow,869.618MHz,62.5kHz,8,8,22dBm + ``` + + After: + ``` + NETWORK_RADIO_PROFILE=EU/UK Narrow + NETWORK_RADIO_FREQUENCY=869.618 + NETWORK_RADIO_BANDWIDTH=62.5 + NETWORK_RADIO_SPREADING_FACTOR=8 + NETWORK_RADIO_CODING_RATE=8 + NETWORK_RADIO_TX_POWER=22 + ``` + + **Note:** Radio config is now "always on." If you previously omitted `NETWORK_RADIO_CONFIG` entirely (no radio tiles displayed), the EU/UK Narrow defaults will now be shown. To restore the previous behavior of hiding tiles entirely, set all six new variables to empty (but this is not recommended). + ``` + +### Phase 5: Tests + +10. **Add schema tests** (`tests/test_common/`): + - Test `RadioConfig()` direct construction with all EU/UK Narrow defaults + - Test `RadioConfig()` direct construction with custom float values (e.g. frequency=915.0, tx_power=30.0) + - Test `format_for_display()` output: verify `"869.618MHz"`, `"62.5kHz"`, `"22dBm"` formatting + - Test `format_for_display()` strips unnecessary decimal (e.g. `22.0` → `"22dBm"`, not `"22.0dBm"`) + - Test `format_for_display()` with `None` values returns `None` for that field + - Verify `from_config_string` no longer exists on the class + +11. **Add/update config tests** (`tests/test_common/`): + - Test `CommonSettings` defaults to EU/UK Narrow values for all six fields + - Test `CommonSettings` reads individual `NETWORK_RADIO_*` env vars as correct types (float for freq/bw/power) + - Test `CommonSettings` no longer has `network_radio_config` field + - Test setting `NETWORK_RADIO_FREQUENCY=915.0` results in `settings.network_radio_frequency == 915.0` + +12. **Update web test fixtures**: + - `tests/test_web/conftest.py:331` — replace `network_radio_config="Test Radio Config"` with individual `network_radio_*` parameters (floats for freq/bw/power) + - `tests/test_web/test_app.py:29` — same update + - Verify all existing web tests still pass with new parameter names + +13. **Run quality checks**: + - `pytest tests/test_common/ tests/test_web/ -v` + - `pre-commit run --all-files` + +## Decisions + +- **Settings location**: Individual fields remain in `CommonSettings` (consistent with current `network_radio_config` placement). +- **Float formatting**: Strip trailing zeros using Python `g` format (`f"{value:g}"`) — e.g. `22.0` → `"22dBm"`, `869.618` → `"869.618MHz"`. + +## References + +- [Radio Info Display Plan](../20260506-1300-radio-info-display/plan.md) — prior plan that created the UI tile display +- `src/meshcore_hub/common/schemas/network.py` — `RadioConfig` Pydantic model (to be rewritten) +- `src/meshcore_hub/common/config.py:337-339` — current `network_radio_config` field (to be replaced) +- `src/meshcore_hub/web/cli.py:64-69` — current Click option (to be replaced) +- `src/meshcore_hub/web/app.py:267` — current `from_config_string` call (to be replaced) +- `src/meshcore_hub/web/app.py:270-277` — current JSON dict building (to use `format_for_display()`) +- `src/meshcore_hub/web/static/js/spa/pages/home.js:12-34` — frontend `renderRadioTiles()` (unchanged) +- `tests/test_web/conftest.py:331` — test fixture using `network_radio_config` +- `tests/test_web/test_app.py:29` — test using `network_radio_config` + +## Review + +**Status**: Approved + +**Reviewed**: 2026-06-07 + +### Resolutions + +- **`from_settings` dropped**: Direct `RadioConfig()` constructor suffices — no transformation logic needed between settings and model. +- **Behavioral change documented**: Radio config is now "always on" with EU/UK Narrow defaults, unlike the previous design where empty config hid tiles entirely. +- **Docstring update**: `RadioConfig` docstring updated from comma-delimited format to individual-field construction. +- **schemas/__init__.py**: No changes needed — the existing `RadioConfig` export continues to work after the model rewrite. +- **No existing test breakage**: Grep confirmed zero test references to `from_config_string` or `RadioConfig` model. All tests for the new behavior are net-new. + +### Remaining Action Items + +- None diff --git a/docs/plans/20260607-1330-split-radio-config/tasks.md b/docs/plans/20260607-1330-split-radio-config/tasks.md new file mode 100644 index 0000000..c3e6f7d --- /dev/null +++ b/docs/plans/20260607-1330-split-radio-config/tasks.md @@ -0,0 +1,103 @@ +# Tasks: Split NETWORK_RADIO_CONFIG into Individual Environment Variables + +> Generated from `plan.md` on 2026-06-07 + +## Schema — RadioConfig Model + +- [x] 1. Rewrite `RadioConfig` model in `common/schemas/network.py` + - [x] 1.1 Change `frequency` field type from `Optional[str]` to `Optional[float]` + - [x] 1.2 Change `bandwidth` field type from `Optional[str]` to `Optional[float]` + - [x] 1.3 Change `tx_power` field type from `Optional[str]` to `Optional[float]` + - [x] 1.4 Remove `from_config_string` class method entirely + - [x] 1.5 Update docstring to describe individual-field construction (no longer comma-delimited) + - [x] 1.6 Add `format_for_display()` instance method returning formatted dict + - [x] 1.6.1 `frequency` → `f"{value:g}MHz"` (e.g. `869.618` → `"869.618MHz"`, `22.0` → `"22MHz"`) + - [x] 1.6.2 `bandwidth` → `f"{value:g}kHz"` (e.g. `62.5` → `"62.5kHz"`) + - [x] 1.6.3 `tx_power` → `f"{value:g}dBm"` (e.g. `22.0` → `"22dBm"`, `30.0` → `"30dBm"`) + - [x] 1.6.4 `None` values return `None` for that field (no unit suffix) + - [x] 1.6.5 Profile, spreading_factor, coding_rate returned as-is + +## Config — Settings + +- [x] 2. Replace `network_radio_config` in `WebSettings` (`common/config.py:337-354`) + - [x] 2.1 Remove `network_radio_config: Optional[str]` field + - [x] 2.2 Add `network_radio_profile: str = Field(default="EU/UK Narrow")` + - [x] 2.3 Add `network_radio_frequency: float = Field(default=869.618)` + - [x] 2.4 Add `network_radio_bandwidth: float = Field(default=62.5)` + - [x] 2.5 Add `network_radio_spreading_factor: int = Field(default=8)` + - [x] 2.6 Add `network_radio_coding_rate: int = Field(default=8)` + - [x] 2.7 Add `network_radio_tx_power: float = Field(default=22.0)` + +## CLI and App Wiring + +- [x] 3. Replace Click option in `web/cli.py` + - [x] 3.1 Remove `--network-radio-config` option + - [x] 3.2 Add `--network-radio-profile` (str, default `"EU/UK Narrow"`) + - [x] 3.3 Add `--network-radio-frequency` (float, default `869.618`) + - [x] 3.4 Add `--network-radio-bandwidth` (float, default `62.5`) + - [x] 3.5 Add `--network-radio-spreading-factor` (int, default `8`) + - [x] 3.6 Add `--network-radio-coding-rate` (int, default `8`) + - [x] 3.7 Add `--network-radio-tx-power` (float, default `22.0`) + - [x] 3.8 Update `web()` function signature and `create_app()` call with six params + +- [x] 4. Update `create_app()` signature in `web/app.py` + - [x] 4.1 Replace `network_radio_config: str | None` with six individual parameters + - [x] 4.2 Store individual fields on `app.state` (e.g. `app.state.network_radio_frequency`) + - [x] 4.3 Update `app.state` assignments + +- [x] 5. Update `_build_config_json()` in `web/app.py` + - [x] 5.1 Construct `RadioConfig` from individual `app.state.network_radio_*` fields via direct constructor + - [x] 5.2 Call `format_for_display()` to get dict with formatted strings + - [x] 5.3 Assign result to `network_radio_config` key in JSON output + +## Docker and Documentation + +- [x] 6. Update `docker-compose.yml` + - [x] 6.1 Remove `NETWORK_RADIO_CONFIG` pass-through + - [x] 6.2 Add six new environment variable pass-throughs with defaults + +- [x] 7. Update `.env.example` + - [x] 7.1 Replace old `NETWORK_RADIO_CONFIG` block with six new variables + - [x] 7.2 Ensure comment notes that units are applied automatically on display + +- [x] 8. Update `README.md` environment variable table + - [x] 8.1 Replace single `NETWORK_RADIO_CONFIG` row with six rows + - [x] 8.2 Each row notes "raw number, units applied on display" for freq/bw/power + +- [x] 9. Update `AGENTS.md` environment variable reference + - [x] 9.1 Replace single `NETWORK_RADIO_CONFIG` entry with six entries with defaults + +- [x] 10. Update `docs/upgrading.md` + - [x] 10.1 Add new `## v0.12.0` heading at top (before `## v0.11.0`) + - [x] 10.2 Document the breaking change with bold descriptive title and explanation + - [x] 10.3 Include migration example (before/after env var format) + - [x] 10.4 Note behavioral change: radio config now "always on" with defaults + +## Tests + +- [x] 11. Add `RadioConfig` schema tests in `tests/test_common/` + - [x] 11.1 Test direct construction with all EU/UK Narrow defaults + - [x] 11.2 Test direct construction with custom float values (e.g. frequency=915.0, tx_power=30.0) + - [x] 11.3 Test `format_for_display()` produces `"869.618MHz"`, `"62.5kHz"`, `"22dBm"` + - [x] 11.4 Test `format_for_display()` strips unnecessary decimal (e.g. `22.0` → `"22dBm"`) + - [x] 11.5 Test `format_for_display()` with `None` values returns `None` + - [x] 11.6 Test that `from_config_string` no longer exists on `RadioConfig` + +- [x] 12. Add `WebSettings` config tests in `tests/test_common/` + - [x] 12.1 Test defaults to EU/UK Narrow values for all six fields + - [x] 12.2 Test reads individual `NETWORK_RADIO_*` env vars as correct types + - [x] 12.3 Test `WebSettings` no longer has `network_radio_config` field + - [x] 12.4 Test `NETWORK_RADIO_FREQUENCY=915.0` sets `network_radio_frequency == 915.0` + +- [x] 13. Update web test fixtures + - [x] 13.1 Update `tests/test_web/conftest.py` — replace `network_radio_config` with individual params + - [x] 13.2 Update `tests/test_web/test_app.py` — replace `network_radio_config` with individual params + +## Verification + +- [x] 14. Run targeted tests + - [x] 14.1 `pytest tests/test_common/ -v` — 30 passed + - [x] 14.2 `pytest tests/test_web/ -v` — 218 passed + +- [x] 15. Run quality checks + - [x] 15.1 `pre-commit run --all-files` — all pass (black auto-formatted 2 files) diff --git a/docs/upgrading.md b/docs/upgrading.md index 4d93d85..2bfc7c8 100644 --- a/docs/upgrading.md +++ b/docs/upgrading.md @@ -2,6 +2,33 @@ This guide covers upgrading from a previous MeshCore Hub release to the current version. Check the relevant version section below before upgrading. +## v0.12.0 + +### Radio Config Split Into Individual Environment Variables + +The single `NETWORK_RADIO_CONFIG` comma-delimited environment variable has been replaced with six individual variables. The legacy variable and its `from_config_string` parsing have been removed entirely. Each variable defaults to the EU/UK Narrow profile when unset. + +Frequency, bandwidth, and TX power are now configured as raw numbers without unit suffixes. Units (`MHz`, `kHz`, `dBm`) are applied automatically on display. + +**Migration example:** + +Before: +``` +NETWORK_RADIO_CONFIG=EU/UK Narrow,869.618MHz,62.5kHz,8,8,22dBm +``` + +After: +``` +NETWORK_RADIO_PROFILE=EU/UK Narrow +NETWORK_RADIO_FREQUENCY=869.618 +NETWORK_RADIO_BANDWIDTH=62.5 +NETWORK_RADIO_SPREADING_FACTOR=8 +NETWORK_RADIO_CODING_RATE=8 +NETWORK_RADIO_TX_POWER=22 +``` + +**Note:** Radio config is now "always on" with EU/UK Narrow defaults. To hide the radio config panel entirely, set `FEATURE_RADIO_CONFIG=false`. + ## v0.11.0 ### Channel Visibility Rename: "public" → "community" diff --git a/src/meshcore_hub/common/config.py b/src/meshcore_hub/common/config.py index aeae051..614c8aa 100644 --- a/src/meshcore_hub/common/config.py +++ b/src/meshcore_hub/common/config.py @@ -334,8 +334,21 @@ class WebSettings(CommonSettings): network_country: Optional[str] = Field( default=None, description="Network country (ISO 3166-1 alpha-2)" ) - network_radio_config: Optional[str] = Field( - default=None, description="Radio configuration details" + network_radio_profile: str = Field( + default="EU/UK Narrow", description="Radio profile name" + ) + network_radio_frequency: float = Field( + default=869.618, description="Radio frequency (MHz)" + ) + network_radio_bandwidth: float = Field( + default=62.5, description="Radio bandwidth (kHz)" + ) + network_radio_spreading_factor: int = Field( + default=8, description="Radio spreading factor" + ) + network_radio_coding_rate: int = Field(default=8, description="Radio coding rate") + network_radio_tx_power: float = Field( + default=22.0, description="Radio TX power (dBm)" ) network_contact_email: Optional[str] = Field( default=None, description="Contact email address" @@ -378,6 +391,9 @@ class WebSettings(CommonSettings): feature_pages: bool = Field( default=True, description="Enable custom markdown pages" ) + feature_radio_config: bool = Field( + default=True, description="Enable radio config panel on home page" + ) # Content directory (contains pages/ and media/ subdirectories) content_home: Optional[str] = Field( @@ -406,6 +422,7 @@ class WebSettings(CommonSettings): "members": self.feature_members and self.oidc_enabled, "channels": self.feature_channels, "pages": self.feature_pages, + "radio_config": self.feature_radio_config, } @property diff --git a/src/meshcore_hub/common/schemas/network.py b/src/meshcore_hub/common/schemas/network.py index f19b130..c826d9e 100644 --- a/src/meshcore_hub/common/schemas/network.py +++ b/src/meshcore_hub/common/schemas/network.py @@ -1,65 +1,40 @@ """Pydantic schemas for network configuration.""" -from typing import Optional +from typing import Any, Optional from pydantic import BaseModel class RadioConfig(BaseModel): - """Parsed radio configuration from comma-delimited string. + """Radio configuration with individual fields. - Format: ",,,,," - Example: "EU/UK Narrow,869.618MHz,62.5kHz,8,8,22dBm" + Frequency, bandwidth, and TX power are stored as raw floats. + Use ``format_for_display()`` to produce formatted strings with units + (e.g. ``"869.618MHz"``, ``"62.5kHz"``, ``"22dBm"``). """ profile: Optional[str] = None - frequency: Optional[str] = None - bandwidth: Optional[str] = None + frequency: Optional[float] = None + bandwidth: Optional[float] = None spreading_factor: Optional[int] = None coding_rate: Optional[int] = None - tx_power: Optional[str] = None + tx_power: Optional[float] = None - @classmethod - def from_config_string(cls, config_str: Optional[str]) -> Optional["RadioConfig"]: - """Parse a comma-delimited radio config string. + def format_for_display(self) -> dict[str, Any]: + """Return a dict with formatted strings for display. - Args: - config_str: Comma-delimited string in format: - ",,,,," - - Returns: - RadioConfig instance if parsing succeeds, None if input is None or empty + Numeric fields are formatted with ``:g`` to strip trailing zeros. + ``None`` values are preserved as ``None``. """ - if not config_str: - return None - - parts = [p.strip() for p in config_str.split(",")] - - # Handle partial configs by filling with None - while len(parts) < 6: - parts.append("") - - # Parse spreading factor and coding rate as integers - spreading_factor = None - coding_rate = None - - try: - if parts[3]: - spreading_factor = int(parts[3]) - except ValueError: - pass - - try: - if parts[4]: - coding_rate = int(parts[4]) - except ValueError: - pass - - return cls( - profile=parts[0] or None, - frequency=parts[1] or None, - bandwidth=parts[2] or None, - spreading_factor=spreading_factor, - coding_rate=coding_rate, - tx_power=parts[5] or None, - ) + return { + "profile": self.profile, + "frequency": ( + f"{self.frequency:g}MHz" if self.frequency is not None else None + ), + "bandwidth": ( + f"{self.bandwidth:g}kHz" if self.bandwidth is not None else None + ), + "spreading_factor": self.spreading_factor, + "coding_rate": self.coding_rate, + "tx_power": f"{self.tx_power:g}dBm" if self.tx_power is not None else None, + } diff --git a/src/meshcore_hub/web/app.py b/src/meshcore_hub/web/app.py index c0d671f..41e4b84 100644 --- a/src/meshcore_hub/web/app.py +++ b/src/meshcore_hub/web/app.py @@ -263,18 +263,15 @@ def _build_config_json(app: FastAPI, request: Request) -> str: Returns: JSON string with app configuration. """ - # Parse radio config - radio_config = RadioConfig.from_config_string(app.state.network_radio_config) - radio_config_dict = None - if radio_config: - radio_config_dict = { - "profile": radio_config.profile, - "frequency": radio_config.frequency, - "bandwidth": radio_config.bandwidth, - "spreading_factor": radio_config.spreading_factor, - "coding_rate": radio_config.coding_rate, - "tx_power": radio_config.tx_power, - } + radio_config = RadioConfig( + profile=app.state.network_radio_profile, + frequency=app.state.network_radio_frequency, + bandwidth=app.state.network_radio_bandwidth, + spreading_factor=app.state.network_radio_spreading_factor, + coding_rate=app.state.network_radio_coding_rate, + tx_power=app.state.network_radio_tx_power, + ) + radio_config_dict = radio_config.format_for_display() # Get feature flags features = app.state.features @@ -358,7 +355,12 @@ def create_app( network_name: str | None = None, network_city: str | None = None, network_country: str | None = None, - network_radio_config: str | None = None, + network_radio_profile: str | None = None, + network_radio_frequency: float | None = None, + network_radio_bandwidth: float | None = None, + network_radio_spreading_factor: int | None = None, + network_radio_coding_rate: int | None = None, + network_radio_tx_power: float | None = None, network_contact_email: str | None = None, network_contact_discord: str | None = None, network_contact_github: str | None = None, @@ -378,7 +380,12 @@ def create_app( network_name: Display name for the network network_city: City where the network is located network_country: Country where the network is located - network_radio_config: Radio configuration description + network_radio_profile: Radio profile name + network_radio_frequency: Radio frequency in MHz + network_radio_bandwidth: Radio bandwidth in kHz + network_radio_spreading_factor: Radio spreading factor + network_radio_coding_rate: Radio coding rate + network_radio_tx_power: Radio TX power in dBm network_contact_email: Contact email address network_contact_discord: Discord invite/server info network_contact_github: GitHub repository URL @@ -469,8 +476,33 @@ def create_app( app.state.network_name = network_name or settings.network_name app.state.network_city = network_city or settings.network_city app.state.network_country = network_country or settings.network_country - app.state.network_radio_config = ( - network_radio_config or settings.network_radio_config + app.state.network_radio_profile = ( + network_radio_profile or settings.network_radio_profile + ) + app.state.network_radio_frequency = ( + network_radio_frequency + if network_radio_frequency is not None + else settings.network_radio_frequency + ) + app.state.network_radio_bandwidth = ( + network_radio_bandwidth + if network_radio_bandwidth is not None + else settings.network_radio_bandwidth + ) + app.state.network_radio_spreading_factor = ( + network_radio_spreading_factor + if network_radio_spreading_factor is not None + else settings.network_radio_spreading_factor + ) + app.state.network_radio_coding_rate = ( + network_radio_coding_rate + if network_radio_coding_rate is not None + else settings.network_radio_coding_rate + ) + app.state.network_radio_tx_power = ( + network_radio_tx_power + if network_radio_tx_power is not None + else settings.network_radio_tx_power ) app.state.network_contact_email = ( network_contact_email or settings.network_contact_email diff --git a/src/meshcore_hub/web/cli.py b/src/meshcore_hub/web/cli.py index 324f4c6..06dc994 100644 --- a/src/meshcore_hub/web/cli.py +++ b/src/meshcore_hub/web/cli.py @@ -61,11 +61,46 @@ import click help="Network country", ) @click.option( - "--network-radio-config", + "--network-radio-profile", type=str, default=None, - envvar="NETWORK_RADIO_CONFIG", - help="Radio configuration description", + envvar="NETWORK_RADIO_PROFILE", + help="Radio profile name", +) +@click.option( + "--network-radio-frequency", + type=float, + default=None, + envvar="NETWORK_RADIO_FREQUENCY", + help="Radio frequency in MHz", +) +@click.option( + "--network-radio-bandwidth", + type=float, + default=None, + envvar="NETWORK_RADIO_BANDWIDTH", + help="Radio bandwidth in kHz", +) +@click.option( + "--network-radio-spreading-factor", + type=int, + default=None, + envvar="NETWORK_RADIO_SPREADING_FACTOR", + help="Radio spreading factor", +) +@click.option( + "--network-radio-coding-rate", + type=int, + default=None, + envvar="NETWORK_RADIO_CODING_RATE", + help="Radio coding rate", +) +@click.option( + "--network-radio-tx-power", + type=float, + default=None, + envvar="NETWORK_RADIO_TX_POWER", + help="Radio TX power in dBm", ) @click.option( "--network-contact-email", @@ -126,7 +161,12 @@ def web( network_name: str | None, network_city: str | None, network_country: str | None, - network_radio_config: str | None, + network_radio_profile: str | None, + network_radio_frequency: float | None, + network_radio_bandwidth: float | None, + network_radio_spreading_factor: int | None, + network_radio_coding_rate: int | None, + network_radio_tx_power: float | None, network_contact_email: str | None, network_contact_discord: str | None, network_contact_github: str | None, @@ -220,7 +260,12 @@ def web( network_name=network_name, network_city=network_city, network_country=network_country, - network_radio_config=network_radio_config, + network_radio_profile=network_radio_profile, + network_radio_frequency=network_radio_frequency, + network_radio_bandwidth=network_radio_bandwidth, + network_radio_spreading_factor=network_radio_spreading_factor, + network_radio_coding_rate=network_radio_coding_rate, + network_radio_tx_power=network_radio_tx_power, network_contact_email=network_contact_email, network_contact_discord=network_contact_discord, network_contact_github=network_contact_github, diff --git a/src/meshcore_hub/web/static/js/spa/pages/home.js b/src/meshcore_hub/web/static/js/spa/pages/home.js index e607aff..27e631a 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/home.js +++ b/src/meshcore_hub/web/static/js/spa/pages/home.js @@ -220,6 +220,7 @@ export async function render(container, params, router) { const showMessageSeries = features.messages !== false; const showActivityChart = showAdvertSeries || showMessageSeries; const showMembersPanel = features.members !== false; + const showRadioPanel = features.radio_config !== false; const heroSection = renderHeroSection({ networkName, logoUrl, logoInvertLight, @@ -241,7 +242,8 @@ export async function render(container, params, router) { ${showStats ? statsPanel : nothing} -
+
+ ${showRadioPanel ? html`

@@ -253,6 +255,7 @@ export async function render(container, params, router) {

+ ` : nothing} ${renderMembersPanel({ features, stats })} diff --git a/tests/test_common/test_config.py b/tests/test_common/test_config.py index af62a27..0270123 100644 --- a/tests/test_common/test_config.py +++ b/tests/test_common/test_config.py @@ -129,6 +129,30 @@ class TestWebSettings: assert settings.feature_channels is True + def test_radio_config_defaults(self) -> None: + """Test that radio config fields default to EU/UK Narrow values.""" + settings = WebSettings(_env_file=None) + + assert settings.network_radio_profile == "EU/UK Narrow" + assert settings.network_radio_frequency == 869.618 + assert settings.network_radio_bandwidth == 62.5 + assert settings.network_radio_spreading_factor == 8 + assert settings.network_radio_coding_rate == 8 + assert settings.network_radio_tx_power == 22.0 + + def test_radio_config_no_legacy_field(self) -> None: + """Test that network_radio_config no longer exists.""" + settings = WebSettings(_env_file=None) + assert not hasattr(settings, "network_radio_config") + + def test_radio_config_custom_frequency(self) -> None: + """Test that frequency can be set as a float.""" + settings = WebSettings( + _env_file=None, + network_radio_frequency=915.0, + ) + assert settings.network_radio_frequency == 915.0 + def test_feature_channels_override(self) -> None: """Test that feature_channels can be disabled.""" settings = WebSettings(_env_file=None, feature_channels=False) @@ -187,3 +211,17 @@ class TestWebSettings: assert features["members"] is True assert features["channels"] is True assert features["pages"] is True + assert features["radio_config"] is True + + def test_feature_radio_config_default_true(self) -> None: + """Test that feature_radio_config defaults to True.""" + settings = WebSettings(_env_file=None) + + assert settings.feature_radio_config is True + + def test_feature_radio_config_can_disable(self) -> None: + """Test that feature_radio_config can be disabled.""" + settings = WebSettings(_env_file=None, feature_radio_config=False) + + assert settings.feature_radio_config is False + assert settings.features["radio_config"] is False diff --git a/tests/test_common/test_radio_config.py b/tests/test_common/test_radio_config.py new file mode 100644 index 0000000..17dc117 --- /dev/null +++ b/tests/test_common/test_radio_config.py @@ -0,0 +1,95 @@ +"""Tests for RadioConfig schema.""" + +from meshcore_hub.common.schemas.network import RadioConfig + + +class TestRadioConfigConstruction: + """Tests for RadioConfig direct construction.""" + + def test_defaults_all_none(self) -> None: + config = RadioConfig() + assert config.profile is None + assert config.frequency is None + assert config.bandwidth is None + assert config.spreading_factor is None + assert config.coding_rate is None + assert config.tx_power is None + + def test_eu_uk_narrow_defaults(self) -> None: + config = RadioConfig( + profile="EU/UK Narrow", + frequency=869.618, + bandwidth=62.5, + spreading_factor=8, + coding_rate=8, + tx_power=22.0, + ) + assert config.profile == "EU/UK Narrow" + assert config.frequency == 869.618 + assert config.bandwidth == 62.5 + assert config.spreading_factor == 8 + assert config.coding_rate == 8 + assert config.tx_power == 22.0 + + def test_custom_float_values(self) -> None: + config = RadioConfig( + profile="US 915", + frequency=915.0, + bandwidth=125.0, + spreading_factor=7, + coding_rate=5, + tx_power=30.0, + ) + assert config.frequency == 915.0 + assert config.tx_power == 30.0 + + def test_from_config_string_does_not_exist(self) -> None: + assert not hasattr(RadioConfig, "from_config_string") + + +class TestRadioConfigFormatForDisplay: + """Tests for RadioConfig.format_for_display().""" + + def test_full_config_formatting(self) -> None: + config = RadioConfig( + profile="EU/UK Narrow", + frequency=869.618, + bandwidth=62.5, + spreading_factor=8, + coding_rate=8, + tx_power=22.0, + ) + result = config.format_for_display() + + assert result["profile"] == "EU/UK Narrow" + assert result["frequency"] == "869.618MHz" + assert result["bandwidth"] == "62.5kHz" + assert result["spreading_factor"] == 8 + assert result["coding_rate"] == 8 + assert result["tx_power"] == "22dBm" + + def test_trailing_zeros_stripped(self) -> None: + config = RadioConfig( + profile="Test", + frequency=915.0, + bandwidth=125.0, + spreading_factor=7, + coding_rate=5, + tx_power=30.0, + ) + result = config.format_for_display() + + assert result["frequency"] == "915MHz" + assert result["bandwidth"] == "125kHz" + assert result["tx_power"] == "30dBm" + + def test_none_values_return_none(self) -> None: + config = RadioConfig() + result = config.format_for_display() + + assert result["profile"] is None + assert result["frequency"] is None + assert result["bandwidth"] is None + assert result["spreading_factor"] is None + assert result["coding_rate"] is None + assert result["tx_power"] is None diff --git a/tests/test_web/conftest.py b/tests/test_web/conftest.py index fd794b1..3fb687b 100644 --- a/tests/test_web/conftest.py +++ b/tests/test_web/conftest.py @@ -328,7 +328,12 @@ def web_app(mock_http_client: MockHttpClient, monkeypatch: pytest.MonkeyPatch) - network_name="Test Network", network_city="Test City", network_country="Test Country", - network_radio_config="Test Radio Config", + network_radio_profile="Test Profile", + network_radio_frequency=868.0, + network_radio_bandwidth=125.0, + network_radio_spreading_factor=7, + network_radio_coding_rate=5, + network_radio_tx_power=20.0, network_contact_email="test@example.com", network_contact_discord="https://discord.gg/test", features=ALL_FEATURES_ENABLED, diff --git a/tests/test_web/test_app.py b/tests/test_web/test_app.py index bd27204..351f4bf 100644 --- a/tests/test_web/test_app.py +++ b/tests/test_web/test_app.py @@ -26,7 +26,12 @@ def xss_app(mock_http_client: MockHttpClient) -> Any: network_name="", network_city="Test City", network_country="Test Country", - network_radio_config="Test Radio Config", + network_radio_profile="Test Profile", + network_radio_frequency=868.0, + network_radio_bandwidth=125.0, + network_radio_spreading_factor=7, + network_radio_coding_rate=5, + network_radio_tx_power=20.0, network_contact_email="test@example.com", features=ALL_FEATURES_ENABLED, )