Scaffold the Flutter app with Italian l10n and region configuration

Creates the app skeleton and the seam everything else in the project hangs
off: region-specific data lives in an asset file, not in code, so adding a
region later is a new JSON file rather than a refactor.

Notable choices:

- applicationId and namespace are it.nuvolari.app rather than the doubled
  it.nuvolari.nuvolari that `flutter create` produces, with the Kotlin package
  and the iOS bundle identifiers moved to match.
- SDK levels are pinned instead of inherited from `flutter.*`. Google Play
  requires API 36, and that is a release blocker rather than something to let a
  Flutter upgrade change silently. minSdk 24 is the highest floor the planned
  dependencies impose.
- Riverpod and Dio-free for now, no code generation: see docs/stack-decisions.md.
- Italian is the l10n source language, so app_it.arb is the template rather
  than a translation of an English original.

The region parser rejects rather than repairs. An inverted bounding box, a map
centre outside its own bounds, an unknown adapter name, a duplicate zone code
or an empty attribution list all throw with the offending field named. Each of
those would otherwise fail silently and visibly wrong: a swapped latitude and
longitude renders the radar in the wrong place, an unknown adapter falling back
to mock would show demo frames where live data was expected, and a missing
attribution is a licence violation rather than a cosmetic gap.

Tests run against the asset that actually ships and against a captured copy of
the live ARPA CAP feed, so the eleven zone codes in the config are checked
against the eleven the feed really emits rather than against a list retyped
from documentation.

Verified: dart format clean, flutter analyze 0 issues, 50 tests passing,
flutter build appbundle --debug produces an AAB with applicationId
it.nuvolari.app, minSdk 24, targetSdk 36.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 11:24:26 +02:00
co-authored by Claude Opus 5
parent 3fcbb9f1c4
commit 85b2c99949
78 changed files with 4110 additions and 2 deletions
+54
View File
@@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nuvolari/l10n/app_localizations.dart';
import 'package:nuvolari/main.dart';
void main() {
testWidgets('starts up and shows the Italian UI', (tester) async {
await tester.pumpWidget(const ProviderScope(child: NuvolariApp()));
await tester.pump();
expect(find.text('Nuvolari'), findsWidgets);
});
group('AppLocalizations', () {
test('supports Italian', () {
expect(
AppLocalizations.supportedLocales.map((locale) => locale.languageCode),
contains('it'),
);
});
test('resolves Italian strings', () async {
final l10n = await AppLocalizations.delegate.load(const Locale('it'));
expect(l10n.appTitle, 'Nuvolari');
expect(l10n.navRadar, 'Radar');
expect(l10n.navForecast, 'Previsioni');
expect(l10n.navAlerts, 'Allerte');
});
// The disclaimer is a compliance requirement, not copy: it must state both
// that the app is independent and that official channels take precedence.
test(
'the disclaimer states independence and defers to official channels',
() async {
final l10n = await AppLocalizations.delegate.load(const Locale('it'));
expect(l10n.sourcesDisclaimerBody, contains('indipendente'));
expect(l10n.sourcesDisclaimerBody, contains('Arpa Piemonte'));
expect(l10n.sourcesDisclaimerBody, contains('Protezione Civile'));
expect(l10n.sourcesDisclaimerBody, contains('ufficiali'));
},
);
test('pluralises the data age message', () async {
final l10n = await AppLocalizations.delegate.load(const Locale('it'));
expect(l10n.dataAgeMinutes(0), 'Aggiornato ora');
expect(l10n.dataAgeMinutes(1), 'Aggiornato 1 minuto fa');
expect(l10n.dataAgeMinutes(12), 'Aggiornato 12 minuti fa');
});
});
}
+134
View File
@@ -0,0 +1,134 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:nuvolari/core/region/geo.dart';
void main() {
group('GeoBounds.fromJson', () {
test('parses [west, south, east, north]', () {
final bounds = GeoBounds.fromJson(<double>[6.55, 43.95, 9.30, 46.55]);
expect(bounds.west, 6.55);
expect(bounds.south, 43.95);
expect(bounds.east, 9.30);
expect(bounds.north, 46.55);
});
test('accepts integers as well as doubles', () {
final bounds = GeoBounds.fromJson(<num>[6, 44, 9, 46]);
expect(bounds.west, 6.0);
expect(bounds.north, 46.0);
});
test('rejects a list of the wrong length', () {
expect(
() => GeoBounds.fromJson(<double>[6.55, 43.95, 9.30]),
throwsFormatException,
);
});
test('rejects non-numeric entries', () {
expect(
() => GeoBounds.fromJson(<Object>[6.55, '43.95', 9.30, 46.55]),
throwsFormatException,
);
});
test('rejects a value that is not a list', () {
expect(() => GeoBounds.fromJson('6.55,43.95'), throwsFormatException);
});
// Swapping latitude and longitude produces an inverted box. Normalising it
// would render the radar in the wrong place with no error, so it must throw.
test('rejects an inverted box rather than normalising it', () {
expect(
() => GeoBounds.fromJson(<double>[9.30, 43.95, 6.55, 46.55]),
throwsFormatException,
);
expect(
() => GeoBounds.fromJson(<double>[6.55, 46.55, 9.30, 43.95]),
throwsFormatException,
);
});
test('rejects a degenerate box', () {
expect(
() => GeoBounds.fromJson(<double>[6.55, 43.95, 6.55, 46.55]),
throwsFormatException,
);
});
test('rejects out-of-range coordinates', () {
expect(
() => GeoBounds.fromJson(<double>[-181, 43.95, 9.30, 46.55]),
throwsFormatException,
);
expect(
() => GeoBounds.fromJson(<double>[6.55, 43.95, 9.30, 91]),
throwsFormatException,
);
});
});
group('GeoBounds', () {
const piemonte = GeoBounds(
west: 6.55,
south: 43.95,
east: 9.30,
north: 46.55,
);
test('computes its centre', () {
expect(piemonte.center.longitude, closeTo(7.925, 1e-9));
expect(piemonte.center.latitude, closeTo(45.25, 1e-9));
});
test('contains points inside and on the edge', () {
expect(piemonte.contains(const GeoPoint(7.686, 45.070)), isTrue);
expect(piemonte.contains(const GeoPoint(6.55, 43.95)), isTrue);
});
test('excludes points outside', () {
// Milan is outside the region but inside the padded box, which is
// intended: the box is a render extent, not an administrative boundary.
expect(piemonte.contains(const GeoPoint(9.19, 45.46)), isTrue);
// Venice, well outside.
expect(piemonte.contains(const GeoPoint(12.33, 45.44)), isFalse);
// Palermo, south.
expect(piemonte.contains(const GeoPoint(13.36, 38.12)), isFalse);
});
test('round-trips through toJson', () {
expect(GeoBounds.fromJson(piemonte.toJson()), piemonte);
});
});
group('WebMercator', () {
test('maps the prime meridian and equator to the centre', () {
expect(WebMercator.xFromLongitude(0), closeTo(0.5, 1e-12));
expect(WebMercator.yFromLatitude(0), closeTo(0.5, 1e-12));
});
test('maps the antimeridian to the edges', () {
expect(WebMercator.xFromLongitude(-180), closeTo(0.0, 1e-12));
expect(WebMercator.xFromLongitude(180), closeTo(1.0, 1e-12));
});
test('y decreases as latitude increases', () {
expect(
WebMercator.yFromLatitude(46.55),
lessThan(WebMercator.yFromLatitude(43.95)),
);
});
// Beyond the Mercator limit the projection diverges; clamping keeps the
// transform finite instead of producing infinity.
test('clamps latitudes beyond the Mercator limit', () {
expect(WebMercator.yFromLatitude(89.9).isFinite, isTrue);
expect(WebMercator.yFromLatitude(-89.9).isFinite, isTrue);
expect(
WebMercator.yFromLatitude(90),
closeTo(WebMercator.yFromLatitude(WebMercator.maxLatitude), 1e-12),
);
});
});
}
@@ -0,0 +1,203 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nuvolari/core/region/region_config.dart';
import 'package:nuvolari/core/region/region_repository.dart';
/// An in-memory bundle so repository behaviour can be tested without the real
/// asset bundle, which `flutter test` does not populate.
class _InMemoryAssetBundle extends CachingAssetBundle {
_InMemoryAssetBundle(this._contents);
final Map<String, String> _contents;
@override
Future<ByteData> load(String key) async {
final value = _contents[key];
if (value == null) {
throw FlutterError('asset not found: $key');
}
return ByteData.sublistView(Uint8List.fromList(utf8.encode(value)));
}
}
void main() {
const assetPath = 'assets/regions/piemonte.json';
// These assertions run against the file that actually ships, so a bad edit to
// the asset fails the build rather than the app at run time.
group('the shipped Piemonte asset', () {
late RegionConfig config;
late String source;
setUpAll(() {
source = File(assetPath).readAsStringSync();
config = RegionConfig.parse(source);
});
test('exists and parses', () {
expect(File(assetPath).existsSync(), isTrue);
expect(config.id, 'piemonte');
expect(config.displayName, 'Piemonte');
expect(config.timeZone, 'Europe/Rome');
});
test('is declared in pubspec.yaml', () {
final pubspec = File('pubspec.yaml').readAsStringSync();
expect(pubspec, contains('assets/regions/'));
});
test('bounds contain the major Piedmont cities', () {
final cities = <String, List<double>>{
'Torino': <double>[7.686, 45.070],
'Cuneo': <double>[7.549, 44.393],
'Novara': <double>[8.622, 45.446],
'Alessandria': <double>[8.615, 44.913],
'Verbania': <double>[8.552, 45.921],
'Asti': <double>[8.206, 44.900],
'Biella': <double>[8.054, 45.563],
'Vercelli': <double>[8.418, 45.322],
};
for (final entry in cities.entries) {
final inside =
entry.value[0] >= config.bounds.west &&
entry.value[0] <= config.bounds.east &&
entry.value[1] >= config.bounds.south &&
entry.value[1] <= config.bounds.north;
expect(inside, isTrue, reason: '${entry.key} is outside the bbox');
}
});
// The eleven codes below are the ones the ARPA CAP feed actually emits.
// Note that the Italian alphabet is used, so J and K do not exist.
test('declares the eleven ARPA alert zones', () {
const expectedCodes = <String>[
'Piem-A',
'Piem-B',
'Piem-C',
'Piem-D',
'Piem-E',
'Piem-F',
'Piem-G',
'Piem-H',
'Piem-I',
'Piem-L',
'Piem-M',
];
expect(
config.alertZones.map((zone) => zone.code),
orderedEquals(expectedCodes),
);
});
test('every alert zone has a name and at least one province', () {
for (final zone in config.alertZones) {
expect(zone.name, isNotEmpty, reason: '${zone.code} has no name');
expect(
zone.provinces,
isNotEmpty,
reason: '${zone.code} has no provinces',
);
}
});
test('matches the zone codes present in the captured CAP feed', () {
final cap = File('test/fixtures/arpa_cap_allerta.xml').readAsStringSync();
final codesInFeed = RegExp(r'<areaDesc>([^<]+)</areaDesc>')
.allMatches(cap)
.map((match) => match.group(1)!)
.toSet();
expect(codesInFeed, isNotEmpty);
for (final code in codesInFeed) {
expect(
config.zoneByCode(code),
isNotNull,
reason: 'the CAP feed emits $code but the region config omits it',
);
}
});
// Radar-DPC is CC BY-SA and OpenStreetMap is ODbL: both credits are legal
// obligations, not decoration, so their absence must fail the build.
test('carries the mandatory attributions', () {
final ids = config.attributions.map((a) => a.id).toSet();
expect(ids, containsAll(<String>['dpc', 'osm']));
final dpc = config.attributions.firstWhere((a) => a.id == 'dpc');
expect(dpc.license, 'CC BY-SA');
expect(dpc.text, contains('Radar-DPC'));
final osm = config.attributions.firstWhere((a) => a.id == 'osm');
expect(osm.license, 'ODbL');
expect(osm.text, contains('OpenStreetMap'));
});
test('links the official alert bulletin', () {
expect(config.alerts.officialBulletinUrl, startsWith('https://'));
expect(config.alerts.officialBulletinUrl, contains('arpa.piemonte.it'));
});
// ARPA Piemonte radar needs an authorization we do not have, so it must not
// be reachable from configuration yet.
test('does not offer the ARPA radar adapter', () {
expect(
config.radar.availableAdapters,
isNot(contains(RadarAdapter.arpa)),
);
});
test('defaults to the offline mock radar source', () {
expect(config.radar.defaultAdapter, RadarAdapter.mock);
expect(config.radar.product, 'VMI');
expect(config.radar.frameInterval, const Duration(minutes: 5));
});
});
group('RegionRepository', () {
test('loads and parses a region from the bundle', () async {
final source = File(assetPath).readAsStringSync();
final repository = RegionRepository(
bundle: _InMemoryAssetBundle(<String, String>{assetPath: source}),
);
final config = await repository.load('piemonte');
expect(config.id, 'piemonte');
});
test('builds the asset path from the region id', () {
expect(RegionRepository.assetPathFor('piemonte'), assetPath);
expect(
RegionRepository.assetPathFor('lombardia'),
'assets/regions/lombardia.json',
);
});
test('propagates a parse failure instead of falling back', () async {
final repository = RegionRepository(
bundle: _InMemoryAssetBundle(<String, String>{assetPath: '{"id": 1}'}),
);
await expectLater(
repository.load('piemonte'),
throwsA(isA<FormatException>()),
);
});
test('propagates a missing asset', () async {
final repository = RegionRepository(
bundle: _InMemoryAssetBundle(<String, String>{}),
);
await expectLater(repository.load('atlantis'), throwsA(isA<Error>()));
});
});
}
@@ -0,0 +1,247 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:nuvolari/core/region/region_config.dart';
/// A minimal valid document. Individual tests mutate a copy of this so each one
/// states exactly the one thing it is about.
Map<String, Object?> validDocument() => <String, Object?>{
'id': 'testregion',
'displayName': 'Test Region',
'timeZone': 'Europe/Rome',
'bbox': <double>[6.55, 43.95, 9.30, 46.55],
'map': <String, Object?>{
'center': <double>[7.95, 45.25],
'zoom': <String, Object?>{'min': 6.0, 'max': 13.0, 'initial': 7.2},
},
'sources': <String, Object?>{
'radar': <String, Object?>{
'defaultAdapter': 'mock',
'availableAdapters': <String>['mock', 'dpc'],
'product': 'VMI',
'frameIntervalMinutes': 5,
},
'forecast': <String, Object?>{
'defaultAdapter': 'metno',
'availableAdapters': <String>['metno'],
},
'alerts': <String, Object?>{
'defaultAdapter': 'arpaCap',
'availableAdapters': <String>['arpaCap'],
'officialBulletinUrl': 'https://example.org/bulletin.pdf',
},
},
'alertZones': <Object?>[
<String, Object?>{
'code': 'Test-A',
'name': 'Zone A',
'provinces': <String>['AA'],
},
],
'attributions': <Object?>[
<String, Object?>{
'id': 'src',
'text': 'Source',
'license': 'CC BY-SA',
'url': 'https://example.org',
},
],
};
RegionConfig parseWith(void Function(Map<String, Object?> doc) mutate) {
final doc = validDocument();
mutate(doc);
return RegionConfig.fromJson(doc);
}
void main() {
group('RegionConfig.fromJson', () {
test('parses a complete document', () {
final config = RegionConfig.fromJson(validDocument());
expect(config.id, 'testregion');
expect(config.displayName, 'Test Region');
expect(config.timeZone, 'Europe/Rome');
expect(config.bounds.west, 6.55);
expect(config.map.center.longitude, 7.95);
expect(config.map.zoom.initial, 7.2);
expect(config.radar.defaultAdapter, RadarAdapter.mock);
expect(config.radar.product, 'VMI');
expect(config.radar.frameInterval, const Duration(minutes: 5));
expect(config.forecast.defaultAdapter, ForecastAdapter.metno);
expect(config.alerts.defaultAdapter, AlertAdapter.arpaCap);
expect(config.alertZones, hasLength(1));
expect(config.attributions, hasLength(1));
});
test('looks zones up by their feed code', () {
final config = RegionConfig.fromJson(validDocument());
expect(config.zoneByCode('Test-A')?.name, 'Zone A');
expect(config.zoneByCode('Test-Z'), isNull);
});
test('reports the missing field by name', () {
expect(
() => parseWith((doc) => doc.remove('displayName')),
throwsA(
isA<FormatException>().having(
(e) => e.message,
'message',
contains('displayName'),
),
),
);
});
// An unknown adapter must not silently fall back: falling back to mock
// would show demo frames where live data was expected.
test('rejects an unknown radar adapter', () {
expect(
() => parseWith((doc) {
final radar =
(doc['sources']! as Map<String, Object?>)['radar']!
as Map<String, Object?>;
radar['defaultAdapter'] = 'satellite';
radar['availableAdapters'] = <String>['satellite'];
}),
throwsA(
isA<FormatException>().having(
(e) => e.message,
'message',
contains('satellite'),
),
),
);
});
test('rejects a default adapter missing from the available list', () {
expect(
() => parseWith((doc) {
final radar =
(doc['sources']! as Map<String, Object?>)['radar']!
as Map<String, Object?>;
radar['defaultAdapter'] = 'dpc';
radar['availableAdapters'] = <String>['mock'];
}),
throwsFormatException,
);
});
test('rejects a map centre outside the bounding box', () {
expect(
() => parseWith((doc) {
(doc['map']! as Map<String, Object?>)['center'] = <double>[
12.33,
45.44,
];
}),
throwsA(
isA<FormatException>().having(
(e) => e.message,
'message',
contains('outside bbox'),
),
),
);
});
test('rejects an initial zoom outside the min/max range', () {
expect(
() => parseWith((doc) {
(doc['map']! as Map<String, Object?>)['zoom'] = <String, Object?>{
'min': 6.0,
'max': 13.0,
'initial': 20.0,
};
}),
throwsFormatException,
);
});
test('rejects a non-positive frame interval', () {
expect(
() => parseWith((doc) {
((doc['sources']! as Map<String, Object?>)['radar']!
as Map<String, Object?>)['frameIntervalMinutes'] =
0;
}),
throwsFormatException,
);
});
test('rejects duplicate alert zone codes', () {
expect(
() => parseWith((doc) {
doc['alertZones'] = <Object?>[
<String, Object?>{
'code': 'Test-A',
'name': 'Zone A',
'provinces': <String>['AA'],
},
<String, Object?>{
'code': 'Test-A',
'name': 'Zone A again',
'provinces': <String>['BB'],
},
];
}),
throwsA(
isA<FormatException>().having(
(e) => e.message,
'message',
contains('duplicate'),
),
),
);
});
// Every source we render carries a credit obligation, so a config with no
// attributions is a licensing bug and must not load.
test('rejects an empty attributions list', () {
expect(
() => parseWith((doc) => doc['attributions'] = <Object?>[]),
throwsFormatException,
);
});
test('rejects an empty alert zone list', () {
expect(
() => parseWith((doc) => doc['alertZones'] = <Object?>[]),
throwsFormatException,
);
});
test('accepts an attribution with no stated license', () {
final config = parseWith((doc) {
doc['attributions'] = <Object?>[
<String, Object?>{
'id': 'arpa',
'text': 'Arpa Piemonte',
'license': null,
'url': 'https://www.arpa.piemonte.it',
},
];
});
expect(config.attributions.single.license, isNull);
expect(config.attributions.single.text, 'Arpa Piemonte');
});
});
group('RegionConfig.parse', () {
test('parses JSON text', () {
final config = RegionConfig.parse(jsonEncode(validDocument()));
expect(config.id, 'testregion');
});
test('rejects JSON that is not an object', () {
expect(() => RegionConfig.parse('[1, 2, 3]'), throwsFormatException);
});
test('rejects malformed JSON', () {
expect(() => RegionConfig.parse('{not json'), throwsFormatException);
});
});
}
+660
View File
@@ -0,0 +1,660 @@
<?xml version="1.0" encoding="UTF-8"?>
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
<identifier>261/2026</identifier>
<sender>centro.funzionale@arpa.piemonte.it</sender>
<sent>2026-09-09T10:00:02+00:00</sent>
<status>Actual</status>
<msgType>Alert</msgType>
<source>Operator</source>
<scope>Public</scope>
<note>AVVISO DI CONDIZIONI METEOROLOGICHE AVVERSE per i dettagli consultare il bollettino di Vigilanza Meteorologica. Temporali localmente intensi oggi, in attenuazione nel corso della mattinata di domani tranne sulle zone appenniniche.</note>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-A</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-B</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Met</category>
<event>VERDE</event>
<responseType>None</responseType>
<urgency>Unknown</urgency>
<severity>Unknown</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>-</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-C</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Met</category>
<event>VERDE</event>
<responseType>None</responseType>
<urgency>Unknown</urgency>
<severity>Unknown</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>-</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-D</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Met</category>
<event>VERDE</event>
<responseType>None</responseType>
<urgency>Unknown</urgency>
<severity>Unknown</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>-</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-E</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-F</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-G</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-H</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-I</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-L</areaDesc>
</area>
</info>
<info>
<language>it-IT</language>
<category>Geo</category>
<event>GIALLO</event>
<responseType>Prepare</responseType>
<urgency>Expected</urgency>
<severity>Moderate</severity>
<certainty>Likely</certainty>
<onset>2026-09-09T11:00:00+00:00</onset>
<expires>2026-09-10T22:00:00+00:00</expires>
<instruction>http://www2.regione.piemonte.it/protezionecivile/index.php/allertamento/cosa-fare-in-caso-di-allerta</instruction>
<parameter>
<valueName>EFFETTI SUL TERRITORIO</valueName>
<value>LOCALI ALLAGAMENTI, CADUTA ALBERI, FULMINAZIONI, GRANDINATE E ISOLATI FENOMENI DI VERSANTE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDRAULICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>IDROGEOLOGICO_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>TEMPORALI_1224</valueName>
<value>GIALLO</value>
</parameter>
<parameter>
<valueName>TEMPORALI_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_1224</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>NEVE_2436</valueName>
<value>VERDE</value>
</parameter>
<parameter>
<valueName>VALANGHE_1224</valueName>
<value>BIANCO</value>
</parameter>
<parameter>
<valueName>VALANGHE_2436</valueName>
<value>BIANCO</value>
</parameter>
<area>
<areaDesc>Piem-M</areaDesc>
</area>
</info>
</alert>