diff --git a/include/Configuration.h b/include/Configuration.h index a6a986f54..7d5492723 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -37,6 +37,7 @@ struct CHANNEL_CONFIG_T { struct INVERTER_CONFIG_T { uint64_t Serial; char Name[INV_MAX_NAME_STRLEN + 1]; + uint8_t CurrentPhase; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; @@ -87,6 +88,8 @@ struct CONFIG_T { bool Mqtt_Hass_Expire; char Security_Password[WIFI_MAX_PASSWORD_STRLEN + 1]; + + bool Mqtt_Victron_Enabled; }; class ConfigurationClass { diff --git a/include/MqttSettings.h b/include/MqttSettings.h index afa3283c8..22a4dd4aa 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -6,6 +6,7 @@ #include #include #include +#include class MqttSettingsClass { public: @@ -16,6 +17,14 @@ class MqttSettingsClass { void publish(const String& subtopic, const String& payload); void publishHass(const String& subtopic, const String& payload); + void publishVictron(const String& hoyserial, const String& payload); + + const char* VictronPortalId; + std::map VictronDeviceInstance; + + String getVictronPortalId(); + String getVictronDeviceInstance(const String& hoyserial); + String getPrefix(); private: diff --git a/include/MqttVictronPublishing.h b/include/MqttVictronPublishing.h new file mode 100644 index 000000000..dbf586830 --- /dev/null +++ b/include/MqttVictronPublishing.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include + +class MqttVictronPublishingClass { +public: + void init(); + void loop(); + void forceRegister(); + void registerInverter(); + +private: + void publishField(std::shared_ptr inv, uint8_t invphase, uint8_t fieldId); + + uint32_t _lastPublishStats[INV_MAX_COUNT]; + uint32_t _lastPublish; + + uint8_t _publishFields[14] = { + FLD_UDC, + FLD_IDC, + FLD_PDC, + FLD_YD, + FLD_YT, + FLD_UAC, + FLD_IAC, + FLD_PAC, + FLD_F, + FLD_T, + FLD_PF, + FLD_EFF, + FLD_IRR, + FLD_PRA + }; + + bool _wasConnected = false; + bool _registerForced = false; +}; + +extern MqttVictronPublishingClass MqttVictronPublishing; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index 9656901d8..9367302a0 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -76,4 +76,6 @@ #define MQTT_HASS_EXPIRE true #define MQTT_HASS_RETAIN true #define MQTT_HASS_TOPIC "homeassistant/" -#define MQTT_HASS_INDIVIDUALPANELS false \ No newline at end of file +#define MQTT_HASS_INDIVIDUALPANELS false + +#define MQTT_VICTRON_ENABLED false \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 0842f4cca..422fce366 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -78,11 +78,15 @@ bool ConfigurationClass::write() JsonObject security = doc.createNestedObject("security"); security["password"] = config.Security_Password; + JsonObject mqtt_victron = mqtt.createNestedObject("victron"); + mqtt_victron["enabled"] = config.Mqtt_Victron_Enabled; + JsonArray inverters = doc.createNestedArray("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters.createNestedObject(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; + inv["phase"] = config.Inverter[i].CurrentPhase; JsonArray channel = inv.createNestedArray("channel"); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { @@ -198,11 +202,15 @@ bool ConfigurationClass::read() JsonObject security = doc["security"]; strlcpy(config.Security_Password, security["password"] | ACCESS_POINT_PASSWORD, sizeof(config.Security_Password)); + JsonObject mqtt_victron = mqtt["victron"]; + config.Mqtt_Victron_Enabled = mqtt_victron["enabled"] | MQTT_VICTRON_ENABLED; + JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { JsonObject inv = inverters[i].as(); config.Inverter[i].Serial = inv["serial"] | 0ULL; strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name)); + config.Inverter[i].CurrentPhase = inv["phase"] | 0; JsonArray channel = inv["channel"]; for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index 516409bdb..3b0fc2039 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "MqttSettings.h" +#include "ArduinoJson.h" #include "Configuration.h" #include "NetworkSettings.h" #include @@ -50,6 +51,18 @@ void MqttSettingsClass::onMqttConnect(bool sessionPresent) mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER).c_str(), 0); mqttClient->subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART).c_str(), 0); + + // Check for doing some Victron parts... + if (!Configuration.get().Mqtt_Victron_Enabled) { + return; + } + // Loop all inverters and subscribe each for Victron Venus messages + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String str_serial = inv->serialString(); + mqttClient->subscribe(String("device/HM" + str_serial + "/DBus").c_str(),0); + } } void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason reason) @@ -102,17 +115,65 @@ void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessagePropertie subtopic = strtok_r(rest, "/", &rest); setting = strtok_r(rest, "/", &rest); + uint64_t serial; + if (serial_str == NULL || subtopic == NULL || setting == NULL) { + + // Victron message only on startup after subscribe + // device/116181045449/DBus = JSON + char* rest = &token_topic[strlen("device/HM")]; + serial_str = strtok_r(rest, "/", &rest); + + if (serial_str == NULL) { + return; + } + + serial = strtoull(serial_str, 0, 16); + auto inv = Hoymiles.getInverterBySerial(serial); + + if (inv == nullptr) { + Serial.print(F("Can not register inverter: ")); + Serial.println(serial); + return; + } + + char* strlimit = new char[len + 1]; + memcpy(strlimit, payload, len); + strlimit[len] = '\0'; + + DynamicJsonDocument docDbus(64); + deserializeJson(docDbus, strlimit); + + delete[] strlimit; + + VictronPortalId = docDbus["portalId"]; + + DynamicJsonDocument docInstance(64); + docInstance = docDbus["deviceInstance"]; + String deviceInstance = docInstance[serial_str]; + + String inverter = serial_str; + VictronDeviceInstance.insert({inverter, deviceInstance}); + + if (VictronDeviceInstance.find(inverter)!=VictronDeviceInstance.end()) { + String valfound = VictronDeviceInstance[inverter]; + Serial.print(F("Register inverter: ")); + Serial.print(serial_str); + Serial.print(F(" to Victron Venus OS with portalId: ")); + Serial.print(VictronPortalId); + Serial.print(F(" and deviceInstance: ")); + Serial.println(valfound); + } return; } - uint64_t serial; serial = strtoull(serial_str, 0, 16); auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { - Serial.println(F("Inverter not found")); + Serial.print(F("Inverter not found: ")); + Serial.println(serial); return; } @@ -228,6 +289,23 @@ bool MqttSettingsClass::getConnected() return mqttClient->connected(); } +String MqttSettingsClass::getVictronPortalId() +{ + return VictronPortalId; +} + +String MqttSettingsClass::getVictronDeviceInstance(const String& hoyserial) +{ + if (VictronDeviceInstance.find(hoyserial)!=VictronDeviceInstance.end()) { + return VictronDeviceInstance[hoyserial]; + } else { + Serial.print(F("No Victron deviceInstance found for inverter: ")); + Serial.println(hoyserial); + String ret = hoyserial + "NOdevInstance"; + return ret; + } +} + String MqttSettingsClass::getPrefix() { return Configuration.get().Mqtt_Topic; @@ -247,6 +325,11 @@ void MqttSettingsClass::publishHass(const String& subtopic, const String& payloa mqttClient->publish(topic.c_str(), 0, Configuration.get().Mqtt_Hass_Retain, payload.c_str()); } +void MqttSettingsClass::publishVictron(const String& topic, const String& payload) +{ + mqttClient->publish(topic.c_str(), 0, 1, payload.c_str()); +} + void MqttSettingsClass::init() { using std::placeholders::_1; diff --git a/src/MqttVictronPublishing.cpp b/src/MqttVictronPublishing.cpp new file mode 100644 index 000000000..ea06d7cca --- /dev/null +++ b/src/MqttVictronPublishing.cpp @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttVictronPublishing.h" +#include "ArduinoJson.h" +#include "MqttPublishing.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" + +MqttVictronPublishingClass MqttVictronPublishing; + +void MqttVictronPublishingClass::init() +{ +} + +void MqttVictronPublishingClass::loop() +{ + if (!Configuration.get().Mqtt_Victron_Enabled) { + // Victron stuff enabled? + return; + } + + if (!Hoymiles.getRadio()->isIdle()) { + // Hoymiles are ready to go? + return; + } + + if (MqttSettings.getConnected() && _registerForced) { + // Connection established, force register inverter @Victron Cerbo + registerInverter(); + _registerForced = false; + } + + if (MqttSettings.getConnected() && !_wasConnected) { + // Connection established, register inverter @Victron Cerbo + _wasConnected = true; + registerInverter(); + } else if (!MqttSettings.getConnected() && _wasConnected) { + // Connection lost, register again next time when connection established + _wasConnected = false; + } + + const CONFIG_T& config = Configuration.get(); + + if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) { + + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String str_serial = inv->serialString(); + + uint32_t lastUpdate = inv->Statistics()->getLastUpdate(); + if (lastUpdate > 0 && lastUpdate != _lastPublishStats[i]) { + _lastPublishStats[i] = lastUpdate; + + String invname = inv->name(); + + // Get Current phase + uint8_t invphase = config.Inverter[i].CurrentPhase; + + // Loop all fields in channel 0 + for (uint8_t f = 0; f < sizeof(_publishFields); f++) { + publishField(inv, invphase, _publishFields[f]); + } + } + + yield(); + } + + _lastPublish = millis(); + } +} + +void MqttVictronPublishingClass::forceRegister() +{ + _registerForced = true; +} + +void MqttVictronPublishingClass::registerInverter() +{ + const CONFIG_T& config = Configuration.get(); + + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + + String str_serial = inv->serialString(); + String invname = inv->name(); + + // Get Current phase + uint8_t invphase = config.Inverter[i].CurrentPhase; + uint8_t invconnected; + + // Current phase = L0 --> No register to Victron Cerbo, only L1, L2, L3 + if (invphase == 0) { + invconnected = 0; + } else { + invconnected = 1; + } + + // Publish inverter as device service to Victron Venus OS with connected=1 + DynamicJsonDocument serviceDoc(64); + serviceDoc[str_serial] = F("pvinverter"); + JsonObject serviceObj = serviceDoc.as(); + + String Vtopic = ("device/HM" + str_serial + "/Status"); + DynamicJsonDocument rootDoc(256); + rootDoc[F("clientId")] = "HM" + str_serial; + rootDoc[F("connected")] = invconnected; + rootDoc[F("version")] = "0.1-L" + String(invphase) + "-" + invname; + rootDoc[F("services")] = serviceObj; + JsonObject rootObj = rootDoc.as(); + String data; + serializeJson(rootObj, data); + + MqttSettings.publishVictron(Vtopic, data); + } +} + +void MqttVictronPublishingClass::publishField(std::shared_ptr inv, uint8_t invphase, uint8_t fieldId) +{ + bool response = false; + String fieldname; + if (fieldId == 4) { + response = true; + fieldname = "YieldTotal"; + } + if (fieldId == 5) { + response = true; + fieldname = "Voltage"; + } + if (fieldId == 6) { + response = true; + fieldname = "Current"; + } + if (fieldId == 7) { + response = true; + fieldname = "Power"; + } + + if (response) { + double fieldval = double(inv->Statistics()->getChannelFieldValue(0, fieldId)); + double fieldvalue = floor(fieldval * 100.0 + .5) / 100.0; + + String portalid = MqttSettings.getVictronPortalId(); + if (portalid == NULL) { + portalid = "NOportalId"; + } + + String topic_sum, topic_phase, topic_err, topic_maxp; + + String invSerial = inv->serialString(); + String deviceInstance = MqttSettings.getVictronDeviceInstance(invSerial); + String topic = "W/" + portalid + "/pvinverter/" + deviceInstance; + + if (fieldId == 4) { + topic_sum += topic + "/Ac/Energy/Forward"; + topic_phase += topic + "/Ac/L" + invphase + "/Energy/Forward"; + } else { + topic_sum += topic + "/Ac/" + fieldname; + topic_phase += topic + "/Ac/L" + invphase + "/" + fieldname; + } + + DynamicJsonDocument valueDoc(32); + valueDoc["value"] = fieldvalue; + JsonObject valueObj = valueDoc.as(); + String data; + serializeJson(valueObj, data); + + Serial.print(F("MqTT publish value: ")); + Serial.print(fieldId); + Serial.print(data); + Serial.print(F(" to Venus OS with topic sum: ")); + Serial.print(topic_sum); + Serial.print(F(" and topic phase: ")); + Serial.println(topic_phase); + + MqttSettings.publishVictron(topic_sum, data); + MqttSettings.publishVictron(topic_phase, data); + + // Publish limit and error code to Victron Venus OS + uint16_t maxpower = inv->DevInfo()->getMaxPower(); + topic_err += topic + "/ErrorCode"; + + DynamicJsonDocument val1Doc(32); + val1Doc["value"] = 0; + JsonObject val1Obj = val1Doc.as(); + String data1; + serializeJson(val1Obj, data1); + MqttSettings.publishVictron(topic_err, data1); + + topic_maxp += topic + "/Ac/MaxPower"; + DynamicJsonDocument val2Doc(32); + val2Doc["value"] = maxpower; + JsonObject val2Obj = val2Doc.as(); + String data2; + serializeJson(val2Obj, data2); + MqttSettings.publishVictron(topic_maxp, data2); + + // Send Value 0 to other current phases and energy forward + uint8_t nonval = 0; + + DynamicJsonDocument nonvalueDoc(32); + nonvalueDoc["value"] = nonval; + JsonObject nonvalueObj = nonvalueDoc.as(); + String nondata; + serializeJson(nonvalueObj, nondata); + + String valA, valB, topicA, topicB; + + switch (invphase) { + case 1: + valA = "L2"; + valB = "L3"; + break; + case 2: + valA = "L1"; + valB = "L3"; + break; + case 3: + valA = "L1"; + valB = "L2"; + break; + default: + return; + break; + } + + if (fieldname == "YieldTotal") { + topicA += topic + "/Ac/" + valA + "/Energy/Forward"; + topicB += topic + "/Ac/" + valB + "/Energy/Forward"; + } else { + topicA += topic + "/Ac/" + valA + "/" + fieldname; + topicB += topic + "/Ac/" + valB + "/" + fieldname; + } + + MqttSettings.publishVictron(topicA, nondata); + MqttSettings.publishVictron(topicB, nondata); + } +} \ No newline at end of file diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index a99fe7c2f..1ae3f2054 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -8,6 +8,7 @@ #include "Configuration.h" #include "Hoymiles.h" #include "MqttHassPublishing.h" +#include "MqttVictronPublishing.h" #include "WebApi.h" #include "helper.h" @@ -62,6 +63,8 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = inv->Statistics()->getChannelCount(); } + obj[F("phase")] = config.Inverter[i].CurrentPhase; + JsonArray channel = obj.createNestedArray("channel"); for (uint8_t c = 0; c < max_channels; c++) { JsonObject chanData = channel.createNestedObject(); @@ -162,6 +165,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } MqttHassPublishing.forceUpdate(); + MqttVictronPublishing.forceRegister(); } void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) @@ -245,6 +249,8 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Serial = new_serial; strncpy(inverter.Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); + inverter.CurrentPhase = root[F("phase")].as(); + uint8_t arrayCount = 0; for (JsonVariant channel : channelArray) { inverter.channel[arrayCount].MaxChannelPower = channel[F("max_power")].as(); @@ -281,6 +287,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } MqttHassPublishing.forceUpdate(); + MqttVictronPublishing.forceRegister(); } void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) @@ -349,4 +356,5 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) request->send(response); MqttHassPublishing.forceUpdate(); + MqttVictronPublishing.forceRegister(); } \ No newline at end of file diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 3083851e2..92577933b 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -48,6 +48,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain; root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic; root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels; + root[F("mqtt_victron_enabled")] = config.Mqtt_Victron_Enabled; response->setLength(); request->send(response); @@ -81,6 +82,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root[F("mqtt_hass_retain")] = config.Mqtt_Hass_Retain; root[F("mqtt_hass_topic")] = config.Mqtt_Hass_Topic; root[F("mqtt_hass_individualpanels")] = config.Mqtt_Hass_IndividualPanels; + root[F("mqtt_victron_enabled")] = config.Mqtt_Victron_Enabled; response->setLength(); request->send(response); @@ -138,7 +140,8 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_hass_expire") && root.containsKey("mqtt_hass_retain") && root.containsKey("mqtt_hass_topic") - && root.containsKey("mqtt_hass_individualpanels"))) { + && root.containsKey("mqtt_hass_individualpanels") + && root.containsKey("mqtt_victron_enabled"))) { retMsg[F("message")] = F("Values are missing!"); response->setLength(); request->send(response); @@ -271,6 +274,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) config.Mqtt_Hass_Retain = root[F("mqtt_hass_retain")].as(); config.Mqtt_Hass_IndividualPanels = root[F("mqtt_hass_individualpanels")].as(); strlcpy(config.Mqtt_Hass_Topic, root[F("mqtt_hass_topic")].as().c_str(), sizeof(config.Mqtt_Hass_Topic)); + config.Mqtt_Victron_Enabled = root[F("mqtt_victron_enabled")].as(); Configuration.write(); retMsg[F("type")] = F("success"); diff --git a/src/main.cpp b/src/main.cpp index 24102c19a..6e8e0d338 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ #include "MqttHassPublishing.h" #include "MqttPublishing.h" #include "MqttSettings.h" +#include "MqttVictronPublishing.h" #include "NetworkSettings.h" #include "NtpSettings.h" #include "Utils.h" @@ -135,6 +136,8 @@ void loop() yield(); MqttHassPublishing.loop(); yield(); + MqttVictronPublishing.loop(); + yield(); WebApi.loop(); yield(); } \ No newline at end of file diff --git a/webapp/src/types/MqttConfig.ts b/webapp/src/types/MqttConfig.ts index 8077ae7b7..1c322a3da 100644 --- a/webapp/src/types/MqttConfig.ts +++ b/webapp/src/types/MqttConfig.ts @@ -17,4 +17,5 @@ export interface MqttConfig { mqtt_hass_retain: boolean; mqtt_hass_topic: string; mqtt_hass_individualpanels: boolean; + mqtt_victron_enabled: boolean; } \ No newline at end of file diff --git a/webapp/src/types/MqttStatus.ts b/webapp/src/types/MqttStatus.ts index fe046edb2..8fab2b689 100644 --- a/webapp/src/types/MqttStatus.ts +++ b/webapp/src/types/MqttStatus.ts @@ -14,4 +14,5 @@ export interface MqttStatus { mqtt_hass_retain: boolean; mqtt_hass_topic: string; mqtt_hass_individualpanels: boolean; + mqtt_victron_enabled: boolean; } \ No newline at end of file diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index 981642eff..0b45b380c 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -39,6 +39,7 @@ Serial Name Type + Phase Action @@ -47,6 +48,7 @@ {{ inverter.serial }} {{ inverter.name }} {{ inverter.type }} + L{{ inverter.phase}} @@ -81,6 +83,17 @@ class="form-control" maxlength="31" /> +
+ +
+ +
+
+
@@ -162,6 +175,7 @@ declare interface Inverter { serial: number; name: string; type: string; + phase: number; channel: Array; } @@ -186,6 +200,12 @@ export default defineComponent({ selectedInverterData: {} as Inverter, inverters: [] as Inverter[], dataLoading: true, + phaseList: [ + { key: 0, value: "None" }, + { key: 1, value: "L1" }, + { key: 2, value: "L2" }, + { key: 3, value: "L3" }, + ], alert: {} as AlertResponse }; }, diff --git a/webapp/src/views/MqttAdminView.vue b/webapp/src/views/MqttAdminView.vue index 769412946..cfd42abab 100644 --- a/webapp/src/views/MqttAdminView.vue +++ b/webapp/src/views/MqttAdminView.vue @@ -28,6 +28,17 @@
+ +
+ +
+
+ +
+
+
+ diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz index d0a8c5248..f7547917e 100644 Binary files a/webapp_dist/index.html.gz and b/webapp_dist/index.html.gz differ diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index e326444a9..45374b962 100644 Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ