PHP cache updater (get the weather forecast)
Save as get_yrno_cache.php (same directory as your emulator). It:
- fetches
locationforecast/2.0/compactfor your lat/lon - sends a proper User-Agent (required by MET Norway)
- uses If-None-Match / If-Modified-Since to be nice to the API
- writes the raw JSON into
cache_forecast.json(exactly what our emulator expects) - keeps a small
.meta.jsonnext to it for ETag/Last-Modified
<?php
/**
get_yrno_cache.php
-----------------------------------------------
Fetch MET Norway (Yr.no) Locationforecast /compact and store raw JSON
to cache_forecast.json for the Loxone emulator to read.
*
Run via cron every 30 minutes.
Requires: PHP with allow_url_fopen enabled (default on Synology).
*/
// === REQUIRED SETTINGS ===
$lat = 48.2080; // your latitude
$lon = 16.3710; // your longitude
$userAgent = 'HomeWeather/1.0 contact@example.com'; // REQUIRED by MET Norway: app + contact
$cacheFile = DIR . '/cache_forecast.json';
$metaFile = DIR . '/cache_forecast.meta.json';
// === Build request ===
$url = "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={$lat}&lon={$lon}";
$headers = [
"User-Agent: {$userAgent}",
];
// Load previous meta (ETag / Last-Modified)
$etag = $lastModified = null;
if (is_file($metaFile)) {
$meta = json_decode(@file_get_contents($metaFile), true) ?: [];
$etag = $meta['etag'] ?? null;
$lastModified = $meta['last_modified'] ?? null;
}
if ($etag) $headers[] = "If-None-Match: {$etag}";
if ($lastModified) $headers[] = "If-Modified-Since: {$lastModified}";
// Context
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 20,
'header' => implode("\r\n", $headers) . "\r\n",
'ignore_errors' => true, // so we can read body on 304/4xx if present
],
]);
// Fetch
$body = @file_get_contents($url, false, $ctx);
$httpCode = 0;
if (isset($http_response_header) && is_array($http_response_header)) {
// Extract status code
if (preg_match('#^HTTP/\S+\s+(\d+)#', $http_response_header[0], $m)) {
$httpCode = (int)$m[1];
}
}
// Handle 304 Not Modified
if ($httpCode === 304) {
// Nothing to update; keep existing cache file
exit(0);
}
// On success, write new JSON + meta
if ($httpCode >= 200 && $httpCode < 300 && $body) {
// Validate JSON
$decoded = json_decode($body, true);
if ($decoded === null) {
// Bad JSON; do not overwrite working cache
fwrite(STDERR, "Yr.no fetch: got invalid JSON, keeping old cache.\n");
exit(1);
}
// Write cache atomically $tmpFile = $cacheFile . '.tmp'; if (file_put_contents($tmpFile, $body) === false) { fwrite(STDERR, "Yr.no fetch: failed to write temp cache.\n"); exit(1); } rename($tmpFile, $cacheFile); // Collect new ETag / Last-Modified $newEtag = $newLM = null; foreach ($http_response_header as $h) { if (stripos($h, 'ETag:') === 0) $newEtag = trim(substr($h, 5)); if (stripos($h, 'Last-Modified:') === 0) $newLM = trim(substr($h, 14)); } $metaOut = [ 'etag' => $newEtag, 'last_modified' => $newLM, 'fetched_at' => gmdate('c'), 'url' => $url, ]; file_put_contents($metaFile, json_encode($metaOut, JSON_PRETTY_PRINT)); exit(0);
}
// Non-success (4xx/5xx): keep old cache
fwrite(STDERR, "Yr.no fetch: HTTP {$httpCode}, keeping old cache.\n");
exit(1);
We need to execute this script automatically every half an hour using Cron. Edit your crontab (or DSM Task Scheduler):
*/30 * * * * /usr/bin/php -d detect_unicode=0 /path/to/get_yrno_cache.php >/dev/null 2>&1
Remember: MET Norway asks you to keep requests sensible (e.g., 20–60 min). Don’t hammer the API. Your conditional headers above already reduce bandwidth.
Where this plugs into your emulator
Your emulator already points to:
$cache_file = __DIR__ . '/cache_forecast.json';
The fetcher above writes exactly that file’s content (raw API JSON), so the emulator we described above will consume it as-is and output desired CSV at ?format=2.
That’s it — you now have the full, reproducible loop:
- Cron updates
cache_forecast.json - Miniserver calls your
/forecast/?format=2endpoint - Emulator renders the CSV Loxone expects
Closing thoughts
Everything described here is the result of many small experiments, trials, and careful observation. This post is not intended to bypass or undermine Loxone’s official services, nor is it meant as a universal plug-and-play solution. To build and maintain such an emulator, you need to understand how the components work, how data flows through your smart-home setup, and how to diagnose issues independently. If those requirements feel too demanding, then the official Loxone Weather Service remains the right choice for you.
But if you enjoy tinkering, solving puzzles, and building things your own way, this guide is meant to inspire you. APIs change, formats evolve, and some scripts may eventually break – that’s part of the journey when experimenting outside of officially supported channels. Feel free to share your improvements or adapt the code to your needs, but please understand that I do not plan to provide ongoing support for this setup.


