Added norwegian as a option for language

This commit is contained in:
EivindH06 2025-10-10 13:09:00 +02:00
parent 068f5e4d46
commit fcb678013b
10 changed files with 940 additions and 148 deletions

122
README.md
View File

@ -8,41 +8,6 @@ Later development have added Energy usage graph for both day and month, as well
Go to the [WiKi](https://github.com/UtilitechAS/amsreader-firmware/wiki) for information on how to get your own device! And find the latest prebuilt firmware file at the [release section](https://github.com/UtilitechAS/amsreader-firmware/releases).
## OTA updates from GitHub releases
The firmware now supports downloading updates straight from GitHub Pages using a
lightweight manifest file. Each time you push a tag like `v1.2.3`, the
`.github/workflows/release.yml` pipeline will:
1. Build every supported PlatformIO environment and publish `.bin`/`.zip`
assets on the release page (existing behaviour).
2. Run `scripts/package_firmware.py` to assemble a static structure under
`dist/firmware/<chip>/<channel>/` with the firmware binary, an MD5 checksum,
and a `manifest.json` pointing at the binary.
3. Deploy the contents of `dist/` to GitHub Pages, yielding public URLs such as
`https://<your-user>.github.io/neas-amsreader-firmware-test/firmware/esp32s2/stable/manifest.json`.
> Make sure GitHub Pages for the repository is configured to "GitHub
> Actions" under *Settings → Pages* the first time you run the workflow.
To make the device follow those releases, set the default OTA endpoint before
flashing:
```cpp
#define FIRMWARE_UPDATE_BASE_URL "https://<your-user>.github.io/neas-amsreader-firmware-test"
#define FIRMWARE_UPDATE_CHANNEL "stable"
```
You can override the defaults either by editing
`lib/AmsFirmwareUpdater/include/UpgradeDefaults.h` or by adding corresponding
`-D` flags in `platformio.ini`. When a release is available, the device fetches
`manifest.json`, compares the `version` against its current firmware, and then
downloads the referenced binary in chunks.
If you need parallel release tracks (for example `beta` versus `stable`), pass
`--channel beta` to `package_firmware.py` inside your automation and override
`FIRMWARE_UPDATE_CHANNEL` for the devices you want on that track.
## Building this project with PlatformIO
To build this project, you need [PlatformIO](https://platformio.org/) installed.
@ -54,39 +19,6 @@ It is recommended to use Visual Studio Code with the PlatformIO plugin for devel
For development purposes, copy the ```platformio-user.ini-example``` to ```platformio-user.ini``` and customize to your preference. The code will adapt to the platform and board set in your profile.
### Running builds on macOS Apple Silicon
The ESP8266 and ESP32-C3 toolchains bundled with the custom Tasmota platform
are still compiled for Intel macOS. To keep `pio run` working without forcing
you to remember `arch -x86_64`, the repository now ships a tiny wrapper:
```bash
./scripts/pio-run.sh run # build every default environment (all boards)
./scripts/pio-run.sh run -e esp8266 # build a specific environment only
```
The script automatically re-launches PlatformIO under Rosetta 2 when you are on
an Apple Silicon Mac. On first run it also provisions a dedicated x86 Python
virtual environment in `~/.platformio/penv-x86` and installs PlatformIO inside
it, so expect an extra minute for the initial setup. If Rosetta is missing you
will see a friendly reminder to install it with
`softwareupdate --install-rosetta --agree-to-license`.
If you prefer using npm scripts, the same wrapper is available through:
```bash
npm run pio:run # builds every configured board
npm run pio:run:env -- esp32c3 # pass any PlatformIO environment name
```
> **Heads-up:** invoking `pio run` directly on an Apple Silicon Mac will still
> fail for the ESP8266/ESP32-C3 environments unless you prefix it with
> `arch -x86_64`. Using `scripts/pio-run.sh` (or the npm aliases) keeps the
> command portable across macOS and Linux without special flags.
> The first macOS run may print `NotOpenSSLWarning` from `urllib3`; this comes
> from Apple's LibreSSL build and can safely be ignored for local development.
## Licensing
Initially, this project began as a hobby, consuming countless hours of our spare time. However, the time required to support this project has expanded beyond the scope of a hobby. As a result, we established Utilitech, a company dedicated to maintaining the software and hardware for this project as part of our regular work.
@ -96,59 +28,7 @@ For more information, please refer to our [LICENSE](/LICENSE) file.
If your usage falls outside the scope of this license and you require a separate license, please contact us at [post@utilitech.no](mailto:post@utilitech.no) for further details.
## MQTT auto-provisioning defaults
If you want devices to connect to a known MQTT broker immediately after flashing, keep credentials in a local `.env` file rather than committing them:
1. Copy `.env.example` to `.env` and fill in the MQTT values (host, port, username/password, client ID, topics, etc.).
2. Commit the `.env.example` changes only—`.env` is ignored so secrets stay local.
3. Build the firmware; the PlatformIO pre-build hook injects these values so the device boots with your broker settings.
Any field you leave empty will fall back to the defaults in `lib/AmsConfiguration/include/MqttDefaults.h`, meaning the web UI will prompt for credentials during first-time setup.
### Shipping credentials with GitHub releases (without committing secrets)
The OTA manifest generated by `scripts/package_firmware.py` now carries an
optional `mqtt` block. If the build machine provides values for
`MQTT_DEFAULT_*` (through environment variables or a `.env` file), the script
embeds those defaults alongside the firmware checksum. Devices that upgrade via
GitHub Pages will download the manifest, detect the `mqtt` section, and apply
the broker settings automatically—unless the installer has already customised
the device through the web UI.
To keep secrets out of source control while still provisioning releases:
1. Store your broker credentials as GitHub Action secrets (for example
`MQTT_DEFAULT_USERNAME`, `MQTT_DEFAULT_PASSWORD`, etc.).
2. In the release workflow, write a temporary `.env` file before invoking the
PlatformIO build:
```yaml
- name: Write MQTT defaults
run: |
cat <<'EOF' > .env
MQTT_DEFAULT_HOST=${{ secrets.MQTT_DEFAULT_HOST }}
MQTT_DEFAULT_PORT=${{ secrets.MQTT_DEFAULT_PORT }}
MQTT_DEFAULT_USERNAME=${{ secrets.MQTT_DEFAULT_USERNAME }}
MQTT_DEFAULT_PASSWORD=${{ secrets.MQTT_DEFAULT_PASSWORD }}
MQTT_DEFAULT_CLIENT_ID=${{ secrets.MQTT_DEFAULT_CLIENT_ID }}
MQTT_DEFAULT_PUBLISH_TOPIC=${{ secrets.MQTT_DEFAULT_PUBLISH_TOPIC }}
MQTT_DEFAULT_SUBSCRIBE_TOPIC=${{ secrets.MQTT_DEFAULT_SUBSCRIBE_TOPIC }}
EOF
```
3. Build the firmware and run `scripts/package_firmware.py` as usual; the
generated `manifest.json` will include the broker defaults.
4. Upload `dist/` to GitHub Pages (the existing release workflow already covers
this), so devices retrieving the manifest can bootstrap the MQTT connection
immediately after flashing.
Because the `.env` file is created on-the-fly inside CI and never committed,
your credentials remain private while every release published to GitHub ships
with working MQTT settings out of the box.
# How to wipe bricked board?
# How to wipe bricked firmware?
To wipe the board you need to set it in USB mode. Connect the board to a usb board on you computor, hold AP/Prog button and shortly click reset before letting go of AP/Prog. To check if device is in usb mode you can check connections on this [site](https://www.amsleser.cloud/flasher)

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
import { getConfiguration, configurationStore } from './ConfigurationStore'
import { sysinfoStore, networksStore } from './DataStores.js';
import fetchWithTimeout from './fetchWithTimeout';
import { translationsStore } from './TranslationService';
import { translationsStore, getTranslations } from './TranslationService';
import { wiki, ipPattern, asciiPattern, asciiPatternExt, charAndNumPattern, hexPattern, numPattern, wifiStateFromRssi } from './Helpers.js';
import UartSelectOptions from './UartSelectOptions.svelte';
import Mask from './Mask.svelte'
@ -94,7 +94,11 @@
cloudenabled = update?.c?.e;
configuration = update;
loading = false;
languages = [{ code: 'en', name: 'English'}];
const hubLabel = translations.consent?.load_from_server ?? 'Load from server';
languages = [
{ code: 'en', name: 'English'},
{ code: 'no', name: 'Norsk'}
];
if(!configuration?.fw) {
configuration = {
...configuration,
@ -111,14 +115,27 @@
e: Number(configuration.fw.e ?? 3)
};
}
if(configuration?.u?.lang && configuration.u.lang != 'en') {
if(configuration?.u?.lang && !languages.find(lang => lang.code === configuration.u.lang)) {
languages.push({ code: configuration.u.lang, name: translations.language?.name ?? "Unknown"})
}
languages.push({ code: 'hub', name: 'Load from server'})
languages.push({ code: 'hub', name: hubLabel })
}
});
getConfiguration();
$: if(languages.length) {
const hubLabel = translations.consent?.load_from_server ?? 'Load from server';
const hasHub = languages.some(lang => lang.code === 'hub');
if(!hasHub) {
languages = [...languages, { code: 'hub', name: hubLabel }];
} else {
const needsUpdate = languages.find(lang => lang.code === 'hub' && lang.name !== hubLabel);
if(needsUpdate) {
languages = languages.map(lang => lang.code === 'hub' ? { ...lang, name: hubLabel } : lang);
}
}
}
let manual = true;
let networks = {};
let networkSignalInfos = [];
@ -253,6 +270,11 @@
const response = await fetchWithTimeout("http://hub.amsleser.no/hub/language/list.json");
languages = (await response.json())
configuration.u.lang = translations.language.code;
return;
}
if(configuration.u.lang) {
await getTranslations(configuration.u.lang);
}
}
@ -329,6 +351,14 @@
</div>
</div>
</div>
<div class="my-1">
{translations.conf?.general?.language ?? translations.conf?.ui?.lang ?? "Language"}<br/>
<select name="ulang" class="in-s" bind:value={configuration.u.lang} on:change={languageChanged}>
{#each languages as lang}
<option value={lang.code}>{lang.name}</option>
{/each}
</select>
</div>
<input type="hidden" name="p" value="true"/>
<div class="my-1">
<div class="flex">
@ -914,14 +944,6 @@
</select>
</div>
{/each}
<div class="w-1/2">
{translations.conf?.ui?.lang ?? "Language"}
<select name="ulang" class="in-s" bind:value={configuration.u.lang} on:change={languageChanged}>
{#each languages as lang}
<option value={lang.code}>{lang.name}</option>
{/each}
</select>
</div>
</div>
</div>
{/if}

View File

@ -1,6 +1,7 @@
<script>
import { sysinfoStore } from './DataStores.js';
import { translationsStore } from './TranslationService.js';
import { translationsStore, getTranslations } from './TranslationService.js';
import fetchWithTimeout from './fetchWithTimeout';
import Mask from './Mask.svelte'
import { navigate } from 'svelte-navigator';
import { wiki } from './Helpers';
@ -16,8 +17,17 @@
let loadingOrSaving = false;
let consentChoice = '';
let autoUpdateChoice = '';
let languageChoice = '';
let languages = [
{ code: 'en', name: 'English' },
{ code: 'no', name: 'Norsk' }
];
let canSave = false;
$: if (translations?.language?.code && !languages.find(lang => lang.code === translations.language.code)) {
languages = [...languages, { code: translations.language.code, name: translations.language.name ?? translations.language.code }];
}
$: if (sysinfo) {
if (sysinfo.fwconsent === 1 || sysinfo.fwconsent === 2) {
consentChoice = String(sysinfo.fwconsent);
@ -28,10 +38,43 @@
} else if (autoFlag === false) {
autoUpdateChoice = 'false';
}
const sysLang = sysinfo?.ui?.lang;
if (sysLang && !languages.find(lang => lang.code === sysLang)) {
languages = [...languages, { code: sysLang, name: translations.language?.name ?? sysLang.toUpperCase() }];
}
if (!languageChoice && sysLang) {
languageChoice = sysLang;
}
}
$: {
if (!languageChoice && translations?.language?.code) {
languageChoice = translations.language.code;
}
}
$: canSave = consentChoice !== '' && autoUpdateChoice !== '' && !loadingOrSaving;
async function handleLanguageChange(e) {
const selected = e.target.value;
if (selected === 'hub') {
try {
const response = await fetchWithTimeout('http://hub.amsleser.no/hub/language/list.json');
languages = await response.json();
languageChoice = translations.language.code;
} catch (err) {
console.error('Failed to load languages from hub', err);
languageChoice = translations.language.code;
}
return;
}
languageChoice = selected;
if (languageChoice) {
await getTranslations(languageChoice);
}
}
async function handleSubmit(e) {
loadingOrSaving = true;
const formData = new FormData(e.target)
@ -54,6 +97,12 @@
sysinfoStore.update(s => {
s.fwconsent = consentValue === '1' ? 1 : consentValue === '2' ? 2 : 0;
if (!s.ui || typeof s.ui !== 'object') {
s.ui = {};
}
if (languageChoice) {
s.ui.lang = languageChoice;
}
if (!s.upgrade || typeof s.upgrade !== 'object') {
s.upgrade = {};
}
@ -86,6 +135,15 @@
<label><input type="radio" name="fwa" value="true" bind:group={autoUpdateChoice} class="rounded m-2" required/> {translations.consent?.yes ?? "Yes"}</label>
<label><input type="radio" name="fwa" value="false" bind:group={autoUpdateChoice} class="rounded m-2" required/> {translations.consent?.no ?? "No"}</label><br/>
</div>
<div class="my-3">
{translations.consent?.language ?? "Language"}<br/>
<select name="ulang" class="in-s" bind:value={languageChoice} on:change={handleLanguageChange}>
{#each languages as lang}
<option value={lang.code}>{lang.name}</option>
{/each}
<option value="hub">{translations.consent?.load_from_server ?? "Load from server"}</option>
</select>
</div>
<div class="my-3">
<button type="submit" class="btn-pri" disabled={!canSave}>{translations.btn?.save ?? "Save"}</button>
</div>

View File

@ -204,11 +204,6 @@
</div>
{/if}
<div>
<div class="my-3">
{translations.consent?.auto_update ?? "Automatic firmware updates"}<br/>
<label><input type="radio" name="fwa" value="true" bind:group={autoUpdateChoice} class="rounded m-2"/> {translations.consent?.yes ?? "Yes"}</label>
<label><input type="radio" name="fwa" value="false" bind:group={autoUpdateChoice} class="rounded m-2"/> {translations.consent?.no ?? "No"}</label>
</div>
{translations.conf?.general?.hostname ?? "Hostname"}
<input name="sh" bind:value={sysinfo.hostname} type="text" class="in-s" maxlength="32" pattern={charAndNumPattern} placeholder="Optional, ex.: ams-reader" autocomplete="off"/>
</div>

View File

@ -0,0 +1,413 @@
{
"language": {
"code": "no",
"name": "Norsk"
},
"common": {
"import": "Import",
"export": "Eksport",
"voltage": "Spenning",
"amperage": "Strøm",
"seconds": "sekunder",
"minute": "minutt",
"minutes": "minutter",
"hour": "time",
"hours": "timer",
"day": "dag",
"days": "dager",
"month": "måned",
"unknown": "Ukjent"
},
"btn": {
"reboot": "Start på nytt",
"save": "Lagre",
"upload": "Last opp"
},
"header": {
"mem": "Ledig minne",
"price": "Pristjeneste",
"booting": "Starter",
"config": "Konfigurasjon",
"status": "Enhetsinformasjon",
"doc": "Dokumentasjon",
"new_version": "Ny versjon",
"upgrade": "Vil du oppgradere denne enheten til {0}?",
"uptime": "Oppetid"
},
"dashboard": {
"phase": "Faseeffekt",
"pf": "Effektfaktor",
"tariffpeak": "Topper for nettleie",
"realtime": "Sanntidsgraf",
"price": "Fremtidig energipris",
"day": "Energibruk siste 24 timer",
"month": "Energibruk siste {0} dager",
"temperature": "Temperaturfølere"
},
"reactive": {
"title": "Reaktiv",
"instant_in": "Øyeblikk inn",
"instant_out": "Øyeblikk ut",
"total_in": "Total inn",
"total_out": "Total ut"
},
"realtime": {
"title": "Sanntidsberegninger",
"consumption": "Forbruk",
"cost": "Kostnad",
"last_month": "Forrige måned",
"last_mo": "Forr. mnd."
},
"status": {
"device": {
"title": "Enhetsinformasjon",
"chip": "Brikke",
"device": "Enhet",
"mac": "MAC",
"apmac": "AP-MAC",
"last_boot": "Siste oppstart",
"reason": "Årsak",
"btn_consents": "Oppdater samtykker",
"reboot_confirm": "Er du sikker på at du vil starte enheten på nytt"
},
"meter": {
"title": "Måler",
"manufacturer": "Produsent",
"model": "Modell",
"id": "ID"
},
"network": {
"title": "Nettverk"
},
"firmware": {
"title": "Fastvare",
"installed": "Installert versjon",
"latest": "Nyeste versjon",
"install": "Installer denne versjonen",
"failed": "Forrige oppgradering fra {0} til {1} mislyktes",
"btn_select_file": "Velg fastvarefil for oppgradering",
"no_one_click": "Du har deaktivert ett-klikks oppgradering, lenken for egen oppgradering er deaktivert"
},
"backup": {
"title": "Sikkerhetskopi og gjenoppretting",
"iw": "WiFi",
"im": "MQTT",
"ie": "Web",
"it": "Måler",
"ih": "Grenser",
"ig": "GPIO",
"in": "NTP",
"is": "Pris-API",
"secrets": "Ta med hemmeligheter",
"secrets_desc": "(SSID, PSK, passord og token)",
"btn_select_file": "Velg fil...",
"btn_download": "Last ned"
},
"mask": {
"firmware": "Laster opp fastvare, vennligst vent",
"config": "Laster opp konfigurasjon, vennligst vent"
}
},
"conf": {
"general": {
"title": "Generelt",
"hostname": "Vertsnavn",
"timezone": "Tidssone",
"security": {
"title": "Sikkerhet",
"none": "Ingen",
"conf": "Kun konfigurasjon",
"all": "Alt"
},
"context": "Kontekst",
"context_placeholder": "[rot]"
},
"price": {
"title": "Priskonfigurasjon",
"region": "Prisområde",
"currency": "Valuta",
"conf": "Konfigurer pris",
"enabled": "Aktiver pris fra ekstern server",
"api_key_placeholder": "ENTSO-E API-nøkkel, valgfritt, se dokumentasjon",
"both": "Begge",
"fixed": "Fast",
"btn_add": "Legg til",
"mask_loading": "Laster priskonfigurasjon",
"mask_saving": "Lagrer priskonfigurasjon"
},
"meter": {
"title": "Måler",
"comm": {
"title": "Kommunikasjon",
"passive": "Passiv (push)",
"pulse": "Puls"
},
"serial": "Seriell konf.",
"inverted": "invertert",
"buffer": "Bufferstørrelse",
"pulses": "Pulser per kWh",
"voltage": "Spenning",
"fuse": "Hovedsikring",
"prod": "Produksjon",
"encrypted": "Måleren er kryptert",
"authkey": "Autentiseringsnøkkel",
"multipliers": {
"title": "Multiplikatorer",
"watt": "Watt",
"volt": "Volt",
"amp": "Ampere",
"kwh": "kWh"
}
},
"connection": {
"title": "Tilkobling",
"wifi": "Koble til WiFi",
"ap": "Eget aksesspunkt",
"eth": "Ethernet",
"ssid": "SSID",
"psk": "Passord",
"ps": {
"title": "Strømsparing",
"default": "Standard",
"off": "Av",
"min": "Minimal",
"max": "Maksimal"
},
"pwr": "Effekt",
"tick_11b": "Tillat 802.11b-hastigheter"
},
"network": {
"title": "Nettverk",
"ip": "IP",
"static": "Statisk",
"dhcp": "DHCP",
"gw": "Gateway",
"dns": "DNS",
"tick_mdns": "aktiver mDNS",
"ntp": "NTP",
"tick_ntp_dhcp": "hent fra DHCP"
},
"mqtt": {
"title": "MQTT",
"server": "Server",
"user": "Brukernavn",
"pass": "Passord",
"id": "Klient-ID",
"payload": "Payload",
"publish": "Publiserings-emne",
"btn_ca_upload": "Last opp CA",
"btn_crt_upload": "Last opp sertifikat",
"btn_key_upload": "Last opp nøkkel",
"ca_ok": "CA ok",
"crt_ok": "Sertifikat ok",
"key_ok": "Nøkkel ok",
"title_ca": "Klikk her for å laste opp CA",
"title_crt": "Klikk her for å laste opp sertifikat",
"title_key": "Klikk her for å laste opp privat nøkkel",
"domoticz": {
"title": "Domoticz",
"eidx": "Strøm-IDX",
"cidx": "Strøm-IDX",
"vidx": "Spenning-IDX"
},
"ha": {
"title": "Home Assistant",
"discovery": "Prefiks for oppdagelsestopic",
"hostname": "Vertsnavn for URL",
"tag": "Navnetagg"
}
},
"cloud": {
"title": "Skytjenester",
"ams": "AMS-leser sky",
"es": "Energy Speedometer"
},
"thresholds": {
"title": "Tariffgrenser",
"avg": "Snitt av"
},
"ui": {
"title": "Brukergrensesnitt",
"i": "Importmåler",
"e": "Eksportmåler",
"v": "Spenning",
"a": "Strøm",
"h": "Per fase",
"f": "Effektfaktor",
"r": "Reaktiv",
"c": "Sanntid",
"t": "Topper",
"l": "Sanntidsgraf",
"p": "Pris",
"d": "Døgngraf",
"m": "Månedsgraf",
"s": "Temperaturgraf",
"k": "Mørk modus",
"lang": "Språk",
"enabled": "Aktivert",
"disabled": "Deaktivert",
"auto": "Auto"
},
"hw": {
"title": "Maskinvare",
"han": {
"rx": "HAN RX",
"tx": "HAN TX",
"pullup": "Pullup"
},
"ap_btn": "AP-knapp",
"led": {
"title": "LED",
"rgb": "RGB",
"inverted": "invertert",
"disable": "LED-deaktiverings-GPIO",
"behaviour": {
"title": "LED-oppførsel",
"enabled": "Aktivert",
"disabled": "Deaktivert"
}
},
"temp": "Temperatur",
"temp_analog": "Analog temp",
"vcc": {
"title": "Vcc",
"offset": "Vcc-offset",
"multiplier": "Multiplikator",
"divider": "Motstandsdelere",
"div_vcc": "VCC",
"div_gnd": "GND",
"boot": "Oppstartsgrense"
}
},
"debug": {
"title": "Feilsøking",
"enable": "Aktiver feilsøking",
"danger": "Feilsøking kan gi plutselige omstarter. Ikke la den stå på!",
"telnet": "Aktiver telnet",
"telnet_danger": "Telnet er usikkert og bør være av når det ikke brukes"
},
"btn_reset": "Fabrikktilbakestill",
"mask": {
"loading": "Laster konfigurasjon",
"saving": "Lagrer konfigurasjon",
"reset": "Utfører fabrikktilbakestilling",
"reset_done": "Enheten er fabrikktilbakestilt og satt i AP-modus"
}
},
"consent": {
"title": "Tillatelser vi trenger for å gjøre jobben",
"one_click": "Aktiver ett-klikks oppgradering? (innebærer datainnsamling)",
"read_more": "Les mer",
"yes": "Ja",
"no": "Nei",
"mask_saving": "Lagrer valg",
"language": "Språk",
"load_from_server": "Last fra server"
},
"upload": {
"title": "Last opp",
"desc": "Velg riktig fil og klikk last opp",
"mask": "Laster opp fil, vennligst vent"
},
"setup": {
"title": "Oppsett",
"static": "Statisk IP",
"mask": "Lagrer konfigurasjonen på enheten"
},
"errors": {
"han": {
"-1": "Analysefeil",
"-2": "Ufullstendige data mottatt",
"-3": "Markør for datafelt mangler",
"-4": "Kontrollsum i header er feil",
"-5": "Kontrollsum i footer er feil",
"-9": "Ukjente data mottatt, sjekk måleroppsettet",
"-41": "Rammelengde stemmer ikke",
"-51": "Autentisering mislyktes",
"-52": "Dekryptering mislyktes",
"-53": "Krypteringsnøkkel er ugyldig",
"89": "Ukjente data mottatt fra måler",
"90": "Ingen HAN-data mottatt på minst 30 s",
"91": "Serielt brudd",
"92": "Seriell buffer er full",
"93": "Seriell FIFO-overløp",
"94": "Seriell rammekonflikt",
"95": "Seriell paritetsfeil",
"96": "RX-feil",
"98": "Unntak i kode, feilsøking nødvendig",
"99": "Autodeteksjon mislyktes"
},
"mqtt": {
"-3": "Tilkobling mislyktes",
"-4": "Nettverkstidsavbrudd",
"-10": "Tilkobling nektet",
"-11": "Klarte ikke å abonnere",
"-13": "Tilkobling mistet"
},
"price": {
"400": "Ukjente data i forespørsel",
"401": "Ikke autorisert, sjekk API-nøkkel",
"403": "Ikke autorisert, sjekk API-nøkkel",
"404": "Pris utilgjengelig, ikke funnet",
"425": "Serveren sier det er for tidlig",
"429": "API-grensen er nådd",
"500": "Intern serverfeil",
"-1": "Tilkoblingsfeil",
"-2": "Ufullstendige data mottatt",
"-3": "Ugyldige data, tag mangler",
"-51": "Autentisering mislyktes",
"-52": "Dekryptering mislyktes",
"-53": "Krypteringsnøkkel er ugyldig"
},
"http": {
"255": "Kan ikke starte oppgradering",
"-1": "Tilkobling ble nektet",
"-2": "Klarte ikke å sende headere",
"-3": "Klarte ikke å sende payload",
"-4": "Ikke tilkoblet",
"-5": "Tilkobling mistet",
"-6": "Ingen datastrøm",
"-7": "Ikke en HTTP-server",
"-8": "Ikke nok minne",
"-9": "Kodingsfeil",
"-10": "Feil ved strøm-skriving",
"-11": "Lese-timeout"
}
},
"esp8266": {
"reason": {
"0": "Normal",
"1": "WDT-reset",
"2": "Unntaksreset",
"3": "Myk WDT-reset",
"4": "Programvarestart",
"5": "Dvale",
"6": "Ekstern reset"
}
},
"esp32": {
"reason": {
"1": "Vbat power on reset",
"3": "Programvarestyrt reset",
"4": "WDT-reset",
"5": "Dvale",
"6": "SLC-reset",
"7": "Timer Group0 WDT-reset",
"8": "Timer Group1 WDT-reset",
"9": "RTC WDT-reset",
"10": "Intrusion test reset CPU",
"11": "Timer Group reset CPU",
"12": "Programvarestyrt reset CPU",
"13": "RTC WTD reset CPU",
"14": "PRO CPU",
"15": "AP CPU",
"16": "Brownout reset",
"17": "HP core reset",
"18": "LP core reset",
"19": "HP-app reset",
"20": "LP-app reset",
"21": "HP-system reset",
"22": "LP-system reset"
}
}
}

View File

@ -300,7 +300,9 @@
"read_more" : "Read more",
"yes" : "Yes",
"no" : "No",
"mask_saving" : "Saving preferences"
"mask_saving" : "Saving preferences",
"language" : "Language",
"load_from_server" : "Load from server"
},
"upload" : {
"title" : "Upload",

View File

@ -0,0 +1,413 @@
{
"language": {
"code": "no",
"name": "Norsk"
},
"common": {
"import": "Import",
"export": "Eksport",
"voltage": "Spenning",
"amperage": "Strøm",
"seconds": "sekunder",
"minute": "minutt",
"minutes": "minutter",
"hour": "time",
"hours": "timer",
"day": "dag",
"days": "dager",
"month": "måned",
"unknown": "Ukjent"
},
"btn": {
"reboot": "Start på nytt",
"save": "Lagre",
"upload": "Last opp"
},
"header": {
"mem": "Ledig minne",
"price": "Pristjeneste",
"booting": "Starter",
"config": "Konfigurasjon",
"status": "Enhetsinformasjon",
"doc": "Dokumentasjon",
"new_version": "Ny versjon",
"upgrade": "Vil du oppgradere denne enheten til {0}?",
"uptime": "Oppetid"
},
"dashboard": {
"phase": "Faseeffekt",
"pf": "Effektfaktor",
"tariffpeak": "Topper for nettleie",
"realtime": "Sanntidsgraf",
"price": "Fremtidig energipris",
"day": "Energibruk siste 24 timer",
"month": "Energibruk siste {0} dager",
"temperature": "Temperaturfølere"
},
"reactive": {
"title": "Reaktiv",
"instant_in": "Øyeblikk inn",
"instant_out": "Øyeblikk ut",
"total_in": "Total inn",
"total_out": "Total ut"
},
"realtime": {
"title": "Sanntidsberegninger",
"consumption": "Forbruk",
"cost": "Kostnad",
"last_month": "Forrige måned",
"last_mo": "Forr. mnd."
},
"status": {
"device": {
"title": "Enhetsinformasjon",
"chip": "Brikke",
"device": "Enhet",
"mac": "MAC",
"apmac": "AP-MAC",
"last_boot": "Siste oppstart",
"reason": "Årsak",
"btn_consents": "Oppdater samtykker",
"reboot_confirm": "Er du sikker på at du vil starte enheten på nytt"
},
"meter": {
"title": "Måler",
"manufacturer": "Produsent",
"model": "Modell",
"id": "ID"
},
"network": {
"title": "Nettverk"
},
"firmware": {
"title": "Fastvare",
"installed": "Installert versjon",
"latest": "Nyeste versjon",
"install": "Installer denne versjonen",
"failed": "Forrige oppgradering fra {0} til {1} mislyktes",
"btn_select_file": "Velg fastvarefil for oppgradering",
"no_one_click": "Du har deaktivert ett-klikks oppgradering, lenken for egen oppgradering er deaktivert"
},
"backup": {
"title": "Sikkerhetskopi og gjenoppretting",
"iw": "WiFi",
"im": "MQTT",
"ie": "Web",
"it": "Måler",
"ih": "Grenser",
"ig": "GPIO",
"in": "NTP",
"is": "Pris-API",
"secrets": "Ta med hemmeligheter",
"secrets_desc": "(SSID, PSK, passord og token)",
"btn_select_file": "Velg fil...",
"btn_download": "Last ned"
},
"mask": {
"firmware": "Laster opp fastvare, vennligst vent",
"config": "Laster opp konfigurasjon, vennligst vent"
}
},
"conf": {
"general": {
"title": "Generelt",
"hostname": "Vertsnavn",
"timezone": "Tidssone",
"security": {
"title": "Sikkerhet",
"none": "Ingen",
"conf": "Kun konfigurasjon",
"all": "Alt"
},
"context": "Kontekst",
"context_placeholder": "[rot]"
},
"price": {
"title": "Priskonfigurasjon",
"region": "Prisområde",
"currency": "Valuta",
"conf": "Konfigurer pris",
"enabled": "Aktiver pris fra ekstern server",
"api_key_placeholder": "ENTSO-E API-nøkkel, valgfritt, se dokumentasjon",
"both": "Begge",
"fixed": "Fast",
"btn_add": "Legg til",
"mask_loading": "Laster priskonfigurasjon",
"mask_saving": "Lagrer priskonfigurasjon"
},
"meter": {
"title": "Måler",
"comm": {
"title": "Kommunikasjon",
"passive": "Passiv (push)",
"pulse": "Puls"
},
"serial": "Seriell konf.",
"inverted": "invertert",
"buffer": "Bufferstørrelse",
"pulses": "Pulser per kWh",
"voltage": "Spenning",
"fuse": "Hovedsikring",
"prod": "Produksjon",
"encrypted": "Måleren er kryptert",
"authkey": "Autentiseringsnøkkel",
"multipliers": {
"title": "Multiplikatorer",
"watt": "Watt",
"volt": "Volt",
"amp": "Ampere",
"kwh": "kWh"
}
},
"connection": {
"title": "Tilkobling",
"wifi": "Koble til WiFi",
"ap": "Eget aksesspunkt",
"eth": "Ethernet",
"ssid": "SSID",
"psk": "Passord",
"ps": {
"title": "Strømsparing",
"default": "Standard",
"off": "Av",
"min": "Minimal",
"max": "Maksimal"
},
"pwr": "Effekt",
"tick_11b": "Tillat 802.11b-hastigheter"
},
"network": {
"title": "Nettverk",
"ip": "IP",
"static": "Statisk",
"dhcp": "DHCP",
"gw": "Gateway",
"dns": "DNS",
"tick_mdns": "aktiver mDNS",
"ntp": "NTP",
"tick_ntp_dhcp": "hent fra DHCP"
},
"mqtt": {
"title": "MQTT",
"server": "Server",
"user": "Brukernavn",
"pass": "Passord",
"id": "Klient-ID",
"payload": "Payload",
"publish": "Publiserings-emne",
"btn_ca_upload": "Last opp CA",
"btn_crt_upload": "Last opp sertifikat",
"btn_key_upload": "Last opp nøkkel",
"ca_ok": "CA ok",
"crt_ok": "Sertifikat ok",
"key_ok": "Nøkkel ok",
"title_ca": "Klikk her for å laste opp CA",
"title_crt": "Klikk her for å laste opp sertifikat",
"title_key": "Klikk her for å laste opp privat nøkkel",
"domoticz": {
"title": "Domoticz",
"eidx": "Strøm-IDX",
"cidx": "Strøm-IDX",
"vidx": "Spenning-IDX"
},
"ha": {
"title": "Home Assistant",
"discovery": "Prefiks for oppdagelsestopic",
"hostname": "Vertsnavn for URL",
"tag": "Navnetagg"
}
},
"cloud": {
"title": "Skytjenester",
"ams": "AMS-leser sky",
"es": "Energy Speedometer"
},
"thresholds": {
"title": "Tariffgrenser",
"avg": "Snitt av"
},
"ui": {
"title": "Brukergrensesnitt",
"i": "Importmåler",
"e": "Eksportmåler",
"v": "Spenning",
"a": "Strøm",
"h": "Per fase",
"f": "Effektfaktor",
"r": "Reaktiv",
"c": "Sanntid",
"t": "Topper",
"l": "Sanntidsgraf",
"p": "Pris",
"d": "Døgngraf",
"m": "Månedsgraf",
"s": "Temperaturgraf",
"k": "Mørk modus",
"lang": "Språk",
"enabled": "Aktivert",
"disabled": "Deaktivert",
"auto": "Auto"
},
"hw": {
"title": "Maskinvare",
"han": {
"rx": "HAN RX",
"tx": "HAN TX",
"pullup": "Pullup"
},
"ap_btn": "AP-knapp",
"led": {
"title": "LED",
"rgb": "RGB",
"inverted": "invertert",
"disable": "LED-deaktiverings-GPIO",
"behaviour": {
"title": "LED-oppførsel",
"enabled": "Aktivert",
"disabled": "Deaktivert"
}
},
"temp": "Temperatur",
"temp_analog": "Analog temp",
"vcc": {
"title": "Vcc",
"offset": "Vcc-offset",
"multiplier": "Multiplikator",
"divider": "Motstandsdelere",
"div_vcc": "VCC",
"div_gnd": "GND",
"boot": "Oppstartsgrense"
}
},
"debug": {
"title": "Feilsøking",
"enable": "Aktiver feilsøking",
"danger": "Feilsøking kan gi plutselige omstarter. Ikke la den stå på!",
"telnet": "Aktiver telnet",
"telnet_danger": "Telnet er usikkert og bør være av når det ikke brukes"
},
"btn_reset": "Fabrikktilbakestill",
"mask": {
"loading": "Laster konfigurasjon",
"saving": "Lagrer konfigurasjon",
"reset": "Utfører fabrikktilbakestilling",
"reset_done": "Enheten er fabrikktilbakestilt og satt i AP-modus"
}
},
"consent": {
"title": "Tillatelser vi trenger for å gjøre jobben",
"one_click": "Aktiver ett-klikks oppgradering? (innebærer datainnsamling)",
"read_more": "Les mer",
"yes": "Ja",
"no": "Nei",
"mask_saving": "Lagrer valg",
"language": "Språk",
"load_from_server": "Last fra server"
},
"upload": {
"title": "Last opp",
"desc": "Velg riktig fil og klikk last opp",
"mask": "Laster opp fil, vennligst vent"
},
"setup": {
"title": "Oppsett",
"static": "Statisk IP",
"mask": "Lagrer konfigurasjonen på enheten"
},
"errors": {
"han": {
"-1": "Analysefeil",
"-2": "Ufullstendige data mottatt",
"-3": "Markør for datafelt mangler",
"-4": "Kontrollsum i header er feil",
"-5": "Kontrollsum i footer er feil",
"-9": "Ukjente data mottatt, sjekk måleroppsettet",
"-41": "Rammelengde stemmer ikke",
"-51": "Autentisering mislyktes",
"-52": "Dekryptering mislyktes",
"-53": "Krypteringsnøkkel er ugyldig",
"89": "Ukjente data mottatt fra måler",
"90": "Ingen HAN-data mottatt på minst 30 s",
"91": "Serielt brudd",
"92": "Seriell buffer er full",
"93": "Seriell FIFO-overløp",
"94": "Seriell rammekonflikt",
"95": "Seriell paritetsfeil",
"96": "RX-feil",
"98": "Unntak i kode, feilsøking nødvendig",
"99": "Autodeteksjon mislyktes"
},
"mqtt": {
"-3": "Tilkobling mislyktes",
"-4": "Nettverkstidsavbrudd",
"-10": "Tilkobling nektet",
"-11": "Klarte ikke å abonnere",
"-13": "Tilkobling mistet"
},
"price": {
"400": "Ukjente data i forespørsel",
"401": "Ikke autorisert, sjekk API-nøkkel",
"403": "Ikke autorisert, sjekk API-nøkkel",
"404": "Pris utilgjengelig, ikke funnet",
"425": "Serveren sier det er for tidlig",
"429": "API-grensen er nådd",
"500": "Intern serverfeil",
"-1": "Tilkoblingsfeil",
"-2": "Ufullstendige data mottatt",
"-3": "Ugyldige data, tag mangler",
"-51": "Autentisering mislyktes",
"-52": "Dekryptering mislyktes",
"-53": "Krypteringsnøkkel er ugyldig"
},
"http": {
"255": "Kan ikke starte oppgradering",
"-1": "Tilkobling ble nektet",
"-2": "Klarte ikke å sende headere",
"-3": "Klarte ikke å sende payload",
"-4": "Ikke tilkoblet",
"-5": "Tilkobling mistet",
"-6": "Ingen datastrøm",
"-7": "Ikke en HTTP-server",
"-8": "Ikke nok minne",
"-9": "Kodingsfeil",
"-10": "Feil ved strøm-skriving",
"-11": "Lese-timeout"
}
},
"esp8266": {
"reason": {
"0": "Normal",
"1": "WDT-reset",
"2": "Unntaksreset",
"3": "Myk WDT-reset",
"4": "Programvarestart",
"5": "Dvale",
"6": "Ekstern reset"
}
},
"esp32": {
"reason": {
"1": "Vbat power on reset",
"3": "Programvarestyrt reset",
"4": "WDT-reset",
"5": "Dvale",
"6": "SLC-reset",
"7": "Timer Group0 WDT-reset",
"8": "Timer Group1 WDT-reset",
"9": "RTC WDT-reset",
"10": "Intrusion test reset CPU",
"11": "Timer Group reset CPU",
"12": "Programvarestyrt reset CPU",
"13": "RTC WTD reset CPU",
"14": "PRO CPU",
"15": "AP CPU",
"16": "Brownout reset",
"17": "HP core reset",
"18": "LP core reset",
"19": "HP-app reset",
"20": "LP-app reset",
"21": "HP-system reset",
"22": "LP-system reset"
}
}
}

View File

@ -300,7 +300,9 @@
"read_more" : "Read more",
"yes" : "Yes",
"no" : "No",
"mask_saving" : "Saving preferences"
"mask_saving" : "Saving preferences",
"language" : "Language",
"load_from_server" : "Load from server"
},
"upload" : {
"title" : "Upload",

View File

@ -36,6 +36,7 @@
#include "html/conf_upgrade_json.h"
#include "html/conf_cloud_json.h"
#include "html/translations_json.h"
#include "html/translations_no_json.h"
#include "html/firmware_html.h"
#include "html/neas_logotype_white_svg.h"
#include "html/wifi_high_light_svg.h"
@ -1267,6 +1268,12 @@ void AmsWebServer::translationsJson() {
server.sendHeader(HEADER_PRAGMA, PRAGMA_NO_CACHE);
server.sendHeader(HEADER_EXPIRES, EXPIRES_OFF);
if(lang.equalsIgnoreCase(F("no"))) {
uint16_t len = pgm_read_word(&TRANSLATIONS_NO_JSON_LEN);
server.send_P(200, MIME_JSON, TRANSLATIONS_NO_JSON, len);
return;
}
uint16_t len = pgm_read_word(&TRANSLATIONS_JSON_LEN);
server.send_P(200, MIME_JSON, TRANSLATIONS_JSON, len);
}