Appearance
WeatherStation Management
Business Context
Weather stations are the source of climate data used across the system for energy consumption normalisation (degree-days), anomaly detection and forecast display. Each client is assigned one or more weather stations:
- a default station used for normalisation of all buildings that have no dedicated assignment
- optionally one or more supplementary stations assigned to specific buildings
Stations are either global — sourced from an external climate-data provider (today ČHMÚ/CHMI, chmi.cz; other providers such as TZB-info are also in use) and shared across all tenants — or owned by the tenant themselves (a physical device, optionally with a remote connection providing automated measurements).
This feature covers the management of the weather station catalogue and the climate data and long-term normals stored against each station. Assignment of stations to clients and buildings is managed in Client Management.
Business-Level Definition
Weather station management provides the catalogue of measurement points that supply climate data to the system. The catalogue has two tiers:
- Global stations — maintained by the E-Manazer superadmin, visible and selectable by all tenants. Climate data for a global station may come from any external provider (e.g. CHMI, TZB-info) or be entered manually.
- Tenant-owned stations — physical devices belonging to a specific client, created by the client's admin, visible only to that tenant. These provide continuous measurements directly from the client's site.
Against each station the system stores:
- climate data: actual measured or reported values
- long-term climate normal: long-term reference averages used to normalise energy consumption. The reference period length and data source are a business decision, not a system constraint — the system stores 12 monthly rows per station regardless of how the underlying average was derived.
The normal can exist for any station regardless of tier — for global stations it is typically pre-loaded by the superadmin; for tenant-owned stations it may be entered manually if available.
Requirements Definition
- Maintain a two-tier catalogue of weather stations: global (superadmin-managed) and tenant-owned (client admin-managed)
- Store and expose climate data per station at monthly, daily and hourly granularity
- Store and expose a long-term climate normal (12 monthly rows per station) used for heating degree-day normalisation
- Connect tenant-owned stations to the remote connection infrastructure for automated data ingestion
- Ensure that global stations are never editable or deletable by tenant-level users
- Support XLSX export of climate data
Acceptance Criteria
- A superadmin can create, edit and delete global weather stations; tenant users cannot
- A client admin can create and edit tenant-owned stations; other tenants cannot see or select them
- Climate data is stored per station and queryable by date range and granularity (monthly / daily / hourly)
- Hourly granularity is available only for stations with a connected remote source
- A long-term climate normal can be created and edited for any station; 12 rows per station (one per calendar month)
- The long-term normal is used in the degree-day normalisation calculation:
daydegree_normal = heating_days_normal × (T_ref − avg_temp_normal) - A station cannot be deleted while it is referenced as
client.defaultWeatherStationIdor in anyclientWeatherStationrow - Climate data can be exported to XLSX
Loom Link
N/A — not available in the source material.
Technical Context
User Stories / Use Cases
Browse the station catalogue (tenant user): A client manager opens the weather station assignment screen (Client Management → Klimadata tab) and selects a default station from the list of all global stations plus the tenant's own stations. The list shows station name, altitude, coordinates and data availability.
Create a global station (superadmin): the superadmin opens the admin panel, creates a new global station with name, GPS coordinates and altitude. The station appears in the catalogue for all tenants.
Create a tenant-owned station (client admin): A client admin creates a new station scoped to their tenant — fills in name, GPS coordinates and altitude. A remote connection can optionally be linked for automated data ingestion; without one, climate data is entered manually. The station is only visible within that tenant.
Load climate data: climate data arrives as remote (via a scheduled batch process from an external feed, or through the standard remote connection pipeline) or is entered as manual, for any station regardless of tier.
Manage long-term normal: The superadmin opens a global station and enters or updates the 12-row monthly normal (heating days and average temperature per month). A client admin can do the same for their own station.
View and export climate data (client user): A user opens the Klimadata tab for a client, selects a station and date range, views data at monthly/daily/hourly granularity and exports to XLSX.
UI/UX Design
A click-through prototype exists — the "Klimadata" tab in the prototype shows three blocks: (1) assigned station list with role badges (default / supplementary), altitude, assigned building count and data availability; (2) a meteogram widget (see External Integrations — yr.no); (3) a climate data table with granularity toggle and XLSX export. The long-term normal is accessible via a button on each station row.
Detailed UI/UX design (spacing, component specs, responsive behaviour) has not been completed. Acceptance criteria describe functional behaviour only.
Functional Requirements
Station visibility and ownership
| Attribute | Global station | Tenant-owned station |
|---|---|---|
| tenantId | null | tenant's UUID |
| Created by | Superadmin (admin panel) | Client admin |
| Visible to | All tenants | Own tenant only |
| Editable by tenant | No | Yes |
| Data source | Remote or manual | Remote or manual |
| Granularity | Determined solely by remoteConnectionId (see below) | Determined solely by remoteConnectionId (see below) |
| Long-term normal | Entered by superadmin | Entered manually by client admin (if available) |
| Client assignment | Via clientWeatherStation (Client Management) | Via clientWeatherStation (Client Management) |
Climate data granularity
Climate data is stored as individual time-stamped records. The API aggregates on query:
- Monthly — available for all stations
- Daily — available for all stations with daily data (global stations typically provide daily values)
- Hourly — available for any station with an active remote connection (
weatherStation.remoteConnectionIdset)
Climate data fields per record: date, heatingDays (počet topných dnů), avgTemp (průměrná denní teplota °C), plus any additional parameters reported by the station's data source (e.g. solar radiation, precipitation, wind speed), stored in climateData.extraParameters. As with gauge remote readings, if the source provides more values, the system stores and displays all of them.
A heating day is a day whose average outdoor temperature falls below 13 °C, using a season-dependent hysteresis rule (see "Derivation of heatingDays and avgTemp" below). The threshold is fixed and does not vary by tenant, station tier, or building — it is an internal business rule, not an externally published CHMI standard.
Degree-day values (D21) are computed at query time from heatingDays using a fixed 21 °C reference temperature (not the building's reference temperature). D21 is shown only as a simple indicator in the climate data overview and does not feed into any further calculation. The normalisation ratio calculation (see "Long-term climate normal" below) uses the building's actual reference temperature (T_ref), which may differ from 21 °C and differs per building.
Derivation of heatingDays and avgTemp (automated stations)
For stations with a remote data source, heatingDays and avgTemp at daily granularity are computed automatically by a scheduled daily job (not imported directly):
- avgTemp (daily): stored as
climateData.avgTemp; calculated asavgTemp = round( (T07 + T14 + 2×T21) / 4, 1 ), where T07/T14/T21 are the temperature readings closest to 7:00, 14:00 and 21:00 of that day. Times are resolved from the station's raw sensor time series:- first look for a reading within ±30 minutes of the target time
- if none exists, fall back to the arithmetic mean of readings within ±1 hour
- if any of the three target times has no reading even after the ±1h fallback, the entire day is skipped — no
climateDatarow is created for that station/date, and it is logged as a warning
- isHeatingDay (daily): stored as
climateData.heatingDays(0/1); heating season is September–December + January–May; fixed 13 °C threshold with a 2-day hysteresis — a day is a heating day unless the current day and both of the previous 2 days were all above 13 °C avgTemp; mixed states default to 1 (prevents a single mild day from switching off heating mid-season); outside the heating season it is always 0 regardless of temperature - monthly aggregation (
monthlygranularity):avgTemp (monthly) = round( sum(daily avgTemp) / daysInMonth, 1 );heatingDays (monthly) = count(days in month with isHeatingDay = 1). If even one day in the month is missing adailyrow, the entire month is skipped for that station — no partial monthly record is created.
This approach applies only to daily/monthly rows for stations with source = remote. For manual entries, heatingDays and avgTemp are set directly by the admin (no computation is applied). Both attributes are null for hourly (and any finer) granularity, regardless of source.
Manual override: An admin may manually enter or correct heatingDays/avgTemp for a daily or monthly row even when it was originally computed automatically for a remote station. This is a standard correction via the existing validFrom versioning. The corrected row's source becomes manual. No validation cross-checks a manually entered value (consistency is the admin's responsibility).
CHMI-sourced stations (exception to the derivation above): Global stations sourced from CHMI's automated feed (opendata.chmi.cz/meteorology/climate/recent/data/daily/) do not run the avgTemp computation described above. CHMI already publishes a ready-made daily average (element T, VTYPE = AVG), verified to match the same 7-14-21 weighted formula. The scheduled import job (see Backward Compatibility and Migration) maps this value directly to climateData.avgTemp — no independent recomputation from raw readings. The isHeatingDay hysteresis logic still runs on top of this imported avgTemp — CHMI provides no equivalent "heating day" value of its own. Stations sourced this way are identified via weatherStation.wsi (WIGOS Station Identifier), used to pair incoming feed records to the correct station.
Long-term climate normal
The long-term normal is a set of 12 rows per station (one per calendar month) representing reference averages used to normalise energy consumption. The reference period length and data source are a business decision, not a system constraint.
Fields per row: month (1–12), heatingDays, avgTemp, plus any additional reference values available for the station (e.g. precipitation, sunshine hours), stored in extraParameters. The normal is mutable — rows can be updated by the appropriate admin. It is used in the normalisation ratio calculation:
daydegree_normal = heatingDays_normal × (T_ref − avgTemp_normal)
daydegree_actual = heatingDays_actual × (T_ref − avgTemp_actual)
normalisation_ratio = daydegree_normal / daydegree_actualWhere T_ref is the building's reference temperature from buildingParameter.temperatureReference. Missing normal data causes a warning in the normalised consumption report ("Nejsou k dispozici klimatická data pro normování spotřeby" — "Climate data is not available for consumption normalisation").
Remote connection
Any station may have a remoteConnectionId linking it to a remote connection record (same infrastructure as gauge remote connections). When set, climate data is ingested as remote via the remote connection pipeline. When null, data must be entered as manual, or imported via a scheduled batch process. The remote connection detail (device config, OBIS codes, communication parameters) is managed in Remote Connection Management (out of scope for this doc) — WeatherStation Management only holds the FK.
Delete reference check
Deletion is a soft delete — the DELETE endpoint sets weatherStation.deletedAt; the row itself is never physically removed. As a consequence, historical climateData and climateNormal rows continue to reference a valid (soft-deleted) weatherStationId indefinitely — no cascade or additional reference check against those tables is needed. Soft-deleted stations are excluded from selection lists (WHERE deletedAt IS NULL) but their historical data remains intact and queryable.
A station cannot be deleted while referenced by any client. On DELETE request: check client.defaultWeatherStationId and clientWeatherStation.weatherStationId; if referenced, return 409 Conflict with {"error": "WEATHER_STATION_REFERENCED", "referencedIn": [...]}.
Permissions model
Pending — to be defined during Users & Access design. Key distinction: superadmin operations on global stations vs. client admin operations on tenant-owned stations must be enforced at API level using the tenantId check (tenantId = null → superadmin-only write).
Non-Functional Requirements
Performance
climateDatais append-only, versioned viavalidFrom. A composite index on(weatherStationId, date, granularity, validFrom DESC)is critical for resolving the current value efficiently (idx_climate_data_current_lookup).climateNormalis rarely mutated — index on(weatherStationId, month)is sufficient.
Transactional Operations
- Creation of a tenant-owned station and its initial
climateNormalplaceholder rows (if seeded) must be atomic
API Analysis
Station catalogue — creating, reading, updating and deleting station records; enforces visibility rules between global (superadmin-only write) and tenant-owned (client admin write) stations.
GET /v1/weather-stations List stations (global + own tenant; superadmin sees all)
GET /v1/weather-stations/:id Station detail
POST /v1/weather-stations Create station (tenantId = null → superadmin only; tenantId set → client admin)
PATCH /v1/weather-stations/:id Update station fields
DELETE /v1/weather-stations/:id Soft-delete station — sets deletedAt (409 Conflict if referenced by any client)Climate data — retrieval and insertion (manual, single-record, or bulk via XLSX import) of time-series climate records per station; supports monthly, daily and hourly granularity with XLSX export and import.
GET /v1/weather-stations/:id/climate Climate data (query params: from, to, granularity)
POST /v1/weather-stations/:id/climate Insert climate data record (manual entry, batch import, or a correction — see climateData entity for validFrom versioning)
POST /v1/weather-stations/:id/climate/import Bulk import climate data from XLSX (heatingDays, avgTemp, and any extraParameters columns present in the file); each row processed with the same validFrom versioning logic as manual insert
GET /v1/weather-stations/:id/climate/export Export climate data to XLSXLong-term climate normal — reading and replacing the set of 12 monthly reference rows per station used for degree-day normalisation.
GET /v1/weather-stations/:id/normals Long-term normal (12 rows, one per calendar month)
PUT /v1/weather-stations/:id/normals Replace all 12 rows atomicallyNote: assignment of stations to clients is managed via Client Management — PATCH /v1/clients/:id/weather-stations.
Domain Model (ER diagram) & Data Attribute Table
An ER diagram exists in the source Confluence page (embedded image, not extractable in this migration pass — flagged as incomplete). Related entity pages (Confluence, not yet migrated into this repo's Entity DAT catalog):
- weatherStation — station catalogue record (global/tenant-owned, GPS, altitude, remote connection FK)
- climateData — time-series climate records per station
- climateNormal — 12-row long-term monthly normal per station
Data
Global CHMI stations and their long-term normals are seeded during initial system setup by the superadmin using data from the CHMI official dataset. No tenant-specific seed data is required — tenant-owned stations are created by client admins as part of their onboarding.
Test Data
N/A — not covered in source material; no Test Data location was specified in the source page.
Logging
.info
- Weather station created, updated, deleted (stationId, tenantId or "global", actor)
- Climate data batch import completed (stationId, record count, date range)
- Climate data correction recorded (stationId, date, granularity, previousValue, newValue, previousValidFrom, newValidFrom) — logged whenever a new climateData row is inserted for a (weatherStationId, date, granularity) combination that already has a prior version
- Long-term normal updated (stationId, month, previous values, new values)
.warn
- Daily avgTemp computation skipped — insufficient sensor data for one or more of the target readings
- Monthly aggregation skipped — one or more daily climateData rows missing for the month (stationId, month, missing date count or list)
.debug
- Climate data query executed (stationId, granularity, date range, row count returned)
- Delete reference check triggered (stationId, result: allowed / blocked with client list)
Monitoring
N/A — not covered in source material.
Caching
N/A — not covered in source material.
Backward Compatibility and Migration
The complete field-by-field migration map, including exact legacy column names and all open decisions, was maintained in a child Confluence page out of scope for this migration pass.
The legacy system used two separate entities for climate data (HMShortTermData for actual records, HMLongTermData for normals) and a single HMStation table for all stations, with the editable boolean flag distinguishing CHMI stations from tenant-owned ones. Client-station assignments were stored as direct PHP associations on the Client entity.
Legal Context
N/A — not addressed in the source Confluence page; migrated as a reference copy without new legal analysis.
Cybersecurity Considerations
N/A — not addressed in the source Confluence page; migrated as a reference copy without new security analysis.
Risk Assessment
N/A — not addressed in the source Confluence page; migrated as a reference copy without new risk analysis.
Auditing, Reporting & Measurement
N/A — not addressed in the source Confluence page; migrated as a reference copy without new analysis.