From b9591222f5d280dd1e656e307973179d2107a1f0 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sun, 30 Oct 2022 13:04:09 +0100 Subject: [PATCH 01/55] [hue] Check HTTPS connection (download of PEM certificate) (#13617) * [hue] Check HTTPS connection (download of PEM certificate) Fix #13586 Signed-off-by: Laurent Garnier --- .../HueTlsTrustManagerProvider.java | 23 ++++++++++--- .../internal/handler/HueBridgeHandler.java | 33 ++++++++++++++----- .../main/resources/OH-INF/i18n/hue.properties | 1 + 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java index 5fa2820edee70..414de92f40af4 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/HueTlsTrustManagerProvider.java @@ -44,6 +44,8 @@ public class HueTlsTrustManagerProvider implements TlsTrustManagerProvider { private final Logger logger = LoggerFactory.getLogger(HueTlsTrustManagerProvider.class); + private @Nullable PEMTrustManager trustManager; + public HueTlsTrustManagerProvider(String hostname, boolean useSelfSignedCertificate) { this.hostname = hostname; this.useSelfSignedCertificate = useSelfSignedCertificate; @@ -56,20 +58,33 @@ public String getHostName() { @Override public X509ExtendedTrustManager getTrustManager() { + PEMTrustManager localTrustManager = getPEMTrustManager(); + if (localTrustManager == null) { + logger.error("Cannot get the PEM certificate - returning a TrustAllTrustManager"); + } + return localTrustManager != null ? localTrustManager : TrustAllTrustManager.getInstance(); + } + + public @Nullable PEMTrustManager getPEMTrustManager() { + PEMTrustManager localTrustManager = trustManager; + if (localTrustManager != null) { + return localTrustManager; + } try { if (useSelfSignedCertificate) { logger.trace("Use self-signed certificate downloaded from Hue Bridge."); // use self-signed certificate downloaded from Hue Bridge - return PEMTrustManager.getInstanceFromServer("https://" + getHostName()); + localTrustManager = PEMTrustManager.getInstanceFromServer("https://" + getHostName()); } else { logger.trace("Use Signify private CA Certificate for Hue Bridges from resources."); // use Signify private CA Certificate for Hue Bridges from resources - return getInstanceFromResource(PEM_FILENAME); + localTrustManager = getInstanceFromResource(PEM_FILENAME); } + this.trustManager = localTrustManager; } catch (CertificateException | MalformedURLException e) { - logger.error("An unexpected exception occurred - returning a TrustAllTrustManager: {}", e.getMessage(), e); + logger.debug("An unexpected exception occurred: {}", e.getMessage(), e); } - return TrustAllTrustManager.getInstance(); + return localTrustManager; } /** diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java index 1afcef5180d51..22686f89c527f 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/HueBridgeHandler.java @@ -706,20 +706,35 @@ public void initialize() { "@text/offline.conf-error-no-ip-address"); } else { if (hueBridge == null) { - if (HueBridgeConfig.HTTPS.equals(hueBridgeConfig.protocol)) { - // register trustmanager service - HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider( - ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate); - serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext() - .registerService(TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null); - } - hueBridge = new HueBridge(httpClient, ip, hueBridgeConfig.getPort(), hueBridgeConfig.protocol, scheduler); updateStatus(ThingStatus.UNKNOWN); + + if (HueBridgeConfig.HTTPS.equals(hueBridgeConfig.protocol)) { + scheduler.submit(() -> { + // register trustmanager service + HueTlsTrustManagerProvider tlsTrustManagerProvider = new HueTlsTrustManagerProvider( + ip + ":" + hueBridgeConfig.getPort(), hueBridgeConfig.useSelfSignedCertificate); + + // Check before registering that the PEM certificate can be downloaded + if (tlsTrustManagerProvider.getPEMTrustManager() == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-https-connection"); + return; + } + + serviceRegistration = FrameworkUtil.getBundle(getClass()).getBundleContext().registerService( + TlsTrustManagerProvider.class.getName(), tlsTrustManagerProvider, null); + + onUpdate(); + }); + } else { + onUpdate(); + } + } else { + onUpdate(); } - onUpdate(); } } diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index 2b2af1df10a66..9f376076c6e65 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -148,6 +148,7 @@ config-status.error.missing-ip-address-configuration = No IP address for the Hue # thing status descriptions offline.communication-error = An unexpected exception occurred during execution. +offline.conf-error-https-connection = HTTPS secure connection failed. Please check your configuration settings (network address, protocol, port, type of certificate) and change protocol to http when using a V1 bridge. offline.conf-error-invalid-ssl-certificate = Invalid certificate for secured connection. You might want to enable the "Use Self-Signed Certificate" configuration. offline.conf-error-no-ip-address = Cannot connect to Hue Bridge. IP address not available in configuration. offline.conf-error-no-username = Cannot connect to Hue Bridge. User name for authentication not available in configuration. From 6024eb84f6e62c4dfd66d805c51738767431ce25 Mon Sep 17 00:00:00 2001 From: Michi <54207108+michiii1337@users.noreply.github.com> Date: Sun, 30 Oct 2022 20:41:13 +0100 Subject: [PATCH 02/55] [miio] Update yeelink.light.light15.json (#13554) * Update yeelink.light.light15.json changed Datatype form "Number" to "Dimmer" at channel "ambientBrightness" same issue openhab#9936 (but already fixed) Signed-off-by: Michi --- bundles/org.openhab.binding.miio/README.md | 64 +++++++------------ .../database/yeelink.light.light15.json | 2 +- 2 files changed, 24 insertions(+), 42 deletions(-) diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index 407302b4b8fe2..d1e5208e79388 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -258,7 +258,7 @@ Currently the miio binding supports more than 330 different models. | HUIZUO ZIWEI Ceiling Lamp | miio:basic | [huayi.light.zw131](#huayi-light-zw131) | Experimental | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | MiJia Rice Cooker | miio:unsupported | hunmi.cooker.normal3 | No | | | Jinxing Smart Air Conditioner | miio:unsupported | idelan.aircondition.v1 | No | | -| Xiaomi Robot Vacuum-Mop 2S | miio:basic | [ijai.vacuum.v19](#ijai-vacuum-v19) | Yes | | +| Xiaomi Robot Vacuum-Mop 2S | miio:basic | [ijai.vacuum.v19](#ijai-vacuum-v19) | Yes | | | IKEA E27 white spectrum opal | miio:lumi | [ikea.light.led1545g12](#ikea-light-led1545g12) | Experimental | Needs to have the Xiaomi gateway configured in the binding as bridge.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | IKEA E27 white spectrum clear | miio:lumi | [ikea.light.led1546g12](#ikea-light-led1546g12) | Experimental | Needs to have the Xiaomi gateway configured in the binding as bridge.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | IKEA E14 white spectrum | miio:lumi | [ikea.light.led1536g5](#ikea-light-led1536g5) | Experimental | Needs to have the Xiaomi gateway configured in the binding as bridge.
Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | @@ -708,7 +708,7 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| -| battery | Number | Battery | | +| battery | Number | Battery | The device with firmware "4.1.8_9999" stops recognizing parameter "battery" in "get_value" command. The "battery" value request was extracted to separate command in order to keep backward compatibility to the devices with older firmware. | | pm25 | Number | PM2.5 | | | co2 | Number | CO2 | | | tvoc | Number | tVOC | | @@ -2604,15 +2604,14 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| | power | Switch | Power | | -| mode | String | Mode | Value mapping `["normal"="Normal","green"="Green"]` | -| powerUsage | Number:Power | Power Consumption | | -| voltage | Number:ElectricPotential | Voltage | | +| powerUsage | Number | Power Consumption | | | led | Switch | wifi LED | | -| power_price | Number | Power Price | | -| power_factor | Number | Power Factor | | -| current | Number:ElectricCurrent | Current | | -| elec_leakage | Number:ElectricCurrent | Electic Leakage | | +| power_price | Number | power_price | | +| current | Number | Current | | | temperature | Number:Temperature | Temperature | | +| lp_autooff | Number | Low Power Auto Off | | +| lp_autooff_delay | Number | Low Power Limit Time | | +| lp_threshold | Number | Low Power Threshold | | ### ROIDMI EVE vacuum (roidmi.vacuum.v60) Channels @@ -4123,7 +4122,7 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | colorTemperature | Number:Temperature | Color Temperature | | | colorMode | Number | Color Mode | Note, currently only supporting switching to RGB or CT mode. Value mapping `["0"="Default","2"="CT mode","1"="RGB mode","3"="HSV mode","4"="Color Flow mode","5"="Night Light mode"]` | | rgbColor | Color | RGB Color | | -| ambientBrightness | Number | Ambient Brightness | | +| ambientBrightness | Dimmer | Ambient Brightness | | | ambientPower | Switch | Ambient Power | | | ambientColor | Color | Ambient Color | | | ambientColorTemperature | Number | Ambient Color Temperature | | @@ -4216,8 +4215,6 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | colorTemperature | Number | Color Temperature | | | colorMode | Number | Color Mode | Note, currently only supporting switching to RGB or CT mode. Value mapping `["0"="Default","2"="CT mode","1"="RGB mode","3"="HSV mode","4"="Color Flow mode","5"="Night Light mode"]` | | name | String | Name | | -| customScene | String | Set Scene | | -| nightlightBrightness | Number | Nightlight Brightness | | ### Yeelight Lightstrip (yeelink.light.strip1) Channels @@ -5194,21 +5191,15 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | Channel | Type | Description | Comment | |----------------------|----------------------|------------------------------------------|------------| | power | Switch | Power | | -| mode | String | Mode | Value mapping `["auto"="Auto","favorite"="Favorite","silent"="Silent","high"="High","medium"="Medium","idle"="Idle","strong"="Strong"]` | +| mode | String | Mode | | | humidity | Number:Dimensionless | Humidity | | | aqi | Number | Air Quality Index | | -| averageaqi | Number | Average Air Quality Index | | +| brightness | Dimmer | Brightness | | | led | Switch | LED Status | | +| act_det | Switch | Air AutoDetect | | | buzzer | Switch | Buzzer Status | | | filtermaxlife | Number | Filter Max Life | | -| filterhours | Number:Time | Filter Hours used | | -| usedhours | Number:Time | Run Time | | -| motorspeed | Number | Motor Speed | | -| filterlife | Number | Filter Life | | -| favoritelevel | Number | Favorite Level | Value mapping `["0"="Favorite 0","1"="Favorite 1","2"="Favorite 2","3"="Favorite 3","4"="Favorite 4","5"="Favorite 5","6"="Favorite 6","7"="Favorite 7","8"="Favorite 8","9"="Favorite 9","10"="Favorite 10","11"="Favorite 11","12"="Favorite 13","13"="Favorite 13","14"="Favorite 14","15"="Favorite 15"]` | -| temperature | Number:Temperature | Temperature | | -| purifyvolume | Number:Volume | Purified Volume | | -| childlock | Switch | Child Lock | | +| filterlive | Number | Filter Life | | ### Mi Air Purifier v2 (zhimi.airpurifier.v2) Channels @@ -8237,15 +8228,14 @@ note: Autogenerated example. Replace the id (powerstrip) in the channel with you ``` Group G_powerstrip "CHINGMI Smart Power Strip v1" Switch power "Power" (G_powerstrip) {channel="miio:basic:powerstrip:power"} -String mode "Mode" (G_powerstrip) {channel="miio:basic:powerstrip:mode"} -Number:Power powerUsage "Power Consumption" (G_powerstrip) {channel="miio:basic:powerstrip:powerUsage"} -Number:ElectricPotential voltage "Voltage" (G_powerstrip) {channel="miio:basic:powerstrip:voltage"} +Number powerUsage "Power Consumption" (G_powerstrip) {channel="miio:basic:powerstrip:powerUsage"} Switch led "wifi LED" (G_powerstrip) {channel="miio:basic:powerstrip:led"} -Number power_price "Power Price" (G_powerstrip) {channel="miio:basic:powerstrip:power_price"} -Number power_factor "Power Factor" (G_powerstrip) {channel="miio:basic:powerstrip:power_factor"} -Number:ElectricCurrent current "Current" (G_powerstrip) {channel="miio:basic:powerstrip:current"} -Number:ElectricCurrent elec_leakage "Electic Leakage" (G_powerstrip) {channel="miio:basic:powerstrip:elec_leakage"} +Number power_price "power_price" (G_powerstrip) {channel="miio:basic:powerstrip:power_price"} +Number current "Current" (G_powerstrip) {channel="miio:basic:powerstrip:current"} Number:Temperature temperature "Temperature" (G_powerstrip) {channel="miio:basic:powerstrip:temperature"} +Number lp_autooff "Low Power Auto Off" (G_powerstrip) {channel="miio:basic:powerstrip:lp_autooff"} +Number lp_autooff_delay "Low Power Limit Time" (G_powerstrip) {channel="miio:basic:powerstrip:lp_autooff_delay"} +Number lp_threshold "Low Power Threshold" (G_powerstrip) {channel="miio:basic:powerstrip:lp_threshold"} ``` ### ROIDMI EVE vacuum (roidmi.vacuum.v60) item file lines @@ -10032,7 +10022,7 @@ Number:Time delayoff "Shutdown Timer" (G_light) {channel="miio:basic:light:delay Number:Temperature colorTemperature "Color Temperature" (G_light) {channel="miio:basic:light:colorTemperature"} Number colorMode "Color Mode" (G_light) {channel="miio:basic:light:colorMode"} Color rgbColor "RGB Color" (G_light) {channel="miio:basic:light:rgbColor"} -Number ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} +Dimmer ambientBrightness "Ambient Brightness" (G_light) {channel="miio:basic:light:ambientBrightness"} Switch ambientPower "Ambient Power" (G_light) {channel="miio:basic:light:ambientPower"} Color ambientColor "Ambient Color" (G_light) {channel="miio:basic:light:ambientColor"} Number ambientColorTemperature "Ambient Color Temperature" (G_light) {channel="miio:basic:light:ambientColorTemperature"} @@ -10149,8 +10139,6 @@ Number:Time delayoff "Shutdown Timer" (G_light) {channel="miio:basic:light:delay Number colorTemperature "Color Temperature" (G_light) {channel="miio:basic:light:colorTemperature"} Number colorMode "Color Mode" (G_light) {channel="miio:basic:light:colorMode"} String name "Name" (G_light) {channel="miio:basic:light:name"} -String customScene "Set Scene" (G_light) {channel="miio:basic:light:customScene"} -Number nightlightBrightness "Nightlight Brightness" (G_light) {channel="miio:basic:light:nightlightBrightness"} ``` ### Yeelight Lightstrip (yeelink.light.strip1) item file lines @@ -11253,18 +11241,12 @@ Switch power "Power" (G_airpurifier) {channel="miio:basic:airpurifier:power"} String mode "Mode" (G_airpurifier) {channel="miio:basic:airpurifier:mode"} Number:Dimensionless humidity "Humidity" (G_airpurifier) {channel="miio:basic:airpurifier:humidity"} Number aqi "Air Quality Index" (G_airpurifier) {channel="miio:basic:airpurifier:aqi"} -Number averageaqi "Average Air Quality Index" (G_airpurifier) {channel="miio:basic:airpurifier:averageaqi"} +Dimmer brightness "Brightness" (G_airpurifier) {channel="miio:basic:airpurifier:brightness"} Switch led "LED Status" (G_airpurifier) {channel="miio:basic:airpurifier:led"} +Switch act_det "Air AutoDetect" (G_airpurifier) {channel="miio:basic:airpurifier:act_det"} Switch buzzer "Buzzer Status" (G_airpurifier) {channel="miio:basic:airpurifier:buzzer"} Number filtermaxlife "Filter Max Life" (G_airpurifier) {channel="miio:basic:airpurifier:filtermaxlife"} -Number:Time filterhours "Filter Hours used" (G_airpurifier) {channel="miio:basic:airpurifier:filterhours"} -Number:Time usedhours "Run Time" (G_airpurifier) {channel="miio:basic:airpurifier:usedhours"} -Number motorspeed "Motor Speed" (G_airpurifier) {channel="miio:basic:airpurifier:motorspeed"} -Number filterlife "Filter Life" (G_airpurifier) {channel="miio:basic:airpurifier:filterlife"} -Number favoritelevel "Favorite Level" (G_airpurifier) {channel="miio:basic:airpurifier:favoritelevel"} -Number:Temperature temperature "Temperature" (G_airpurifier) {channel="miio:basic:airpurifier:temperature"} -Number:Volume purifyvolume "Purified Volume" (G_airpurifier) {channel="miio:basic:airpurifier:purifyvolume"} -Switch childlock "Child Lock" (G_airpurifier) {channel="miio:basic:airpurifier:childlock"} +Number filterlive "Filter Life" (G_airpurifier) {channel="miio:basic:airpurifier:filterlive"} ``` ### Mi Air Purifier v2 (zhimi.airpurifier.v2) item file lines diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.light15.json b/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.light15.json index fac444533ff06..91b877954f983 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.light15.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/yeelink.light.light15.json @@ -232,7 +232,7 @@ "property": "bg_bright", "friendlyName": "Ambient Brightness", "channel": "ambientBrightness", - "type": "Number", + "type": "Dimmer", "refresh": true, "ChannelGroup": "actions", "actions": [ From e528564bb6f401e297649a3cfe2ad868bbdfcb8e Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Mon, 31 Oct 2022 09:22:35 +0100 Subject: [PATCH 03/55] New translations hue.properties (Italian) (#13627) --- .../resources/OH-INF/i18n/hue_it.properties | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_it.properties diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_it.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_it.properties new file mode 100644 index 0000000000000..872b919b42c3f --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_it.properties @@ -0,0 +1,184 @@ +# binding + +binding.hue.name = Gestore Hue +binding.hue.description = Il Gestore Hue integra il sistema Philips Hue. Permette di controllare le lampadine Hue. + +# thing types + +thing-type.hue.0000.label = Lampadina Accesa/Spenta +thing-type.hue.0000.description = Una lampadina che può essere accesa e spenta. +thing-type.hue.0010.label = Presa Accesa/Spenta +thing-type.hue.0010.description = Una presa che può essere accesa e spenta. +thing-type.hue.0100.label = Lampadina regolabile +thing-type.hue.0100.description = Una lampadina regolabile. +thing-type.hue.0106.label = Sensore di luminosità Hue +thing-type.hue.0106.description = Un sensore che fornisce informazioni sul livello di luce. +thing-type.hue.0107.label = Sensore di presenza +thing-type.hue.0107.description = Un sensore di movimento che fornisce il rilevamento di presenza. +thing-type.hue.0110.label = Presa regolabile +thing-type.hue.0110.description = Una presa che può essere regolata in tensione. +thing-type.hue.0200.label = Lampadina colorata +thing-type.hue.0200.description = Una luce regolabile a cui si può cambiare il colore. +thing-type.hue.0210.label = Lampadina colorata avanzata +thing-type.hue.0210.description = Una luce regolabile a cui si può cambiare il colore e la temperatura del colore. +thing-type.hue.0220.label = Lampadina a temperatura di colore +thing-type.hue.0220.description = Una luce regolabile a cui si può cambiare la temperatura del colore. +thing-type.hue.0302.label = Sensore di temperatura Hue +thing-type.hue.0302.description = Un sensore che fornisce il valore di temperatura. +thing-type.hue.0820.label = Interruttore varialuce di Hue +thing-type.hue.0820.description = Un interruttore varialuce di Hue. +thing-type.hue.0830.label = Interruttore a tocco Hue +thing-type.hue.0830.description = Un interruttore a tocco personalizzabile. +thing-type.hue.0840.label = Sensore di stato generico CLIP +thing-type.hue.0840.description = Un oggetto sensore generico numerico creato dagli scenari del sistema. +thing-type.hue.0850.label = Sensore di stato generico vero/falso CLIP +thing-type.hue.0850.description = Un oggetto sensore generico vero/falso creato dagli scenari del sistema. +thing-type.hue.bridge.label = "Ponte" Hue +thing-type.hue.bridge.description = Il "ponte" Hue connette il sistema al Hue Bridge della Philips . +thing-type.hue.geofencesensor.label = Sensore perimetrale +thing-type.hue.geofencesensor.description = Un sensore che fornisce rilevamento della presenza basata su di un perimetro definito. +thing-type.hue.group.label = Gruppo Hue +thing-type.hue.group.description = Un gruppo di lampadine o una stanza che potrebbe essere accesa e spenta. + +# thing types config + +thing-type.config.hue.bridge.ipAddress.label = Indirizzo di rete +thing-type.config.hue.bridge.ipAddress.description = Indirizzo di rete dell'Hue Bridge. +thing-type.config.hue.bridge.pollingInterval.label = Intervallo d'interrogazione +thing-type.config.hue.bridge.pollingInterval.description = Secondi tra due letture di valori dall'Hue Bridge. Il valore predefinito è 10. +thing-type.config.hue.bridge.port.label = Porta +thing-type.config.hue.bridge.port.description = Porta dell'Hue Bridge. +thing-type.config.hue.bridge.protocol.label = Protocollo +thing-type.config.hue.bridge.protocol.description = Protocollo per connettersi all'Hue Bridge (http o https). +thing-type.config.hue.bridge.protocol.option.http = HTTP +thing-type.config.hue.bridge.protocol.option.https = HTTPS +thing-type.config.hue.bridge.sensorPollingInterval.label = Intervallo interrogazione sensore +thing-type.config.hue.bridge.sensorPollingInterval.description = Millisecondi tra una lettura ed un altra dei valori dei sensori dall'Hue Bridge. Un valore più alto significa un ritardo maggiore dei valori del sensore, ma un valore troppo basso può causare congestione sull'Hue Bridge. Usa 0 per disabilitare l'interrogazione dei sensori. Il valore predefinito è 500. +thing-type.config.hue.bridge.useSelfSignedCertificate.label = Usa un certificato auto-firmato +thing-type.config.hue.bridge.useSelfSignedCertificate.description = Utilizzare un certificato auto-firmato per la connessione HTTPS ad un Hue Bridge. +thing-type.config.hue.bridge.userName.label = Nome utente +thing-type.config.hue.bridge.userName.description = Nome di un utente registrato sull'Hue Bridge, che consenta l'accesso alle API. +thing-type.config.hue.group.groupId.label = ID gruppo +thing-type.config.hue.group.groupId.description = L'identificatore di gruppo identifica un certo gruppo o stanza di Hue. +thing-type.config.hue.lightlevelsensor.tholddark.label = Soglia oscurità +thing-type.config.hue.lightlevelsensor.tholddark.description = Soglia configurabile dall'utente utilizzata nelle regole per determinare un livello di luce insufficiente (cioè sotto la soglia). Valore predefinito 16000. +thing-type.config.hue.lightlevelsensor.tholdoffset.label = Soglia di scostamento +thing-type.config.hue.lightlevelsensor.tholdoffset.description = Soglia configurabile dall'utente utilizzata in regole per determinare un livello di luce sufficiente (cioè sopra la soglia). Specificato come spostamento relativo alla soglia di oscurità precedentemente definita. Deve essere >\=1. Valore predefinito 7000. +thing-type.config.hue.presencesensor.sensitivity.label = Sensibilità +thing-type.config.hue.presencesensor.sensitivity.description = La sensibilità corrente del sensore di presenza. Impossibile superare la sensibilità massima. +thing-type.config.hue.presencesensor.sensitivitymax.label = Sensibilità massima +thing-type.config.hue.presencesensor.sensitivitymax.description = La massima sensibilità del sensore di presenza. + +# channel types + +channel-type.hue.alert.label = Allerta +channel-type.hue.alert.description = Il canale di allerta consente una modifica temporanea dello stato della lampadina. +channel-type.hue.alert.state.option.NONE = Nessuno +channel-type.hue.alert.state.option.SELECT = Allerta +channel-type.hue.alert.state.option.LSELECT = Allarme lungo +channel-type.hue.dark.label = Oscurità +channel-type.hue.dark.description = Il livello di luminosità sotto la quale si definisce oscurità. +channel-type.hue.daylight.label = Luce diurna +channel-type.hue.daylight.description = Il livello di luminosità sopra la quale si definisce luce diurna. +channel-type.hue.dimmer_switch.label = Stato interruttore varialuce +channel-type.hue.dimmer_switch.description = Il pulsante che è stato premuto l'ultima volta sull'interruttore varialuce. +channel-type.hue.dimmer_switch.state.option.1000 = On (Pressione iniziale) +channel-type.hue.dimmer_switch.state.option.1001 = On (premuto) +channel-type.hue.dimmer_switch.state.option.1002 = On (rilascio corto) +channel-type.hue.dimmer_switch.state.option.1003 = On (rilascio lungo) +channel-type.hue.dimmer_switch.state.option.2000 = Aumento luminosità (pressione iniziale) +channel-type.hue.dimmer_switch.state.option.2001 = Aumento luminosità (premuto) +channel-type.hue.dimmer_switch.state.option.2002 = Aumento luminosità (rilascio breve) +channel-type.hue.dimmer_switch.state.option.2003 = Aumento luminosità (rilascio lungo) +channel-type.hue.dimmer_switch.state.option.3000 = Diminuzione luminosità (pressione iniziale) +channel-type.hue.dimmer_switch.state.option.3001 = Diminuzione luminosità (premuto) +channel-type.hue.dimmer_switch.state.option.3002 = Diminuzione luminosità (rilscio breve) +channel-type.hue.dimmer_switch.state.option.3003 = Diminuzione luminosità (rilscio lungo) +channel-type.hue.dimmer_switch.state.option.4000 = Off (Pressione iniziale) +channel-type.hue.dimmer_switch.state.option.4001 = Off (premuto) +channel-type.hue.dimmer_switch.state.option.4002 = Off (rilascio corto) +channel-type.hue.dimmer_switch.state.option.4003 = Off (rilascio lungo) +channel-type.hue.dimmer_switch_event.label = Evento interruttore varialuce +channel-type.hue.dimmer_switch_event.description = Si attiva quando viene premuto un pulsante sull'interruttore varialuce. +channel-type.hue.effect.label = Rotazione colore +channel-type.hue.effect.description = L'effetto del canale permette di mettere la lampadina in una modalità di rotazione colore. +channel-type.hue.flag.label = Indicatore +channel-type.hue.flag.description = Indicatore del sensore CLIP. +channel-type.hue.illuminance.label = Illuminazione +channel-type.hue.illuminance.description = Illuminazione corrente. +channel-type.hue.last_updated.label = Ultimo aggiornamento +channel-type.hue.last_updated.description = Data e orario dell'ultimo aggiornamento del sensore. +channel-type.hue.last_updated.state.pattern = %1$td-%1$tm-%1$tY %1$tH\:%1$tM\:%1$tS +channel-type.hue.light_level.label = Livello luminosità +channel-type.hue.light_level.description = Il livello di luminosità corrente. +channel-type.hue.scene.label = Scena +channel-type.hue.scene.description = La scena di un canale permette di richiamare una scena e tutte le luci associate. +channel-type.hue.status.label = Stato +channel-type.hue.status.description = Lo stato di un sensore CLIP. +channel-type.hue.tap_switch.label = Stato interruttore a pulsante +channel-type.hue.tap_switch.description = Il pulsante premuto di un interruttore a pulsante. +channel-type.hue.tap_switch.state.option.34 = Pulsante 1 +channel-type.hue.tap_switch.state.option.16 = Pulsante 2 +channel-type.hue.tap_switch.state.option.17 = Pulsante 3 +channel-type.hue.tap_switch.state.option.18 = Pulsante 4 +channel-type.hue.tap_switch_event.label = Evento interruttore a pulsante +channel-type.hue.tap_switch_event.description = Si attiva quando viene premuto un pulsante sull'interruttore a pulsante. +channel-type.hue.temperature.label = Temperatura +channel-type.hue.temperature.description = Temperatura corrente. + +# thing types config + +config.fadetime.label = Tempo di dissolvenza +config.fadetime.description = Tempo di dissolvenza in millisecondi per cambiarne i valori. +config.ledindication.label = Indicatore LED +config.ledindication.description = Attiva o disattiva il LED del dispositivo durante il funzionamento normale. I dispositivi potrebbero ancora indicare un funzionamento particolare (riavvio, aggiornamento SW, livello basso batteria). +config.lightId.label = Id lampadina +config.lightId.description = L'identificatore di luce che viene utilizzato all'interno dell'Hue Bridge. +config.plugId.label = ID presa +config.plugId.description = L'identificatore della presa che viene utilizzato all'interno dell'Hue Bridge. +config.sensorId.label = ID sensore +config.sensorId.description = L'identificatore del sensore che viene utilizzato all'interno dell'Hue Bridge. +config.on.label = Stato sensore +config.on.description = Abilita o disabilita il sensore. + +# config status messages + +config-status.error.missing-ip-address-configuration = Non è stato fornito alcun indirizzo IP per l'Hue Bridge. + +# thing status descriptions + +offline.communication-error = Si è verificata un'eccezione inattesa durante l'esecuzione. +offline.conf-error-https-connection = Connessione sicura HTTPS non riuscita. Controllare le impostazioni di configurazione (indirizzo di rete, protocollo, porta, tipo di certificato) e modificare il protocollo in http quando si utilizza un bridge V1. +offline.conf-error-invalid-ssl-certificate = Certificato non valido per la connessione protetta. Potresti dover abilitare l'opzione "Usa un certificato auto-firmato". +offline.conf-error-no-ip-address = Impossibile connettersi all'Hue Bridge. Indirizzo IP non definito nella configurazione. +offline.conf-error-no-username = Impossibile connettersi all'Hue Bridge. Nome utente per l'autenticazione non definitio nella configurazione. +offline.conf-error-invalid-username = Autenticazione fallita. Rimuovere il nome utente dalla configurazione per generarne una nuova. +offline.conf-error-press-pairing-button = Non autenticato. Premere il pulsante di accoppiamento sull'Hue Bridge o impostare un nome utente valido in configurazione. +offline.conf-error-creation-username = Impossibile creare un nuovo utente sull'Hue Bridge. +offline.bridge-connection-lost = Collegamento perso con l'Hue Bridge. +offline.conf-error-no-light-id = ID della lampadina non disponibile in configurazione. +offline.conf-error-no-sensor-id = ID del sensore non disponibile in configurazione. +offline.conf-error-no-group-id = ID del gruppo non disponibile in configurazione. +offline.conf-error-wrong-light-id = Sull'Hue Bridge non è disponibile alcuna lampadina con l'ID fornito. +offline.conf-error-wrong-sensor-id = Sull'Hue Bridge non è disponibile alcun sensore con l'ID fornito. +offline.conf-error-wrong-group-id = Sull'Hue Bridge non è disponibile alcun gruppo con l'ID fornito. +offline.light-not-reachable = L'Hue Bridge indica che la luce non è raggiungibile. +offline.sensor-not-reachable = L'Hue Bridge indica che il sensore non è raggiungibile. +offline.light-removed = L'Hue Bridge segnala che la luce è stata rimossa. +offline.sensor-removed = L'Hue Bridge segnala che il sensore è stato rimosso. +offline.group-removed = L'Hue Bridge segnala che il gruppo è stato rimosso. + +# lightactions + +actionInputChannelLabel = Canale +actionInputChannelDesc = Il canale a cui inviare il comando. +actionInputCommandLabel = Comando +actionInputCommandDesc = Il comando per la lampadina. +actionInputFadeTimeLabel = Tempo di dissolvenza +actionInputFadeTimeDesc = Il tempo di dissolvenza da usare per il relativo comando alla lampadina, in ms. +actionLabel = invia un comando alla lampadina con un inervallo di dissolvenza personalizzato +actionDesc = Invia un comando alla lampadina con un inervallo di dissolvenza personalizzato. + +# discovery results + +discovery.group.all_lights.label = Tutte le lampadine From 71da06dac776685c5192446738e167185ec3afba Mon Sep 17 00:00:00 2001 From: David Pace Date: Mon, 31 Oct 2022 17:21:25 +0100 Subject: [PATCH 04/55] [boschshc] Support for Compact Smart Plugs (#13528) (#13533) * add thing definition with ID "smart-plug-compact" * add constant for thing type UID * extract abstract implementation for devices with power switch and energy monitoring * let in-wall switch handler and smart plug handler extend the abstract implementation * register new handler * add method with boolean parameter to fetch initial state actively * make BoschSHCDeviceHandler abstract * add documentation * add unit tests closes #13528 Signed-off-by: David Pace --- .../org.openhab.binding.boschshc/README.md | 35 ++++-- .../devices/AbstractPowerSwitchHandler.java | 110 ++++++++++++++++ .../devices/BoschSHCBindingConstants.java | 1 + .../devices/BoschSHCDeviceHandler.java | 7 +- .../internal/devices/BoschSHCHandler.java | 41 ++++-- .../devices/BoschSHCHandlerFactory.java | 6 +- .../lightcontrol/LightControlHandler.java | 75 +---------- .../internal/devices/plug/PlugHandler.java | 30 +++++ .../resources/OH-INF/thing/thing-types.xml | 18 +++ .../AbstractBoschSHCDeviceHandlerTest.java | 35 ++++++ .../AbstractPowerSwitchHandlerTest.java | 118 ++++++++++++++++++ .../devices/AbstractSHCHandlerTest.java | 96 ++++++++++++++ .../lightcontrol/LightControlHandlerTest.java | 43 +++++++ .../devices/plug/PlugHandlerTest.java | 43 +++++++ 14 files changed, 559 insertions(+), 99 deletions(-) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index 9331afa9fd0c6..979f54659e496 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -4,9 +4,10 @@ Binding for the Bosch Smart Home. - [Bosch Smart Home Binding](#bosch-smart-home-binding) - [Supported Things](#supported-things) - - [In-Wall switches & Smart Plugs](#in-wall-switches-smart-plugs) - - [TwinGuard smoke detector](#twinguard-smoke-detector) - - [Door/Window contact](#door-window-contact) + - [In-Wall Switch](#in-wall-switch) + - [Compact Smart Plug](#compact-smart-plug) + - [Twinguard Smoke Detector](#twinguard-smoke-detector) + - [Door/Window Contact](#door-window-contact) - [Motion Detector](#motion-detector) - [Shutter Control](#shutter-control) - [Thermostat](#thermostat) @@ -24,19 +25,31 @@ Binding for the Bosch Smart Home. ## Supported Things -### In-Wall switches & Smart Plugs +### In-Wall Switch A simple light control. **Thing Type ID**: `in-wall-switch` -| Channel Type ID | Item Type | Writable | Description | -| ------------------ | ------------- | :------: | -------------------------------------------- | -| power-switch | Switch | ☑ | Current state of the switch. | -| power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | -| energy-consumption | Number:Energy | ☐ | Energy consumption of the device. | +| Channel Type ID | Item Type | Writable | Description | +| ------------------ | ------------- | :------: | ------------------------------------------------ | +| power-switch | Switch | ☑ | Current state of the switch. | +| power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | +| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | -### TwinGuard smoke detector +### Compact Smart Plug + +A compact smart plug with energy monitoring capabilities. + +**Thing Type ID**: `smart-plug-compact` + +| Channel Type ID | Item Type | Writable | Description | +| ------------------ | ------------- | :------: | ------------------------------------------------ | +| power-switch | Switch | ☑ | Current state of the switch. | +| power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | +| energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | + +### Twinguard smoke detector The Twinguard smoke detector warns you in case of fire and constantly monitors the air. @@ -53,7 +66,7 @@ The Twinguard smoke detector warns you in case of fire and constantly monitors t | air-description | String | ☐ | Overall description of the air quality. | | combined-rating | String | ☐ | Combined rating of the air quality. | -### Door/Window contact +### Door/Window Contact Detects open windows and doors. diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java new file mode 100644 index 0000000000000..21503db0580cf --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandler.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; + +import java.util.List; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.powermeter.PowerMeterService; +import org.openhab.binding.boschshc.internal.services.powermeter.dto.PowerMeterServiceState; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchService; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState; +import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; + +/** + * Abstract handler implementation for devices with power switches and energy monitoring. + *

+ * This implementation provides the functionality to + *

    + *
  • Switch the device on and off using the PowerSwitch service
  • + *
  • Measuring the current power consumption and the overall energy consumption using the PowerMeter + * service
  • + *
+ * + * @author David Pace - Initial contribution (extracted from LightControlHandler) + */ +@NonNullByDefault +public abstract class AbstractPowerSwitchHandler extends BoschSHCDeviceHandler { + + /** + * Service for switching the device on and off + */ + private final PowerSwitchService powerSwitchService; + + protected AbstractPowerSwitchHandler(Thing thing) { + super(thing); + this.powerSwitchService = new PowerSwitchService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + this.registerService(this.powerSwitchService, this::updateChannels, List.of(CHANNEL_POWER_SWITCH), true); + this.createService(PowerMeterService::new, this::updateChannels, + List.of(CHANNEL_POWER_CONSUMPTION, CHANNEL_ENERGY_CONSUMPTION), true); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + super.handleCommand(channelUID, command); + + switch (channelUID.getId()) { + case CHANNEL_POWER_SWITCH: + if (command instanceof OnOffType) { + updatePowerSwitchState((OnOffType) command); + } + break; + } + } + + /** + * Updates the channels which are linked to the {@link PowerMeterService} of the device. + * + * @param state Current state of {@link PowerMeterService}. + */ + private void updateChannels(PowerMeterServiceState state) { + super.updateState(CHANNEL_POWER_CONSUMPTION, new QuantityType(state.powerConsumption, Units.WATT)); + super.updateState(CHANNEL_ENERGY_CONSUMPTION, + new QuantityType(state.energyConsumption, Units.WATT_HOUR)); + } + + /** + * Updates the channels which are linked to the {@link PowerSwitchService} of the device. + * + * @param state Current state of {@link PowerSwitchService}. + */ + private void updateChannels(PowerSwitchServiceState state) { + State powerState = OnOffType.from(state.switchState.toString()); + super.updateState(CHANNEL_POWER_SWITCH, powerState); + } + + private void updatePowerSwitchState(OnOffType command) { + PowerSwitchServiceState state = new PowerSwitchServiceState(); + state.switchState = PowerSwitchState.valueOf(command.toFullString()); + this.updateServiceState(this.powerSwitchService, state); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java index 6b3cf4e3eb5c6..7097deba80a60 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java @@ -44,6 +44,7 @@ public class BoschSHCBindingConstants { public static final ThingTypeUID THING_TYPE_CAMERA_EYES = new ThingTypeUID(BINDING_ID, "security-camera-eyes"); public static final ThingTypeUID THING_TYPE_INTRUSION_DETECTION_SYSTEM = new ThingTypeUID(BINDING_ID, "intrusion-detection-system"); + public static final ThingTypeUID THING_TYPE_SMART_PLUG_COMPACT = new ThingTypeUID(BINDING_ID, "smart-plug-compact"); // List of all Channel IDs // Auto-generated from thing-types.xml via script, don't modify diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java index a7e1fc88fbb68..7479efd09b408 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCDeviceHandler.java @@ -28,19 +28,19 @@ * The device ID of physical devices has to be configured in the thing configuration. *

* Examples for physical device IDs are: - * + * *

  * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
  * hdm:ZigBee:000d6f0016d1cdae
  * 
- * + * * @author Stefan Kästle - Initial contribution * @author Christian Oeing - refactorings of e.g. server registration * @author David Pace - Handler abstraction * */ @NonNullByDefault -public class BoschSHCDeviceHandler extends BoschSHCHandler { +public abstract class BoschSHCDeviceHandler extends BoschSHCHandler { /** * Bosch SHC configuration loaded from openHAB configuration. @@ -85,6 +85,7 @@ public void initialize() { * * @return Unique id of the Bosch device. */ + @Override public @Nullable String getBoschID() { if (config != null) { return config.id; diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java index 5fbbe50a65341..9e3d798369987 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java @@ -100,14 +100,14 @@ protected BoschSHCHandler(Thing thing) { * Returns the unique id of the Bosch device or service. *

* For physical devices, the ID looks like - * + * *

      * hdm:Cameras:d20354de-44b5-3acc-924c-24c98d59da42
      * hdm:ZigBee:000d6f0016d1c087
      * 
- * + * * For virtual devices / services, static IDs like the following are used: - * + * *
      * ventilationService
      * smokeDetectionSystem
@@ -241,8 +241,29 @@ protected BridgeHandler getBridgeHandler() throws BoschSHCException {
     protected , TState extends BoschSHCServiceState> TService createService(
             Supplier newService, Consumer stateUpdateListener, Collection affectedChannels)
             throws BoschSHCException {
+        return createService(newService, stateUpdateListener, affectedChannels, false);
+    }
+
+    /**
+     * Creates and registers a new service for this device.
+     *
+     * @param  Type of service.
+     * @param  Type of service state.
+     * @param newService Supplier function to create a new instance of the service.
+     * @param stateUpdateListener Function to call when a state update was received
+     *            from the device.
+     * @param affectedChannels Channels which are affected by the state of this
+     *            service.
+     * @param shouldFetchInitialState indicates whether the initial state should be actively requested from the device
+     *            or service. Useful if state updates are not included in long poll results.
+     * @return Instance of registered service.
+     * @throws BoschSHCException
+     */
+    protected , TState extends BoschSHCServiceState> TService createService(
+            Supplier newService, Consumer stateUpdateListener, Collection affectedChannels,
+            boolean shouldFetchInitialState) throws BoschSHCException {
         TService service = newService.get();
-        this.registerService(service, stateUpdateListener, affectedChannels);
+        this.registerService(service, stateUpdateListener, affectedChannels, shouldFetchInitialState);
         return service;
     }
 
@@ -296,7 +317,7 @@ protected , TState extends BoschSHCServ
     /**
      * Actively requests the initial state for the given service. This is required if long poll results do not contain
      * status updates for the given service.
-     * 
+     *
      * @param  Type of the service for which the state should be obtained
      * @param  Type of the objects to serialize and deserialize the service state
      * @param service Service for which the state should be requested
@@ -325,7 +346,7 @@ private , TState extends BoschSHCServic
      * Registers a write-only service that does not receive states from the bridge.
      * 

* Examples for such services are the actions of the intrusion detection service. - * + * * @param Type of service. * @param service Service to register. * @throws BoschSHCException If no device ID is set. @@ -340,7 +361,7 @@ protected void registerStatelessServi /** * Verifies that a Bosch device or service ID is set and throws an exception if this is not the case. - * + * * @return the Bosch ID, if present * @throws BoschSHCException if no Bosch ID is set */ @@ -404,7 +425,7 @@ protected , TState extends BoschSHCServ /** * Requests a service to refresh its state. * Sets the device offline if request fails. - * + * * @param Type of service. * @param Type of service state. * @param service Service to refresh state for. @@ -438,7 +459,7 @@ private void registerService(BoschSHCServi /** * Sends a HTTP POST request with empty body. - * + * * @param Type of service. * @param service Service implementing the action */ @@ -457,7 +478,7 @@ protected void postAction(TS /** * Sends a HTTP POST request with the given request body. - * + * * @param Type of service. * @param Type of the request to be sent. * @param service Service implementing the action diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java index b686acb5172fa..81dffc59f60c5 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandlerFactory.java @@ -26,6 +26,7 @@ import org.openhab.binding.boschshc.internal.devices.intrusion.IntrusionDetectionHandler; import org.openhab.binding.boschshc.internal.devices.lightcontrol.LightControlHandler; import org.openhab.binding.boschshc.internal.devices.motiondetector.MotionDetectorHandler; +import org.openhab.binding.boschshc.internal.devices.plug.PlugHandler; import org.openhab.binding.boschshc.internal.devices.shuttercontrol.ShutterControlHandler; import org.openhab.binding.boschshc.internal.devices.thermostat.ThermostatHandler; import org.openhab.binding.boschshc.internal.devices.twinguard.TwinguardHandler; @@ -47,7 +48,7 @@ * @author Stefan Kästle - Initial contribution * @author Christian Oeing - Added Shutter Control and ThermostatHandler; refactored handler mapping * @author Christian Oeing - Added WallThermostatHandler - * @author David Pace - Added cameras and intrusion detection system + * @author David Pace - Added cameras, intrusion detection system and smart plugs */ @NonNullByDefault @Component(configurationPid = "binding.boschshc", service = ThingHandlerFactory.class) @@ -75,7 +76,8 @@ public ThingTypeHandlerMapping(ThingTypeUID thingTypeUID, Function(state.powerConsumption, Units.WATT)); - super.updateState(CHANNEL_ENERGY_CONSUMPTION, - new QuantityType(state.energyConsumption, Units.WATT_HOUR)); - } - - /** - * Updates the channels which are linked to the {@link PowerSwitchService} of the device. - * - * @param state Current state of {@link PowerSwitchService}. - */ - private void updateChannels(PowerSwitchServiceState state) { - State powerState = OnOffType.from(state.switchState.toString()); - super.updateState(CHANNEL_POWER_SWITCH, powerState); - } - - private void updatePowerSwitchState(OnOffType command) { - PowerSwitchServiceState state = new PowerSwitchServiceState(); - state.switchState = PowerSwitchState.valueOf(command.toFullString()); - this.updateServiceState(this.powerSwitchService, state); } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java new file mode 100644 index 0000000000000..927e6789c9fa1 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandler.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.plug; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandler; +import org.openhab.core.thing.Thing; + +/** + * A handler for compact smart plugs. + * + * @author David Pace - Initial contribution + */ +@NonNullByDefault +public class PlugHandler extends AbstractPowerSwitchHandler { + + public PlugHandler(Thing thing) { + super(thing); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml index 34c55fc801c58..12c1dda89eacb 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -30,6 +30,24 @@ + + + + + + + A compact smart plug with energy monitoring capabilities. + + + + + + + + + + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java new file mode 100644 index 0000000000000..f5f9608c2555f --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBoschSHCDeviceHandlerTest.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import org.openhab.core.config.core.Configuration; + +/** + * Abstract unit test implementation for device handlers. + * + * @author David Pace - Initial contribution + * + * @param type of the device handler to be tested + */ +public abstract class AbstractBoschSHCDeviceHandlerTest + extends AbstractSHCHandlerTest { + + @Override + protected Configuration getConfiguration() { + Configuration configuration = super.getConfiguration(); + configuration.put("id", getDeviceID()); + return configuration; + } + + protected abstract String getDeviceID(); +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java new file mode 100644 index 0000000000000..38432d552aab4 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import javax.measure.quantity.Energy; +import javax.measure.quantity.Power; + +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.powerswitch.PowerSwitchState; +import org.openhab.binding.boschshc.internal.services.powerswitch.dto.PowerSwitchServiceState; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * Abstract unit test implementation for devices with power switches and energy monitoring. + * + * @author David Pace - Initial contribution + * + * @param type of the handler to be tested + */ +public abstract class AbstractPowerSwitchHandlerTest + extends AbstractBoschSHCDeviceHandlerTest { + + @Captor + private ArgumentCaptor serviceStateCaptor; + + @Captor + private ArgumentCaptor> powerCaptor; + + @Captor + private ArgumentCaptor> energyCaptor; + + @Test + public void testHandleCommand() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + + when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef")); + + getFixture().handleCommand(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), + OnOffType.ON); + verify(getBridgeHandler()).putState(eq(getDeviceID()), eq("PowerSwitch"), serviceStateCaptor.capture()); + PowerSwitchServiceState state = serviceStateCaptor.getValue(); + assertSame(PowerSwitchState.ON, state.switchState); + + getFixture().handleCommand(new ChannelUID(new ThingUID(getThingTypeUID(), "abcdef"), + BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF); + verify(getBridgeHandler(), times(2)).putState(eq(getDeviceID()), eq("PowerSwitch"), + serviceStateCaptor.capture()); + state = serviceStateCaptor.getValue(); + assertSame(PowerSwitchState.OFF, state.switchState); + } + + protected abstract ThingTypeUID getThingTypeUID(); + + @Test + public void testUpdateChannel_PowerSwitchState() { + when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef")); + + JsonElement jsonObject = JsonParser + .parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"ON\"\n" + "}"); + getFixture().processUpdate("PowerSwitch", jsonObject); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.ON); + + jsonObject = JsonParser + .parseString("{\n" + " \"@type\": \"powerSwitchState\",\n" + " \"switchState\": \"OFF\"\n" + "}"); + getFixture().processUpdate("PowerSwitch", jsonObject); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_SWITCH), OnOffType.OFF); + } + + @Test + public void testUpdateChannel_PowerMeterServiceState() { + when(getThing().getUID()).thenReturn(new ThingUID("boschshc", "abcdef")); + + JsonElement jsonObject = JsonParser.parseString("{\n" + " \"@type\": \"powerMeterState\",\n" + + " \"powerConsumption\": \"23\",\n" + " \"energyConsumption\": 42\n" + "}"); + getFixture().processUpdate("PowerMeter", jsonObject); + + verify(getCallback()).stateUpdated( + eq(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_POWER_CONSUMPTION)), + powerCaptor.capture()); + QuantityType powerValue = powerCaptor.getValue(); + assertEquals(23, powerValue.intValue()); + + verify(getCallback()).stateUpdated( + eq(new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_ENERGY_CONSUMPTION)), + energyCaptor.capture()); + QuantityType energyValue = energyCaptor.getValue(); + assertEquals(42, energyValue.intValue()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java new file mode 100644 index 0000000000000..872ca5336b8e4 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractSHCHandlerTest.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; + +/** + * Abstract unit test implementation for all types of handlers. + * + * @author David Pace - Initial contribution + * + * @param type of the handler to be tested + */ +@ExtendWith(MockitoExtension.class) +public abstract class AbstractSHCHandlerTest { + + private T fixture; + + @Mock + private Thing thing; + + @Mock + private Bridge bridge; + + @Mock + private BridgeHandler bridgeHandler; + + @Mock + private ThingHandlerCallback callback; + + @BeforeEach + public void beforeEach() { + fixture = createFixture(); + when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID")); + when(callback.getBridge(any())).thenReturn(bridge); + fixture.setCallback(callback); + when(bridge.getHandler()).thenReturn(bridgeHandler); + when(thing.getConfiguration()).thenReturn(getConfiguration()); + + fixture.initialize(); + } + + protected abstract T createFixture(); + + protected T getFixture() { + return fixture; + } + + protected Configuration getConfiguration() { + return new Configuration(); + } + + protected Thing getThing() { + return thing; + } + + public BridgeHandler getBridgeHandler() { + return bridgeHandler; + } + + public ThingHandlerCallback getCallback() { + return callback; + } + + @Test + public void testInitialize() { + ThingStatusInfo expectedStatusInfo = new ThingStatusInfo(ThingStatus.ONLINE, ThingStatusDetail.NONE, null); + verify(callback).statusUpdated(same(thing), eq(expectedStatusInfo)); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java new file mode 100644 index 0000000000000..94ae904ad400b --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/lightcontrol/LightControlHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.lightcontrol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit tests for {@link LightControlHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class LightControlHandlerTest extends AbstractPowerSwitchHandlerTest { + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_INWALL_SWITCH; + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:50325ffffe61d7b9c6e"; + } + + @Override + protected LightControlHandler createFixture() { + return new LightControlHandler(getThing()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java new file mode 100644 index 0000000000000..0c285912a8152 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/plug/PlugHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.plug; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractPowerSwitchHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit tests for {@link PlugHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class PlugHandlerTest extends AbstractPowerSwitchHandlerTest { + + @Override + protected PlugHandler createFixture() { + return new PlugHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:50325ffffe61d7b9c6e"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_SMART_PLUG_COMPACT; + } +} From ec90a091c657ae8edc7677cc7f497fa169713de6 Mon Sep 17 00:00:00 2001 From: Konstantin Polihronov Date: Mon, 31 Oct 2022 19:11:15 +0200 Subject: [PATCH 05/55] Fixes #13628 (#13629) * Change static config in bridge handler to non-static (fixes #13628) Signed-off-by: Konstantin Polihronov --- .../internal/handlers/ParadoxIP150BridgeHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java index 483c310aedbd8..9082112669780 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java @@ -70,7 +70,7 @@ public class ParadoxIP150BridgeHandler extends BaseBridgeHandler private IParadoxCommunicator communicator; - private static ParadoxIP150BridgeConfiguration config; + private ParadoxIP150BridgeConfiguration config; private @Nullable ScheduledFuture refreshCacheUpdateSchedule; private long timeStamp = 0; From b6c073d088032f0834aaac2e8768dc448acb720a Mon Sep 17 00:00:00 2001 From: lolodomo Date: Mon, 31 Oct 2022 19:57:46 +0100 Subject: [PATCH 06/55] [lgwebos] Actions: sendButton updated, sendRCButton removed, sendKeyboard added (#13618) * README: make the list of remote control buttons less specific to a model Fix #13600 Signed-off-by: Laurent Garnier --- bundles/org.openhab.binding.lgwebos/README.md | 31 +++++++-- .../internal/action/LGWebOSActions.java | 65 ++++++------------- .../handler/LGWebOSTVMouseSocket.java | 8 +++ .../resources/OH-INF/i18n/lgwebos.properties | 10 +-- 4 files changed, 58 insertions(+), 56 deletions(-) diff --git a/bundles/org.openhab.binding.lgwebos/README.md b/bundles/org.openhab.binding.lgwebos/README.md index 82e8ea929ddf4..ae1ef859ba3ed 100644 --- a/bundles/org.openhab.binding.lgwebos/README.md +++ b/bundles/org.openhab.binding.lgwebos/README.md @@ -70,8 +70,8 @@ Here are examples of values that could be available for your TV: airplay, amazon ### Remote Control Buttons -The rcButton channel has only been tested on an LGUJ657A TV. and this is a list of button codes that are known to work with this device. -This list has been compiled mostly through trial and error. Your mileage may vary. +This is a list of button codes that are known to work with several LG WebOS TV models. +This list has been compiled mostly through trial and error, but the codes applicable to your model may vary. | Code String | Description | |-------------|----------------------------------------------------------| @@ -314,14 +314,33 @@ Sends a button press event to a WebOS device. Parameters: -| Name | Description | -|---------|------------------------------------------------------------------------| -| button | Can be one of UP, DOWN, LEFT, RIGHT, BACK, DELETE, ENTER, HOME, or OK | +| Name | Description | +|---------|------------------------------------------------------------------------------------------------| +| button | Can be one of UP, DOWN, LEFT, RIGHT, BACK, EXIT, ENTER, HOME, OK or any other supported value. | Example: ``` -actions.sendButton("OK") +actions.sendButton("HOME") +``` + +### sendKeyboard(key) + +Sends a keyboard input to the WebOS on-screen keyboard. + +Parameters: + +| Name | Description | +|---------|--------------------------------| +| key | Can be either DELETE or ENTER. | + +DELETE will delete the last character when on-screen keyboard is displayed with focus in the text field. +ENTER will remove the keyboard when on-screen keyboard is displayed with focus in the text field. + +Example: + +``` +actions.sendKeyboard("ENTER") ``` ### increaseChannel() diff --git a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java index 33addc7b51935..d9af4fe8d5e2a 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java +++ b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/action/LGWebOSActions.java @@ -30,7 +30,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lgwebos.internal.handler.LGWebOSHandler; -import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVMouseSocket.ButtonType; import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket; import org.openhab.binding.lgwebos.internal.handler.LGWebOSTVSocket.State; import org.openhab.binding.lgwebos.internal.handler.command.ServiceSubscription; @@ -82,16 +81,9 @@ private LGWebOSHandler getLGWebOSHandler() { return lgWebOSHandler; } - private enum Button { - UP, - DOWN, - LEFT, - RIGHT, - BACK, + private enum Key { DELETE, - ENTER, - HOME, - OK + ENTER } @RuleAction(label = "@text/actionShowToastLabel", description = "@text/actionShowToastDesc") @@ -182,40 +174,29 @@ public void sendText( @RuleAction(label = "@text/actionSendButtonLabel", description = "@text/actionSendButtonDesc") public void sendButton( - @ActionInput(name = "text", label = "@text/actionSendButtonInputButtonLabel", description = "@text/actionSendButtonInputButtonDesc") String button) { + @ActionInput(name = "button", label = "@text/actionSendButtonInputButtonLabel", description = "@text/actionSendButtonInputButtonDesc") String button) { + if ("OK".equals(button)) { + getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.click())); + } else { + getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(button))); + } + } + + @RuleAction(label = "@text/actionSendKeyboardLabel", description = "@text/actionSendKeyboardDesc") + public void sendKeyboard( + @ActionInput(name = "key", label = "@text/actionSendKeyboardInputKeyLabel", description = "@text/actionSendKeyboardInputKeyDesc") String key) { try { - switch (Button.valueOf(button)) { - case UP: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.UP))); - break; - case DOWN: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.DOWN))); - break; - case LEFT: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.LEFT))); - break; - case RIGHT: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.RIGHT))); - break; - case BACK: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(ButtonType.BACK))); - break; + switch (Key.valueOf(key)) { case DELETE: getConnectedSocket().ifPresent(control -> control.sendDelete()); break; case ENTER: getConnectedSocket().ifPresent(control -> control.sendEnter()); break; - case HOME: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button("HOME"))); - break; - case OK: - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.click())); - break; } } catch (IllegalArgumentException ex) { - logger.warn("{} is not a valid value for button - available are: {}", button, - Stream.of(Button.values()).map(b -> b.name()).collect(Collectors.joining(", "))); + logger.warn("{} is not a valid value for key - available are: {}", key, + Stream.of(Key.values()).map(b -> b.name()).collect(Collectors.joining(", "))); } } @@ -229,12 +210,6 @@ public void decreaseChannel() { getConnectedSocket().ifPresent(control -> control.channelDown(createResponseListener())); } - @RuleAction(label = "@text/actionSendRCButtonLabel", description = "@text/actionSendRCButtonDesc") - public void sendRCButton( - @ActionInput(name = "text", label = "@text/actionSendRCButtonInputTextLabel", description = "@text/actionSendRCButtonInputTextDesc") String rcButton) { - getConnectedSocket().ifPresent(control -> control.executeMouse(s -> s.button(rcButton))); - } - private Optional getConnectedSocket() { LGWebOSHandler lgWebOSHandler = getLGWebOSHandler(); final LGWebOSTVSocket socket = lgWebOSHandler.getSocket(); @@ -307,6 +282,10 @@ public static void sendButton(ThingActions actions, String button) { ((LGWebOSActions) actions).sendButton(button); } + public static void sendKeyboard(ThingActions actions, String key) { + ((LGWebOSActions) actions).sendKeyboard(key); + } + public static void increaseChannel(ThingActions actions) { ((LGWebOSActions) actions).increaseChannel(); } @@ -314,8 +293,4 @@ public static void increaseChannel(ThingActions actions) { public static void decreaseChannel(ThingActions actions) { ((LGWebOSActions) actions).decreaseChannel(); } - - public static void sendRCButton(ThingActions actions, String rcButton) { - ((LGWebOSActions) actions).sendRCButton(rcButton); - } } diff --git a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/handler/LGWebOSTVMouseSocket.java b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/handler/LGWebOSTVMouseSocket.java index bc8462f77f853..356164cdb15f5 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/handler/LGWebOSTVMouseSocket.java +++ b/bundles/org.openhab.binding.lgwebos/src/main/java/org/openhab/binding/lgwebos/internal/handler/LGWebOSTVMouseSocket.java @@ -48,7 +48,9 @@ public enum State { public enum ButtonType { HOME, + ENTER, BACK, + EXIT, UP, DOWN, LEFT, @@ -164,9 +166,15 @@ public void button(ButtonType type) { case HOME: keyName = "HOME"; break; + case ENTER: + keyName = "ENTER"; + break; case BACK: keyName = "BACK"; break; + case EXIT: + keyName = "EXIT"; + break; case UP: keyName = "UP"; break; diff --git a/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos.properties b/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos.properties index dca1a52701dd6..361f4d17ce974 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos.properties +++ b/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos.properties @@ -51,11 +51,11 @@ actionLaunchBrowserInputUrlDesc = The URL to open actionSendButtonLabel = send a button press actionSendButtonDesc = Sends a button press event to a WebOS device. actionSendButtonInputButtonLabel = Button -actionSendButtonInputButtonDesc = Can be one of UP, DOWN, LEFT, RIGHT, BACK, DELETE, ENTER, HOME, or OK -actionSendRCButtonLabel = simulate remote control button press -actionSendRCButtonDesc = Simulates pressing of a Remote Control Button. -actionSendRCButtonInputTextLabel = Remote Control button name -actionSendRCButtonInputTextDesc = The Remote Control button name to send to the WebOS device. +actionSendButtonInputButtonDesc = Can be one of UP, DOWN, LEFT, RIGHT, BACK, EXIT, ENTER, HOME, OK or any other supported value. +actionSendKeyboardLabel = send a keyboard input +actionSendKeyboardDesc = Sends a keyboard input to the WebOS on-screen keyboard. +actionSendKeyboardInputKeyLabel = Key +actionSendKeyboardInputKeyDesc = Can be either DELETE or ENTER. actionSendTextLabel = send a text input actionSendTextDesc = Sends a text input to a WebOS device. actionSendTextInputTextLabel = Text From becb30187e26de0b7cbd999a52d1a7cfdd52805c Mon Sep 17 00:00:00 2001 From: lolodomo Date: Mon, 31 Oct 2022 20:55:26 +0100 Subject: [PATCH 07/55] [netatmo] No restriction on max value for rain quantity (#13620) * [netatmo] No max value for rain quantity Fix #13619 Signed-off-by: Laurent Garnier --- .../binding/netatmo/internal/api/data/NetatmoConstants.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/NetatmoConstants.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/NetatmoConstants.java index 6d10366966f3a..d22bb31ed706a 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/NetatmoConstants.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/NetatmoConstants.java @@ -84,7 +84,7 @@ public enum MeasureClass { PRESSURE(260, 1260, 0.1, HECTO(SIUnits.PASCAL), "pressure", "measure", true), CO2(0, 5000, 50, Units.PARTS_PER_MILLION, "co2", "measure", true), NOISE(35, 120, 1, Units.DECIBEL, "noise", "measure", true), - RAIN_QUANTITY(0, 150, 0.1, MILLI(SIUnits.METRE), "sum_rain", "sum_rain", false), + RAIN_QUANTITY(0, Double.MAX_VALUE, 0.1, MILLI(SIUnits.METRE), "sum_rain", "sum_rain", false), RAIN_INTENSITY(0, 150, 0.1, Units.MILLIMETRE_PER_HOUR, "", "", false), WIND_SPEED(0, 160, 1.8, SIUnits.KILOMETRE_PER_HOUR, "", "", false), WIND_ANGLE(0, 360, 5, Units.DEGREE_ANGLE, "", "", false), From 813489c2de6f519f7c350031522de3f2948c8bf1 Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Tue, 1 Nov 2022 10:55:11 +0100 Subject: [PATCH 08/55] New translations hue.properties (German) (#13634) --- .../main/resources/OH-INF/i18n/hue_de.properties | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties index 350c6bc7513ee..da6c8a2ae4168 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_de.properties @@ -3,11 +3,6 @@ binding.hue.name = Hue Binding binding.hue.description = Dieses Binding integriert das Philips Hue System. Durch diese können die Hue Lampen und Leuchten gesteuert werden. -# binding config - -binding.config.hue.removalGracePeriod.label = Gnadenfrist -binding.config.hue.removalGracePeriod.description = Zusätzliche Gnadenfrist (in Sekunden), die UPnP-Discovery abwarten soll, bevor eine verlorene Bridge aus dem Posteingang entfernt wird. Standard sind 50 Sekunden. - # thing types thing-type.hue.0000.label = Lampe (weiß) @@ -53,8 +48,14 @@ thing-type.config.hue.bridge.pollingInterval.label = Abfrageintervall thing-type.config.hue.bridge.pollingInterval.description = Intervall zur Abfrage der Hue Bridge (in Sekunden). thing-type.config.hue.bridge.port.label = Port thing-type.config.hue.bridge.port.description = Port der Hue Bridge. +thing-type.config.hue.bridge.protocol.label = Protokoll +thing-type.config.hue.bridge.protocol.description = Protokoll für den Verbindungsaufbau zur Hue Bridge (HTTP oder HTTPS). +thing-type.config.hue.bridge.protocol.option.http = HTTP +thing-type.config.hue.bridge.protocol.option.https = HTTPS thing-type.config.hue.bridge.sensorPollingInterval.label = Sensor-Abfrageintervall thing-type.config.hue.bridge.sensorPollingInterval.description = Intervall zur Abfrage der Sensoren der Hue Bridge (in Millisekunden). +thing-type.config.hue.bridge.useSelfSignedCertificate.label = Selbstsigniertes Zertifikat Verwenden +thing-type.config.hue.bridge.useSelfSignedCertificate.description = Selbstsigniertes Zertifikat für die HTTPS-Verbindung zu Hue Bridge verwenden. thing-type.config.hue.bridge.userName.label = Benutzername thing-type.config.hue.bridge.userName.description = Benutzername zur Authentifizierung an der Hue Bridge. thing-type.config.hue.group.groupId.label = ID der Gruppe @@ -146,12 +147,15 @@ config-status.error.missing-ip-address-configuration = Es wurde keine IP-Adresse # thing status descriptions +offline.communication-error = Ein unerwarteter Fehler ist während der Aktualisierung aufgetreten. +offline.conf-error-https-connection = HTTPS-Verbindung fehlgeschlagen. Bitte Einstellungen (z.B. IP-Adresse, Protokoll, Port, Zertifikatstyp) prüfen oder bei Verwendung einer V1 Hue Bridge das Protokoll auf HTTP anpassen. +offline.conf-error-invalid-ssl-certificate = Ungültiges Zertifikat für die HTTPS-Verbindung. Ggf. die Einstellung "Selbstsigniertes Zertifikat Verwenden" aktivieren. offline.conf-error-no-ip-address = Verbindung zur Hue Bridge kann nicht aufgebaut werden. Es wurde keine IP-Adresse angegeben. offline.conf-error-no-username = Verbindung zur Hue Bridge kann nicht aufgebaut werden. Es wurde kein Benutzername angegeben. offline.conf-error-invalid-username = Fehler bei der Authentifizierung. Entfernen Sie den Benutzernamen zur automatischen Generierung eines neuen Benutzernamens über die Hue Bridge. offline.conf-error-press-pairing-button = Bitte drücken Sie den Knopf an der Hue Bridge zur automatischen Generierung eines neuen Benutzernamens. offline.conf-error-creation-username = Fehler bei der automatischen Generierung eines neuen Benutzernamens. -offline.bridge-connection-lost = Verbindung zur Hue Bridge unterbrochen. +offline.bridge-connection-lost = Verbindung zur Hue Bridge unterbrochen. offline.conf-error-no-light-id = Es wurde keine ID der Lampe angegeben. offline.conf-error-no-sensor-id = Es wurde keine ID des Sensors angegeben. offline.conf-error-no-group-id = Es wurde keine ID der Gruppe angegeben. From 33de7b426bc22c5093ff29a67d3b836d7f096500 Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Tue, 1 Nov 2022 14:03:15 +0100 Subject: [PATCH 09/55] New Crowdin updates (#13635) * New translations hue.properties (French) * New translations lgwebos.properties (French) * New translations lgwebos.properties (French) --- .../resources/OH-INF/i18n/hue_fr.properties | 56 ++++++++++--------- .../OH-INF/i18n/lgwebos_fr.properties | 10 ++-- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_fr.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_fr.properties index d2648e712d1d2..26108a51ed66c 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_fr.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue_fr.properties @@ -3,11 +3,6 @@ binding.hue.name = Extension hue binding.hue.description = L'extension hue intègre le système Philips hue et permet de contrôler des ampoules hue. -# binding config - -binding.config.hue.removalGracePeriod.label = Période de grâce pour la suppression -binding.config.hue.removalGracePeriod.description = Période de grâce supplémentaire (en secondes) que la découverte UPnP doit attendre avant de retirer un pont disparu de la boîte de réception. La valeur par défaut est de 50 secondes. - # thing types thing-type.hue.0000.label = Ampoule Sans Variation @@ -48,17 +43,23 @@ thing-type.hue.group.description = Un groupe d'ampoules ou une pièce pouvant ê # thing types config thing-type.config.hue.bridge.ipAddress.label = Adresse réseau -thing-type.config.hue.bridge.ipAddress.description = L'adresse réseau du pont de connexion hue. +thing-type.config.hue.bridge.ipAddress.description = Adresse réseau du pont Hue. thing-type.config.hue.bridge.pollingInterval.label = Intervalle d'interrogation -thing-type.config.hue.bridge.pollingInterval.description = Le nombre de secondes entre chaque récupération des valeurs du pont. +thing-type.config.hue.bridge.pollingInterval.description = Nombre de secondes entre chaque récupération des valeurs du pont Hue. 10 par défaut. thing-type.config.hue.bridge.port.label = Port -thing-type.config.hue.bridge.port.description = Port du pont de connexion hue. +thing-type.config.hue.bridge.port.description = Port du pont Hue. +thing-type.config.hue.bridge.protocol.label = Protocole +thing-type.config.hue.bridge.protocol.description = Protocole pour se connecter au pont Hue (http ou https). +thing-type.config.hue.bridge.protocol.option.http = HTTP +thing-type.config.hue.bridge.protocol.option.https = HTTPS thing-type.config.hue.bridge.sensorPollingInterval.label = Intervalle d'interrogation du capteur -thing-type.config.hue.bridge.sensorPollingInterval.description = Millisecondes entre chaque récupération des valeurs de capteur depuis le pont de connexion Hue. Une valeur plus élevée signifie plus de délai entre chaque récupération des valeurs de capteur, mais une valeur trop faible peut causer une congestion sur le pont de connexion Hue. Utilisez 0 pour désactiver l'interrogation des capteurs. La valeur par défaut est 500. +thing-type.config.hue.bridge.sensorPollingInterval.description = Nombre de millisecondes entre chaque récupération des valeurs de capteur depuis le pont Hue. Une valeur plus élevée signifie davantage de temps entre deux valeurs de capteur, mais une valeur trop faible peut causer une congestion sur le pont Hue. Utilisez 0 pour désactiver l'interrogation des capteurs. La valeur par défaut est 500. +thing-type.config.hue.bridge.useSelfSignedCertificate.label = Utiliser un certificat auto-signé +thing-type.config.hue.bridge.useSelfSignedCertificate.description = Utiliser un certificat auto-signé pour la connexion HTTPS au pont Hue. thing-type.config.hue.bridge.userName.label = Nom d'utilisateur -thing-type.config.hue.bridge.userName.description = Le nom d'un utilisateur enregistré sur le pont de connexion hue, autorisant un accès à l'API. +thing-type.config.hue.bridge.userName.description = Nom d'un utilisateur enregistré du pont Hue, permettant d'accéder à l'API. thing-type.config.hue.group.groupId.label = ID du groupe -thing-type.config.hue.group.groupId.description = L'identifiant de groupe identifie l'un des groupes d'ampoules hue ou une pièce. +thing-type.config.hue.group.groupId.description = L'identifiant du groupe détermine un certain groupe Hue ou une certaine pièce. thing-type.config.hue.lightlevelsensor.tholddark.label = Seuil d'obscurité thing-type.config.hue.lightlevelsensor.tholddark.description = Seuil configuré par l'utilisateur utilisé dans les règles pour déterminer le niveau insuffisant de lumière (c'est-à-dire en dessous du seuil). Valeur par défaut 16000. thing-type.config.hue.lightlevelsensor.tholdoffset.label = Décalage de seuil @@ -132,37 +133,40 @@ config.fadetime.description = La durée de fondu en millisecondes lors du change config.ledindication.label = Témoin LED config.ledindication.description = Active ou désactive la LED de l'appareil pendant le fonctionnement normal. Les appareils peuvent toujours indiquer un fonctionnement exceptionnel (réinitialisation, mise à jour du logiciel, batterie faible). config.lightId.label = ID de l'ampoule -config.lightId.description = L'identifiant d'ampoule qui est utilisé dans le pont de connexion Hue. +config.lightId.description = L'identifiant d'ampoule qui est utilisé dans le pont Hue. config.plugId.label = ID de la prise -config.plugId.description = L'identifiant de prise commandée qui est utilisé dans le pont de connexion Hue. +config.plugId.description = L'identifiant de prise qui est utilisé dans le pont Hue. config.sensorId.label = ID du capteur -config.sensorId.description = L'identifiant de capteur qui est utilisé dans le pont de connexion Hue. +config.sensorId.description = L'identifiant de capteur qui est utilisé dans le pont Hue. config.on.label = État du capteur config.on.description = Active ou désactive le capteur. # config status messages -config-status.error.missing-ip-address-configuration = Aucune adresse IP fournie pour le pont de connexion hue. +config-status.error.missing-ip-address-configuration = Aucune adresse IP n'a été fournie pour le pont Hue. # thing status descriptions +offline.communication-error = Une exception inattendue s'est produite pendant l'exécution. +offline.conf-error-https-connection = Échec de la connexion sécurisée HTTPS. Veuillez vérifier vos paramètres de configuration (adresse réseau, protocole, port, type de certificat) et changer le protocole pour http si vous possédez un pont V1. +offline.conf-error-invalid-ssl-certificate = Certificat invalide pour la connexion sécurisée. Vous pourriez souhaiter activer le paramètre "Utiliser un certificat auto-signé". offline.conf-error-no-ip-address = Échec de la connexion au pont hue. Adresse IP non renseignée dans la configuration. offline.conf-error-no-username = Échec de la connexion au pont hue. Nom d'utilisateur pour l’authentification non renseigné dans la configuration. offline.conf-error-invalid-username = Échec de l’authentification. Supprimer le nom d'utilisateur de la configuration pour en générer un nouveau. -offline.conf-error-press-pairing-button = Non authentifié. Appuyer sur le bouton d'appairage du pont de connexion hue ou définir un nom valide d'utilisateur dans la configuration. -offline.conf-error-creation-username = Échec de la création du nouvel utilisateur sur le pont de connexion hue. -offline.bridge-connection-lost = Perte de la connexion au pont hue. +offline.conf-error-press-pairing-button = Non authentifié. Appuyez sur le bouton d'appairage sur le pont Hue ou définissez un nom d'utilisateur valide dans la configuration. +offline.conf-error-creation-username = Impossible de créer un nouvel utilisateur sur le pont Hue. +offline.bridge-connection-lost = Connexion au pont Hue perdue. offline.conf-error-no-light-id = ID ampoule non renseigné dans la configuration. offline.conf-error-no-sensor-id = ID capteur non renseigné dans la configuration. offline.conf-error-no-group-id = ID groupe non renseigné dans la configuration. -offline.conf-error-wrong-light-id = Pas d'ampoule avec cet ID dans le pont de connexion hue. -offline.conf-error-wrong-sensor-id = Pas de capteur avec cet ID dans le pont de connexion hue. -offline.conf-error-wrong-group-id = Pas de groupe avec cet ID dans le pont de connexion hue. -offline.light-not-reachable = Le pont de connexion hue signale l'ampoule comme inaccessible. -offline.sensor-not-reachable = Le pont de connexion hue signale le capteur comme inaccessible. -offline.light-removed = Le pont de connexion hue signale l'ampoule comme supprimée. -offline.sensor-removed = Le pont de connexion hue signale le capteur comme supprimé. -offline.group-removed = Le pont de connexion hue signale le groupe comme supprimé. +offline.conf-error-wrong-light-id = Pas d'ampoule avec cet ID dans le pont hue. +offline.conf-error-wrong-sensor-id = Pas de capteur avec cet ID dans le pont hue. +offline.conf-error-wrong-group-id = Pas de groupe avec cet ID dans le pont hue. +offline.light-not-reachable = Le pont hue signale l'ampoule comme inaccessible. +offline.sensor-not-reachable = Le pont hue signale le capteur comme inaccessible. +offline.light-removed = Le pont hue signale l'ampoule comme supprimée. +offline.sensor-removed = Le pont hue signale le capteur comme supprimé. +offline.group-removed = Le pont hue signale le groupe comme supprimé. # lightactions diff --git a/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos_fr.properties b/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos_fr.properties index ff61c4a572f65..45a5c7695c4f3 100644 --- a/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos_fr.properties +++ b/bundles/org.openhab.binding.lgwebos/src/main/resources/OH-INF/i18n/lgwebos_fr.properties @@ -51,11 +51,11 @@ actionLaunchBrowserInputUrlDesc = L'URL à ouvrir actionSendButtonLabel = envoyer un appui de bouton actionSendButtonDesc = Envoie un événement d'appui de bouton à un appareil WebOS. actionSendButtonInputButtonLabel = Bouton -actionSendButtonInputButtonDesc = Peut être UP, DOWN, LEFT, RIGHT, BACK, DELETE, ENTER, HOME ou OK -actionSendRCButtonLabel = simuler l'appui sur un bouton de la télécommande -actionSendRCButtonDesc = Simule l'appui sur un bouton de la télécommande. -actionSendRCButtonInputTextLabel = Nom du bouton de la télécommande -actionSendRCButtonInputTextDesc = Le nom du bouton de la télécommande à envoyer à un appareil WebOS. +actionSendButtonInputButtonDesc = Une des valeurs parmi UP, DOWN, LEFT, RIGHT, BACK, EXIT, ENTER, HOME, OK ou toute autre valeur prise en charge. +actionSendKeyboardLabel = envoyer une entrée clavier +actionSendKeyboardDesc = Envoie une entrée au clavier WebOS affiché à l'écran. +actionSendKeyboardInputKeyLabel = Touche +actionSendKeyboardInputKeyDesc = Soit DELETE, soit ENTER. actionSendTextLabel = envoyer une saisie de texte actionSendTextDesc = Envoie une saisie de texte à un appareil WebOS. actionSendTextInputTextLabel = Texte From 894668ffce41bacbf95b9f0f2beb1431711d0380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= Date: Tue, 1 Nov 2022 08:06:58 -0500 Subject: [PATCH 10/55] [linuxinput] handle keys not known by libevdev (#13632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [linuxinput] handle keys not known by libevdev Previously if libevdev could not resolve a numeric event code to a symbolic name the name "null" was used. This is useless for the user and may lead to duplicate-channel errors if multiple unknown keys are encountered. Instead use the numeric code itself as channel name if no symbolic code could be determined. Reported-in: https://community.openhab.org/t/linuxinput-binding-and-mouse-capture/122612/8 Signed-off-by: Thomas Weißschuh * [linuxinput] add channel description Signed-off-by: Thomas Weißschuh --- bundles/org.openhab.binding.linuxinput/README.md | 7 +++++++ .../binding/linuxinput/internal/LinuxInputHandler.java | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.linuxinput/README.md b/bundles/org.openhab.binding.linuxinput/README.md index 5430a2c18d4a8..5723bab852766 100644 --- a/bundles/org.openhab.binding.linuxinput/README.md +++ b/bundles/org.openhab.binding.linuxinput/README.md @@ -81,3 +81,10 @@ The following happens when pressing and releasing a key: #### Rationale Channel states are updated first to allow rules triggered by channel triggers to access the new state. + +#### Channel names + +The binding tries to translate the numeric event codes to their symbolic names; `KEY_1`, `KEY_A`, `KEY_BACKSPACE` etc. + +If the currently installed version of libevdev does not know the symbolic name of a key, the numeric value is used. +Please note that future versions of libevdev may start translating the symbolic names. diff --git a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/LinuxInputHandler.java b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/LinuxInputHandler.java index 7b4e74b8bf218..7f8e40c893fb4 100644 --- a/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/LinuxInputHandler.java +++ b/bundles/org.openhab.binding.linuxinput/src/main/java/org/openhab/binding/linuxinput/internal/LinuxInputHandler.java @@ -95,9 +95,13 @@ boolean delayedSetup() throws IOException { EvdevDevice newDevice = new EvdevDevice(config.path); for (EvdevDevice.Key o : newDevice.enumerateKeys()) { String name = o.getName(); + if (name == null) { + name = Integer.toString(o.getCode()); + } Channel channel = ChannelBuilder .create(new ChannelUID(thing.getUID(), CHANNEL_GROUP_KEYPRESSES_ID, name), CoreItemFactory.CONTACT) - .withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).build(); + .withLabel(name).withType(CHANNEL_TYPE_KEY_PRESS).withDescription("Event Code " + o.getCode()) + .build(); channels.put(o.getCode(), channel); newChannels.add(channel); } From 7728bbc2323ff03d49292fa9dec45f4f2d1fc755 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 1 Nov 2022 15:30:29 +0100 Subject: [PATCH 11/55] [vigicrues] Fix few code analysis findings (#13639) Signed-off-by: Laurent Garnier --- .../vigicrues/internal/api/ApiHandler.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/api/ApiHandler.java b/bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/api/ApiHandler.java index f98567aa61085..5bd8585e9227c 100644 --- a/bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/api/ApiHandler.java +++ b/bundles/org.openhab.binding.vigicrues/src/main/java/org/openhab/binding/vigicrues/internal/api/ApiHandler.java @@ -71,28 +71,28 @@ private T execute(String url, Class responseType) throws VigiCruesExcepti } public InfoVigiCru getTronconStatus(String tronconId) throws VigiCruesException { - final String BASE_URL = "https://www.vigicrues.gouv.fr/services/1/InfoVigiCru.jsonld/?TypEntVigiCru=8&CdEntVigiCru=%s"; - return execute(String.format(BASE_URL, tronconId), InfoVigiCru.class); + final String baseUrl = "https://www.vigicrues.gouv.fr/services/1/InfoVigiCru.jsonld/?TypEntVigiCru=8&CdEntVigiCru=%s"; + return execute(String.format(baseUrl, tronconId), InfoVigiCru.class); } public TronEntVigiCru getTroncon(String stationId) throws VigiCruesException { - final String BASE_URL = "https://www.vigicrues.gouv.fr/services/1/TronEntVigiCru.jsonld/?TypEntVigiCru=8&CdEntVigiCru=%s"; - return execute(String.format(BASE_URL, stationId), TronEntVigiCru.class); + final String baseUrl = "https://www.vigicrues.gouv.fr/services/1/TronEntVigiCru.jsonld/?TypEntVigiCru=8&CdEntVigiCru=%s"; + return execute(String.format(baseUrl, stationId), TronEntVigiCru.class); } public TerEntVigiCru getTerritoire(String stationId) throws VigiCruesException { - final String BASE_URL = "https://www.vigicrues.gouv.fr/services/1/TerEntVigiCru.jsonld/?TypEntVigiCru=5&CdEntVigiCru=%s"; - return execute(String.format(BASE_URL, stationId), TerEntVigiCru.class); + final String baseUrl = "https://www.vigicrues.gouv.fr/services/1/TerEntVigiCru.jsonld/?TypEntVigiCru=5&CdEntVigiCru=%s"; + return execute(String.format(baseUrl, stationId), TerEntVigiCru.class); } public CdStationHydro getStationDetails(String stationId) throws VigiCruesException { - final String BASE_URL = "https://www.vigicrues.gouv.fr/services/station.json/index.php?CdStationHydro=%s"; - return execute(String.format(BASE_URL, stationId), CdStationHydro.class); + final String baseUrl = "https://www.vigicrues.gouv.fr/services/station.json/index.php?CdStationHydro=%s"; + return execute(String.format(baseUrl, stationId), CdStationHydro.class); } public OpenDatasoftResponse getMeasures(String stationId) throws VigiCruesException { - final String BASE_URL = "https://public.opendatasoft.com/api/records/1.0/search/?dataset=vigicrues&sort=timestamp&q=%s"; - return execute(String.format(BASE_URL, stationId), OpenDatasoftResponse.class); + final String baseUrl = "https://public.opendatasoft.com/api/records/1.0/search/?dataset=vigicrues&sort=timestamp&q=%s"; + return execute(String.format(baseUrl, stationId), OpenDatasoftResponse.class); } public HubEauResponse discoverStations(PointType location, int range) throws VigiCruesException { From aea2fd4002dbc964e44a02e7fa8f8be737f6aaec Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 1 Nov 2022 15:31:37 +0100 Subject: [PATCH 12/55] [lifx] Fix all code analysis findings (#13638) Signed-off-by: Laurent Garnier --- .../openhab/binding/lifx/internal/dto/HevCycleState.java | 5 +---- .../java/org/openhab/binding/lifx/internal/dto/Packet.java | 5 +---- .../java/org/openhab/binding/lifx/internal/fields/HSBK.java | 5 +---- .../openhab/binding/lifx/internal/fields/MACAddress.java | 6 +----- .../binding/lifx/internal/util/LifxThrottlingUtil.java | 4 ---- 5 files changed, 4 insertions(+), 21 deletions(-) diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/HevCycleState.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/HevCycleState.java index 4139dd027ddf9..02acc15259514 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/HevCycleState.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/HevCycleState.java @@ -73,10 +73,7 @@ public boolean equals(Object obj) { } else if (!duration.equals(other.duration)) { return false; } - if (enable != other.enable) { - return false; - } - return true; + return enable == other.enable; } @Override diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java index a8cd4307e7637..bf78f0b426d52 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/dto/Packet.java @@ -389,9 +389,6 @@ public boolean isExpectedResponse(int type) { } public boolean isFulfilled(Packet somePacket) { - if (isExpectedResponse(somePacket.getPacketType())) { - return true; - } - return false; + return isExpectedResponse(somePacket.getPacketType()); } } diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java index 9a6c24246b4b4..0a3d081c1aeed 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/HSBK.java @@ -126,10 +126,7 @@ public boolean equals(@Nullable Object obj) { if (brightness != other.brightness) { return false; } - if (kelvin != other.kelvin) { - return false; - } - return true; + return kelvin == other.kelvin; } @Override diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java index a5ad018037edc..312fb40ffe042 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/fields/MACAddress.java @@ -101,10 +101,6 @@ public boolean equals(@Nullable Object obj) { } final MACAddress other = (MACAddress) obj; - if (!this.hex.equalsIgnoreCase(other.hex)) { - return false; - } - - return true; + return this.hex.equalsIgnoreCase(other.hex); } } diff --git a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java index b55d81c1be8f3..248f74be64ba1 100644 --- a/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java +++ b/bundles/org.openhab.binding.lifx/src/main/java/org/openhab/binding/lifx/internal/util/LifxThrottlingUtil.java @@ -23,8 +23,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lifx.internal.fields.MACAddress; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link LifxThrottlingUtil} is a utility class that regulates the frequency at which messages/packets are @@ -37,8 +35,6 @@ @NonNullByDefault public final class LifxThrottlingUtil { - private static final Logger LOGGER = LoggerFactory.getLogger(LifxThrottlingUtil.class); - private LifxThrottlingUtil() { // hidden utility class constructor } From b85e9b43df95d250f493d1eb52a64bc5c15859dd Mon Sep 17 00:00:00 2001 From: lolodomo Date: Tue, 1 Nov 2022 15:33:54 +0100 Subject: [PATCH 13/55] [sagercaster] Fix all code analysis findings (#13637) Signed-off-by: Laurent Garnier --- bundles/org.openhab.binding.sagercaster/README.md | 3 +-- .../internal/SagerCasterBindingConstants.java | 4 ++-- .../internal/caster/SagerWeatherCaster.java | 10 +++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bundles/org.openhab.binding.sagercaster/README.md b/bundles/org.openhab.binding.sagercaster/README.md index c9eadf8a16753..3cc96bde95d0e 100644 --- a/bundles/org.openhab.binding.sagercaster/README.md +++ b/bundles/org.openhab.binding.sagercaster/README.md @@ -6,8 +6,7 @@ The Sager Weathercaster is a scientific instrument for accurate prediction of th * To operate, this binding will need to use channel values provided by other means (e.g. Weather Binding, Netatmo, a 1-Wire personal weather station...) -* This binding buffers readings for some hours before producing weather forecasts(wind direction and sea level pressure). -SagerWeatherCaster needs an observation period of minimum 6 hours. +* This binding buffers readings for some hours before producing weather forecasts (wind direction and sea level pressure). SagerWeatherCaster needs an observation period of minimum 6 hours. For these reasons, this binding is not a binding in the usual sense. diff --git a/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/SagerCasterBindingConstants.java b/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/SagerCasterBindingConstants.java index c5a55cc7e2715..a1cf0b6e743a8 100644 --- a/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/SagerCasterBindingConstants.java +++ b/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/SagerCasterBindingConstants.java @@ -60,6 +60,6 @@ public class SagerCasterBindingConstants { public static final String CHANNEL_WIND_ANGLE = "wind-angle"; // Some algorythms constants - public final static String FORECAST_PENDING = "0"; - public final static Set SHOWERS = Set.of("G", "K", "L", "R", "S", "T", "U", "W"); + public static final String FORECAST_PENDING = "0"; + public static final Set SHOWERS = Set.of("G", "K", "L", "R", "S", "T", "U", "W"); } diff --git a/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/caster/SagerWeatherCaster.java b/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/caster/SagerWeatherCaster.java index 75e2203498496..8437651d12521 100644 --- a/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/caster/SagerWeatherCaster.java +++ b/bundles/org.openhab.binding.sagercaster/src/main/java/org/openhab/binding/sagercaster/internal/caster/SagerWeatherCaster.java @@ -76,15 +76,15 @@ @Component(service = SagerWeatherCaster.class, scope = ServiceScope.SINGLETON) @NonNullByDefault public class SagerWeatherCaster { - public final static String UNDEF = "-"; + public static final String UNDEF = "-"; // Northern Polar Zone & Northern Tropical Zone - private final static String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" }; + private static final String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" }; // Northern Temperate Zone - private final static String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; + private static final String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; // Southern Polar Zone & Southern Tropical Zone - private final static String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" }; + private static final String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" }; // Southern Temperate Zone - private final static String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" }; + private static final String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" }; private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class); private final Properties forecaster = new Properties(); From 79bf9724fc159894be3a3a4e08347ac387d3eaeb Mon Sep 17 00:00:00 2001 From: lolodomo Date: Wed, 2 Nov 2022 08:16:25 +0100 Subject: [PATCH 14/55] [netatmo] eventBuffer in SecurityCapability should not be static (#13636) Also fix few findings by code analysis Signed-off-by: Laurent Garnier --- .../binding/netatmo/internal/deserialization/NAPushType.java | 2 +- .../netatmo/internal/handler/capability/SecurityCapability.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/NAPushType.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/NAPushType.java index cac492ec68f6a..8e6ebd7c2c719 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/NAPushType.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/NAPushType.java @@ -23,7 +23,7 @@ */ @NonNullByDefault public class NAPushType { - public final static NAPushType UNKNOWN = new NAPushType(ModuleType.UNKNOWN, EventType.UNKNOWN); + public static final NAPushType UNKNOWN = new NAPushType(ModuleType.UNKNOWN, EventType.UNKNOWN); private final ModuleType moduleType; private final EventType event; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SecurityCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SecurityCapability.java index d7c3a2469af54..164edae775f77 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SecurityCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SecurityCapability.java @@ -47,7 +47,7 @@ class SecurityCapability extends RestCapability { private final Logger logger = LoggerFactory.getLogger(SecurityCapability.class); - private static final Map eventBuffer = new HashMap<>(); + private final Map eventBuffer = new HashMap<>(); private @Nullable ZonedDateTime freshestEventTime; SecurityCapability(CommonInterface handler) { From d505c01a6fb2dec73973caee52b9fd2f2e5aa2a6 Mon Sep 17 00:00:00 2001 From: Andrea Cioni Date: Wed, 2 Nov 2022 08:20:58 +0100 Subject: [PATCH 15/55] Update README.md (#13642) `color_mode` is not recognized as a channel parameter, instead, `colorMode` is --- bundles/org.openhab.binding.mqtt.generic/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mqtt.generic/README.md b/bundles/org.openhab.binding.mqtt.generic/README.md index f010c8b146a3f..d87dc0d3abbb0 100644 --- a/bundles/org.openhab.binding.mqtt.generic/README.md +++ b/bundles/org.openhab.binding.mqtt.generic/README.md @@ -137,7 +137,7 @@ You can connect this channel to a Contact or Switch item. ### Channel Type "color" -* __color_mode__: An optional string that defines the color representation: `HSB`, `RGB` or `XYY` (x,y,brightness). Defaults to `HSB` when not specified. +* __colorMode__: An optional string that defines the color representation: `HSB`, `RGB` or `XYY` (x,y,brightness). Defaults to `HSB` when not specified. * __on__: An optional string (like "BRIGHT") that is recognized as on state. (ON will always be recognized.) * __off__: An optional string (like "DARK") that is recognized as off state. (OFF will always be recognized.) * __onBrightness__: If you connect this channel to a Switch item and turn it on, From 54ccf847dab8fd6f644f31f2b4962162d4fb65da Mon Sep 17 00:00:00 2001 From: Martin Herbst Date: Thu, 3 Nov 2022 19:21:13 +0100 Subject: [PATCH 16/55] Some HM devices are using relative humidity (% rH/rF) as units (#13626) Fix #13553 Signed-off-by: Martin Herbst --- .../internal/converter/type/QuantityTypeConverter.java | 2 ++ .../openhab/binding/homematic/internal/type/MetadataUtils.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/converter/type/QuantityTypeConverter.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/converter/type/QuantityTypeConverter.java index d1647c0315d0d..2e6ca6ae00d5e 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/converter/type/QuantityTypeConverter.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/converter/type/QuantityTypeConverter.java @@ -104,6 +104,8 @@ protected QuantityType> fromBinding(HmDatapoint dp) throws return new QuantityType<>(number, SIUnits.CELSIUS); case "V": return new QuantityType<>(number, Units.VOLT); + case "% rH": + case "% rF": case "%": return new QuantityType<>(number, Units.PERCENT); case "mHz": diff --git a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java index 8f62c70fcc25b..11e2481e01571 100644 --- a/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java +++ b/bundles/org.openhab.binding.homematic/src/main/java/org/openhab/binding/homematic/internal/type/MetadataUtils.java @@ -311,6 +311,8 @@ public static String getItemType(HmDatapoint dp) { case "V": return ITEM_TYPE_NUMBER + ":ElectricPotential"; case "100%": + case "% rH": + case "% rF": case "%": return ITEM_TYPE_NUMBER + ":Dimensionless"; case "mHz": From a4f6159f091324cdcd34d284a8bca40adb7474e1 Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Thu, 3 Nov 2022 22:16:07 +0100 Subject: [PATCH 17/55] [mercedesme] Fix unstable ConfigurationTest (#13650) * Make sure IPv4 is used with default callback URL * Use a more reasonable timeout Fixes #13531 Signed-off-by: Wouter Born --- .../openhab/binding/mercedesme/internal/server/Utils.java | 5 ++++- .../org/openhab/binding/mercedesme/ConfigurationTest.java | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java index 0c98f72ac6b13..c9545dfde17c5 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.mercedesme.internal.server; +import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; @@ -70,7 +71,9 @@ public static String getCallbackIP() throws SocketException { for (Enumeration addresses = iface.getInetAddresses(); addresses .hasMoreElements();) { InetAddress address = addresses.nextElement(); - return address.getHostAddress(); + if (address instanceof Inet4Address) { + return address.getHostAddress(); + } } } } diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java index d8361a7515a48..72714670684b0 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java @@ -62,10 +62,11 @@ void testRound() { @Test public void testCallbackUrl() throws SocketException { String ip = Utils.getCallbackIP(); + String message = "IP " + ip + " not reachable"; try { - assertTrue(InetAddress.getByName(ip).isReachable(10)); + assertTrue(InetAddress.getByName(ip).isReachable(10000), message); } catch (IOException e) { - assertTrue(false, "IP " + ip + " not reachable"); + fail(message); } } } From cf2a1afd569e350d448a7ecd169a063afc94b2be Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 4 Nov 2022 13:28:27 +0100 Subject: [PATCH 18/55] [systeminfo] dynamic channels (#13562) * Dynamic channels * Status messages i8n * Format fix * Cache process load values * Restore channel configs * Fix test * Stabilize tests * Fix CpuLoad1-5-15 update * Fix test bndrun * String equals cleanup * Fix potential null pointer in test Signed-off-by: Mark Herwege --- .../org.openhab.binding.systeminfo/README.md | 34 ++- .../internal/SysteminfoBindingConstants.java | 135 ++++++++- .../internal/SysteminfoHandlerFactory.java | 35 ++- .../internal/SysteminfoThingTypeProvider.java | 279 ++++++++++++++++++ .../internal/handler/SysteminfoHandler.java | 276 ++++++++++++++--- .../internal/model/OSHISysteminfo.java | 44 ++- .../internal/model/SysteminfoInterface.java | 54 +++- .../OH-INF/i18n/systeminfo.properties | 12 +- .../main/resources/OH-INF/thing/channels.xml | 34 ++- .../main/resources/OH-INF/thing/computer.xml | 2 +- .../itest.bndrun | 4 +- .../systeminfo/test/SysteminfoOSGiTest.java | 226 ++++++++------ 12 files changed, 975 insertions(+), 160 deletions(-) create mode 100644 bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java diff --git a/bundles/org.openhab.binding.systeminfo/README.md b/bundles/org.openhab.binding.systeminfo/README.md index 197482b835e74..500c631315753 100644 --- a/bundles/org.openhab.binding.systeminfo/README.md +++ b/bundles/org.openhab.binding.systeminfo/README.md @@ -36,9 +36,6 @@ The discovery service implementation tries to resolve the computer name. If the resolving process fails, the computer name is set to "Unknown". In both cases it creates a Discovery Result with thing type **computer**. -It will be possible to implement creation of dynamic channels (e.g. the binding will scan how many storage devices are present and create channel groups for them). -At the moment this is not supported. - ## Thing configuration The configuration of the Thing gives the user the possibility to update channels at different intervals. @@ -82,23 +79,34 @@ In the list below, you can find, how are channel group and channels id`s related * **channel** `cpuTemp, cpuVoltage, fanSpeed` * **group** `network` (deviceIndex) * **channel** `ip, mac, networkDisplayName, networkName, packetsSent, packetsReceived, dataSent, dataReceived` +* **group** `currentProcess` + * **channel** `load, used, name, threads, path` * **group** `process` (pid) * **channel** `load, used, name, threads, path` The groups marked with "(deviceIndex)" may have device index attached to the Channel Group. - channel ::= channel_group & (deviceIndex) & # channel_id -- deviceIndex ::= number > 0 +- deviceIndex ::= number >= 0 - (e.g. *storage1#available*) +The `fanSpeed` channel in the `sensors` group may have a device index attached to the Channel. + +- channel ::= channel_group & # channel_id & (deviceIndex) +- deviceIndex ::= number >= 0 + +Channels or channel groups without a trailing index will show the data for the first device (index 0) if multiple exist. +If only one device for a group exists, no channels or channel groups with indexes will be created. + The group `process` is using a configuration parameter "pid" instead of "deviceIndex". This makes it possible to change the tracked process at runtime. +The group `currentProcess` has the same channels as the `process` group without the "pid" configuration parameter. +The PID is dynamically set to the PID of the process running openHAB. + The binding uses this index to get information about a specific device from a list of devices (e.g on a single computer several local disks could be installed with names C:\, D:\, E:\ - the first will have deviceIndex=0, the second deviceIndex=1 etc). If device with this index is not existing, the binding will display an error message on the console. -Unfortunately this feature can't be used at the moment without manually adding these new channel groups to the thing description (located in OH-INF/thing/computer.xml). - The table shows more detailed information about each Channel type. The binding introduces the following channels: @@ -244,6 +252,13 @@ Number Sensor_CPUTemp "CPU Temperature" { chann Number Sensor_CPUVoltage "CPU Voltage" { channel="systeminfo:computer:work:sensors#cpuVoltage" } Number Sensor_FanSpeed "Fan speed" { channel="systeminfo:computer:work:sensors#fanSpeed" } +/* Current process information*/ +Number Current_process_load "Load" { channel="systeminfo:computer:work:currentProcess#load" } +Number Current_process_used "Used" { channel="systeminfo:computer:work:currentProcess#used" } +String Current_process_name "Name" { channel="systeminfo:computer:work:currentProcess#name" } +Number Current_process_threads "Threads" { channel="systeminfo:computer:work:currentProcess#threads" } +String Current_process_path "Path" { channel="systeminfo:computer:work:currentProcess#path" } + /* Process information*/ Number Process_load "Load" { channel="systeminfo:computer:work:process#load" } Number Process_used "Used" { channel="systeminfo:computer:work:process#used" } @@ -313,6 +328,13 @@ sitemap systeminfo label="Systeminfo" { Default item=Sensor_CPUVoltage Default item=Sensor_FanSpeed } + Frame label="Current Process Information" { + Default item=Current_process_load + Default item=Current_process_used + Default item=Current_process_name + Default item=Current_process_threads + Default item=Current_process_path + } Frame label="Process Information" { Default item=Process_load Default item=Process_used diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java index d88327fe3afd0..9412716252427 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoBindingConstants.java @@ -20,13 +20,15 @@ * used across the whole binding. * * @author Svilen Valkanov - Initial contribution + * @author Mark Herwege - Add dynamic creation of extra channels */ @NonNullByDefault public class SysteminfoBindingConstants { public static final String BINDING_ID = "systeminfo"; - public static final ThingTypeUID THING_TYPE_COMPUTER = new ThingTypeUID(BINDING_ID, "computer"); + public static final String THING_TYPE_COMPUTER_ID = "computer"; + public static final ThingTypeUID THING_TYPE_COMPUTER = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID); // Thing properties /** @@ -56,6 +58,16 @@ public class SysteminfoBindingConstants { // List of all Channel IDs + /** + * Name of the channel group type for memory information + */ + public static final String CHANNEL_GROUP_TYPE_MEMORY = "memoryGroup"; + + /** + * Name of the channel group for memory information + */ + public static final String CHANNEL_GROUP_MEMORY = "memory"; + /** * Size of the available memory */ @@ -91,6 +103,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_MEMORY_HEAP_AVAILABLE = "memory#availableHeap"; + /** + * Name of the channel group type for swap information + */ + public static final String CHANNEL_GROUP_TYPE_SWAP = "swapGroup"; + + /** + * Name of the channel group for swap information + */ + public static final String CHANNEL_GROUP_SWAP = "swap"; + /** * Total size of swap memory */ @@ -116,6 +138,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_SWAP_USED_PERCENT = "swap#usedPercent"; + /** + * Name of the channel group type for drive information + */ + public static final String CHANNEL_GROUP_TYPE_DRIVE = "driveGroup"; + + /** + * Name of the channel group for drive information + */ + public static final String CHANNEL_GROUP_DRIVE = "drive"; + /** * Physical storage drive name */ @@ -131,6 +163,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_DRIVE_SERIAL = "drive#serial"; + /** + * Name of the channel group type for storage information + */ + public static final String CHANNEL_GROUP_TYPE_STORAGE = "storageGroup"; + + /** + * Name of the channel group for storage information + */ + public static final String CHANNEL_GROUP_STORAGE = "storage"; + /** * Name of the logical volume storage */ @@ -171,6 +213,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_STORAGE_USED_PERCENT = "storage#usedPercent"; + /** + * Name of the channel group type for sensors information + */ + public static final String CHANNEL_GROUP_TYPE_SENSORS = "sensorsGroup"; + + /** + * Name of the channel group for sensors information + */ + public static final String CHANNEL_GROUP_SENSORS = "sensors"; + /** * Temperature of the CPU measured from the sensors. */ @@ -186,6 +238,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_SENSORS_FAN_SPEED = "sensors#fanSpeed"; + /** + * Name of the channel group type for battery information + */ + public static final String CHANNEL_GROUP_TYPE_BATTERY = "batteryGroup"; + + /** + * Name of the channel group for battery information + */ + public static final String CHANNEL_GROUP_BATTERY = "battery"; + /** * Name of the battery */ @@ -201,6 +263,16 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_BATTERY_REMAINING_TIME = "battery#remainingTime"; + /** + * Name of the channel group type for CPU information + */ + public static final String CHANNEL_GROUP_TYPE_CPU = "cpuGroup"; + + /** + * Name of the channel group for CPU information + */ + public static final String CHANNEL_GROUP_CPU = "cpu"; + /** * Detailed description about the CPU */ @@ -241,11 +313,31 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_CPU_THREADS = "cpu#threads"; + /** + * Name of the channel group type for display information + */ + public static final String CHANNEL_GROUP_TYPE_DISPLAY = "displayGroup"; + + /** + * Name of the channel group for display information + */ + public static final String CHANNEL_GROUP_DISPLAY = "display"; + /** * Information about the display device */ public static final String CHANNEL_DISPLAY_INFORMATION = "display#information"; + /** + * Name of the channel group type for network information + */ + public static final String CHANNEL_GROUP_TYPE_NETWORK = "networkGroup"; + + /** + * Name of the channel group for network information + */ + public static final String CHANNEL_GROUP_NETWORK = "network"; + /** * Host IP address of the network */ @@ -286,6 +378,47 @@ public class SysteminfoBindingConstants { */ public static final String CHANNEL_NETWORK_MAC = "network#mac"; + /** + * Name of the channel group type for process information + */ + public static final String CHANNEL_GROUP_TYPE_CURRENT_PROCESS = "currentProcessGroup"; + + /** + * Name of the channel group for process information + */ + public static final String CHANNEL_GROUP_CURRENT_PROCESS = "currentProcess"; + + /** + * CPU load used from a process + */ + + public static final String CHANNEL_CURRENT_PROCESS_LOAD = "currentProcess#load"; + + /** + * Size of memory used from a process in MB + */ + public static final String CHANNEL_CURRENT_PROCESS_MEMORY = "currentProcess#used"; + + /** + * Name of the process + */ + public static final String CHANNEL_CURRENT_PROCESS_NAME = "currentProcess#name"; + + /** + * Number of threads, used form the process + */ + public static final String CHANNEL_CURRENT_PROCESS_THREADS = "currentProcess#threads"; + + /** + * The full path of the process + */ + public static final String CHANNEL_CURRENT_PROCESS_PATH = "currentProcess#path"; + + /** + * Name of the channel group type for process information + */ + public static final String CHANNEL_GROUP_TYPE_PROCESS = "processGroup"; + /** * Name of the channel group for process information */ diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java index efc75acc58e0a..6b88bff8e2fed 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoHandlerFactory.java @@ -12,10 +12,7 @@ */ package org.openhab.binding.systeminfo.internal; -import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.THING_TYPE_COMPUTER; - -import java.util.Collections; -import java.util.Set; +import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -36,28 +33,33 @@ * @author Svilen Valkanov - Initial contribution * @author Lyubomir Papazov - Pass systeminfo service to the SysteminfoHandler constructor * @author Wouter Born - Add null annotations + * @author Mark Herwege - Add dynamic creation of extra channels */ @NonNullByDefault @Component(service = ThingHandlerFactory.class, configurationPid = "binding.systeminfo") public class SysteminfoHandlerFactory extends BaseThingHandlerFactory { - - private static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_COMPUTER); - private @NonNullByDefault({}) SysteminfoInterface systeminfo; + private @NonNullByDefault({}) SysteminfoThingTypeProvider thingTypeProvider; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + return BINDING_ID.equals(thingTypeUID.getBindingId()) + && thingTypeUID.getId().startsWith(THING_TYPE_COMPUTER_ID); } @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); - - if (thingTypeUID.equals(THING_TYPE_COMPUTER)) { - return new SysteminfoHandler(thing, systeminfo); + if (supportsThingType(thingTypeUID)) { + String extString = "-" + thing.getUID().getId(); + ThingTypeUID extThingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID + extString); + if (thingTypeProvider.getThingType(extThingTypeUID, null) == null) { + thingTypeProvider.createThingType(extThingTypeUID); + thingTypeProvider.storeChannelsConfig(thing); // Save the current channels configs, will be restored + // after thing type change. + } + return new SysteminfoHandler(thing, thingTypeProvider, systeminfo); } - return null; } @@ -69,4 +71,13 @@ public void bindSystemInfo(SysteminfoInterface systeminfo) { public void unbindSystemInfo(SysteminfoInterface systeminfo) { this.systeminfo = null; } + + @Reference + public void setSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) { + this.thingTypeProvider = thingTypeProvider; + } + + public void unsetSysteminfoThingTypeProvider(SysteminfoThingTypeProvider thingTypeProvider) { + this.thingTypeProvider = null; + } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java new file mode 100644 index 0000000000000..61cd3aad7dd0c --- /dev/null +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/SysteminfoThingTypeProvider.java @@ -0,0 +1,279 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.systeminfo.internal; + +import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.ThingTypeProvider; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; +import org.openhab.core.thing.type.ChannelGroupType; +import org.openhab.core.thing.type.ChannelGroupTypeRegistry; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelType; +import org.openhab.core.thing.type.ChannelTypeRegistry; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.thing.type.ThingType; +import org.openhab.core.thing.type.ThingTypeBuilder; +import org.openhab.core.thing.type.ThingTypeRegistry; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Extended channels can be auto discovered and added to newly created groups in the {@link SystemInfoHandler}. The + * thing needs to be updated to add the groups. The `SysteminfoThingTypeProvider` OSGi service gives access to the + * `ThingTypeRegistry` and serves the updated `ThingType`. + * + * @author Mark Herwege - Initial contribution + * + */ +@NonNullByDefault +@Component(service = { SysteminfoThingTypeProvider.class, ThingTypeProvider.class }) +public class SysteminfoThingTypeProvider implements ThingTypeProvider { + private final Logger logger = LoggerFactory.getLogger(SysteminfoThingTypeProvider.class); + + private final ThingTypeRegistry thingTypeRegistry; + private final ChannelGroupTypeRegistry channelGroupTypeRegistry; + private final ChannelTypeRegistry channelTypeRegistry; + + private final Map thingTypes = new HashMap<>(); + + private final Map> thingChannelsConfig = new HashMap<>(); + + @Activate + public SysteminfoThingTypeProvider(@Reference ThingTypeRegistry thingTypeRegistry, + @Reference ChannelGroupTypeRegistry channelGroupTypeRegistry, + @Reference ChannelTypeRegistry channelTypeRegistry) { + super(); + this.thingTypeRegistry = thingTypeRegistry; + this.channelGroupTypeRegistry = channelGroupTypeRegistry; + this.channelTypeRegistry = channelTypeRegistry; + } + + @Override + public Collection getThingTypes(@Nullable Locale locale) { + return thingTypes.values(); + } + + @Override + public @Nullable ThingType getThingType(ThingTypeUID thingTypeUID, @Nullable Locale locale) { + return thingTypes.get(thingTypeUID); + } + + private void setThingType(ThingTypeUID uid, ThingType type) { + thingTypes.put(uid, type); + } + + /** + * Create thing type with the provided typeUID and add it to the thing type registry. + * + * @param typeUID + * @return false if base type UID `systeminfo:computer` cannot be found in the thingTypeRegistry + */ + public boolean createThingType(ThingTypeUID typeUID) { + logger.trace("Creating thing type {}", typeUID); + return updateThingType(typeUID, getChannelGroupDefinitions(typeUID)); + } + + /** + * Update `ThingType`with `typeUID`, replacing the channel group definitions with `groupDefs`. + * + * @param typeUID + * @param groupDefs + * @return false if `typeUID` or its base type UID `systeminfo:computer` cannot be found in the thingTypeRegistry + */ + public boolean updateThingType(ThingTypeUID typeUID, List groupDefs) { + ThingTypeUID baseTypeUID = THING_TYPE_COMPUTER; + if (thingTypes.containsKey(typeUID)) { + baseTypeUID = typeUID; + } + ThingType baseType = thingTypeRegistry.getThingType(baseTypeUID); + ThingTypeBuilder builder = createThingTypeBuilder(typeUID, baseTypeUID); + if (baseType != null && builder != null) { + logger.trace("Adding channel group definitions to thing type"); + ThingType type = builder.withChannelGroupDefinitions(groupDefs).build(); + + setThingType(typeUID, type); + return true; + } else { + logger.debug("Error adding channel groups"); + return false; + } + } + + /** + * Return a {@link ThingTypeBuilder} that can create an exact copy of the `ThingType` with `baseTypeUID`. + * Further build steps can be performed on the returned object before recreating the `ThingType` from the builder. + * + * @param newTypeUID + * @param baseTypeUID + * @return the ThingTypeBuilder, null if `baseTypeUID` cannot be found in the thingTypeRegistry + */ + private @Nullable ThingTypeBuilder createThingTypeBuilder(ThingTypeUID newTypeUID, ThingTypeUID baseTypeUID) { + ThingType type = thingTypeRegistry.getThingType(baseTypeUID); + + if (type == null) { + return null; + } + + ThingTypeBuilder result = ThingTypeBuilder.instance(newTypeUID, type.getLabel()) + .withChannelGroupDefinitions(type.getChannelGroupDefinitions()) + .withChannelDefinitions(type.getChannelDefinitions()) + .withExtensibleChannelTypeIds(type.getExtensibleChannelTypeIds()) + .withSupportedBridgeTypeUIDs(type.getSupportedBridgeTypeUIDs()).withProperties(type.getProperties()) + .isListed(false); + + String representationProperty = type.getRepresentationProperty(); + if (representationProperty != null) { + result = result.withRepresentationProperty(representationProperty); + } + URI configDescriptionURI = type.getConfigDescriptionURI(); + if (configDescriptionURI != null) { + result = result.withConfigDescriptionURI(configDescriptionURI); + } + String category = type.getCategory(); + if (category != null) { + result = result.withCategory(category); + } + String description = type.getDescription(); + if (description != null) { + result = result.withDescription(description); + } + + return result; + } + + /** + * Return List of {@link ChannelGroupDefinition} for `ThingType` with `typeUID`. If the `ThingType` does not exist + * in the thingTypeRegistry yet, retrieve list of `ChannelGroupDefinition` for base type systeminfo:computer. + * + * @param typeUID UID for ThingType + * @return list of channel group definitions, empty list if no channel group definitions + */ + public List getChannelGroupDefinitions(ThingTypeUID typeUID) { + ThingType type = thingTypeRegistry.getThingType(typeUID); + if (type == null) { + type = thingTypeRegistry.getThingType(THING_TYPE_COMPUTER); + } + if (type != null) { + return type.getChannelGroupDefinitions(); + } else { + logger.debug("Cannot retrieve channel group definitions, no base thing type found"); + return Collections.emptyList(); + } + } + + /** + * Create a new channel group definition with index appended to id and label. + * + * @param channelGroupID id of channel group without index + * @param channelGroupTypeID id ChannelGroupType for new channel group definition + * @param i index + * @return channel group definition, null if provided channelGroupTypeID cannot be found in ChannelGroupTypeRegistry + */ + public @Nullable ChannelGroupDefinition createChannelGroupDefinitionWithIndex(String channelGroupID, + String channelGroupTypeID, int i) { + ChannelGroupTypeUID channelGroupTypeUID = new ChannelGroupTypeUID(BINDING_ID, channelGroupTypeID); + ChannelGroupType channelGroupType = channelGroupTypeRegistry.getChannelGroupType(channelGroupTypeUID); + if (channelGroupType == null) { + logger.debug("Cannot create channel group definition, group type {} invalid", channelGroupTypeID); + return null; + } + String index = String.valueOf(i); + return new ChannelGroupDefinition(channelGroupID + index, channelGroupTypeUID, + channelGroupType.getLabel() + " " + index, channelGroupType.getDescription()); + } + + /** + * Create a new channel with index appended to id and label of an existing channel. + * + * @param thing containing the existing channel + * @param channelID id of channel without index + * @param i index + * @return channel, null if provided channelID does not match a channel, or no type can be retrieved for the + * provided channel + */ + public @Nullable Channel createChannelWithIndex(Thing thing, String channelID, int i) { + Channel baseChannel = thing.getChannel(channelID); + if (baseChannel == null) { + logger.debug("Cannot create channel, ID {} invalid", channelID); + return null; + } + ChannelTypeUID channelTypeUID = baseChannel.getChannelTypeUID(); + ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeUID); + if (channelType == null) { + logger.debug("Cannot create channel, type {} invalid", + channelTypeUID != null ? channelTypeUID.getId() : "null"); + return null; + } + ThingUID thingUID = thing.getUID(); + String index = String.valueOf(i); + ChannelUID channelUID = new ChannelUID(thingUID, channelID + index); + ChannelBuilder builder = ChannelBuilder.create(channelUID).withType(channelTypeUID) + .withConfiguration(baseChannel.getConfiguration()); + builder.withLabel(channelType.getLabel() + " " + index); + String description = channelType.getDescription(); + if (description != null) { + builder.withDescription(description); + } + return builder.build(); + } + + /** + * Store the channel configurations for a thing, to be able to restore them later when the thing handler for the + * same thing gets recreated with a new thing type. This is necessary because the + * {@link BaseThingHandler#changeThingType()} method reverts channel configurations to their defaults. + * + * @param thing + */ + public void storeChannelsConfig(Thing thing) { + Map channelsConfig = thing.getChannels().stream() + .collect(Collectors.toMap(c -> c.getUID().getId(), c -> c.getConfiguration())); + thingChannelsConfig.put(thing.getUID(), channelsConfig); + } + + /** + * Restore previous channel configurations of matching channels when the thing handler gets recreated with a new + * thing type. Return an empty map if no channel configurations where stored. Before returning previous channel + * configurations, clear the store, so they can only be retrieved ones, immediately after a thing type change. See + * also {@link #storeChannelsConfig(Thing)}. + * + * @param UID + * @return Map of ChannelId and Configuration for the channel + */ + public Map restoreChannelsConfig(ThingUID UID) { + Map configs = thingChannelsConfig.remove(UID); + return configs != null ? configs : Collections.emptyMap(); + } +} diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java index 50be4a14ca551..3b8efb316e6e2 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/handler/SysteminfoHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants.*; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -22,12 +24,17 @@ import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.systeminfo.internal.SysteminfoThingTypeProvider; import org.openhab.binding.systeminfo.internal.model.DeviceNotFoundException; import org.openhab.binding.systeminfo.internal.model.SysteminfoInterface; +import org.openhab.core.cache.ExpiringCache; +import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; @@ -36,7 +43,11 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelGroupDefinition; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; import org.openhab.core.types.State; @@ -51,6 +62,7 @@ * @author Svilen Valkanov - Initial contribution * @author Lyubomir Papzov - Separate the creation of the systeminfo object and its initialization * @author Wouter Born - Add null annotations + * @author Mark Herwege - Add dynamic creation of extra channels */ @NonNullByDefault public class SysteminfoHandler extends BaseThingHandler { @@ -91,31 +103,59 @@ public class SysteminfoHandler extends BaseThingHandler { */ public static final int WAIT_TIME_CHANNEL_ITEM_LINK_INIT = 1; + /** + * String used to extend thingUID and channelGroupTypeUID for thing definition with added dynamic channels and + * extended channels. It is set in the constructor and unique to the thing. + */ + public final String idExtString; + + public final SysteminfoThingTypeProvider thingTypeProvider; + private SysteminfoInterface systeminfo; private @Nullable ScheduledFuture highPriorityTasks; private @Nullable ScheduledFuture mediumPriorityTasks; - private Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class); + /** + * Caches for cpu process load and process load for a given pid. Using this cache limits the process load refresh + * interval to the minimum interval. Too frequent refreshes leads to inaccurate results. This could happen when the + * same process is tracked as current process and as a channel with pid parameter, or when the task interval is set + * too low. + */ + private static final int MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS = 2000; + private ExpiringCache cpuLoadCache = new ExpiringCache<>(MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS, + () -> getSystemCpuLoad()); + private ExpiringCacheMap processLoadCache = new ExpiringCacheMap<>( + MIN_PROCESS_LOAD_REFRESH_INTERVAL_MS); - public SysteminfoHandler(Thing thing, @Nullable SysteminfoInterface systeminfo) { + private final Logger logger = LoggerFactory.getLogger(SysteminfoHandler.class); + + public SysteminfoHandler(Thing thing, SysteminfoThingTypeProvider thingTypeProvider, + SysteminfoInterface systeminfo) { super(thing); - if (systeminfo != null) { - this.systeminfo = systeminfo; - } else { - throw new IllegalArgumentException("No systeminfo service was provided"); - } + this.thingTypeProvider = thingTypeProvider; + this.systeminfo = systeminfo; + + idExtString = "-" + thing.getUID().getId(); } @Override public void initialize() { + logger.trace("Initializing thing {} with thing type {}", thing.getUID().getId(), + thing.getThingTypeUID().getId()); + restoreChannelsConfig(); // After a thing type change, previous channel configs will have been stored, and will + // be restored here. if (instantiateSysteminfoLibrary() && isConfigurationValid() && updateProperties()) { - groupChannelsByPriority(); - scheduleUpdates(); - updateStatus(ThingStatus.ONLINE); + if (!addDynamicChannels()) { // If there are new channel groups, the thing will get recreated with a new + // thing type and this handler will be disposed. Therefore do not do anything + // further here. + groupChannelsByPriority(); + scheduleUpdates(); + updateStatus(ThingStatus.ONLINE); + } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, - "Thing cannot be initialized!"); + "@text/offline.cannot-initialize"); } } @@ -169,15 +209,129 @@ private boolean updateProperties() { } } + /** + * Retrieve info on available storages, drives, displays, batteries, network interfaces and fans in the system. If + * there is more than 1, create additional channel groups and channels representing each of the entities with an + * index added to the channel groups and channels. The base channel groups and channels will remain without index + * and are equal to the channel groups and channels with index 0. If there is only one entity in a group, do not add + * a channels group and channels with index 0. + *

+ * If channel groups are added, the thing type will change to systeminfo:computer-Ext, with Ext equal to the thing + * id. A new handler will be created and initialization restarted. Therefore further initialization of the current + * handler can be aborted if the method returns true. + * + * @return true if channel groups where added + */ + private boolean addDynamicChannels() { + ThingUID thingUID = thing.getUID(); + + List newChannelGroups = new ArrayList<>(); + newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_STORAGE, CHANNEL_GROUP_TYPE_STORAGE, + systeminfo.getFileOSStoreCount())); + newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_DRIVE, CHANNEL_GROUP_TYPE_DRIVE, + systeminfo.getDriveCount())); + newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_DISPLAY, CHANNEL_GROUP_TYPE_DISPLAY, + systeminfo.getDisplayCount())); + newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_BATTERY, CHANNEL_GROUP_TYPE_BATTERY, + systeminfo.getPowerSourceCount())); + newChannelGroups.addAll(createChannelGroups(thingUID, CHANNEL_GROUP_NETWORK, CHANNEL_GROUP_TYPE_NETWORK, + systeminfo.getNetworkIFCount())); + if (!newChannelGroups.isEmpty()) { + logger.debug("Creating additional channel groups"); + newChannelGroups.addAll(0, thingTypeProvider.getChannelGroupDefinitions(thing.getThingTypeUID())); + ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, THING_TYPE_COMPUTER_ID + idExtString); + if (thingTypeProvider.updateThingType(thingTypeUID, newChannelGroups)) { + logger.trace("Channel groups were added, changing the thing type"); + changeThingType(thingTypeUID, thing.getConfiguration()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, + "@text/offline.cannot-initialize"); + } + return true; + } + + List newChannels = new ArrayList<>(); + newChannels.addAll(createChannels(thingUID, CHANNEL_SENSORS_FAN_SPEED, systeminfo.getFanCount())); + if (!newChannels.isEmpty()) { + logger.debug("Creating additional channels"); + newChannels.addAll(0, thing.getChannels()); + ThingBuilder thingBuilder = editThing(); + thingBuilder.withChannels(newChannels); + updateThing(thingBuilder.build()); + } + + return false; + } + + private List createChannelGroups(ThingUID thingUID, String channelGroupID, + String channelGroupTypeID, int count) { + if (count <= 1) { + return Collections.emptyList(); + } + + List channelGroups = thingTypeProvider.getChannelGroupDefinitions(thing.getThingTypeUID()).stream() + .map(ChannelGroupDefinition::getId).collect(Collectors.toList()); + + List newChannelGroups = new ArrayList<>(); + for (int i = 0; i < count; i++) { + String index = String.valueOf(i); + ChannelGroupDefinition channelGroupDef = thingTypeProvider + .createChannelGroupDefinitionWithIndex(channelGroupID, channelGroupTypeID, i); + if (!(channelGroupDef == null || channelGroups.contains(channelGroupID + index))) { + logger.trace("Adding channel group {}", channelGroupID + index); + newChannelGroups.add(channelGroupDef); + } + } + return newChannelGroups; + } + + private List createChannels(ThingUID thingUID, String channelID, int count) { + if (count <= 1) { + return Collections.emptyList(); + } + + List newChannels = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Channel channel = thingTypeProvider.createChannelWithIndex(thing, channelID, i); + if (channel != null && thing.getChannel(channel.getUID()) == null) { + logger.trace("Creating channel {}", channel.getUID().getId()); + newChannels.add(channel); + } + } + return newChannels; + } + + private void storeChannelsConfig() { + logger.trace("Storing channel configurations"); + thingTypeProvider.storeChannelsConfig(thing); + } + + private void restoreChannelsConfig() { + logger.trace("Restoring channel configurations"); + Map channelsConfig = thingTypeProvider.restoreChannelsConfig(thing.getUID()); + for (String channelId : channelsConfig.keySet()) { + Channel channel = thing.getChannel(channelId); + Configuration config = channelsConfig.get(channelId); + if (channel != null && config != null) { + Configuration currentConfig = channel.getConfiguration(); + for (String param : config.keySet()) { + if (isConfigurationKeyChanged(currentConfig, config, param)) { + handleChannelConfigurationChange(channel, config, param); + } + } + } + } + } + private void groupChannelsByPriority() { - logger.trace("Grouping channels by priority."); + logger.trace("Grouping channels by priority"); List channels = this.thing.getChannels(); for (Channel channel : channels) { Configuration properties = channel.getConfiguration(); String priority = (String) properties.get(PRIOIRITY_PARAM); if (priority == null) { - logger.debug("Channel with UID {} will not be updated. The channel has no priority set !", + logger.debug("Channel with UID {} will not be updated. The channel has no priority set!", channel.getUID()); break; } @@ -220,23 +374,27 @@ private void changeChannelPriority(ChannelUID channelUID, String priority) { } private void scheduleUpdates() { - logger.debug("Schedule high priority tasks at fixed rate {} s.", refreshIntervalHighPriority); + logger.debug("Schedule high priority tasks at fixed rate {} s", refreshIntervalHighPriority); highPriorityTasks = scheduler.scheduleWithFixedDelay(() -> { publishData(highPriorityChannels); }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalHighPriority.intValue(), TimeUnit.SECONDS); - logger.debug("Schedule medium priority tasks at fixed rate {} s.", refreshIntervalMediumPriority); + logger.debug("Schedule medium priority tasks at fixed rate {} s", refreshIntervalMediumPriority); mediumPriorityTasks = scheduler.scheduleWithFixedDelay(() -> { publishData(mediumPriorityChannels); }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, refreshIntervalMediumPriority.intValue(), TimeUnit.SECONDS); - logger.debug("Schedule one time update for low priority tasks."); + logger.debug("Schedule one time update for low priority tasks"); scheduler.schedule(() -> { publishData(lowPriorityChannels); }, WAIT_TIME_CHANNEL_ITEM_LINK_INIT, TimeUnit.SECONDS); } private void publishData(Set channels) { + // if handler disposed while waiting for the links, don't update the channel states + if (!ThingStatus.ONLINE.equals(thing.getStatus())) { + return; + } Iterator iter = channels.iterator(); while (iter.hasNext()) { ChannelUID channeUID = iter.next(); @@ -276,16 +434,16 @@ private State getInfoForChannel(ChannelUID channelUID) { State state = null; String channelID = channelUID.getId(); - String channelIDWithoutGroup = channelUID.getIdWithoutGroup(); - String channelGroupID = channelUID.getGroupId(); - int deviceIndex = getDeviceIndex(channelUID); - // The channelGroup may contain deviceIndex. It must be deleted from the channelID, because otherwise the - // switch will not find the correct method below. - // All digits are deleted from the ID - if (channelGroupID != null) { - channelID = channelGroupID.replaceAll("\\d+", "") + "#" + channelIDWithoutGroup; + logger.trace("Getting state for channel {} with device index {}", channelID, deviceIndex); + + // The channelGroup or channel may contain deviceIndex. It must be deleted from the channelID, because otherwise + // the switch will not find the correct method below. + // All digits are deleted from the ID, except for CpuLoad channels. + if (!(CHANNEL_CPU_LOAD_1.equals(channelID) || CHANNEL_CPU_LOAD_5.equals(channelID) + || CHANNEL_CPU_LOAD_15.equals(channelID))) { + channelID = channelID.replaceAll("\\d+", ""); } try { @@ -319,7 +477,7 @@ private State getInfoForChannel(ChannelUID channelUID) { state = systeminfo.getSensorsFanSpeed(deviceIndex); break; case CHANNEL_CPU_LOAD: - PercentType cpuLoad = systeminfo.getSystemCpuLoad(); + PercentType cpuLoad = cpuLoadCache.getValue(); state = (cpuLoad != null) ? new QuantityType<>(cpuLoad, Units.PERCENT) : null; break; case CHANNEL_CPU_LOAD_1: @@ -431,34 +589,52 @@ private State getInfoForChannel(ChannelUID channelUID) { state = systeminfo.getNetworkPacketsSent(deviceIndex); break; case CHANNEL_PROCESS_LOAD: - PercentType processLoad = systeminfo.getProcessCpuUsage(deviceIndex); + case CHANNEL_CURRENT_PROCESS_LOAD: + DecimalType processLoad = processLoadCache.putIfAbsentAndGet(deviceIndex, + () -> getProcessCpuUsage(deviceIndex)); state = (processLoad != null) ? new QuantityType<>(processLoad, Units.PERCENT) : null; break; case CHANNEL_PROCESS_MEMORY: + case CHANNEL_CURRENT_PROCESS_MEMORY: state = systeminfo.getProcessMemoryUsage(deviceIndex); break; case CHANNEL_PROCESS_NAME: + case CHANNEL_CURRENT_PROCESS_NAME: state = systeminfo.getProcessName(deviceIndex); break; case CHANNEL_PROCESS_PATH: + case CHANNEL_CURRENT_PROCESS_PATH: state = systeminfo.getProcessPath(deviceIndex); break; case CHANNEL_PROCESS_THREADS: + case CHANNEL_CURRENT_PROCESS_THREADS: state = systeminfo.getProcessThreads(deviceIndex); break; default: logger.debug("Channel with unknown ID: {} !", channelID); } } catch (DeviceNotFoundException e) { - logger.warn("No information for channel {} with device index {} :", channelID, deviceIndex); + logger.warn("No information for channel {} with device index: {}", channelID, deviceIndex); } catch (Exception e) { logger.debug("Unexpected error occurred while getting system information!", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Cannot get system info as result of unexpected error. Please try to restart the binding (remove and re-add the thing)!"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/offline.unexpected-error"); } return state != null ? state : UnDefType.UNDEF; } + private @Nullable PercentType getSystemCpuLoad() { + return systeminfo.getSystemCpuLoad(); + } + + private @Nullable DecimalType getProcessCpuUsage(int pid) { + try { + return systeminfo.getProcessCpuUsage(pid); + } catch (DeviceNotFoundException e) { + logger.warn("Process with pid {} does not exist", pid); + return null; + } + } + /** * The device index is an optional part of the channelID - the last characters of the groupID. It is used to * identify unique device, when more than one devices are available (e.g. local disks with names C:\, D:\, E"\ - the @@ -469,6 +645,7 @@ private State getInfoForChannel(ChannelUID channelUID) { * @return natural number (number >=0) */ private int getDeviceIndex(ChannelUID channelUID) { + String channelID = channelUID.getId(); String channelGroupID = channelUID.getGroupId(); if (channelGroupID == null) { return 0; @@ -481,13 +658,23 @@ private int getDeviceIndex(ChannelUID channelUID) { return pid; } - char lastChar = channelGroupID.charAt(channelGroupID.length() - 1); - if (Character.isDigit(lastChar)) { - // All non-digits are deleted from the ID + if (channelGroupID.contains(CHANNEL_GROUP_CURRENT_PROCESS)) { + int pid = systeminfo.getCurrentProcessID(); + return pid; + } + + // First try to get device index in group id, delete all non-digits from id + if (Character.isDigit(channelGroupID.charAt(channelGroupID.length() - 1))) { String deviceIndexPart = channelGroupID.replaceAll("\\D+", ""); return Integer.parseInt(deviceIndexPart); } + // If not found, try to find it in channel id, delete all non-digits from id + if (Character.isDigit(channelID.charAt(channelID.length() - 1))) { + String deviceIndexPart = channelID.replaceAll("\\D+", ""); + return Integer.parseInt(deviceIndexPart); + } + return 0; } @@ -510,10 +697,10 @@ private int getPID(ChannelUID channelUID) { pid = pidValue.intValue(); } } else { - logger.debug("Channel does not exist ! Fall back to default value."); + logger.debug("Channel does not exist! Fall back to default value."); } } catch (ClassCastException e) { - logger.debug("Channel configuration cannot be read ! Fall back to default value.", e); + logger.debug("Channel configuration cannot be read! Fall back to default value.", e); } catch (IllegalArgumentException e) { logger.debug("PID (Process Identifier) must be positive number. Fall back to default value. ", e); } @@ -524,10 +711,10 @@ private int getPID(ChannelUID channelUID) { public void handleCommand(ChannelUID channelUID, Command command) { if (thing.getStatus().equals(ThingStatus.ONLINE)) { if (command instanceof RefreshType) { - logger.debug("Refresh command received for channel {}!", channelUID); + logger.debug("Refresh command received for channel {} !", channelUID); publishDataForChannel(channelUID); } else { - logger.debug("Unsupported command {}! Supported commands: REFRESH", command); + logger.debug("Unsupported command {} ! Supported commands: REFRESH", command); } } else { logger.debug("Cannot handle command. Thing is not ONLINE."); @@ -546,9 +733,10 @@ private boolean isConfigurationKeyChanged(Configuration currentConfig, Configura } @Override - public void thingUpdated(Thing thing) { - logger.trace("About to update thing."); + public synchronized void thingUpdated(Thing thing) { + logger.trace("About to update thing"); boolean isChannelConfigChanged = false; + List channels = thing.getChannels(); for (Channel channel : channels) { @@ -557,7 +745,7 @@ public void thingUpdated(Thing thing) { Channel oldChannel = this.thing.getChannel(channelUID.getId()); if (oldChannel == null) { - logger.warn("Channel with UID {} cannot be updated, as it cannot be found !", channelUID); + logger.warn("Channel with UID {} cannot be updated, as it cannot be found!", channelUID); continue; } Configuration currentChannelConfig = oldChannel.getConfiguration(); @@ -594,16 +782,22 @@ private void handleChannelConfigurationChange(Channel channel, Configuration new publishDataForChannel(channel.getUID()); } + @Override + protected void changeThingType(ThingTypeUID thingTypeUID, Configuration configuration) { + storeChannelsConfig(); + super.changeThingType(thingTypeUID, configuration); + } + private void stopScheduledUpdates() { ScheduledFuture localHighPriorityTasks = highPriorityTasks; if (localHighPriorityTasks != null) { - logger.debug("High prioriy tasks will not be run anymore !"); + logger.debug("High prioriy tasks will not be run anymore!"); localHighPriorityTasks.cancel(true); } ScheduledFuture localMediumPriorityTasks = mediumPriorityTasks; if (localMediumPriorityTasks != null) { - logger.debug("Medium prioriy tasks will not be run anymore !"); + logger.debug("Medium prioriy tasks will not be run anymore!"); localMediumPriorityTasks.cancel(true); } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java index 5ab70b3adc764..4c4b3903c7ad8 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/OSHISysteminfo.java @@ -52,6 +52,7 @@ * @author Christoph Weitkamp - Update to OSHI 3.13.0 - Replaced deprecated method * CentralProcessor#getSystemSerialNumber() * @author Wouter Born - Update to OSHI 4.0.0 and add null annotations + * @author Mark Herwege - Add dynamic creation of extra channels * * @see OSHI GitHub repository */ @@ -350,6 +351,8 @@ public StringType getDisplayInformation(int index) throws DeviceNotFoundExceptio int speed = 0; // 0 means unable to measure speed if (index < fanSpeeds.length) { speed = fanSpeeds[index]; + } else { + throw new DeviceNotFoundException(); } return speed > 0 ? new DecimalType(speed) : null; } @@ -608,6 +611,11 @@ public DecimalType getNetworkDataReceived(int networkIndex) throws DeviceNotFoun return new DecimalType(getSizeInMB(bytesRecv)); } + @Override + public int getCurrentProcessID() { + return operatingSystem.getProcessId(); + } + @Override public @Nullable StringType getProcessName(int pid) throws DeviceNotFoundException { if (pid > 0) { @@ -620,11 +628,11 @@ public DecimalType getNetworkDataReceived(int networkIndex) throws DeviceNotFoun } @Override - public @Nullable PercentType getProcessCpuUsage(int pid) throws DeviceNotFoundException { + public @Nullable DecimalType getProcessCpuUsage(int pid) throws DeviceNotFoundException { if (pid > 0) { OSProcess process = getProcess(pid); - PercentType load = (processTicks.containsKey(pid)) - ? new PercentType(getPercentsValue(process.getProcessCpuLoadBetweenTicks(processTicks.get(pid)))) + DecimalType load = (processTicks.containsKey(pid)) + ? new DecimalType(getPercentsValue(process.getProcessCpuLoadBetweenTicks(processTicks.get(pid)))) : null; processTicks.put(pid, process); return load; @@ -666,4 +674,34 @@ public DecimalType getNetworkDataReceived(int networkIndex) throws DeviceNotFoun return null; } } + + @Override + public int getNetworkIFCount() { + return networks.size(); + } + + @Override + public int getDisplayCount() { + return displays.size(); + } + + @Override + public int getFileOSStoreCount() { + return fileStores.size(); + } + + @Override + public int getPowerSourceCount() { + return powerSources.size(); + } + + @Override + public int getDriveCount() { + return drives.size(); + } + + @Override + public int getFanCount() { + return sensors.getFanSpeeds().length; + } } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java index a606f7a26c81d..87a7c97610ac6 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java +++ b/bundles/org.openhab.binding.systeminfo/src/main/java/org/openhab/binding/systeminfo/internal/model/SysteminfoInterface.java @@ -23,6 +23,7 @@ * * @author Svilen Valkanov - Initial contribution * @author Wouter Born - Add null annotations + * @author Mark Herwege - Add dynamic creation of extra channels */ @NonNullByDefault public interface SysteminfoInterface { @@ -404,6 +405,13 @@ public interface SysteminfoInterface { */ public StringType getBatteryName(int deviceIndex) throws DeviceNotFoundException; + /** + * Get PID of process executing this code + * + * @return current process ID + */ + int getCurrentProcessID(); + /** * Returns the name of the process * @@ -416,10 +424,10 @@ public interface SysteminfoInterface { * Returns the CPU usage of the process * * @param pid - the PID of the process - * @return - percentage value /0-100/ + * @return - percentage value, can be above 100% if process uses multiple cores * @throws DeviceNotFoundException - thrown if process with this PID can not be found */ - public @Nullable PercentType getProcessCpuUsage(int pid) throws DeviceNotFoundException; + public @Nullable DecimalType getProcessCpuUsage(int pid) throws DeviceNotFoundException; /** * Returns the size of RAM memory only usage of the process @@ -445,4 +453,46 @@ public interface SysteminfoInterface { * @throws DeviceNotFoundException - thrown if process with this PID can not be found */ public @Nullable DecimalType getProcessThreads(int pid) throws DeviceNotFoundException; + + /** + * Returns the number of network interfaces. + * + * @return network interface count + */ + public int getNetworkIFCount(); + + /** + * Returns the number of displays. + * + * @return display count + */ + public int getDisplayCount(); + + /** + * Returns the number of storages. + * + * @return storage count + */ + public int getFileOSStoreCount(); + + /** + * Returns the number of power sources/batteries. + * + * @return power source count + */ + public int getPowerSourceCount(); + + /** + * Returns the number of drives. + * + * @return drive count + */ + public int getDriveCount(); + + /** + * Returns the number of fans. + * + * @return fan count + */ + int getFanCount(); } diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties index ebb3eded3dba0..289ef8f0d6e70 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/i18n/systeminfo.properties @@ -29,6 +29,8 @@ channel-group-type.systeminfo.memoryGroup.label = Physical Memory channel-group-type.systeminfo.memoryGroup.description = Physical memory information channel-group-type.systeminfo.networkGroup.label = Network channel-group-type.systeminfo.networkGroup.description = Network parameters +channel-group-type.systeminfo.currentProcessGroup.label = Current Process +channel-group-type.systeminfo.currentProcessGroup.description = Current process information channel-group-type.systeminfo.processGroup.label = Process channel-group-type.systeminfo.processGroup.description = System process information channel-group-type.systeminfo.sensorsGroup.label = Sensor @@ -62,8 +64,8 @@ channel-type.systeminfo.information.label = Display Information channel-type.systeminfo.information.description = Product, manufacturer, SN, width and height of the display in cm channel-type.systeminfo.ip.label = IP Address channel-type.systeminfo.ip.description = Host IP address of the network -channel-type.systeminfo.cpuLoad.label = CPU Load -channel-type.systeminfo.cpuLoad.description = CPU load in percent +channel-type.systeminfo.load.label = Load +channel-type.systeminfo.load.description = Load in percent channel-type.systeminfo.loadAverage.label = Load Average channel-type.systeminfo.loadAverage.description = Load as a number of processes for the last 1,5 or 15 minutes channel-type.systeminfo.load_process.label = Load @@ -84,6 +86,8 @@ channel-type.systeminfo.packetsReceived.label = Packets Received channel-type.systeminfo.packetsReceived.description = Number of packets received channel-type.systeminfo.packetsSent.label = Packets Sent channel-type.systeminfo.packetsSent.description = Number of packets sent +channel-type.systeminfo.path.label = Path +channel-type.systeminfo.path.description = The full path channel-type.systeminfo.path_process.label = Path channel-type.systeminfo.path_process.description = The full path channel-type.systeminfo.remainingCapacity.label = Remaining Capacity @@ -151,3 +155,7 @@ channel-type.config.systeminfo.mediumpriority_process.priority.description = Ref channel-type.config.systeminfo.mediumpriority_process.priority.option.High = High channel-type.config.systeminfo.mediumpriority_process.priority.option.Medium = Medium channel-type.config.systeminfo.mediumpriority_process.priority.option.Low = Low + +# thing status messages +offline.cannot-initialize = Thing cannot be initialized! +offline.unexpected-error = Cannot get system info as result of unexpected error. Please try to restart the binding (remove and re-add the thing)! diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml index 16ddabce8ebc7..5add605847cf0 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/channels.xml @@ -107,7 +107,7 @@ - + @@ -116,6 +116,18 @@ + + + Current process information + + + + + + + + + System process information @@ -144,6 +156,14 @@ + + String + + The full path + + + + String @@ -288,20 +308,20 @@ - + Number:Dimensionless Load in percent - + - + Number:Dimensionless - - CPU load in percent + + Load in percent - + diff --git a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml index ef680220dc7a4..31c543de77c9b 100644 --- a/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml +++ b/bundles/org.openhab.binding.systeminfo/src/main/resources/OH-INF/thing/computer.xml @@ -16,7 +16,7 @@ - + diff --git a/itests/org.openhab.binding.systeminfo.tests/itest.bndrun b/itests/org.openhab.binding.systeminfo.tests/itest.bndrun index 146b3bc642026..022b2e258e372 100644 --- a/itests/org.openhab.binding.systeminfo.tests/itest.bndrun +++ b/itests/org.openhab.binding.systeminfo.tests/itest.bndrun @@ -74,4 +74,6 @@ Fragment-Host: org.openhab.binding.systeminfo org.openhab.core.io.console;version='[3.4.0,3.4.1)',\ org.openhab.core.test;version='[3.4.0,3.4.1)',\ org.openhab.core.thing;version='[3.4.0,3.4.1)',\ - org.openhab.core.thing.xml;version='[3.4.0,3.4.1)' + org.openhab.core.thing.xml;version='[3.4.0,3.4.1)',\ + org.mockito.junit-jupiter;version='[4.1.0,4.1.1)',\ + com.github.oshi.oshi-core;version='[6.2.2,6.2.3)' diff --git a/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java b/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java index 24d90803624e0..57b78ead6e22d 100644 --- a/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java +++ b/itests/org.openhab.binding.systeminfo.tests/src/main/java/org/openhab/binding/systeminfo/test/SysteminfoOSGiTest.java @@ -24,15 +24,21 @@ import java.util.Hashtable; import java.util.List; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.openhab.binding.systeminfo.internal.SysteminfoBindingConstants; import org.openhab.binding.systeminfo.internal.SysteminfoHandlerFactory; +import org.openhab.binding.systeminfo.internal.SysteminfoThingTypeProvider; import org.openhab.binding.systeminfo.internal.discovery.SysteminfoDiscoveryService; import org.openhab.binding.systeminfo.internal.handler.SysteminfoHandler; import org.openhab.binding.systeminfo.internal.model.DeviceNotFoundException; +import org.openhab.binding.systeminfo.internal.model.OSHISysteminfo; import org.openhab.binding.systeminfo.internal.model.SysteminfoInterface; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; @@ -61,6 +67,7 @@ import org.openhab.core.thing.ThingUID; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.openhab.core.thing.binding.ThingTypeProvider; import org.openhab.core.thing.binding.builder.ChannelBuilder; import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.link.ItemChannelLink; @@ -78,6 +85,8 @@ * but mock data will be used instead, avoiding potential errors from the OS queries. * @author Wouter Born - Migrate Groovy to Java tests */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) public class SysteminfoOSGiTest extends JavaOSGiTest { private static final String DEFAULT_TEST_THING_NAME = "work"; private static final String DEFAULT_TEST_ITEM_NAME = "test"; @@ -97,15 +106,13 @@ public class SysteminfoOSGiTest extends JavaOSGiTest { */ private static final int DEFAULT_TEST_INTERVAL_MEDIUM = 3; - private Thing systemInfoThing; - private SysteminfoHandler systemInfoHandler; - private GenericItem testItem; + private @Nullable Thing systemInfoThing; + private @Nullable GenericItem testItem; - private SysteminfoInterface mockedSystemInfo; - private ManagedThingProvider managedThingProvider; - private ThingRegistry thingRegistry; - private ItemRegistry itemRegistry; - private SysteminfoHandlerFactory systeminfoHandlerFactory; + private @Mock @NonNullByDefault({}) OSHISysteminfo mockedSystemInfo; + private @NonNullByDefault({}) SysteminfoHandlerFactory systeminfoHandlerFactory; + private @NonNullByDefault({}) ThingRegistry thingRegistry; + private @NonNullByDefault({}) ItemRegistry itemRegistry; @BeforeEach public void setUp() { @@ -113,48 +120,75 @@ public void setUp() { registerService(volatileStorageService); // Preparing the mock with OS properties, that are used in the initialize method of SysteminfoHandler - mockedSystemInfo = mock(SysteminfoInterface.class); - when(mockedSystemInfo.getCpuLogicalCores()).thenReturn(new DecimalType(2)); - when(mockedSystemInfo.getCpuPhysicalCores()).thenReturn(new DecimalType(2)); - when(mockedSystemInfo.getOsFamily()).thenReturn(new StringType("Mock OS")); - when(mockedSystemInfo.getOsManufacturer()).thenReturn(new StringType("Mock OS Manufacturer")); - when(mockedSystemInfo.getOsVersion()).thenReturn(new StringType("Mock Os Version")); - - systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class); - SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class); - - // Unbind oshiSystemInfo service and bind the mock service to make the systeminfobinding tests independent of - // the external OSHI library - if (oshiSystemInfo != null) { - systeminfoHandlerFactory.unbindSystemInfo(oshiSystemInfo); + // Make this lenient because the assertInvalidThingConfigurationValuesAreHandled test does not require them + lenient().when(mockedSystemInfo.getCpuLogicalCores()).thenReturn(new DecimalType(2)); + lenient().when(mockedSystemInfo.getCpuPhysicalCores()).thenReturn(new DecimalType(2)); + lenient().when(mockedSystemInfo.getOsFamily()).thenReturn(new StringType("Mock OS")); + lenient().when(mockedSystemInfo.getOsManufacturer()).thenReturn(new StringType("Mock OS Manufacturer")); + lenient().when(mockedSystemInfo.getOsVersion()).thenReturn(new StringType("Mock Os Version")); + // Following mock method returns will make sure the thing does not get recreated with extra channels + lenient().when(mockedSystemInfo.getNetworkIFCount()).thenReturn(1); + lenient().when(mockedSystemInfo.getDisplayCount()).thenReturn(1); + lenient().when(mockedSystemInfo.getFileOSStoreCount()).thenReturn(1); + lenient().when(mockedSystemInfo.getPowerSourceCount()).thenReturn(1); + lenient().when(mockedSystemInfo.getDriveCount()).thenReturn(1); + lenient().when(mockedSystemInfo.getFanCount()).thenReturn(1); + + registerService(mockedSystemInfo); + + waitForAssert(() -> { + systeminfoHandlerFactory = getService(ThingHandlerFactory.class, SysteminfoHandlerFactory.class); + assertThat(systeminfoHandlerFactory, is(notNullValue())); + }); + if (systeminfoHandlerFactory != null) { + // Unbind oshiSystemInfo service and bind the mock service to make the systeminfo binding tests independent + // of the external OSHI library + SysteminfoInterface oshiSystemInfo = getService(SysteminfoInterface.class); + if (oshiSystemInfo != null) { + systeminfoHandlerFactory.unbindSystemInfo(oshiSystemInfo); + } + systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo); } - systeminfoHandlerFactory.bindSystemInfo(mockedSystemInfo); - managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class); - assertThat(managedThingProvider, is(notNullValue())); + waitForAssert(() -> { + ThingTypeProvider thingTypeProvider = getService(ThingTypeProvider.class, + SysteminfoThingTypeProvider.class); + assertThat(thingTypeProvider, is(notNullValue())); + }); + waitForAssert(() -> { + SysteminfoThingTypeProvider systeminfoThingTypeProvider = getService(SysteminfoThingTypeProvider.class); + assertThat(systeminfoThingTypeProvider, is(notNullValue())); + }); - thingRegistry = getService(ThingRegistry.class); - assertThat(thingRegistry, is(notNullValue())); + waitForAssert(() -> { + thingRegistry = getService(ThingRegistry.class); + assertThat(thingRegistry, is(notNullValue())); + }); - itemRegistry = getService(ItemRegistry.class); - assertThat(itemRegistry, is(notNullValue())); + waitForAssert(() -> { + itemRegistry = getService(ItemRegistry.class); + assertThat(itemRegistry, is(notNullValue())); + }); } @AfterEach public void tearDown() { - if (systemInfoThing != null) { + Thing thing = systemInfoThing; + if (thing != null) { // Remove the systeminfo thing. The handler will be also disposed automatically - Thing removedThing = thingRegistry.forceRemove(systemInfoThing.getUID()); + Thing removedThing = thingRegistry.forceRemove(thing.getUID()); assertThat("The systeminfo thing cannot be deleted", removedThing, is(notNullValue())); + waitForAssert(() -> { + ThingHandler systemInfoHandler = thing.getHandler(); + assertThat(systemInfoHandler, is(nullValue())); + }); } - waitForAssert(() -> { - ThingHandler systemInfoHandler = systemInfoThing.getHandler(); - assertThat(systemInfoHandler, is(nullValue())); - }); if (testItem != null) { itemRegistry.remove(DEFAULT_TEST_ITEM_NAME); } + + unregisterService(mockedSystemInfo); } private void initializeThingWithChannelAndPID(String channelID, String acceptedItemType, int pid) { @@ -214,18 +248,24 @@ private void initializeThing(Configuration thingConfiguration, String channelID, Channel channel = ChannelBuilder.create(channelUID, acceptedItemType).withType(channelTypeUID) .withKind(ChannelKind.STATE).withConfiguration(channelConfig).build(); - systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration) + Thing thing = ThingBuilder.create(thingTypeUID, thingUID).withConfiguration(thingConfiguration) .withChannel(channel).build(); + systemInfoThing = thing; - managedThingProvider.add(systemInfoThing); + ManagedThingProvider managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class); + assertThat(managedThingProvider, is(notNullValue())); + + if (managedThingProvider != null) { + managedThingProvider.add(thing); + } waitForAssert(() -> { - systemInfoHandler = (SysteminfoHandler) systemInfoThing.getHandler(); - assertThat(systemInfoHandler, is(notNullValue())); + SysteminfoHandler handler = (SysteminfoHandler) thing.getHandler(); + assertThat(handler, is(notNullValue())); }); waitForAssert(() -> { - assertThat("Thing is not initilized, before an Item is created", systemInfoThing.getStatus(), + assertThat("Thing is not initialized, before an Item is created", thing.getStatus(), anyOf(equalTo(ThingStatus.OFFLINE), equalTo(ThingStatus.ONLINE))); }); @@ -233,11 +273,15 @@ private void initializeThing(Configuration thingConfiguration, String channelID, } private void assertItemState(String acceptedItemType, String itemName, String priority, State expectedState) { + Thing thing = systemInfoThing; + if (thing == null) { + throw new AssertionError("Thing is null"); + } waitForAssert(() -> { - ThingStatusDetail thingStatusDetail = systemInfoThing.getStatusInfo().getStatusDetail(); - String description = systemInfoThing.getStatusInfo().getDescription(); + ThingStatusDetail thingStatusDetail = thing.getStatusInfo().getStatusDetail(); + String description = thing.getStatusInfo().getDescription(); assertThat("Thing status detail is " + thingStatusDetail + " with description " + description, - systemInfoThing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); }); // The binding starts all refresh tasks in SysteminfoHandler.scheduleUpdates() after this delay ! try { @@ -254,9 +298,9 @@ private void assertItemState(String acceptedItemType, String itemName, String pr } int waitTime; - if (priority.equals("High")) { + if ("High".equals(priority)) { waitTime = DEFAULT_TEST_INTERVAL_HIGH * 1000; - } else if (priority.equals("Medium")) { + } else if ("Medium".equals(priority)) { waitTime = DEFAULT_TEST_INTERVAL_MEDIUM * 1000; } else { waitTime = 100; @@ -269,16 +313,25 @@ private void assertItemState(String acceptedItemType, String itemName, String pr } private void intializeItem(ChannelUID channelUID, String itemName, String acceptedItemType) { - if (acceptedItemType.equals("Number")) { - testItem = new NumberItem(itemName); - } else if (acceptedItemType.equals("String")) { - testItem = new StringItem(itemName); + GenericItem item = null; + if ("Number".equals(acceptedItemType)) { + item = new NumberItem(itemName); + } else if ("String".equals(acceptedItemType)) { + item = new StringItem(itemName); + } + if (item == null) { + throw new AssertionError("Item is null"); } - itemRegistry.add(testItem); + itemRegistry.add(item); + testItem = item; ManagedItemChannelLinkProvider itemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class); assertThat(itemChannelLinkProvider, is(notNullValue())); + if (itemChannelLinkProvider == null) { + return; + } + itemChannelLinkProvider.add(new ItemChannelLink(itemName, channelUID)); } @@ -300,29 +353,13 @@ public void assertInvalidThingConfigurationValuesAreHandled() { private void testInvalidConfiguration() { waitForAssert(() -> { - assertThat("Invalid configuratuin is used !", systemInfoThing.getStatus(), - is(equalTo(ThingStatus.OFFLINE))); - assertThat(systemInfoThing.getStatusInfo().getStatusDetail(), - is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR))); - assertThat(systemInfoThing.getStatusInfo().getDescription(), is(equalTo("Thing cannot be initialized!"))); - }); - } - - @Test - public void assertThingStatusIsUninitializedWhenThereIsNoSysteminfoServiceProvided() { - // Unbind the mock service to verify the systeminfo thing will not be initialized when no systeminfo service is - // provided - systeminfoHandlerFactory.unbindSystemInfo(mockedSystemInfo); - - ThingTypeUID thingTypeUID = SysteminfoBindingConstants.THING_TYPE_COMPUTER; - ThingUID thingUID = new ThingUID(thingTypeUID, DEFAULT_TEST_THING_NAME); - - systemInfoThing = ThingBuilder.create(thingTypeUID, thingUID).build(); - managedThingProvider.add(systemInfoThing); - - waitForAssert(() -> { - assertThat("The thing status is uninitialized when systeminfo service is missing", - systemInfoThing.getStatus(), equalTo(ThingStatus.UNINITIALIZED)); + Thing thing = systemInfoThing; + if (thing != null) { + assertThat("Invalid configuration is used !", thing.getStatus(), is(equalTo(ThingStatus.OFFLINE))); + assertThat(thing.getStatusInfo().getStatusDetail(), + is(equalTo(ThingStatusDetail.HANDLER_INITIALIZING_ERROR))); + assertThat(thing.getStatusInfo().getDescription(), is(equalTo("@text/offline.cannot-initialize"))); + } }); } @@ -672,7 +709,7 @@ public void assertChannelDriveSerialIsUpdated() throws DeviceNotFoundException { mockedDriveSerialNumber); } - @Disabled + // Re-enable this previously disabled test, as it is not relying on hardware anymore, but a mocked object // There is a bug opened for this issue - https://github.com/dblock/oshi/issues/185 @Test public void assertChannelSensorsCpuTempIsUpdated() { @@ -874,7 +911,7 @@ class SysteminfoDiscoveryServiceMock extends SysteminfoDiscoveryService { @Override protected String getHostName() throws UnknownHostException { - if (hostname.equals("unresolved")) { + if ("unresolved".equals(hostname)) { throw new UnknownHostException(); } return hostname; @@ -938,6 +975,10 @@ private void testDiscoveryService(String expectedHostname, String hostname) { Inbox inbox = getService(Inbox.class); assertThat(inbox, is(notNullValue())); + if (inbox == null) { + return; + } + waitForAssert(() -> { List results = inbox.stream().filter(InboxPredicates.forThingUID(computerUID)) .collect(toList()); @@ -951,8 +992,13 @@ private void testDiscoveryService(String expectedHostname, String hostname) { assertThat(systemInfoThing, is(notNullValue())); }); + Thing thing = systemInfoThing; + if (thing == null) { + return; + } + waitForAssert(() -> { - assertThat("Thing is not initialized.", systemInfoThing.getStatus(), is(equalTo(ThingStatus.ONLINE))); + assertThat("Thing is not initialized.", thing.getStatus(), is(equalTo(ThingStatus.ONLINE))); }); } @@ -1020,7 +1066,7 @@ public void assertChannelProcessLoadIsUpdatedWithPIDset() throws DeviceNotFoundE // The pid of the System idle process in Windows int pid = 0; - PercentType mockedProcessLoad = new PercentType(3); + DecimalType mockedProcessLoad = new DecimalType(3); when(mockedSystemInfo.getProcessCpuUsage(pid)).thenReturn(mockedProcessLoad); initializeThingWithChannelAndPID(channnelID, acceptedItemType, pid); @@ -1037,15 +1083,27 @@ public void testThingHandlesChannelPriorityChange() { String acceptedItemType = "Number"; initializeThingWithChannel(DEFAULT_TEST_CHANNEL_ID, acceptedItemType); - Channel channel = systemInfoThing.getChannel(DEFAULT_TEST_CHANNEL_ID); + Thing thing = systemInfoThing; + if (thing == null) { + throw new AssertionError("Thing is null"); + } + Channel channel = thing.getChannel(DEFAULT_TEST_CHANNEL_ID); if (channel == null) { throw new AssertionError("Channel '" + DEFAULT_TEST_CHANNEL_ID + "' is null"); } + ThingHandler thingHandler = thing.getHandler(); + if (thingHandler == null) { + throw new AssertionError("Thing handler is null"); + } + if (!(thingHandler.getClass().equals(SysteminfoHandler.class))) { + throw new AssertionError("Thing handler not of class SysteminfoHandler"); + } + SysteminfoHandler handler = (SysteminfoHandler) thingHandler; waitForAssert(() -> { assertThat("The initial priority of channel " + channel.getUID() + " is not as expected.", channel.getConfiguration().get(priorityKey), is(equalTo(initialPriority))); - assertThat(systemInfoHandler.getHighPriorityChannels().contains(channel.getUID()), is(true)); + assertThat(handler.getHighPriorityChannels().contains(channel.getUID()), is(true)); }); // Change the priority of a channel, keep the pid @@ -1056,15 +1114,15 @@ public void testThingHandlesChannelPriorityChange() { .withType(channel.getChannelTypeUID()).withKind(channel.getKind()).withConfiguration(updatedConfig) .build(); - Thing updatedThing = ThingBuilder.create(systemInfoThing.getThingTypeUID(), systemInfoThing.getUID()) - .withConfiguration(systemInfoThing.getConfiguration()).withChannel(updatedChannel).build(); + Thing updatedThing = ThingBuilder.create(thing.getThingTypeUID(), thing.getUID()) + .withConfiguration(thing.getConfiguration()).withChannel(updatedChannel).build(); - systemInfoHandler.thingUpdated(updatedThing); + handler.thingUpdated(updatedThing); waitForAssert(() -> { assertThat("The prority of the channel was not updated: ", channel.getConfiguration().get(priorityKey), is(equalTo(newPriority))); - assertThat(systemInfoHandler.getLowPriorityChannels().contains(channel.getUID()), is(true)); + assertThat(handler.getLowPriorityChannels().contains(channel.getUID()), is(true)); }); } } From c114714a7b21b0eeef60f75a3377ec2bc2310624 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Fri, 4 Nov 2022 18:24:39 +0100 Subject: [PATCH 19/55] [hdpowerview] Restructure DTO classes (#13630) * Extract nested DTO's to separate classes * Rename api to dto * Move test classes into internal * Finish moving of files and fix namespaces Signed-off-by: Jacob Laursen --- .../internal/HDPowerViewWebTargets.java | 52 +++---- .../api/responses/SceneCollections.java | 84 ----------- .../internal/api/responses/Scenes.java | 86 ------------ .../api/responses/ScheduledEvents.java | 132 ------------------ .../internal/api/responses/Shades.java | 70 ---------- .../builders/AutomationChannelBuilder.java | 17 ++- .../builders/SceneChannelBuilder.java | 2 +- .../builders/SceneGroupChannelBuilder.java | 2 +- .../console/HDPowerViewCommandExtension.java | 4 +- .../HDPowerViewDeviceDiscoveryService.java | 6 +- .../internal/{api => dto}/BatteryKind.java | 2 +- .../internal/{api => dto}/Color.java | 2 +- .../{api => dto}/CoordinateSystem.java | 2 +- .../internal/{api => dto}/Firmware.java | 2 +- .../internal/{api => dto}/HubFirmware.java | 2 +- .../hdpowerview/internal/dto/Scene.java | 73 ++++++++++ .../internal/dto/SceneCollection.java | 71 ++++++++++ .../internal/dto/ScheduledEvent.java | 117 ++++++++++++++++ .../hdpowerview/internal/dto/ShadeData.java | 52 +++++++ .../internal/{api => dto}/ShadePosition.java | 4 +- .../internal/{api => dto}/SurveyData.java | 2 +- .../internal/{api => dto}/Times.java | 2 +- .../internal/{api => dto}/UserData.java | 2 +- .../requests/RepeaterBlinking.java | 2 +- .../{api => dto}/requests/RepeaterColor.java | 4 +- .../{api => dto}/requests/ShadeCalibrate.java | 2 +- .../{api => dto}/requests/ShadeJog.java | 2 +- .../{api => dto}/requests/ShadeMotion.java | 2 +- .../{api => dto}/requests/ShadeMove.java | 4 +- .../{api => dto}/requests/ShadePositions.java | 4 +- .../{api => dto}/requests/ShadeStop.java | 2 +- .../responses/FirmwareVersion.java | 4 +- .../{api => dto}/responses/Repeater.java | 2 +- .../{api => dto}/responses/RepeaterData.java | 6 +- .../{api => dto}/responses/Repeaters.java | 2 +- .../dto/responses/SceneCollections.java | 30 ++++ .../internal/dto/responses/Scenes.java | 30 ++++ .../dto/responses/ScheduledEvents.java | 30 ++++ .../{api => dto}/responses/Shade.java | 5 +- .../internal/dto/responses/Shades.java | 30 ++++ .../{api => dto}/responses/Survey.java | 4 +- .../responses/UserDataResponse.java | 4 +- .../handler/HDPowerViewHubHandler.java | 22 +-- .../handler/HDPowerViewRepeaterHandler.java | 6 +- .../handler/HDPowerViewShadeHandler.java | 14 +- .../AutomationChannelBuilderTest.java | 41 +++--- .../{ => internal}/HDPowerViewJUnitTests.java | 20 +-- .../OnlineCommunicationTest.java | 15 +- .../SceneChannelBuilderTest.java | 10 +- .../SceneGroupChannelBuilderTest.java | 10 +- .../{ => internal}/ShadePositionTest.java | 6 +- .../providers/MockedLocaleProvider.java | 2 +- .../providers/MockedTranslationProvider.java | 2 +- .../hdpowerview/{ => internal}/duette.json | 0 .../{ => internal}/sceneCollections.json | 0 .../hdpowerview/{ => internal}/scenes.json | 0 .../hdpowerview/{ => internal}/shades.json | 0 57 files changed, 578 insertions(+), 527 deletions(-) delete mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java delete mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java delete mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java delete mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/BatteryKind.java (96%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/Color.java (96%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/CoordinateSystem.java (98%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/Firmware.java (94%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/HubFirmware.java (93%) create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Scene.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SceneCollection.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ScheduledEvent.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadeData.java rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/ShadePosition.java (99%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/SurveyData.java (94%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/Times.java (94%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/UserData.java (96%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/RepeaterBlinking.java (93%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/RepeaterColor.java (88%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadeCalibrate.java (91%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadeJog.java (91%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadeMotion.java (93%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadeMove.java (85%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadePositions.java (84%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/requests/ShadeStop.java (91%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/FirmwareVersion.java (84%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/Repeater.java (91%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/RepeaterData.java (84%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/Repeaters.java (92%) create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/SceneCollections.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Scenes.java create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/ScheduledEvents.java rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/Shade.java (82%) create mode 100644 bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shades.java rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/Survey.java (87%) rename bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/{api => dto}/responses/UserDataResponse.java (84%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/AutomationChannelBuilderTest.java (88%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/HDPowerViewJUnitTests.java (91%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/OnlineCommunicationTest.java (93%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/SceneChannelBuilderTest.java (90%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/SceneGroupChannelBuilderTest.java (90%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/ShadePositionTest.java (99%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/providers/MockedLocaleProvider.java (92%) rename bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/{ => internal}/providers/MockedTranslationProvider.java (97%) rename bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/{ => internal}/duette.json (100%) rename bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/{ => internal}/sceneCollections.json (100%) rename bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/{ => internal}/scenes.json (100%) rename bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/{ => internal}/shades.json (100%) diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java index f8ba04c153391..aa3525e7d9d18 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/HDPowerViewWebTargets.java @@ -27,32 +27,32 @@ import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; -import org.openhab.binding.hdpowerview.internal.api.Color; -import org.openhab.binding.hdpowerview.internal.api.HubFirmware; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; -import org.openhab.binding.hdpowerview.internal.api.SurveyData; -import org.openhab.binding.hdpowerview.internal.api.UserData; -import org.openhab.binding.hdpowerview.internal.api.requests.RepeaterBlinking; -import org.openhab.binding.hdpowerview.internal.api.requests.RepeaterColor; -import org.openhab.binding.hdpowerview.internal.api.requests.ShadeCalibrate; -import org.openhab.binding.hdpowerview.internal.api.requests.ShadeJog; -import org.openhab.binding.hdpowerview.internal.api.requests.ShadeMove; -import org.openhab.binding.hdpowerview.internal.api.requests.ShadeStop; -import org.openhab.binding.hdpowerview.internal.api.responses.FirmwareVersion; -import org.openhab.binding.hdpowerview.internal.api.responses.Repeater; -import org.openhab.binding.hdpowerview.internal.api.responses.RepeaterData; -import org.openhab.binding.hdpowerview.internal.api.responses.Repeaters; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent; -import org.openhab.binding.hdpowerview.internal.api.responses.Shade; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; -import org.openhab.binding.hdpowerview.internal.api.responses.Survey; -import org.openhab.binding.hdpowerview.internal.api.responses.UserDataResponse; +import org.openhab.binding.hdpowerview.internal.dto.Color; +import org.openhab.binding.hdpowerview.internal.dto.HubFirmware; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.SurveyData; +import org.openhab.binding.hdpowerview.internal.dto.UserData; +import org.openhab.binding.hdpowerview.internal.dto.requests.RepeaterBlinking; +import org.openhab.binding.hdpowerview.internal.dto.requests.RepeaterColor; +import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeCalibrate; +import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeJog; +import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeMove; +import org.openhab.binding.hdpowerview.internal.dto.requests.ShadeStop; +import org.openhab.binding.hdpowerview.internal.dto.responses.FirmwareVersion; +import org.openhab.binding.hdpowerview.internal.dto.responses.Repeater; +import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData; +import org.openhab.binding.hdpowerview.internal.dto.responses.Repeaters; +import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections; +import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes; +import org.openhab.binding.hdpowerview.internal.dto.responses.ScheduledEvents; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shade; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shades; +import org.openhab.binding.hdpowerview.internal.dto.responses.Survey; +import org.openhab.binding.hdpowerview.internal.dto.responses.UserDataResponse; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java deleted file mode 100644 index bef9e94b93329..0000000000000 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/SceneCollections.java +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.hdpowerview.internal.api.responses; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * State of all Scene Collections in an HD PowerView hub - * - * @author Jacob Laursen - Initial contribution - */ -@NonNullByDefault -public class SceneCollections { - - public @Nullable List sceneCollectionData; - public @Nullable List sceneCollectionIds; - - /* - * the following SuppressWarnings annotation is because the Eclipse compiler - * does NOT expect a NonNullByDefault annotation on the inner class, since it is - * implicitly inherited from the outer class, whereas the Maven compiler always - * requires an explicit NonNullByDefault annotation on all classes - */ - @SuppressWarnings("null") - @NonNullByDefault - public static class SceneCollection implements Comparable { - public int id; - public @Nullable String name; - public int order; - public int colorId; - public int iconId; - - @Override - public boolean equals(@Nullable Object o) { - if (o == this) { - return true; - } - if (!(o instanceof SceneCollection)) { - return false; - } - SceneCollection other = (SceneCollection) o; - - return this.id == other.id && this.name.equals(other.name) && this.order == other.order - && this.colorId == other.colorId && this.iconId == other.iconId; - } - - @Override - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + id; - result = prime * result + (name == null ? 0 : name.hashCode()); - result = prime * result + order; - result = prime * result + colorId; - result = prime * result + iconId; - - return result; - } - - @Override - public int compareTo(SceneCollection other) { - return Integer.compare(order, other.order); - } - - public String getName() { - return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8); - } - } -} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java deleted file mode 100644 index 5cb25a8aab0b4..0000000000000 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Scenes.java +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.hdpowerview.internal.api.responses; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * State of all Scenes in an HD PowerView hub - * - * @author Andy Lintner - Initial contribution - */ -@NonNullByDefault -public class Scenes { - - public @Nullable List sceneData; - public @Nullable List sceneIds; - - /* - * the following SuppressWarnings annotation is because the Eclipse compiler - * does NOT expect a NonNullByDefault annotation on the inner class, since it is - * implicitly inherited from the outer class, whereas the Maven compiler always - * requires an explicit NonNullByDefault annotation on all classes - */ - @SuppressWarnings("null") - @NonNullByDefault - public static class Scene implements Comparable { - public int id; - public @Nullable String name; - public int roomId; - public int order; - public int colorId; - public int iconId; - - @Override - public boolean equals(@Nullable Object o) { - if (o == this) { - return true; - } - if (!(o instanceof Scene)) { - return false; - } - Scene other = (Scene) o; - - return this.id == other.id && this.name.equals(other.name) && this.roomId == other.roomId - && this.order == other.order && this.colorId == other.colorId && this.iconId == other.iconId; - } - - @Override - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + id; - result = prime * result + (name == null ? 0 : name.hashCode()); - result = prime * result + roomId; - result = prime * result + order; - result = prime * result + colorId; - result = prime * result + iconId; - - return result; - } - - @Override - public int compareTo(Scene other) { - return Integer.compare(order, other.order); - } - - public String getName() { - return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8); - } - } -} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java deleted file mode 100644 index e7ae6ac1ae440..0000000000000 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/ScheduledEvents.java +++ /dev/null @@ -1,132 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.hdpowerview.internal.api.responses; - -import java.time.DayOfWeek; -import java.util.EnumSet; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * State of all Scheduled Events in an HD PowerView hub - * - * @author Jacob Laursen - Initial contribution - */ -@NonNullByDefault -public class ScheduledEvents { - - public static final EnumSet WEEKDAYS = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); - - public static final EnumSet WEEKENDS = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); - - public static final int SCHEDULED_EVENT_TYPE_TIME = 0; - public static final int SCHEDULED_EVENT_TYPE_SUNRISE = 1; - public static final int SCHEDULED_EVENT_TYPE_SUNSET = 2; - - public @Nullable List scheduledEventData; - public @Nullable List scheduledEventIds; - - /* - * the following SuppressWarnings annotation is because the Eclipse compiler - * does NOT expect a NonNullByDefault annotation on the inner class, since it is - * implicitly inherited from the outer class, whereas the Maven compiler always - * requires an explicit NonNullByDefault annotation on all classes - */ - @SuppressWarnings("null") - @NonNullByDefault - public static class ScheduledEvent { - public int id; - public boolean enabled; - public int sceneId; - public int sceneCollectionId; - public boolean daySunday; - public boolean dayMonday; - public boolean dayTuesday; - public boolean dayWednesday; - public boolean dayThursday; - public boolean dayFriday; - public boolean daySaturday; - public int eventType; - public int hour; - public int minute; - - @Override - public boolean equals(@Nullable Object o) { - if (o == this) { - return true; - } - if (!(o instanceof ScheduledEvent)) { - return false; - } - ScheduledEvent other = (ScheduledEvent) o; - - return this.id == other.id && this.enabled == other.enabled && this.sceneId == other.sceneId - && this.sceneCollectionId == other.sceneCollectionId && this.daySunday == other.daySunday - && this.dayMonday == other.dayMonday && this.dayTuesday == other.dayTuesday - && this.dayWednesday == other.dayWednesday && this.dayThursday == other.dayThursday - && this.dayFriday == other.dayFriday && this.daySaturday == other.daySaturday - && this.eventType == other.eventType && this.hour == other.hour && this.minute == other.minute; - } - - @Override - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + id; - result = prime * result + (enabled ? 1 : 0); - result = prime * result + sceneId; - result = prime * result + sceneCollectionId; - result = prime * result + (daySunday ? 1 : 0); - result = prime * result + (dayMonday ? 1 : 0); - result = prime * result + (dayTuesday ? 1 : 0); - result = prime * result + (dayWednesday ? 1 : 0); - result = prime * result + (dayThursday ? 1 : 0); - result = prime * result + (dayFriday ? 1 : 0); - result = prime * result + (daySaturday ? 1 : 0); - result = prime * result + eventType; - result = prime * result + hour; - result = prime * result + minute; - - return result; - } - - public EnumSet getDays() { - EnumSet days = EnumSet.noneOf(DayOfWeek.class); - if (daySunday) { - days.add(DayOfWeek.SUNDAY); - } - if (dayMonday) { - days.add(DayOfWeek.MONDAY); - } - if (dayTuesday) { - days.add(DayOfWeek.TUESDAY); - } - if (dayWednesday) { - days.add(DayOfWeek.WEDNESDAY); - } - if (dayThursday) { - days.add(DayOfWeek.THURSDAY); - } - if (dayFriday) { - days.add(DayOfWeek.FRIDAY); - } - if (daySaturday) { - days.add(DayOfWeek.SATURDAY); - } - return days; - } - } -} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java deleted file mode 100644 index 2746523b47a97..0000000000000 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shades.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.hdpowerview.internal.api.responses; - -import java.util.Base64; -import java.util.List; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.BatteryKind; -import org.openhab.binding.hdpowerview.internal.api.Firmware; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; - -/** - * State of all Shades, as returned by an HD PowerView hub - * - * @author Andy Lintner - Initial contribution - */ -@NonNullByDefault -public class Shades { - - public @Nullable List shadeData; - public @Nullable List shadeIds; - - /* - * the following SuppressWarnings annotation is because the Eclipse compiler - * does NOT expect a NonNullByDefault annotation on the inner class, since it is - * implicitly inherited from the outer class, whereas the Maven compiler always - * requires an explicit NonNullByDefault annotation on all classes - */ - @SuppressWarnings("null") - @NonNullByDefault - public static class ShadeData { - public int id; - public @Nullable String name; - public int roomId; - public int groupId; - public int order; - public int type; - public double batteryStrength; - public int batteryStatus; - public boolean batteryIsLow; - public @Nullable ShadePosition positions; - public @Nullable Boolean timedOut; - public int signalStrength; - public @Nullable Integer capabilities; - public @Nullable Firmware firmware; - public @Nullable Firmware motor; - // note: in old JSON batteryKind was a string but now it's a number; fortunately GSON string accepts either - public @Nullable String batteryKind; - - public String getName() { - return new String(Base64.getDecoder().decode(name)); - } - - public BatteryKind getBatteryKind() { - return BatteryKind.fromString(batteryKind); - } - } -} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/AutomationChannelBuilder.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/AutomationChannelBuilder.java index 10782b22b991c..214734694685c 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/AutomationChannelBuilder.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/AutomationChannelBuilder.java @@ -25,10 +25,9 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; @@ -193,10 +192,10 @@ private String getScheduledEventName(String sceneName, ScheduledEvent scheduledE String timeString, daysString; switch (scheduledEvent.eventType) { - case ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME: + case ScheduledEvent.SCHEDULED_EVENT_TYPE_TIME: timeString = LocalTime.of(scheduledEvent.hour, scheduledEvent.minute).toString(); break; - case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE: + case ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE: if (scheduledEvent.minute == 0) { timeString = translationProvider.getText("dynamic-channel.automation.at-sunrise"); } else if (scheduledEvent.minute < 0) { @@ -207,7 +206,7 @@ private String getScheduledEventName(String sceneName, ScheduledEvent scheduledE getFormattedTimeOffset(scheduledEvent.minute)); } break; - case ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET: + case ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNSET: if (scheduledEvent.minute == 0) { timeString = translationProvider.getText("dynamic-channel.automation.at-sunset"); } else if (scheduledEvent.minute < 0) { @@ -225,9 +224,9 @@ private String getScheduledEventName(String sceneName, ScheduledEvent scheduledE EnumSet days = scheduledEvent.getDays(); if (EnumSet.allOf(DayOfWeek.class).equals(days)) { daysString = translationProvider.getText("dynamic-channel.automation.all-days"); - } else if (ScheduledEvents.WEEKDAYS.equals(days)) { + } else if (ScheduledEvent.WEEKDAYS.equals(days)) { daysString = translationProvider.getText("dynamic-channel.automation.weekdays"); - } else if (ScheduledEvents.WEEKENDS.equals(days)) { + } else if (ScheduledEvent.WEEKENDS.equals(days)) { daysString = translationProvider.getText("dynamic-channel.automation.weekends"); } else { StringJoiner joiner = new StringJoiner(", "); diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneChannelBuilder.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneChannelBuilder.java index 01c05137e9ece..213a60def07d5 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneChannelBuilder.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneChannelBuilder.java @@ -18,7 +18,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; +import org.openhab.binding.hdpowerview.internal.dto.Scene; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneGroupChannelBuilder.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneGroupChannelBuilder.java index 4cc4436ea2f32..d4dbd2cde6ce6 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneGroupChannelBuilder.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/builders/SceneGroupChannelBuilder.java @@ -18,7 +18,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; import org.openhab.core.library.CoreItemFactory; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/console/HDPowerViewCommandExtension.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/console/HDPowerViewCommandExtension.java index 4252f8d57bb2b..2e16968bab892 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/console/HDPowerViewCommandExtension.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/console/HDPowerViewCommandExtension.java @@ -19,8 +19,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.responses.RepeaterData; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.handler.HDPowerViewHubHandler; import org.openhab.core.io.console.Console; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewDeviceDiscoveryService.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewDeviceDiscoveryService.java index 98d108befcfb1..65bc262365fb6 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/discovery/HDPowerViewDeviceDiscoveryService.java @@ -21,13 +21,13 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.responses.RepeaterData; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewRepeaterConfiguration; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shades; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/BatteryKind.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/BatteryKind.java similarity index 96% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/BatteryKind.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/BatteryKind.java index 229d0abce377c..a3b79bf47f998 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/BatteryKind.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/BatteryKind.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Color.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Color.java similarity index 96% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Color.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Color.java index 39f1d0f47b24f..7179690638457 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Color.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Color.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.library.types.HSBType; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/CoordinateSystem.java similarity index 98% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/CoordinateSystem.java index 236e468947820..47c4a1bb7d2b6 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/CoordinateSystem.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/CoordinateSystem.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Firmware.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Firmware.java similarity index 94% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Firmware.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Firmware.java index fd627c8b9b146..c17d76e99eff2 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Firmware.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Firmware.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/HubFirmware.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/HubFirmware.java similarity index 93% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/HubFirmware.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/HubFirmware.java index 0a625b2f160ff..5deb23bd566b9 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/HubFirmware.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/HubFirmware.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Scene.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Scene.java new file mode 100644 index 0000000000000..daf5601d6eeec --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Scene.java @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * State of a single Scene, as returned by an HD PowerView Hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class Scene implements Comparable { + public int id; + public @Nullable String name; + public int roomId; + public int order; + public int colorId; + public int iconId; + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof Scene)) { + return false; + } + Scene other = (Scene) o; + + return this.id == other.id && Objects.equals(name, other.name) && this.roomId == other.roomId + && this.order == other.order && this.colorId == other.colorId && this.iconId == other.iconId; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + String name = this.name; + result = prime * result + id; + result = prime * result + (name == null ? 0 : name.hashCode()); + result = prime * result + roomId; + result = prime * result + order; + result = prime * result + colorId; + result = prime * result + iconId; + + return result; + } + + @Override + public int compareTo(Scene other) { + return Integer.compare(order, other.order); + } + + public String getName() { + return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SceneCollection.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SceneCollection.java new file mode 100644 index 0000000000000..35e4f3ce07495 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SceneCollection.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * State of a single Scene Collection, as returned by an HD PowerView Hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class SceneCollection implements Comparable { + public int id; + public @Nullable String name; + public int order; + public int colorId; + public int iconId; + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof SceneCollection)) { + return false; + } + SceneCollection other = (SceneCollection) o; + + return this.id == other.id && Objects.equals(name, other.name) && this.order == other.order + && this.colorId == other.colorId && this.iconId == other.iconId; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + String name = this.name; + result = prime * result + id; + result = prime * result + (name == null ? 0 : name.hashCode()); + result = prime * result + order; + result = prime * result + colorId; + result = prime * result + iconId; + + return result; + } + + @Override + public int compareTo(SceneCollection other) { + return Integer.compare(order, other.order); + } + + public String getName() { + return new String(Base64.getDecoder().decode(name), StandardCharsets.UTF_8); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ScheduledEvent.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ScheduledEvent.java new file mode 100644 index 0000000000000..16c9de0c918d7 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ScheduledEvent.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto; + +import java.time.DayOfWeek; +import java.util.EnumSet; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * State of a single Scheduled Event, as returned by an HD PowerView Hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ScheduledEvent { + public static final EnumSet WEEKDAYS = EnumSet.of(DayOfWeek.MONDAY, DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, DayOfWeek.FRIDAY); + + public static final EnumSet WEEKENDS = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY); + + public static final int SCHEDULED_EVENT_TYPE_TIME = 0; + public static final int SCHEDULED_EVENT_TYPE_SUNRISE = 1; + public static final int SCHEDULED_EVENT_TYPE_SUNSET = 2; + + public int id; + public boolean enabled; + public int sceneId; + public int sceneCollectionId; + public boolean daySunday; + public boolean dayMonday; + public boolean dayTuesday; + public boolean dayWednesday; + public boolean dayThursday; + public boolean dayFriday; + public boolean daySaturday; + public int eventType; + public int hour; + public int minute; + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + if (!(o instanceof ScheduledEvent)) { + return false; + } + ScheduledEvent other = (ScheduledEvent) o; + + return this.id == other.id && this.enabled == other.enabled && this.sceneId == other.sceneId + && this.sceneCollectionId == other.sceneCollectionId && this.daySunday == other.daySunday + && this.dayMonday == other.dayMonday && this.dayTuesday == other.dayTuesday + && this.dayWednesday == other.dayWednesday && this.dayThursday == other.dayThursday + && this.dayFriday == other.dayFriday && this.daySaturday == other.daySaturday + && this.eventType == other.eventType && this.hour == other.hour && this.minute == other.minute; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + result = prime * result + (enabled ? 1 : 0); + result = prime * result + sceneId; + result = prime * result + sceneCollectionId; + result = prime * result + (daySunday ? 1 : 0); + result = prime * result + (dayMonday ? 1 : 0); + result = prime * result + (dayTuesday ? 1 : 0); + result = prime * result + (dayWednesday ? 1 : 0); + result = prime * result + (dayThursday ? 1 : 0); + result = prime * result + (dayFriday ? 1 : 0); + result = prime * result + (daySaturday ? 1 : 0); + result = prime * result + eventType; + result = prime * result + hour; + result = prime * result + minute; + + return result; + } + + public EnumSet getDays() { + EnumSet days = EnumSet.noneOf(DayOfWeek.class); + if (daySunday) { + days.add(DayOfWeek.SUNDAY); + } + if (dayMonday) { + days.add(DayOfWeek.MONDAY); + } + if (dayTuesday) { + days.add(DayOfWeek.TUESDAY); + } + if (dayWednesday) { + days.add(DayOfWeek.WEDNESDAY); + } + if (dayThursday) { + days.add(DayOfWeek.THURSDAY); + } + if (dayFriday) { + days.add(DayOfWeek.FRIDAY); + } + if (daySaturday) { + days.add(DayOfWeek.SATURDAY); + } + return days; + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadeData.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadeData.java new file mode 100644 index 0000000000000..033426750b5df --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadeData.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto; + +import java.util.Base64; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Shade data for a single Shade, as returned by an HD PowerView hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ShadeData { + public int id; + public @Nullable String name; + public int roomId; + public int groupId; + public int order; + public int type; + public double batteryStrength; + public int batteryStatus; + public boolean batteryIsLow; + public @Nullable ShadePosition positions; + public @Nullable Boolean timedOut; + public int signalStrength; + public @Nullable Integer capabilities; + public @Nullable Firmware firmware; + public @Nullable Firmware motor; + // note: in old JSON batteryKind was a string but now it's a number; fortunately GSON string accepts either + public @Nullable String batteryKind; + + public String getName() { + return new String(Base64.getDecoder().decode(name)); + } + + public BatteryKind getBatteryKind() { + return BatteryKind.fromString(batteryKind); + } +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadePosition.java similarity index 99% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadePosition.java index 0eee7aff49ce8..d6bced8181e19 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/ShadePosition.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/ShadePosition.java @@ -10,9 +10,9 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; -import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/SurveyData.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SurveyData.java similarity index 94% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/SurveyData.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SurveyData.java index bfb5301bac539..7f3da5ff9c372 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/SurveyData.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/SurveyData.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Times.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Times.java similarity index 94% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Times.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Times.java index 0dbc911bfd94f..33a9bc99f3b91 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/Times.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/Times.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/UserData.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/UserData.java similarity index 96% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/UserData.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/UserData.java index 70c33a9189e31..fd36992246cb7 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/UserData.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/UserData.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api; +package org.openhab.binding.hdpowerview.internal.dto; import java.util.Base64; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterBlinking.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterBlinking.java similarity index 93% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterBlinking.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterBlinking.java index ea3886af1f1c4..e50a308448b90 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterBlinking.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterBlinking.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterColor.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterColor.java similarity index 88% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterColor.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterColor.java index 960e95f5878c9..fc317e7aa1dab 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/RepeaterColor.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/RepeaterColor.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.hdpowerview.internal.api.Color; +import org.openhab.binding.hdpowerview.internal.dto.Color; /** * Color state of a single Repeater for being updated by an HD PowerView Hub diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeCalibrate.java similarity index 91% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeCalibrate.java index e1ec80e48743c..0d1d50ec9dc19 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeCalibrate.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeCalibrate.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeJog.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeJog.java similarity index 91% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeJog.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeJog.java index 71fb0477a09ec..b1317a7eabb32 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeJog.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeJog.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMotion.java similarity index 93% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMotion.java index c6b16a000065e..f5aeb57a53a8d 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMotion.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMotion.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMove.java similarity index 85% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMove.java index fe688f4f75b58..4f87fdf0c3358 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeMove.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeMove.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; /** * A request to set the position of a shade diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadePositions.java similarity index 84% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadePositions.java index 1c544c047e878..fd98950f5d816 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadePositions.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadePositions.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; /** * The position of a shade to set diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeStop.java similarity index 91% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeStop.java index ed7ab4c4b0a8a..34f47f038ca7a 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/requests/ShadeStop.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/requests/ShadeStop.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.requests; +package org.openhab.binding.hdpowerview.internal.dto.requests; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/FirmwareVersion.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/FirmwareVersion.java similarity index 84% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/FirmwareVersion.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/FirmwareVersion.java index d046f06a8d760..34b9f202eb942 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/FirmwareVersion.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/FirmwareVersion.java @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.HubFirmware; +import org.openhab.binding.hdpowerview.internal.dto.HubFirmware; /** * Firmware information for an HD PowerView hub diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeater.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeater.java similarity index 91% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeater.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeater.java index 55ba66665b14f..d743626d9605c 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeater.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeater.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/RepeaterData.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/RepeaterData.java similarity index 84% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/RepeaterData.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/RepeaterData.java index a083f91a35a76..1b789637ddded 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/RepeaterData.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/RepeaterData.java @@ -10,14 +10,14 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import java.util.Base64; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.Color; -import org.openhab.binding.hdpowerview.internal.api.Firmware; +import org.openhab.binding.hdpowerview.internal.dto.Color; +import org.openhab.binding.hdpowerview.internal.dto.Firmware; /** * Repeater data for a single Repeater, as returned by an HD PowerView Hub diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeaters.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeaters.java similarity index 92% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeaters.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeaters.java index 8b5f15b12fd69..e1aa699b49484 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Repeaters.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Repeaters.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import java.util.List; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/SceneCollections.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/SceneCollections.java new file mode 100644 index 0000000000000..582178bc7b57c --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/SceneCollections.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto.responses; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; + +/** + * State of all Scene Collections in an HD PowerView hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class SceneCollections { + public @Nullable List sceneCollectionData; + public @Nullable List sceneCollectionIds; +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Scenes.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Scenes.java new file mode 100644 index 0000000000000..7458d6f90d8e8 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Scenes.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto.responses; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hdpowerview.internal.dto.Scene; + +/** + * State of all Scenes in an HD PowerView hub + * + * @author Andy Lintner - Initial contribution + */ +@NonNullByDefault +public class Scenes { + public @Nullable List sceneData; + public @Nullable List sceneIds; +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/ScheduledEvents.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/ScheduledEvents.java new file mode 100644 index 0000000000000..4b4a8b416a7e6 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/ScheduledEvents.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto.responses; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent; + +/** + * State of all Scheduled Events in an HD PowerView hub + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ScheduledEvents { + public @Nullable List scheduledEventData; + public @Nullable List scheduledEventIds; +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shade.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shade.java similarity index 82% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shade.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shade.java index 69da9378395b7..29b0794587e64 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Shade.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shade.java @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; /** * State of a single Shade, as returned by an HD PowerView hub @@ -23,6 +23,5 @@ */ @NonNullByDefault public class Shade { - public @Nullable ShadeData shade; } diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shades.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shades.java new file mode 100644 index 0000000000000..ab177146eb184 --- /dev/null +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Shades.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hdpowerview.internal.dto.responses; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; + +/** + * State of all Shades, as returned by an HD PowerView hub + * + * @author Andy Lintner - Initial contribution + */ +@NonNullByDefault +public class Shades { + public @Nullable List shadeData; + public @Nullable List shadeIds; +} diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Survey.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Survey.java similarity index 87% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Survey.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Survey.java index 21709fe1a0b9a..e55a1f99f84b4 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/Survey.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/Survey.java @@ -10,13 +10,13 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.SurveyData; +import org.openhab.binding.hdpowerview.internal.dto.SurveyData; import com.google.gson.annotations.SerializedName; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/UserDataResponse.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/UserDataResponse.java similarity index 84% rename from bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/UserDataResponse.java rename to bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/UserDataResponse.java index 3509ffdb85c6f..15c4cbeae7b06 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/api/responses/UserDataResponse.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/dto/responses/UserDataResponse.java @@ -10,11 +10,11 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.internal.api.responses; +package org.openhab.binding.hdpowerview.internal.dto.responses; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.hdpowerview.internal.api.UserData; +import org.openhab.binding.hdpowerview.internal.dto.UserData; /** * Response with {@link UserData} for an HD PowerView hub diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java index b3991f3aad3bd..7cfe8778f0421 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewHubHandler.java @@ -30,22 +30,22 @@ import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.Firmware; -import org.openhab.binding.hdpowerview.internal.api.HubFirmware; -import org.openhab.binding.hdpowerview.internal.api.UserData; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.builders.AutomationChannelBuilder; import org.openhab.binding.hdpowerview.internal.builders.SceneChannelBuilder; import org.openhab.binding.hdpowerview.internal.builders.SceneGroupChannelBuilder; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewHubConfiguration; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; +import org.openhab.binding.hdpowerview.internal.dto.Firmware; +import org.openhab.binding.hdpowerview.internal.dto.HubFirmware; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.UserData; +import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections; +import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes; +import org.openhab.binding.hdpowerview.internal.dto.responses.ScheduledEvents; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shades; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewRepeaterHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewRepeaterHandler.java index 037c85184d167..c8080fa2c354b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewRepeaterHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewRepeaterHandler.java @@ -20,10 +20,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.Color; -import org.openhab.binding.hdpowerview.internal.api.Firmware; -import org.openhab.binding.hdpowerview.internal.api.responses.RepeaterData; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewRepeaterConfiguration; +import org.openhab.binding.hdpowerview.internal.dto.Color; +import org.openhab.binding.hdpowerview.internal.dto.Firmware; +import org.openhab.binding.hdpowerview.internal.dto.responses.RepeaterData; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java index 53e77e0425cdb..f787ad0902caa 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java +++ b/bundles/org.openhab.binding.hdpowerview/src/main/java/org/openhab/binding/hdpowerview/internal/handler/HDPowerViewShadeHandler.java @@ -13,7 +13,7 @@ package org.openhab.binding.hdpowerview.internal.handler; import static org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants.*; -import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*; import java.util.ArrayList; import java.util.HashMap; @@ -29,15 +29,15 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.BatteryKind; -import org.openhab.binding.hdpowerview.internal.api.CoordinateSystem; -import org.openhab.binding.hdpowerview.internal.api.Firmware; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; -import org.openhab.binding.hdpowerview.internal.api.SurveyData; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.config.HDPowerViewShadeConfiguration; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; +import org.openhab.binding.hdpowerview.internal.dto.BatteryKind; +import org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem; +import org.openhab.binding.hdpowerview.internal.dto.Firmware; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.SurveyData; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubInvalidResponseException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/AutomationChannelBuilderTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/AutomationChannelBuilderTest.java similarity index 88% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/AutomationChannelBuilderTest.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/AutomationChannelBuilderTest.java index ba6cb99574fc4..6326ea587e61c 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/AutomationChannelBuilderTest.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/AutomationChannelBuilderTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; @@ -22,15 +22,12 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; -import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents; -import org.openhab.binding.hdpowerview.internal.api.responses.ScheduledEvents.ScheduledEvent; import org.openhab.binding.hdpowerview.internal.builders.AutomationChannelBuilder; -import org.openhab.binding.hdpowerview.providers.MockedLocaleProvider; -import org.openhab.binding.hdpowerview.providers.MockedTranslationProvider; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.ScheduledEvent; +import org.openhab.binding.hdpowerview.internal.providers.MockedLocaleProvider; +import org.openhab.binding.hdpowerview.internal.providers.MockedTranslationProvider; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ThingUID; @@ -78,7 +75,7 @@ private void setUp() { @Test public void sceneSunriseWeekends() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); scheduledEvent.daySaturday = true; scheduledEvent.daySunday = true; @@ -91,7 +88,7 @@ public void sceneSunriseWeekends() { @Test public void sceneSunsetWeekdays() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNSET); scheduledEvent.dayMonday = true; scheduledEvent.dayTuesday = true; scheduledEvent.dayWednesday = true; @@ -107,7 +104,7 @@ public void sceneSunsetWeekdays() { @Test public void sceneTimeAllDays() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_TIME); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_TIME); scheduledEvent.dayMonday = true; scheduledEvent.dayTuesday = true; scheduledEvent.dayWednesday = true; @@ -127,7 +124,7 @@ public void sceneTimeAllDays() { @Test public void sceneMinutesBeforeSunriseMondayTuesday() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); scheduledEvent.dayMonday = true; scheduledEvent.dayTuesday = true; scheduledEvent.minute = -15; @@ -141,7 +138,7 @@ public void sceneMinutesBeforeSunriseMondayTuesday() { @Test public void sceneHoursMinutesAfterSunriseMonday() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); scheduledEvent.dayMonday = true; scheduledEvent.minute = 61; @@ -154,7 +151,7 @@ public void sceneHoursMinutesAfterSunriseMonday() { @Test public void sceneMinutesBeforeSunsetWednesdayThursdayFriday() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNSET); scheduledEvent.dayWednesday = true; scheduledEvent.dayThursday = true; scheduledEvent.dayFriday = true; @@ -169,7 +166,7 @@ public void sceneMinutesBeforeSunsetWednesdayThursdayFriday() { @Test public void sceneHourAfterSunsetFridaySaturdaySunday() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNSET); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNSET); scheduledEvent.dayFriday = true; scheduledEvent.daySaturday = true; scheduledEvent.daySunday = true; @@ -185,7 +182,7 @@ public void sceneHourAfterSunsetFridaySaturdaySunday() { @Test public void sceneCollection() { ScheduledEvent scheduledEvent = createScheduledEventWithSceneCollection( - ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List channels = builder.withSceneCollections(sceneCollections).withScheduledEvents(scheduledEvents) @@ -197,7 +194,7 @@ public void sceneCollection() { @Test public void suppliedListIsUsed() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List existingChannels = new ArrayList<>(0); List channels = builder.withScenes(scenes).withScheduledEvents(scheduledEvents) @@ -215,7 +212,7 @@ public void emptyListWhenNoScheduledEvents() { @Test public void emptyListWhenNoScenesOrSceneCollections() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List channels = builder.withScheduledEvents(scheduledEvents).build(); @@ -225,7 +222,7 @@ public void emptyListWhenNoScenesOrSceneCollections() { @Test public void emptyListWhenNoSceneForScheduledEvent() { ScheduledEvent scheduledEvent = createScheduledEventWithSceneCollection( - ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List channels = builder.withScenes(scenes).withScheduledEvents(scheduledEvents).build(); @@ -234,7 +231,7 @@ public void emptyListWhenNoSceneForScheduledEvent() { @Test public void emptyListWhenNoSceneCollectionForScheduledEvent() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List channels = builder.withSceneCollections(sceneCollections).withScheduledEvents(scheduledEvents) @@ -245,7 +242,7 @@ public void emptyListWhenNoSceneCollectionForScheduledEvent() { @Test public void groupAndIdAreCorrect() { - ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvents.SCHEDULED_EVENT_TYPE_SUNRISE); + ScheduledEvent scheduledEvent = createScheduledEventWithScene(ScheduledEvent.SCHEDULED_EVENT_TYPE_SUNRISE); scheduledEvent.id = 42; List scheduledEvents = new ArrayList<>(List.of(scheduledEvent)); List channels = builder.withScenes(scenes).withScheduledEvents(scheduledEvents).build(); diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/HDPowerViewJUnitTests.java similarity index 91% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/HDPowerViewJUnitTests.java index 19cd35c7c33ee..b28e2cbf7665b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/HDPowerViewJUnitTests.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/HDPowerViewJUnitTests.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*; import java.io.IOException; import java.io.InputStream; @@ -23,16 +23,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.api.BatteryKind; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; +import org.openhab.binding.hdpowerview.internal.dto.BatteryKind; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.responses.SceneCollections; +import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shades; import org.openhab.core.library.types.PercentType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/OnlineCommunicationTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/OnlineCommunicationTest.java similarity index 93% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/OnlineCommunicationTest.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/OnlineCommunicationTest.java index b8670bb91ec92..d5415f675c0e2 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/OnlineCommunicationTest.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/OnlineCommunicationTest.java @@ -10,10 +10,10 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*; import java.util.List; import java.util.regex.Pattern; @@ -21,14 +21,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jetty.client.HttpClient; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.HDPowerViewWebTargets; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades; -import org.openhab.binding.hdpowerview.internal.api.responses.Shades.ShadeData; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.dto.ShadeData; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; +import org.openhab.binding.hdpowerview.internal.dto.responses.Scenes; +import org.openhab.binding.hdpowerview.internal.dto.responses.Shades; import org.openhab.binding.hdpowerview.internal.exceptions.HubException; import org.openhab.binding.hdpowerview.internal.exceptions.HubMaintenanceException; import org.openhab.binding.hdpowerview.internal.exceptions.HubProcessingException; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneChannelBuilderTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneChannelBuilderTest.java similarity index 90% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneChannelBuilderTest.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneChannelBuilderTest.java index eca2a1f2e9ebb..4d6be154a4f37 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneChannelBuilderTest.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneChannelBuilderTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; @@ -22,12 +22,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; -import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.Scenes.Scene; import org.openhab.binding.hdpowerview.internal.builders.SceneChannelBuilder; -import org.openhab.binding.hdpowerview.providers.MockedLocaleProvider; -import org.openhab.binding.hdpowerview.providers.MockedTranslationProvider; +import org.openhab.binding.hdpowerview.internal.dto.Scene; +import org.openhab.binding.hdpowerview.internal.providers.MockedLocaleProvider; +import org.openhab.binding.hdpowerview.internal.providers.MockedTranslationProvider; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ThingUID; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneGroupChannelBuilderTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneGroupChannelBuilderTest.java similarity index 90% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneGroupChannelBuilderTest.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneGroupChannelBuilderTest.java index e2dc8b1b7a0da..fc1344ea8553b 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/SceneGroupChannelBuilderTest.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/SceneGroupChannelBuilderTest.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; @@ -22,12 +22,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.HDPowerViewBindingConstants; -import org.openhab.binding.hdpowerview.internal.HDPowerViewTranslationProvider; -import org.openhab.binding.hdpowerview.internal.api.responses.SceneCollections.SceneCollection; import org.openhab.binding.hdpowerview.internal.builders.SceneGroupChannelBuilder; -import org.openhab.binding.hdpowerview.providers.MockedLocaleProvider; -import org.openhab.binding.hdpowerview.providers.MockedTranslationProvider; +import org.openhab.binding.hdpowerview.internal.dto.SceneCollection; +import org.openhab.binding.hdpowerview.internal.providers.MockedLocaleProvider; +import org.openhab.binding.hdpowerview.internal.providers.MockedTranslationProvider; import org.openhab.core.thing.Channel; import org.openhab.core.thing.ChannelGroupUID; import org.openhab.core.thing.ThingUID; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/ShadePositionTest.java similarity index 99% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/ShadePositionTest.java index 50dcd0e7ff4c2..5ced717f203b0 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/ShadePositionTest.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/ShadePositionTest.java @@ -10,16 +10,16 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview; +package org.openhab.binding.hdpowerview.internal; import static org.junit.jupiter.api.Assertions.*; -import static org.openhab.binding.hdpowerview.internal.api.CoordinateSystem.*; +import static org.openhab.binding.hdpowerview.internal.dto.CoordinateSystem.*; import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.hdpowerview.internal.api.ShadePosition; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase; import org.openhab.binding.hdpowerview.internal.database.ShadeCapabilitiesDatabase.Capabilities; +import org.openhab.binding.hdpowerview.internal.dto.ShadePosition; import org.openhab.core.library.types.PercentType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedLocaleProvider.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedLocaleProvider.java similarity index 92% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedLocaleProvider.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedLocaleProvider.java index 8161cc8e28274..d95879f454ea6 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedLocaleProvider.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedLocaleProvider.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.providers; +package org.openhab.binding.hdpowerview.internal.providers; import java.util.Locale; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedTranslationProvider.java b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedTranslationProvider.java similarity index 97% rename from bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedTranslationProvider.java rename to bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedTranslationProvider.java index da7152ad7504c..9c9503ef1eee7 100644 --- a/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/providers/MockedTranslationProvider.java +++ b/bundles/org.openhab.binding.hdpowerview/src/test/java/org/openhab/binding/hdpowerview/internal/providers/MockedTranslationProvider.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.hdpowerview.providers; +package org.openhab.binding.hdpowerview.internal.providers; import static java.util.Map.entry; diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/duette.json b/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/duette.json similarity index 100% rename from bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/duette.json rename to bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/duette.json diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/sceneCollections.json b/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/sceneCollections.json similarity index 100% rename from bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/sceneCollections.json rename to bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/sceneCollections.json diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/scenes.json b/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/scenes.json similarity index 100% rename from bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/scenes.json rename to bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/scenes.json diff --git a/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/shades.json b/bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/shades.json similarity index 100% rename from bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/shades.json rename to bundles/org.openhab.binding.hdpowerview/src/test/resources/org/openhab/binding/hdpowerview/internal/shades.json From 5654b9f97a1eb44e9fc763521bfab6d2b0e9e6a7 Mon Sep 17 00:00:00 2001 From: bruestel Date: Fri, 4 Nov 2022 18:28:40 +0100 Subject: [PATCH 20/55] [homeconnect] Fix login for simulator environment (#13653) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add redirect URI to oAuth request Signed-off-by: Jonas Brüstel --- .../internal/servlet/HomeConnectServlet.java | 13 ++++++++++--- .../src/main/resources/assets/js/homeconnect.js | 1 + .../src/main/resources/templates/bridges.html | 7 ++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/servlet/HomeConnectServlet.java b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/servlet/HomeConnectServlet.java index 27229541ae780..4c288f1119659 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/servlet/HomeConnectServlet.java +++ b/bundles/org.openhab.binding.homeconnect/src/main/java/org/openhab/binding/homeconnect/internal/servlet/HomeConnectServlet.java @@ -95,6 +95,7 @@ public class HomeConnectServlet extends HttpServlet { private static final String PARAM_BRIDGE_ID = "bridgeId"; private static final String PARAM_THING_ID = "thingId"; private static final String PARAM_PATH = "path"; + private static final String PARAM_REDIRECT_URI = "redirectUri"; private static final String ACTION_AUTHORIZE = "authorize"; private static final String ACTION_CLEAR_CREDENTIALS = "clearCredentials"; private static final String ACTION_SHOW_DETAILS = "show-details"; @@ -392,8 +393,11 @@ private void postBridgesPage(HttpServletRequest request, HttpServletResponse res HomeConnectBridgeHandler bridgeHandler = bridgeHandlerOptional.get(); if (ACTION_AUTHORIZE.equals(action)) { try { - String authorizationUrl = bridgeHandler.getOAuthClientService().getAuthorizationUrl(null, null, - bridgeHandler.getThing().getUID().getAsString()); + String redirectUri = bridgeHandler.getConfiguration().isSimulator() + ? request.getParameter(PARAM_REDIRECT_URI) + : null; + String authorizationUrl = bridgeHandler.getOAuthClientService().getAuthorizationUrl(redirectUri, + null, bridgeHandler.getThing().getUID().getAsString()); logger.debug("Generated authorization url: {}", authorizationUrl); response.sendRedirect(authorizationUrl); @@ -491,8 +495,11 @@ private void getBridgeAuthenticationPage(HttpServletRequest request, HttpServlet Optional bridgeHandler = getBridgeHandler(state); if (bridgeHandler.isPresent()) { try { + String redirectUri = bridgeHandler.get().getConfiguration().isSimulator() + ? request.getRequestURL().toString() + : null; AccessTokenResponse accessTokenResponse = bridgeHandler.get().getOAuthClientService() - .getAccessTokenResponseByAuthorizationCode(code, null); + .getAccessTokenResponseByAuthorizationCode(code, redirectUri); logger.debug("access token response: {}", accessTokenResponse); diff --git a/bundles/org.openhab.binding.homeconnect/src/main/resources/assets/js/homeconnect.js b/bundles/org.openhab.binding.homeconnect/src/main/resources/assets/js/homeconnect.js index 9f7fb05ef11e6..4ffe25d5ca7b0 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/resources/assets/js/homeconnect.js +++ b/bundles/org.openhab.binding.homeconnect/src/main/resources/assets/js/homeconnect.js @@ -6,6 +6,7 @@ feather.replace(); $(".redirectUri").text(window.location.href.substring(0, window.location.href.lastIndexOf('/homeconnect') + 12)); + $(".redirectUriInput").val(window.location.href.substring(0, window.location.href.lastIndexOf('/homeconnect') + 12)); $('#apiDetailModal').on('show.bs.modal', function (event) { var button = $(event.relatedTarget); diff --git a/bundles/org.openhab.binding.homeconnect/src/main/resources/templates/bridges.html b/bundles/org.openhab.binding.homeconnect/src/main/resources/templates/bridges.html index b2bf3086a9c28..091db72e4bfa9 100644 --- a/bundles/org.openhab.binding.homeconnect/src/main/resources/templates/bridges.html +++ b/bundles/org.openhab.binding.homeconnect/src/main/resources/templates/bridges.html @@ -71,12 +71,13 @@

Card subtitle
- +
- - + + +
From 1e07d1af03fff4410b84d950479b1474595fa1e9 Mon Sep 17 00:00:00 2001 From: Silviu Chingaru Date: Fri, 4 Nov 2022 20:01:39 +0200 Subject: [PATCH 21/55] [paradoxalarm] Handle multiple panels (#13641) * Fixes #13640 Working with multiple Paradox panels Tested with: - rename things files; - thing disable / enable on same instance of OpenHab; - disable on one OpenHab instance > enable on other; Everything works as espected. All test passed even if no Login/Logout commands were sent to IP module Signed-off-by: Silviu Chingaru --- .../internal/communication/CommunicationState.java | 2 -- .../discovery/ParadoxDiscoveryService.java | 2 +- .../internal/handlers/EntityBaseHandler.java | 3 ++- .../handlers/ParadoxIP150BridgeHandler.java | 7 ++++++- .../internal/handlers/ParadoxPanelHandler.java | 3 ++- .../internal/handlers/ParadoxPartitionHandler.java | 4 +++- .../internal/handlers/ParadoxZoneHandler.java | 4 +++- .../paradoxalarm/internal/model/Entity.java | 8 +++++++- .../paradoxalarm/internal/model/ParadoxPanel.java | 14 +++----------- .../paradoxalarm/internal/model/Partition.java | 8 +++----- .../binding/paradoxalarm/internal/model/Zone.java | 4 ++-- .../src/test/java/main/Main.java | 9 +++++---- 12 files changed, 37 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/communication/CommunicationState.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/communication/CommunicationState.java index 147ba6477ae61..067501a19593a 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/communication/CommunicationState.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/communication/CommunicationState.java @@ -22,7 +22,6 @@ import org.openhab.binding.paradoxalarm.internal.communication.messages.IPPacket; import org.openhab.binding.paradoxalarm.internal.communication.messages.IpMessagesConstants; import org.openhab.binding.paradoxalarm.internal.communication.messages.ParadoxIPPacket; -import org.openhab.binding.paradoxalarm.internal.model.ParadoxPanel; import org.openhab.binding.paradoxalarm.internal.util.ParadoxUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -368,7 +367,6 @@ protected void runPhase(IParadoxInitialLoginCommunicator communicator, Object... if (communicator != null) { communicator.close(); } - ParadoxPanel.getInstance().dispose(); } }; diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/discovery/ParadoxDiscoveryService.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/discovery/ParadoxDiscoveryService.java index f96073f210f99..a3f388dede866 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/discovery/ParadoxDiscoveryService.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/discovery/ParadoxDiscoveryService.java @@ -53,7 +53,7 @@ public ParadoxDiscoveryService(ParadoxIP150BridgeHandler ip150BridgeHandler) { protected void startScan() { IParadoxCommunicator communicator = ip150BridgeHandler.getCommunicator(); if (communicator != null && communicator.isOnline()) { - ParadoxPanel panel = ParadoxPanel.getInstance(); + ParadoxPanel panel = ip150BridgeHandler.getPanel(); discoverPanel(panel.getPanelInformation()); discoverPartitions(panel.getPartitions()); discoverZones(panel.getZones()); diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/EntityBaseHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/EntityBaseHandler.java index 4d51a9b4166f3..7d5d82f6e8f65 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/EntityBaseHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/EntityBaseHandler.java @@ -59,7 +59,8 @@ public void initialize() { private void initializeDelayed() { logger.debug("Start initializeDelayed() in {}", getThing().getUID()); - ParadoxPanel panel = ParadoxPanel.getInstance(); + ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler(); + ParadoxPanel panel = bridge.getPanel(); // Asynchronous update not yet done if (panel.getPanelInformation() == null) { // Retry until reach MAX_WAIT_TIME diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java index 9082112669780..c107dac3c02a1 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxIP150BridgeHandler.java @@ -69,6 +69,7 @@ public class ParadoxIP150BridgeHandler extends BaseBridgeHandler private final Logger logger = LoggerFactory.getLogger(ParadoxIP150BridgeHandler.class); private IParadoxCommunicator communicator; + private ParadoxPanel panel = new ParadoxPanel(); private ParadoxIP150BridgeConfiguration config; private @Nullable ScheduledFuture refreshCacheUpdateSchedule; @@ -169,7 +170,6 @@ protected void createDiscoveredCommunicatorJob(PanelType panelType) { .withMaxPartitions(config.getMaxPartitions()).withMaxZones(config.getMaxZones()) .withScheduler(scheduler).withEncryption(config.isEncrypt()).build(); - ParadoxPanel panel = ParadoxPanel.getInstance(); panel.setCommunicator(communicator); Collection listeners = Arrays.asList(panel, this); @@ -208,6 +208,7 @@ private void doPostOnlineFinalCommunicatorJob() { public void dispose() { cancelSchedule(refreshCacheUpdateSchedule); CommunicationState.logout(communicator); + panel.dispose(); super.dispose(); } @@ -323,6 +324,10 @@ public IParadoxCommunicator getCommunicator() { return communicator; } + public ParadoxPanel getPanel() { + return panel; + } + @Override public void onSocketTimeOutOccurred(IOException exception) { logger.warn("TIMEOUT! {} received message for socket timeout. ", this, exception); diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPanelHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPanelHandler.java index 54228b9dfef3c..a15b0140894a6 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPanelHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPanelHandler.java @@ -49,7 +49,8 @@ public void initialize() { @Override protected void updateEntity() { - ParadoxPanel panel = ParadoxPanel.getInstance(); + ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler(); + ParadoxPanel panel = bridge.getPanel(); StringType panelState = panel.isOnline() ? STATE_ONLINE : STATE_OFFLINE; updateState(PANEL_STATE_CHANNEL_UID, panelState); ParadoxInformation panelInformation = panel.getPanelInformation(); diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPartitionHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPartitionHandler.java index ebf3280ce79d7..2dd9bcc8be14c 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPartitionHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxPartitionHandler.java @@ -80,7 +80,9 @@ protected void updateEntity() { protected Partition getPartition() { int index = calculateEntityIndex(); - List partitions = ParadoxPanel.getInstance().getPartitions(); + ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler(); + ParadoxPanel panel = bridge.getPanel(); + List partitions = panel.getPartitions(); if (partitions == null) { logger.debug( "Partitions collection of Paradox Panel object is null. Probably not yet initialized. Skipping update."); diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxZoneHandler.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxZoneHandler.java index 5e7e9c5833f6c..8b16cb9e13f50 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxZoneHandler.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/handlers/ParadoxZoneHandler.java @@ -42,7 +42,9 @@ public ParadoxZoneHandler(@NonNull Thing thing) { @Override protected void updateEntity() { int index = calculateEntityIndex(); - List zones = ParadoxPanel.getInstance().getZones(); + ParadoxIP150BridgeHandler bridge = (ParadoxIP150BridgeHandler) getBridge().getHandler(); + ParadoxPanel panel = bridge.getPanel(); + List zones = panel.getZones(); if (zones == null) { logger.debug( "Zones collection of Paradox Panel object is null. Probably not yet initialized. Skipping update."); diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Entity.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Entity.java index 7ddbac3fff57b..64c2efa43188f 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Entity.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Entity.java @@ -24,10 +24,12 @@ public abstract class Entity { private final Logger logger = LoggerFactory.getLogger(Entity.class); + private ParadoxPanel panel; private int id; private String label; - public Entity(int id, String label) { + public Entity(ParadoxPanel panel, int id, String label) { + this.panel = panel; this.id = id; this.label = label.trim(); logger.debug("Creating entity with label: {} and ID: {}", label, id); @@ -49,6 +51,10 @@ public void setLabel(String label) { this.label = label; } + public ParadoxPanel getPanel() { + return panel; + } + @Override public String toString() { return "Entity [id=" + id + ", label=" + label + "]"; diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/ParadoxPanel.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/ParadoxPanel.java index ccd962b4ea1f0..99a8ff69324e0 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/ParadoxPanel.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/ParadoxPanel.java @@ -20,7 +20,6 @@ import java.util.Map; import java.util.TimeZone; -import org.eclipse.jdt.annotation.NonNull; import org.openhab.binding.paradoxalarm.internal.communication.IDataUpdateListener; import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator; import org.openhab.binding.paradoxalarm.internal.exceptions.ParadoxRuntimeException; @@ -38,9 +37,6 @@ public class ParadoxPanel implements IDataUpdateListener { private final Logger logger = LoggerFactory.getLogger(ParadoxPanel.class); - @NonNull - private static ParadoxPanel paradoxPanel = new ParadoxPanel(); - private ParadoxInformation panelInformation; private List partitions; private List zones; @@ -51,7 +47,7 @@ public class ParadoxPanel implements IDataUpdateListener { private double dcLevel; private ZonedDateTime panelTime; - private ParadoxPanel() { + public ParadoxPanel() { this.parser = new EvoParser(); } @@ -69,10 +65,6 @@ public void createModelEntities() { } } - public static ParadoxPanel getInstance() { - return paradoxPanel; - } - public boolean isPanelSupported() { PanelType panelType = panelInformation.getPanelType(); return panelType == PanelType.EVO48 || panelType == PanelType.EVO192 || panelType == PanelType.EVOHD; @@ -123,7 +115,7 @@ private List createZones() { zones = new ArrayList<>(); Map zoneLabels = communicator.getZoneLabels(); for (int i = 0; i < zoneLabels.size(); i++) { - Zone zone = new Zone(i + 1, zoneLabels.get(i)); + Zone zone = new Zone(this, i + 1, zoneLabels.get(i)); zones.add(zone); } return zones; @@ -133,7 +125,7 @@ private List createPartitions() { partitions = new ArrayList<>(); Map partitionLabels = communicator.getPartitionLabels(); for (int i = 0; i < partitionLabels.size(); i++) { - Partition partition = new Partition(i + 1, partitionLabels.get(i)); + Partition partition = new Partition(this, i + 1, partitionLabels.get(i)); partitions.add(partition); logger.debug("Partition {}:\t{}", i + 1, partition.getState().getMainState()); } diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Partition.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Partition.java index 79c6a51b9e6ac..42966d2bc919a 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Partition.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Partition.java @@ -12,7 +12,6 @@ */ package org.openhab.binding.paradoxalarm.internal.model; -import org.openhab.binding.paradoxalarm.internal.communication.IParadoxCommunicator; import org.openhab.binding.paradoxalarm.internal.communication.PartitionCommandRequest; import org.openhab.binding.paradoxalarm.internal.communication.RequestType; import org.openhab.binding.paradoxalarm.internal.communication.messages.CommandPayload; @@ -35,8 +34,8 @@ public class Partition extends Entity implements Commandable { private PartitionState state = new PartitionState(); - public Partition(int id, String label) { - super(id, label); + public Partition(ParadoxPanel panel, int id, String label) { + super(panel, id, label); } public PartitionState getState() { @@ -62,7 +61,6 @@ public void handleCommand(String command) { ParadoxIPPacket packet = new ParadoxIPPacket(payload.getBytes()) .setMessageType(HeaderMessageType.SERIAL_PASSTHRU_REQUEST); PartitionCommandRequest request = new PartitionCommandRequest(RequestType.PARTITION_COMMAND, packet, null); - IParadoxCommunicator communicator = ParadoxPanel.getInstance().getCommunicator(); - communicator.submitRequest(request); + getPanel().getCommunicator().submitRequest(request); } } diff --git a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Zone.java b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Zone.java index 70f6e1cb6f9d9..51ec5028dcf41 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Zone.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/main/java/org/openhab/binding/paradoxalarm/internal/model/Zone.java @@ -28,8 +28,8 @@ public class Zone extends Entity { private ZoneState zoneState; - public Zone(int id, String label) { - super(id, label); + public Zone(ParadoxPanel panel, int id, String label) { + super(panel, id, label); } public ZoneState getZoneState() { diff --git a/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java b/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java index 3236947b9e90e..40dcc75141db4 100644 --- a/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java +++ b/bundles/org.openhab.binding.paradoxalarm/src/test/java/main/Main.java @@ -46,6 +46,7 @@ public class Main { private static ScheduledExecutorService scheduler; private static IParadoxCommunicator communicator; + private static ParadoxPanel panel; public static void main(String[] args) { readArguments(args); @@ -59,14 +60,14 @@ public static void main(String[] args) { .withTcpPort(port).withMaxPartitions(4).withMaxZones(20).withScheduler(scheduler) .withEncryption(true).build(); - ParadoxPanel panel = ParadoxPanel.getInstance(); + panel = new ParadoxPanel(); panel.setCommunicator(communicator); communicator.setListeners(Arrays.asList(panel)); communicator.startLoginSequence(); scheduler.scheduleWithFixedDelay(() -> { - refreshMemoryMap(communicator, false); + refreshMemoryMap(panel, false); }, 7, 5, TimeUnit.SECONDS); } catch (Exception e) { logger.error("Exception: ", e); @@ -74,10 +75,10 @@ public static void main(String[] args) { } } - private static void refreshMemoryMap(IParadoxCommunicator communicator, boolean withEpromValues) { + private static void refreshMemoryMap(ParadoxPanel panel, boolean withEpromValues) { logger.debug("Refreshing memory map"); + IParadoxCommunicator communicator = panel.getCommunicator(); communicator.refreshMemoryMap(); - ParadoxPanel panel = ParadoxPanel.getInstance(); panel.getPartitions().stream().forEach(partition -> logger.debug("Partition={}", partition)); panel.getZones().stream().filter(zone -> zone.getId() == 19).forEach(zone -> logger.debug("Zone={}", zone)); logger.debug("PanelTime={}, ACLevel={}, DCLevel={}, BatteryLevel={}", panel.getPanelTime(), panel.getVdcLevel(), From f880ca91f9294d9d228a7e59e8b4888d9a038b19 Mon Sep 17 00:00:00 2001 From: alexf2015 Date: Fri, 4 Nov 2022 19:56:30 +0100 Subject: [PATCH 22/55] adopted date parser due to changes in EASEE API (#13654) Signed-off-by: Alexander Friese --- .../java/org/openhab/binding/easee/internal/Utils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.easee/src/main/java/org/openhab/binding/easee/internal/Utils.java b/bundles/org.openhab.binding.easee/src/main/java/org/openhab/binding/easee/internal/Utils.java index 586042944ef49..d5fe4f375e954 100644 --- a/bundles/org.openhab.binding.easee/src/main/java/org/openhab/binding/easee/internal/Utils.java +++ b/bundles/org.openhab.binding.easee/src/main/java/org/openhab/binding/easee/internal/Utils.java @@ -56,7 +56,12 @@ private Utils() { * @return */ public static ZonedDateTime parseDate(String date) throws DateTimeParseException { - final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX"); + DateTimeFormatter formatter; + if (date.length() == 24) { + formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX"); + } else { + formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX"); + } LOGGER.trace("parsing: {}", date); ZonedDateTime zdt = ZonedDateTime.parse(date, formatter); return zdt; From 70abb5d1f64efa17958ae32b9baab741efac53d1 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 5 Nov 2022 10:41:31 +0100 Subject: [PATCH 23/55] [jdbc] Add support for case sensitive table names reflecting item names 1:1 (#13544) * Do not append number when using real item names * Extract getTableName to separate class * Add initial test coverage * Extract migration logic to separate class * Support migration from real names back to numbered * Simplify zero-padding * Fix NullPointerException * Fix MySQL compatibility when CLIENT_MULTI_STATEMENTS option is not set * Add option for case sensitive table names * Add real name with suffix mode for backwards compatibility * Remove real name in lower case without suffix mode * Map directly from item name to table name * Fix ambiguous table name scenario * Add additional testcase * Add migration path for changed table prefix * Drop items table when using direct mapping * Add configuration note * Fix table alignment * Extend description as more migration paths are now supported * Do not stop halfway through a migration * For clarity, do not use abbreviation for operating system Signed-off-by: Jacob Laursen --- .../org.openhab.persistence.jdbc/README.md | 66 +-- .../persistence/jdbc/db/JdbcBaseDAO.java | 29 +- .../persistence/jdbc/db/JdbcDerbyDAO.java | 2 +- .../persistence/jdbc/db/JdbcHsqldbDAO.java | 2 +- .../jdbc/db/JdbcPostgresqlDAO.java | 2 +- .../openhab/persistence/jdbc/dto/ItemsVO.java | 48 +- .../jdbc/internal/JdbcConfiguration.java | 20 + .../persistence/jdbc/internal/JdbcMapper.java | 192 ++++---- .../jdbc/internal/JdbcPersistenceService.java | 5 +- .../jdbc/internal/NamingStrategy.java | 120 +++++ .../main/resources/OH-INF/config/config.xml | 17 +- .../resources/OH-INF/i18n/jdbc.properties | 6 +- .../jdbc/internal/NamingStrategyTest.java | 444 ++++++++++++++++++ 13 files changed, 780 insertions(+), 173 deletions(-) create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index 0a828875f8741..1cf0ef8f6903e 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -38,38 +38,39 @@ The following databases are currently supported and tested: This service can be configured in the file `services/jdbc.cfg`. -| Property | Default | Required | Description | -| ------------------------- | ------------------------------------------------------------ | :-------: | ------------------------------------------------------------ | -| url | | Yes | JDBC URL to establish a connection to your database. Examples:

`jdbc:derby:./testDerby;create=true`
`jdbc:h2:./testH2`
`jdbc:hsqldb:./testHsqlDb`
`jdbc:mariadb://192.168.0.1:3306/testMariadb`
`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`
`jdbc:postgresql://192.168.0.1:5432/testPostgresql`
`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`
`jdbc:sqlite:./testSqlite.db`.

If no database is available it will be created; for example the url `jdbc:h2:./testH2` creates a new H2 database in openHAB folder. Example to create your own MySQL database directly:

`CREATE DATABASE 'yourDB' CHARACTER SET utf8 COLLATE utf8_general_ci;` | -| user | | if needed | database user name | -| password | | if needed | database user password | -| errReconnectThreshold | 0 | No | when the service is deactivated (0 means ignore) | -| sqltype.CALL | `VARCHAR(200)` | No | All `sqlType` options allow you to change the SQL data type used to store values for different openHAB item states. See the following links for further information: [mybatis](https://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html) [H2](https://www.h2database.com/html/datatypes.html) [PostgresSQL](https://www.postgresql.org/docs/9.3/static/datatype.html) | -| sqltype.COLOR | `VARCHAR(70)` | No | see above | -| sqltype.CONTACT | `VARCHAR(6)` | No | see above | -| sqltype.DATETIME | `DATETIME` | No | see above | -| sqltype.DIMMER | `TINYINT` | No | see above | -| sqltype.IMAGE | `VARCHAR(65500)` | No | see above | -| sqltype.LOCATION | `VARCHAR(50)` | No | see above | -| sqltype.NUMBER | `DOUBLE` | No | see above | -| sqltype.PLAYER | `VARCHAR(20)` | No | see above | -| sqltype.ROLLERSHUTTER | `TINYINT` | No | see above | -| sqltype.STRING | `VARCHAR(65500)` | No | see above | -| sqltype.SWITCH | `VARCHAR(6)` | No | see above | -| sqltype.tablePrimaryKey | `TIMESTAMP` | No | type of `time` column for newly created item tables | -| sqltype.tablePrimaryValue | `NOW()` | No | value of `time` column for newly inserted rows | -| numberDecimalcount | 3 | No | for Itemtype "Number" default decimal digit count | -| tableNamePrefix | `item` | No | table name prefix. For Migration from MySQL Persistence, set to `Item`. | -| tableUseRealItemNames | `false` | No | table name prefix generation. When set to `true`, real item names are used for table names and `tableNamePrefix` is ignored. When set to `false`, the `tableNamePrefix` is used to generate table names with sequential numbers. | -| tableIdDigitCount | 4 | No | when `tableUseRealItemNames` is `false` and thus table names are generated sequentially, this controls how many zero-padded digits are used in the table name. With the default of 4, the first table name will end with `0001`. For migration from the MySQL persistence service, set this to 0. | -| rebuildTableNames | false | No | rename existing tables using `tableUseRealItemNames` and `tableIdDigitCount`. USE WITH CARE! Deactivate after Renaming is done! | -| jdbc.maximumPoolSize | configured per database in package `org.openhab.persistence.jdbc.db.*` | No | Some embedded databases can handle only one connection. See [this link](https://github.com/brettwooldridge/HikariCP/issues/256) for more information | -| jdbc.minimumIdle | see above | No | see above | -| enableLogTime | `false` | No | timekeeping | +| Property | Default | Required | Description | +| --------------------------- | ------------------------------------------------------------ | :-------: | ------------------------------------------------------------ | +| url | | Yes | JDBC URL to establish a connection to your database. Examples:

`jdbc:derby:./testDerby;create=true`
`jdbc:h2:./testH2`
`jdbc:hsqldb:./testHsqlDb`
`jdbc:mariadb://192.168.0.1:3306/testMariadb`
`jdbc:mysql://192.168.0.1:3306/testMysql?serverTimezone=UTC`
`jdbc:postgresql://192.168.0.1:5432/testPostgresql`
`jdbc:timescaledb://192.168.0.1:5432/testPostgresql`
`jdbc:sqlite:./testSqlite.db`.

If no database is available it will be created; for example the url `jdbc:h2:./testH2` creates a new H2 database in openHAB folder. Example to create your own MySQL database directly:

`CREATE DATABASE 'yourDB' CHARACTER SET utf8 COLLATE utf8_general_ci;` | +| user | | if needed | database user name | +| password | | if needed | database user password | +| errReconnectThreshold | 0 | No | when the service is deactivated (0 means ignore) | +| sqltype.CALL | `VARCHAR(200)` | No | All `sqlType` options allow you to change the SQL data type used to store values for different openHAB item states. See the following links for further information: [mybatis](https://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html) [H2](https://www.h2database.com/html/datatypes.html) [PostgresSQL](https://www.postgresql.org/docs/9.3/static/datatype.html) | +| sqltype.COLOR | `VARCHAR(70)` | No | see above | +| sqltype.CONTACT | `VARCHAR(6)` | No | see above | +| sqltype.DATETIME | `DATETIME` | No | see above | +| sqltype.DIMMER | `TINYINT` | No | see above | +| sqltype.IMAGE | `VARCHAR(65500)` | No | see above | +| sqltype.LOCATION | `VARCHAR(50)` | No | see above | +| sqltype.NUMBER | `DOUBLE` | No | see above | +| sqltype.PLAYER | `VARCHAR(20)` | No | see above | +| sqltype.ROLLERSHUTTER | `TINYINT` | No | see above | +| sqltype.STRING | `VARCHAR(65500)` | No | see above | +| sqltype.SWITCH | `VARCHAR(6)` | No | see above | +| sqltype.tablePrimaryKey | `TIMESTAMP` | No | type of `time` column for newly created item tables | +| sqltype.tablePrimaryValue | `NOW()` | No | value of `time` column for newly inserted rows | +| numberDecimalcount | 3 | No | for Itemtype "Number" default decimal digit count | +| tableNamePrefix | `item` | No | table name prefix. For Migration from MySQL Persistence, set to `Item`. | +| tableUseRealItemNames | `false` | No | table name prefix generation. When set to `true`, real item names are used for table names and `tableNamePrefix` is ignored. When set to `false`, the `tableNamePrefix` is used to generate table names with sequential numbers. | +| tableCaseSensitiveItemNames | `false` | No | table name case when `tableUseRealItemNames` is `true`. When set to `true`, item name case is preserved in table names and no suffix is used. When set to `false`, table names are lower cased and a numeric suffix is added. Please read [this](#case-sensitive-item-names) before enabling. | +| tableIdDigitCount | 4 | No | when `tableUseRealItemNames` is `false` and thus table names are generated sequentially, this controls how many zero-padded digits are used in the table name. With the default of 4, the first table name will end with `0001`. For migration from the MySQL persistence service, set this to 0. | +| rebuildTableNames | false | No | rename existing tables using `tableUseRealItemNames` and `tableIdDigitCount`. USE WITH CARE! Deactivate after Renaming is done! | +| jdbc.maximumPoolSize | configured per database in package `org.openhab.persistence.jdbc.db.*` | No | Some embedded databases can handle only one connection. See [this link](https://github.com/brettwooldridge/HikariCP/issues/256) for more information | +| jdbc.minimumIdle | see above | No | see above | +| enableLogTime | `false` | No | timekeeping | All item- and event-related configuration is done in the file `persistence/jdbc.persist`. -To configure this service as the default persistence service for openHAB 2, add or change the line +To configure this service as the default persistence service for openHAB, add or change the line ``` org.openhab.core.persistence:default=jdbc @@ -85,6 +86,13 @@ services/jdbc.cfg url=jdbc:postgresql://192.168.0.1:5432/testPostgresql ``` +### Case Sensitive Item Names + +To avoid numbered suffixes entirely, `tableUseRealItemNames` and `tableCaseSensitiveItemNames` must both be enabled. +With this configuration, tables are named exactly like their corresponding items. +In order for this to work correctly, the underlying operating system, database server and configuration must support case sensitive table names. +For MySQL, see [MySQL: Identifier Case Sensitivity](https://dev.mysql.com/doc/refman/8.0/en/identifier-case-sensitivity.html) for more information. + ### Migration from MySQL to JDBC Persistence Services The JDBC Persistence service can act as a replacement for the MySQL Persistence service. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index 63ed8717273a3..074530b953810 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -84,8 +84,9 @@ public class JdbcBaseDAO { protected String sqlIfTableExists = "SHOW TABLES LIKE '#searchTable#'"; protected String sqlCreateNewEntryInItemsTable = "INSERT INTO #itemsManageTable# (ItemName) VALUES ('#itemname#')"; protected String sqlCreateItemsTableIfNot = "CREATE TABLE IF NOT EXISTS #itemsManageTable# (ItemId INT NOT NULL AUTO_INCREMENT,#colname# #coltype# NOT NULL,PRIMARY KEY (ItemId))"; + protected String sqlDropItemsTableIfExists = "DROP TABLE IF EXISTS #itemsManageTable#"; protected String sqlDeleteItemsEntry = "DELETE FROM items WHERE ItemName=#itemname#"; - protected String sqlGetItemIDTableNames = "SELECT itemid, itemname FROM #itemsManageTable#"; + protected String sqlGetItemIDTableNames = "SELECT ItemId, ItemName FROM #itemsManageTable#"; protected String sqlGetItemTables = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema='#jdbcUriDatabaseName#' AND NOT table_name='#itemsManageTable#'"; protected String sqlCreateItemTable = "CREATE TABLE IF NOT EXISTS #tableName# (time #tablePrimaryKey# NOT NULL, value #dbType#, PRIMARY KEY(time))"; protected String sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?"; @@ -266,7 +267,7 @@ public boolean doIfTableExists(ItemsVO vo) { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } @@ -280,9 +281,17 @@ public ItemsVO doCreateItemsTableIfNot(ItemsVO vo) { return vo; } + public ItemsVO doDropItemsTableIfExists(ItemsVO vo) { + String sql = StringUtilsExt.replaceArrayMerge(sqlDropItemsTableIfExists, new String[] { "#itemsManageTable#" }, + new String[] { vo.getItemsManageTable() }); + logger.debug("JDBC::doDropItemsTableIfExists sql={}", sql); + Yank.execute(sql, null); + return vo; + } + public void doDeleteItemsEntry(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, new String[] { "#itemname#" }, - new String[] { vo.getItemname() }); + new String[] { vo.getItemName() }); logger.debug("JDBC::doDeleteItemsEntry sql={}", sql); Yank.execute(sql, null); } @@ -306,8 +315,9 @@ public List doGetItemTables(ItemsVO vo) { * ITEM DAOs * *************/ public void doUpdateItemTableNames(List vol) { - if (!vol.isEmpty()) { - String sql = updateItemTableNamesProvider(vol); + logger.debug("JDBC::doUpdateItemTableNames vol.size = {}", vol.size()); + for (ItemVO itemTable : vol) { + String sql = updateItemTableNamesProvider(itemTable); Yank.execute(sql, null); } } @@ -416,13 +426,8 @@ protected String resolveTimeFilter(FilterCriteria filter, ZoneId timeZone) { return filterString; } - private String updateItemTableNamesProvider(List namesList) { - logger.debug("JDBC::updateItemTableNamesProvider namesList.size = {}", namesList.size()); - String queryString = ""; - for (int i = 0; i < namesList.size(); i++) { - ItemVO it = namesList.get(i); - queryString += "ALTER TABLE " + it.getTableName() + " RENAME TO " + it.getNewTableName() + ";"; - } + private String updateItemTableNamesProvider(ItemVO itemTable) { + String queryString = "ALTER TABLE " + itemTable.getTableName() + " RENAME TO " + itemTable.getNewTableName(); logger.debug("JDBC::query queryString = {}", queryString); return queryString; } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java index 4ee6c2d4cedb8..f0d12bff03bdd 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcDerbyDAO.java @@ -123,7 +123,7 @@ public boolean doIfTableExists(ItemsVO vo) { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable().toUpperCase(), vo.getItemname() }); + new String[] { vo.getItemsManageTable().toUpperCase(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java index 2fd77b4c78f36..fe9eb8f78e92e 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcHsqldbDAO.java @@ -101,7 +101,7 @@ public ItemsVO doCreateItemsTableIfNot(ItemsVO vo) { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java index 9c419af8c2c58..974fd8fd0bf7b 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcPostgresqlDAO.java @@ -121,7 +121,7 @@ public ItemsVO doCreateItemsTableIfNot(ItemsVO vo) { public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, - new String[] { vo.getItemsManageTable(), vo.getItemname() }); + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doCreateNewEntryInItemsTable sql={}", sql); return Yank.insert(sql, null); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java index 37ecc22b38d3b..0e203f5981770 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/dto/ItemsVO.java @@ -28,9 +28,9 @@ public class ItemsVO implements Serializable { private String coltype = "VARCHAR(500)"; private String colname = "itemname"; private String itemsManageTable = "items"; - private int itemid; - private String itemname; - private String table_name; + private int itemId; + private String itemName; + private String tableName; private String jdbcUriDatabaseName; public String getColtype() { @@ -57,28 +57,28 @@ public void setItemsManageTable(String itemsManageTable) { this.itemsManageTable = itemsManageTable.replaceAll(STR_FILTER, ""); } - public int getItemid() { - return itemid; + public int getItemId() { + return itemId; } - public void setItemid(int itemid) { - this.itemid = itemid; + public void setItemId(int itemId) { + this.itemId = itemId; } - public String getItemname() { - return itemname; + public String getItemName() { + return itemName; } - public void setItemname(String itemname) { - this.itemname = itemname; + public void setItemName(String itemName) { + this.itemName = itemName; } - public String getTable_name() { - return table_name; + public String getTableName() { + return tableName; } - public void setTable_name(String table_name) { - this.table_name = table_name; + public void setTableName(String tableName) { + this.tableName = tableName; } public String getJdbcUriDatabaseName() { @@ -98,8 +98,8 @@ public void setJdbcUriDatabaseName(String jdbcUriDatabaseName) { public int hashCode() { final int prime = 31; int result = 1; - result = prime * result + ((itemname == null) ? 0 : itemname.hashCode()); - result = prime * result + (itemid ^ (itemid >>> 32)); + result = prime * result + ((itemName == null) ? 0 : itemName.hashCode()); + result = prime * result + (itemId ^ (itemId >>> 32)); return result; } @@ -120,14 +120,14 @@ public boolean equals(Object obj) { return false; } ItemsVO other = (ItemsVO) obj; - if (itemname == null) { - if (other.itemname != null) { + if (itemName == null) { + if (other.itemName != null) { return false; } - } else if (!itemname.equals(other.itemname)) { + } else if (!itemName.equals(other.itemName)) { return false; } - return itemid == other.itemid; + return itemId == other.itemId; } @Override @@ -140,11 +140,11 @@ public String toString() { builder.append(", itemsManageTable="); builder.append(itemsManageTable); builder.append(", itemid="); - builder.append(itemid); + builder.append(itemId); builder.append(", itemname="); - builder.append(itemname); + builder.append(itemName); builder.append(", table_name="); - builder.append(table_name); + builder.append(tableName); builder.append("]"); return builder.toString(); } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java index a2342050c77ab..56f76d745f3ef 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java @@ -57,6 +57,7 @@ public class JdbcConfiguration { // private String password; private int numberDecimalcount = 3; private boolean tableUseRealItemNames = false; + private boolean tableCaseSensitiveItemNames = false; private String tableNamePrefix = "item"; private int tableIdDigitCount = 4; private boolean rebuildTableNames = false; @@ -163,6 +164,12 @@ private boolean updateConfig() { logger.debug("JDBC::updateConfig: tableUseRealItemNames={}", tableUseRealItemNames); } + String lc = (String) configuration.get("tableCaseSensitiveItemNames"); + if (lc != null && !lc.isBlank()) { + tableCaseSensitiveItemNames = Boolean.parseBoolean(lc); + logger.debug("JDBC::updateConfig: tableCaseSensitiveItemNames={}", tableCaseSensitiveItemNames); + } + String td = (String) configuration.get("tableIdDigitCount"); if (td != null && !td.isBlank() && isNumericPattern.matcher(td).matches()) { tableIdDigitCount = Integer.parseInt(td); @@ -363,6 +370,19 @@ public boolean getTableUseRealItemNames() { return tableUseRealItemNames; } + public boolean getTableCaseSensitiveItemNames() { + return tableCaseSensitiveItemNames; + } + + /** + * Checks if real item names (without number suffix) is enabled. + * + * @return true if both tableUseRealItemNames and tableCaseSensitiveItemNames are enabled. + */ + public boolean getTableUseRealCaseSensitiveItemNames() { + return tableUseRealItemNames && tableCaseSensitiveItemNames; + } + public int getTableIdDigitCount() { return tableIdDigitCount; } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java index f3aa81a4dc071..f59e7c8b6bd05 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -53,10 +54,10 @@ public class JdbcMapper { protected int errCnt; protected boolean initialized = false; protected @NonNullByDefault({}) JdbcConfiguration conf; - protected final Map sqlTables = new HashMap<>(); + protected final Map itemNameToTableNameMap = new HashMap<>(); + protected @NonNullByDefault({}) NamingStrategy namingStrategy; private long afterAccessMin = 10000; private long afterAccessMax = 0; - private static final String ITEM_NAME_PATTERN = "[^a-zA-Z_0-9\\-]"; public JdbcMapper(TimeZoneProvider timeZoneProvider) { this.timeZoneProvider = timeZoneProvider; @@ -97,11 +98,19 @@ public String getDB() { return res != null ? res : ""; } + public boolean ifItemsTableExists() { + logger.debug("JDBC::ifItemsTableExists"); + long timerStart = System.currentTimeMillis(); + boolean res = conf.getDBDAO().doIfTableExists(new ItemsVO()); + logTime("doIfTableExists", timerStart, System.currentTimeMillis()); + return res; + } + public ItemsVO createNewEntryInItemsTable(ItemsVO vo) { logger.debug("JDBC::createNewEntryInItemsTable"); long timerStart = System.currentTimeMillis(); Long i = conf.getDBDAO().doCreateNewEntryInItemsTable(vo); - vo.setItemid(i.intValue()); + vo.setItemId(i.intValue()); logTime("doCreateNewEntryInItemsTable", timerStart, System.currentTimeMillis()); return vo; } @@ -114,6 +123,14 @@ public boolean createItemsTableIfNot(ItemsVO vo) { return true; } + public boolean dropItemsTableIfExists(ItemsVO vo) { + logger.debug("JDBC::dropItemsTableIfExists"); + long timerStart = System.currentTimeMillis(); + conf.getDBDAO().doDropItemsTableIfExists(vo); + logTime("doDropItemsTableIfExists", timerStart, System.currentTimeMillis()); + return true; + } + public ItemsVO deleteItemsEntry(ItemsVO vo) { logger.debug("JDBC::deleteItemsEntry"); long timerStart = System.currentTimeMillis(); @@ -252,47 +269,66 @@ protected boolean checkDBAccessability() { * DATABASE TABLEHANDLING * **************************/ protected void checkDBSchema() { - // Create Items Table if does not exist - createItemsTableIfNot(new ItemsVO()); + if (!conf.getTableUseRealCaseSensitiveItemNames()) { + createItemsTableIfNot(new ItemsVO()); + } if (conf.getRebuildTableNames()) { formatTableNames(); + + if (conf.getTableUseRealCaseSensitiveItemNames()) { + dropItemsTableIfExists(new ItemsVO()); + } logger.info( "JDBC::checkDBSchema: Rebuild complete, configure the 'rebuildTableNames' setting to 'false' to stop rebuilds on startup"); - } else { // Reset the error counter errCnt = 0; + } + populateItemNameToTableNameMap(); + } + + private void populateItemNameToTableNameMap() { + itemNameToTableNameMap.clear(); + if (conf.getTableUseRealCaseSensitiveItemNames()) { + for (String itemName : getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList())) { + itemNameToTableNameMap.put(itemName, itemName); + } + } else { for (ItemsVO vo : getItemIDTableNames()) { - sqlTables.put(vo.getItemname(), getTableName(vo.getItemid(), vo.getItemname())); + itemNameToTableNameMap.put(vo.getItemName(), + namingStrategy.getTableName(vo.getItemId(), vo.getItemName())); } } } protected String getTable(Item item) { - int rowId = 0; + int itemId = 0; ItemsVO isvo; ItemVO ivo; String itemName = item.getName(); - String tableName = sqlTables.get(itemName); + String tableName = itemNameToTableNameMap.get(itemName); // Table already exists - return the name - if (tableName != null) { + if (!Objects.isNull(tableName)) { return tableName; } logger.debug("JDBC::getTable: no table found for item '{}' in sqlTables", itemName); - // Create a new entry in items table - isvo = new ItemsVO(); - isvo.setItemname(itemName); - isvo = createNewEntryInItemsTable(isvo); - rowId = isvo.getItemid(); - if (rowId == 0) { - logger.error("JDBC::getTable: Creating table for item '{}' failed.", itemName); + if (!conf.getTableUseRealCaseSensitiveItemNames()) { + // Create a new entry in items table + isvo = new ItemsVO(); + isvo.setItemName(itemName); + isvo = createNewEntryInItemsTable(isvo); + itemId = isvo.getItemId(); + if (itemId == 0) { + logger.error("JDBC::getTable: Creating items entry for item '{}' failed.", itemName); + } } + // Create the table name - logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", rowId, itemName); - tableName = getTableName(rowId, itemName); + logger.debug("JDBC::getTable: getTableName with rowId={} itemName={}", itemId, itemName); + tableName = namingStrategy.getTableName(itemId, itemName); // Create table for item String dataType = conf.getDBDAO().getDataType(item); @@ -301,18 +337,8 @@ protected String getTable(Item item) { ivo = createItemTable(ivo); logger.debug("JDBC::getTable: Table created for item '{}' with dataType {} in SQL database.", itemName, dataType); - sqlTables.put(itemName, tableName); - - // Check if the new entry is in the table list - // If it's not in the list, then there was an error and we need to do - // some tidying up - // The item needs to be removed from the index table to avoid duplicates - if (sqlTables.get(itemName) == null) { - logger.error("JDBC::getTable: Item '{}' was not added to the table - removing index", itemName); - isvo = new ItemsVO(); - isvo.setItemname(itemName); - deleteItemsEntry(isvo); - } + + itemNameToTableNameMap.put(itemName, tableName); return tableName; } @@ -323,93 +349,57 @@ private void formatTableNames() { initialized = false; } - Map tableIds = new HashMap<>(); - - // - for (ItemsVO vo : getItemIDTableNames()) { - String t = getTableName(vo.getItemid(), vo.getItemname()); - sqlTables.put(vo.getItemname(), t); - tableIds.put(vo.getItemid(), t); - } + List itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList(); + List itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList()); + List oldNewTableNames; - // - List al = getItemTables(); - - String oldName = ""; - String newName = ""; - List oldNewTablenames = new ArrayList<>(); - for (int i = 0; i < al.size(); i++) { - int id = -1; - oldName = al.get(i).getTable_name(); - logger.info("JDBC::formatTableNames: found Table Name= {}", oldName); - - if (oldName.startsWith(conf.getTableNamePrefix()) && !oldName.contains("_")) { - id = Integer.parseInt(oldName.substring(conf.getTableNamePrefix().length())); - logger.info("JDBC::formatTableNames: found Table with Prefix '{}' Name= {} id= {}", - conf.getTableNamePrefix(), oldName, (id)); - } else if (oldName.contains("_")) { - id = Integer.parseInt(oldName.substring(oldName.lastIndexOf("_") + 1)); - logger.info("JDBC::formatTableNames: found Table Name= {} id= {}", oldName, (id)); + if (itemIdTableNames.isEmpty()) { + // Without mappings we can only migrate from direct item name to numeric mapping. + if (conf.getTableUseRealCaseSensitiveItemNames()) { + logger.info("JDBC::formatTableNames: Nothing to migrate."); + initialized = tmpinit; + return; } - logger.info("JDBC::formatTableNames: found Table id= {}", id); - - newName = tableIds.get(id); - logger.info("JDBC::formatTableNames: found Table newName= {}", newName); - - if (newName != null) { - if (!oldName.equalsIgnoreCase(newName)) { - oldNewTablenames.add(new ItemVO(oldName, newName)); - logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", oldName, newName); + oldNewTableNames = new ArrayList<>(); + for (String itemName : itemTables) { + ItemsVO isvo = new ItemsVO(); + isvo.setItemName(itemName); + isvo = createNewEntryInItemsTable(isvo); + int itemId = isvo.getItemId(); + if (itemId == 0) { + logger.error("JDBC::formatTableNames: Creating items entry for item '{}' failed.", itemName); } else { - logger.info("JDBC::formatTableNames: Table oldName='{}' newName='{}' nothing to rename", oldName, - newName); + String newTableName = namingStrategy.getTableName(itemId, itemName); + oldNewTableNames.add(new ItemVO(itemName, newTableName)); + logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", itemName, newTableName); } - } else { - logger.error("JDBC::formatTableNames: Table '{}' could NOT be renamed to '{}'", oldName, newName); - break; } - } + } else { + String itemsManageTable = new ItemsVO().getItemsManageTable(); + Map itemIdToItemNameMap = new HashMap<>(); - updateItemTableNames(oldNewTablenames); - logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTablenames.size()); + for (ItemsVO vo : itemIdTableNames) { + int itemId = vo.getItemId(); + String itemName = vo.getItemName(); + itemIdToItemNameMap.put(itemId, itemName); + } - initialized = tmpinit; - } + oldNewTableNames = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, itemsManageTable); + } - private String getTableName(int rowId, String itemName) { - return getTableNamePrefix(itemName) + formatRight(rowId, conf.getTableIdDigitCount()); - } + updateItemTableNames(oldNewTableNames); + logger.info("JDBC::formatTableNames: Finished updating {} item table names", oldNewTableNames.size()); - private String getTableNamePrefix(String itemName) { - String name = conf.getTableNamePrefix(); - if (conf.getTableUseRealItemNames()) { - // Create the table name with real Item Names - name = (itemName.replaceAll(ITEM_NAME_PATTERN, "") + "_").toLowerCase(); - } - return name; + initialized = tmpinit; } public Set getItems() { // TODO: in general it would be possible to query the count, earliest and latest values for each item too but it // would be a very costly operation - return sqlTables.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName)) + return itemNameToTableNameMap.keySet().stream().map(itemName -> new JdbcPersistenceItemInfo(itemName)) .collect(Collectors. toSet()); } - private static String formatRight(final Object value, final int len) { - final String valueAsString = String.valueOf(value); - if (valueAsString.length() < len) { - final StringBuffer result = new StringBuffer(len); - for (int i = len - valueAsString.length(); i > 0; i--) { - result.append('0'); - } - result.append(valueAsString); - return result.toString(); - } else { - return valueAsString; - } - } - /***************** * H E L P E R S * *****************/ diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java index 20a0fff89ece5..bac25c3e5a11e 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java @@ -206,7 +206,7 @@ public Iterable query(FilterCriteria filter) { } } - String table = sqlTables.get(itemName); + String table = itemNameToTableNameMap.get(itemName); if (table == null) { logger.debug("JDBC::query: unable to find table for item with name: '{}', no data in database.", itemName); return List.of(); @@ -229,6 +229,7 @@ public void updateConfig(Map configuration) { conf = new JdbcConfiguration(configuration); if (conf.valid && checkDBAccessability()) { + namingStrategy = new NamingStrategy(conf); checkDBSchema(); // connection has been established ... initialization completed! initialized = true; @@ -259,7 +260,7 @@ public boolean remove(FilterCriteria filter) throws IllegalArgumentException { throw new IllegalArgumentException("Item name must not be null"); } - String table = sqlTables.get(itemName); + String table = itemNameToTableNameMap.get(itemName); if (table == null) { logger.debug("JDBC::remove: unable to find table for item with name: '{}', no data in database.", itemName); return false; diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java new file mode 100644 index 0000000000000..3fcf9f065fa1c --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/NamingStrategy.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.items.ItemUtil; +import org.openhab.persistence.jdbc.dto.ItemVO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This class manages strategy for table names. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class NamingStrategy { + + private final Logger logger = LoggerFactory.getLogger(NamingStrategy.class); + + private JdbcConfiguration configuration; + + public NamingStrategy(JdbcConfiguration configuration) { + this.configuration = configuration; + } + + public String getTableName(int itemId, String itemName) { + if (!ItemUtil.isValidItemName(itemName)) { + throw new IllegalArgumentException(itemName + " is not a valid item name"); + } + if (configuration.getTableUseRealItemNames()) { + return formatTableName(itemName, itemId); + } else { + return configuration.getTableNamePrefix() + getSuffix(itemId); + } + } + + private String formatTableName(String itemName, int itemId) { + if (configuration.getTableCaseSensitiveItemNames()) { + return itemName; + } else { + return itemName.toLowerCase() + "_" + getSuffix(itemId); + } + } + + private String getSuffix(int itemId) { + int digits = configuration.getTableIdDigitCount(); + if (digits > 0) { + return String.format("%0" + configuration.getTableIdDigitCount() + "d", itemId); + } else { + return String.valueOf(itemId); + } + } + + public List prepareMigration(List itemTables, Map itemIdToItemNameMap, + String itemsManageTable) { + List oldNewTableNames = new ArrayList<>(); + Map tableNameToItemIdMap = new HashMap<>(); + + for (Entry entry : itemIdToItemNameMap.entrySet()) { + String itemName = entry.getValue(); + tableNameToItemIdMap.put(itemName, entry.getKey()); + } + + for (String oldName : itemTables) { + Integer itemIdBoxed = tableNameToItemIdMap.get(oldName); + int itemId = -1; + + if (Objects.nonNull(itemIdBoxed)) { + itemId = itemIdBoxed; + logger.info("JDBC::formatTableNames: found by name; table name= {} id= {}", oldName, itemId); + } else { + try { + itemId = Integer.parseInt(oldName.replaceFirst("^.*\\D", "")); + logger.info("JDBC::formatTableNames: found by id; table name= {} id= {}", oldName, itemId); + } catch (NumberFormatException e) { + // Fall through. + } + } + + String itemName = itemIdToItemNameMap.get(itemId); + + if (!Objects.isNull(itemName)) { + String newName = getTableName(itemId, itemName); + if (newName.equalsIgnoreCase(itemsManageTable)) { + logger.error( + "JDBC::formatTableNames: Table '{}' could NOT be renamed to '{}' since it conflicts with manage table", + oldName, newName); + } else if (!oldName.equals(newName)) { + oldNewTableNames.add(new ItemVO(oldName, newName)); + logger.info("JDBC::formatTableNames: Table '{}' will be renamed to '{}'", oldName, newName); + } else { + logger.info("JDBC::formatTableNames: Table oldName='{}' newName='{}' nothing to rename", oldName, + newName); + } + } else { + logger.error("JDBC::formatTableNames: Table '{}' could NOT be renamed for id '{}'", oldName, itemId); + } + } + + return oldNewTableNames; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml index 31d16a61bdcaa..cc8411376b4f3 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/config/config.xml @@ -143,6 +143,11 @@ #tableUseRealItemNames= tableUseRealItemNames=true + # Tablename Prefix generation, using case sensitive item names (optional, default: disabled -> table names are lower cased + # with numeric suffix appended). + # If true, no suffix is used. + #tableCaseSensitiveItemNames=true + # Tablename Suffix length (optional, default: 4 -> 0001-9999) # for Migration from MYSQL-Bundle set to 0. #tableIdDigitCount= @@ -165,6 +170,15 @@ + + + + If true, no suffix is used. (optional, default: disabled -> table names are lower cased with numeric suffix appended).]]> + + + + + (optional, default: 4 -> 0001-9999).
@@ -172,7 +186,8 @@
- + USE WITH CARE! Deactivate after renaming is done!]]> diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties index 110e4b8d9b9f6..670c072cca840 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc.properties @@ -9,7 +9,7 @@ persistence.config.jdbc.minimumIdle.description = Overrides min idle database co persistence.config.jdbc.password.label = Database Password persistence.config.jdbc.password.description = Defines the database password. persistence.config.jdbc.rebuildTableNames.label = Tablename Rebuild -persistence.config.jdbc.rebuildTableNames.description = Rename existing tables using 'Tablename Realname Generation' and 'Tablename Suffix ID Count', (optional, default: disabled).
USE WITH CARE! Deactivate after renaming is done! +persistence.config.jdbc.rebuildTableNames.description = Rename existing tables using 'Tablename Prefix String', 'Tablename Realname Generation', 'Tablename Case Sensitive' and 'Tablename Suffix ID Count'. (optional, default: disabled).
USE WITH CARE! Deactivate after renaming is done! persistence.config.jdbc.rebuildTableNames.option.true = Enable persistence.config.jdbc.rebuildTableNames.option.false = Disable persistence.config.jdbc.sqltype.CALL.label = SqlType CALL @@ -36,6 +36,10 @@ persistence.config.jdbc.sqltype.STRING.label = SqlType STRING persistence.config.jdbc.sqltype.STRING.description = Overrides used JDBC/SQL datatype for STRING
(optional, default: "VARCHAR(65500)"). persistence.config.jdbc.sqltype.SWITCH.label = SqlType SWITCH persistence.config.jdbc.sqltype.SWITCH.description = Overrides used JDBC/SQL datatype for SWITCH
(optional, default: "VARCHAR(6)"). +persistence.config.jdbc.tableCaseSensitiveItemNames.label = Tablename Case Sensitive +persistence.config.jdbc.tableCaseSensitiveItemNames.description = Enables Tablename generation with case sensitive item names case when "Tablename Realname Generation" is enabled
If true, no suffix is used. (optional, default: disabled -> table names are lower cased with numeric suffix appended). +persistence.config.jdbc.tableCaseSensitiveItemNames.option.true = Enable +persistence.config.jdbc.tableCaseSensitiveItemNames.option.false = Disable persistence.config.jdbc.tableIdDigitCount.label = Tablename Suffix ID Count persistence.config.jdbc.tableIdDigitCount.description = Tablename Suffix ID Count
(optional, default: 4 -> 0001-9999).
For migration from MYSQL-Bundle set to 0. persistence.config.jdbc.tableNamePrefix.label = Tablename Prefix String diff --git a/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java new file mode 100644 index 0000000000000..257bf97acf568 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/test/java/org/openhab/persistence/jdbc/internal/NamingStrategyTest.java @@ -0,0 +1,444 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.internal; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.persistence.jdbc.dto.ItemVO; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +/** + * Tests the {@link NamingStrategy} class. + * + * @author Jacob Laursen - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +@NonNullByDefault +public class NamingStrategyTest { + private static final String ITEMS_MANAGE_TABLE_NAME = "items"; + + private @Mock @NonNullByDefault({}) JdbcConfiguration configurationMock; + private NamingStrategy namingStrategy = new NamingStrategy(configurationMock); + + @BeforeEach + public void initialize() { + final Logger logger = (Logger) LoggerFactory.getLogger(NamingStrategy.class); + logger.setLevel(Level.OFF); + namingStrategy = new NamingStrategy(configurationMock); + } + + @Test + public void getTableNameWhenInvalidItemNameThrows() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + namingStrategy.getTableName(1, "4Two"); + }); + } + + @Test + public void getTableNameWhenUseRealItemNamesNameIsLowerCaseAndNumbered() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "Test"), is("test_1")); + } + + @Test + public void getTableNameWhenUseRealCaseSensitiveItemNamesNameIsSameCase() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "Camel"), is("Camel")); + } + + @Test + public void getTableNameWhenUseRealCaseSensitiveItemNamesNameIsSameCaseLower() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + assertThat(namingStrategy.getTableName(1, "lower"), is("lower")); + } + + @Test + public void getTableNameWhenNotUseRealItemNamesAndCount4NameHasLeavingZeros() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(4).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + assertThat(namingStrategy.getTableName(2, "Test"), is("Item0002")); + } + + @Test + public void getTableNameWhenNotUseRealItemNamesAndCount0() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(0).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + assertThat(namingStrategy.getTableName(12345, "Test"), is("Item12345")); + } + + @Test + public void prepareMigrationFromNumberedToRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertTableName(actual, "Test"); + } + + @Test + public void prepareMigrationWithChangedPrefix() { + Mockito.doReturn(0).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigration(itemId, itemName, tableName, "item"); + + assertTableName(actual, "item1"); + } + + @Test + public void prepareMigrationShouldNotStopWhenEncounteringUnknownItem() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item2"); + itemTables.add("Item3"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("First")); + assertThat(actual.get(1).getNewTableName(), is("Third")); + } + + @Test + public void prepareMigrationFromMixedNumberedToNumberedRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(3); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(2, "Second"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item002"); + itemTables.add("third_0003"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(3)); + assertThat(actual.get(0).getNewTableName(), is("first_1")); + assertThat(actual.get(1).getNewTableName(), is("second_2")); + assertThat(actual.get(2).getNewTableName(), is("third_3")); + } + + @Test + public void prepareMigrationFromMixedNumberedToCaseSensitiveRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(3); + itemIdToItemNameMap.put(1, "First"); + itemIdToItemNameMap.put(2, "Second"); + itemIdToItemNameMap.put(3, "Third"); + + List itemTables = new ArrayList(3); + itemTables.add("Item1"); + itemTables.add("Item002"); + itemTables.add("third_0003"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(3)); + assertThat(actual.get(0).getNewTableName(), is("First")); + assertThat(actual.get(1).getNewTableName(), is("Second")); + assertThat(actual.get(2).getNewTableName(), is("Third")); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToCaseSensitiveRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName, true); + + assertTableName(actual, "Test"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedRealNames() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Test"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName, false); + + assertTableName(actual, "test_0001"); + } + + @Test + public void prepareMigrationRealNamesWithTwoItemsWithDifferentCaseToNumbered() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(1).when(configurationMock).getTableIdDigitCount(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("MyItem"); + itemTables.add("myItem"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("Item1")); + assertThat(actual.get(1).getNewTableName(), is("Item2")); + } + + @Test + public void prepareMigrationNumberedWithTwoItemsWithDifferentCaseToNumberedRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(false).when(configurationMock).getTableCaseSensitiveItemNames(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("Item1"); + itemTables.add("Item2"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("myitem_1")); + assertThat(actual.get(1).getNewTableName(), is("myitem_2")); + } + + @Test + public void prepareMigrationNumberedWithTwoItemsWithDifferentCaseToCaseSensitiveRealNames() { + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + Mockito.doReturn(true).when(configurationMock).getTableCaseSensitiveItemNames(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "MyItem"); + itemIdToItemNameMap.put(2, "myItem"); + + List itemTables = new ArrayList(2); + itemTables.add("Item1"); + itemTables.add("Item2"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("MyItem")); + assertThat(actual.get(1).getNewTableName(), is("myItem")); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToCaseSensitiveRealNamesWhenUnknownItemIdThenSkip() { + final int itemId = 2; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertThat(actual.size(), is(0)); + } + + @Test + public void prepareMigrationFromNumberedRealNamesToNumbered() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "test_0001"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromNumberedToNumberedWithCorrectPadding() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Item1"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName, 2); + + assertTableName(actual, "Item01"); + } + + @Test + public void prepareMigrationFromNumberedToNumberedExceedingPadding() { + final int itemId = 101; + final String itemName = "Test"; + final String tableName = "Item0101"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName, 2); + + assertTableName(actual, "Item101"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumbered() { + final int itemId = 1; + final String itemName = "Test"; + final String tableName = "Test"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedHavingUnderscore() { + final int itemId = 1; + final String itemName = "My_Test"; + final String tableName = "My_Test"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0001"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesHavingUnderscoreAndNumberToNumbered() { + final int itemId = 2; + final String itemName = "My_Test_1"; + final String tableName = "My_Test_1"; + + List actual = prepareMigrationNumbered(itemId, itemName, tableName); + + assertTableName(actual, "Item0002"); + } + + @Test + public void prepareMigrationFromCaseSensitiveRealNamesToNumberedShouldSwap() { + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn("Item").when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = new HashMap<>(2); + itemIdToItemNameMap.put(1, "Item2"); + itemIdToItemNameMap.put(2, "Item1"); + + List itemTables = new ArrayList(2); + itemTables.add("Item2"); + itemTables.add("Item1"); + + List actual = namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + + assertThat(actual.size(), is(2)); + assertThat(actual.get(0).getNewTableName(), is("Item1")); + assertThat(actual.get(1).getNewTableName(), is("Item2")); + } + + @Test + public void prepareMigrationWhenConflictWithItemsManageTableThenSkip() { + final int itemId = 1; + final String itemName = "items"; + final String tableName = "Item1"; + + List actual = prepareMigrationRealItemNames(itemId, itemName, tableName); + + assertThat(actual.size(), is(0)); + } + + private List prepareMigrationNumbered(int itemId, String itemName, String tableName) { + return prepareMigrationNumbered(itemId, itemName, tableName, 4); + } + + private List prepareMigrationNumbered(int itemId, String itemName, String tableName, + int tableIdDigitCount) { + Mockito.doReturn(tableIdDigitCount).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(false).when(configurationMock).getTableUseRealItemNames(); + return prepareMigration(itemId, itemName, tableName); + } + + private List prepareMigrationRealItemNames(int itemId, String itemName, String tableName) { + return prepareMigrationRealItemNames(itemId, itemName, tableName, true); + } + + private List prepareMigrationRealItemNames(int itemId, String itemName, String tableName, + boolean caseSensitive) { + Mockito.doReturn(4).when(configurationMock).getTableIdDigitCount(); + Mockito.doReturn(true).when(configurationMock).getTableUseRealItemNames(); + Mockito.doReturn(caseSensitive).when(configurationMock).getTableCaseSensitiveItemNames(); + return prepareMigration(itemId, itemName, tableName); + } + + private List prepareMigration(int itemId, String itemName, String tableName) { + return prepareMigration(itemId, itemName, tableName, "Item"); + } + + private List prepareMigration(int itemId, String itemName, String tableName, String prefix) { + Mockito.doReturn(prefix).when(configurationMock).getTableNamePrefix(); + + Map itemIdToItemNameMap = getItemIdToItemNameMap(itemId, itemName); + List itemTables = getItemTables(tableName); + + return namingStrategy.prepareMigration(itemTables, itemIdToItemNameMap, ITEMS_MANAGE_TABLE_NAME); + } + + private Map getItemIdToItemNameMap(int itemId, String itemName) { + Map itemIdToItemNameMap = new HashMap<>(1); + itemIdToItemNameMap.put(itemId, itemName); + return itemIdToItemNameMap; + } + + private List getItemTables(String tableName) { + List itemTables = new ArrayList(1); + itemTables.add(tableName); + return itemTables; + } + + private void assertTableName(List actual, String expected) { + assertThat(actual.size(), is(1)); + assertThat(actual.get(0).getNewTableName(), is(expected)); + } +} From a5823d8df071d14bcd228e7a6ff21f099636af7f Mon Sep 17 00:00:00 2001 From: Leif Bladt Date: Sat, 5 Nov 2022 13:10:08 +0100 Subject: [PATCH 24/55] Add support for Dreame L10 Pro (#12803) Signed-off-by: Leif Bladt --- bundles/org.openhab.binding.miio/README.md | 100 ++ .../binding/miio/internal/MiIoDevices.java | 1 + .../resources/OH-INF/i18n/basic.properties | 81 ++ .../database/dreame.vacuum.p2029-miot.json | 1072 +++++++++++++++++ 4 files changed, 1254 insertions(+) create mode 100644 bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2029-miot.json diff --git a/bundles/org.openhab.binding.miio/README.md b/bundles/org.openhab.binding.miio/README.md index d1e5208e79388..85d5044a6e0a4 100644 --- a/bundles/org.openhab.binding.miio/README.md +++ b/bundles/org.openhab.binding.miio/README.md @@ -231,6 +231,7 @@ Currently the miio binding supports more than 330 different models. | Dreame Robot Vacuum D9 | miio:basic | [dreame.vacuum.p2009](#dreame-vacuum-p2009) | Yes | | | Dreame Bot W10 | miio:basic | [dreame.vacuum.p2027](#dreame-vacuum-p2027) | Experimental | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | Dreame Bot Z10 Pro | miio:basic | [dreame.vacuum.p2028](#dreame-vacuum-p2028) | Experimental | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | +| Dreame Bot L10 Pro | miio:basic | [dreame.vacuum.p2029](#dreame-vacuum-p2029) | Yes | | | Trouver Robot LDS Vacuum-Mop Finder | miio:basic | [dreame.vacuum.p2036](#dreame-vacuum-p2036) | Yes | | | Mi Robot Vacuum-Mop 2 Pro+ | miio:basic | [dreame.vacuum.p2041o](#dreame-vacuum-p2041o) | Experimental | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | | MOVA Z500 Robot Vacuum and Mop Cleaner | miio:basic | [dreame.vacuum.p2156o](#dreame-vacuum-p2156o) | Experimental | Experimental support. Please report back if all channels are functional. Preferably share the debug log of property refresh and command responses | @@ -1253,6 +1254,54 @@ Note, not all the values need to be in the json file, e.g. a subset of the param | clean_times | Number | Collect Dust - Clean Times | | | dust_enable | Number | Collect Dust - Dust Enable | Value mapping `["0"="Disable","1"="Enable"]` | +### Dreame Bot L10 Pro (dreame.vacuum.p2029) Channels + +| Channel | Type | Description | Comment | +|----------------------|----------------------|------------------------------------------|------------| +| actions | String | Actions | | +| status | Number | Robot Cleaner - Status | Value mapping `["1"="Sweeping","2"="Idle","3"="Paused","4"="Error","5"="Go Charging","6"="Charging","7"="Mopping"]` | +| fault | Number | Robot Cleaner - Device Fault | | +| battery_level | Number:Dimensionless | Battery - Battery Level | | +| charging_state | Number | Battery - Charging State | Value mapping `["1"="Charging","2"="Not Charging","5"="Go Charging"]` | +| brush_left_time | Number:Time | Main Cleaning Brush - Brush Left Time | | +| brush_life_level | Number:Dimensionless | Main Cleaning Brush - Brush Life Level | | +| brush_left_time1 | Number:Time | Side Cleaning Brush - Brush Left Time | | +| brush_life_level1 | Number:Dimensionless | Side Cleaning Brush - Brush Life Level | | +| filter_life_level | Number:Dimensionless | Filter - Filter Life Level | | +| filter_left_time | Number:Time | Filter - Filter Left Time | | +| work_mode | Number | Vacuum Extend - Work Mode | | +| cleaning_time | Number:Time | Vacuum Extend - Cleaning Time | | +| cleaning_area | Number | Vacuum Extend - Cleaning Area | | +| cleaning_mode | Number | Vacuum Extend - Cleaning Mode | Value mapping `["0"="Silent","1"="Standard","2"="Strong","3"="Turbo"]` | +| mop_mode | Number | Vacuum Extend - Mop Mode | Value mapping `["1"="Low","2"="Medium","3"="High"]` | +| waterbox_status | Number | Vacuum Extend - Waterbox Status | Value mapping `["0"="Removed","1"="Installed"]` | +| task_status | Number | Vacuum Extend - Task Status | | +| clean_extend_data | String | Vacuum Extend - Clean Extend Data | | +| break_point_restart | Number | Vacuum Extend - Break Point Restart | Value mapping `["0"="Off","1"="On"]` | +| carpet_press | Number | Vacuum Extend - Carpet Press | Value mapping `["0"="On","1"="Off"]` | +| serial_number | String | Vacuum Extend - Serial Number | | +| remote_state | String | Vacuum Extend - Remote State | | +| clean_rags_tip | Number:Time | Vacuum Extend - Clean Rags Tip | | +| keep_sweeper_time | Number:Time | Vacuum Extend - Keep Sweeper Time | | +| faults | String | Vacuum Extend - Faults | | +| enable | Switch | Do Not Disturb - Enable | | +| start_time | String | Do Not Disturb - Start Time | | +| end_time | String | Do Not Disturb - End Time | | +| frame_info | String | Map - Frame Info | | +| map_extend_data | String | Map - Map Extend Data | | +| mult_map_state | Number | Map - Mult Map State | Value mapping `["0"="Close","1"="Open"]` | +| mult_map_info | String | Map - Mult Map Info | | +| volume | Number | Audio - Volume | | +| voice_packet_id | String | Audio - Voice Packet Id | | +| voice_change_state | String | Audio - Voice Change State | | +| set_voice | String | Audio - Set Voice | | +| time_zone | String | Time - Time Zone | | +| timer_clean | String | Time - Timer Clean | | +| first_clean_time | Number | Clean Logs - First Clean Time | | +| total_clean_time | Number:Time | Clean Logs - Total Clean Time | | +| total_clean_times | Number | Clean Logs - Total Clean Times | | +| total_clean_area | Number | Clean Logs - Total Clean Area | | + ### Trouver Robot LDS Vacuum-Mop Finder (dreame.vacuum.p2036) Channels | Channel | Type | Description | Comment | @@ -6584,6 +6633,57 @@ Number clean_times "Collect Dust - Clean Times" (G_vacuum) {channel="miio:basic: Number dust_enable "Collect Dust - Dust Enable" (G_vacuum) {channel="miio:basic:vacuum:dust_enable"} ``` +### Dreame Bot L10 Pro (dreame.vacuum.p2029) item file lines + +note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. + +``` +Group G_vacuum "Dreame Bot L10 Pro" +String actions "Actions" (G_vacuum) {channel="miio:basic:vacuum:actions"} +Number status "Robot Cleaner - Status" (G_vacuum) {channel="miio:basic:vacuum:status"} +Number fault "Robot Cleaner - Device Fault" (G_vacuum) {channel="miio:basic:vacuum:fault"} +Number:Dimensionless battery_level "Battery - Battery Level" (G_vacuum) {channel="miio:basic:vacuum:battery_level"} +Number charging_state "Battery - Charging State" (G_vacuum) {channel="miio:basic:vacuum:charging_state"} +Number:Time brush_left_time "Main Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time"} +Number:Dimensionless brush_life_level "Main Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level"} +Number:Time brush_left_time1 "Side Cleaning Brush - Brush Left Time" (G_vacuum) {channel="miio:basic:vacuum:brush_left_time1"} +Number:Dimensionless brush_life_level1 "Side Cleaning Brush - Brush Life Level" (G_vacuum) {channel="miio:basic:vacuum:brush_life_level1"} +Number:Dimensionless filter_life_level "Filter - Filter Life Level" (G_vacuum) {channel="miio:basic:vacuum:filter_life_level"} +Number:Time filter_left_time "Filter - Filter Left Time" (G_vacuum) {channel="miio:basic:vacuum:filter_left_time"} +Number work_mode "Vacuum Extend - Work Mode" (G_vacuum) {channel="miio:basic:vacuum:work_mode"} +Number:Time cleaning_time "Vacuum Extend - Cleaning Time" (G_vacuum) {channel="miio:basic:vacuum:cleaning_time"} +Number cleaning_area "Vacuum Extend - Cleaning Area" (G_vacuum) {channel="miio:basic:vacuum:cleaning_area"} +Number cleaning_mode "Vacuum Extend - Cleaning Mode" (G_vacuum) {channel="miio:basic:vacuum:cleaning_mode"} +Number mop_mode "Vacuum Extend - Mop Mode" (G_vacuum) {channel="miio:basic:vacuum:mop_mode"} +Number waterbox_status "Vacuum Extend - Waterbox Status" (G_vacuum) {channel="miio:basic:vacuum:waterbox_status"} +Number task_status "Vacuum Extend - Task Status" (G_vacuum) {channel="miio:basic:vacuum:task_status"} +String clean_extend_data "Vacuum Extend - Clean Extend Data" (G_vacuum) {channel="miio:basic:vacuum:clean_extend_data"} +Number break_point_restart "Vacuum Extend - Break Point Restart" (G_vacuum) {channel="miio:basic:vacuum:break_point_restart"} +Number carpet_press "Vacuum Extend - Carpet Press" (G_vacuum) {channel="miio:basic:vacuum:carpet_press"} +String serial_number "Vacuum Extend - Serial Number" (G_vacuum) {channel="miio:basic:vacuum:serial_number"} +String remote_state "Vacuum Extend - Remote State" (G_vacuum) {channel="miio:basic:vacuum:remote_state"} +Number:Time clean_rags_tip "Vacuum Extend - Clean Rags Tip" (G_vacuum) {channel="miio:basic:vacuum:clean_rags_tip"} +Number:Time keep_sweeper_time "Vacuum Extend - Keep Sweeper Time" (G_vacuum) {channel="miio:basic:vacuum:keep_sweeper_time"} +String faults "Vacuum Extend - Faults" (G_vacuum) {channel="miio:basic:vacuum:faults"} +Switch enable "Do Not Disturb - Enable" (G_vacuum) {channel="miio:basic:vacuum:enable"} +String start_time "Do Not Disturb - Start Time" (G_vacuum) {channel="miio:basic:vacuum:start_time"} +String end_time "Do Not Disturb - End Time" (G_vacuum) {channel="miio:basic:vacuum:end_time"} +String frame_info "Map - Frame Info" (G_vacuum) {channel="miio:basic:vacuum:frame_info"} +String map_extend_data "Map - Map Extend Data" (G_vacuum) {channel="miio:basic:vacuum:map_extend_data"} +Number mult_map_state "Map - Mult Map State" (G_vacuum) {channel="miio:basic:vacuum:mult_map_state"} +String mult_map_info "Map - Mult Map Info" (G_vacuum) {channel="miio:basic:vacuum:mult_map_info"} +Number volume "Audio - Volume" (G_vacuum) {channel="miio:basic:vacuum:volume"} +String voice_packet_id "Audio - Voice Packet Id" (G_vacuum) {channel="miio:basic:vacuum:voice_packet_id"} +String voice_change_state "Audio - Voice Change State" (G_vacuum) {channel="miio:basic:vacuum:voice_change_state"} +String set_voice "Audio - Set Voice" (G_vacuum) {channel="miio:basic:vacuum:set_voice"} +String time_zone "Time - Time Zone" (G_vacuum) {channel="miio:basic:vacuum:time_zone"} +String timer_clean "Time - Timer Clean" (G_vacuum) {channel="miio:basic:vacuum:timer_clean"} +Number first_clean_time "Clean Logs - First Clean Time" (G_vacuum) {channel="miio:basic:vacuum:first_clean_time"} +Number:Time total_clean_time "Clean Logs - Total Clean Time" (G_vacuum) {channel="miio:basic:vacuum:total_clean_time"} +Number total_clean_times "Clean Logs - Total Clean Times" (G_vacuum) {channel="miio:basic:vacuum:total_clean_times"} +Number total_clean_area "Clean Logs - Total Clean Area" (G_vacuum) {channel="miio:basic:vacuum:total_clean_area"} +``` + ### Trouver Robot LDS Vacuum-Mop Finder (dreame.vacuum.p2036) item file lines note: Autogenerated example. Replace the id (vacuum) in the channel with your own. Replace `basic` with `generic` in the thing UID depending on how your thing was discovered. diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java index d1b06a82d3d37..593524bbb7780 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java @@ -68,6 +68,7 @@ public enum MiIoDevices { DREAME_VACUUM_P2009("dreame.vacuum.p2009", "Dreame Robot Vacuum D9 ", THING_TYPE_BASIC), DREAME_VACUUM_P2027("dreame.vacuum.p2027", "Dreame Bot W10", THING_TYPE_BASIC), DREAME_VACUUM_P2028("dreame.vacuum.p2028", "Dreame Bot Z10 Pro", THING_TYPE_BASIC), + DREAME_VACUUM_P2029("dreame.vacuum.p2029", "Dreame Bot L10 Pro", THING_TYPE_BASIC), DREAME_VACUUM_P2036("dreame.vacuum.p2036", "Trouver Robot LDS Vacuum-Mop Finder", THING_TYPE_BASIC), DREAME_VACUUM_P2041O("dreame.vacuum.p2041o", "Mi Robot Vacuum-Mop 2 Pro+", THING_TYPE_BASIC), DREAME_VACUUM_P2156O("dreame.vacuum.p2156o", "MOVA Z500 Robot Vacuum and Mop Cleaner", THING_TYPE_BASIC), diff --git a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties index 2fa611200fe38..558fce83bc6fc 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties +++ b/bundles/org.openhab.binding.miio/src/main/resources/OH-INF/i18n/basic.properties @@ -45,6 +45,7 @@ thing.dreame.vacuum.p2008 = Dreame Robot Vacuum-Mop F9 thing.dreame.vacuum.p2009 = Dreame Robot Vacuum D9 thing.dreame.vacuum.p2027 = Dreame Bot W10 thing.dreame.vacuum.p2028 = Dreame Bot Z10 Pro +thing.dreame.vacuum.p2029 = Dreame Bot L10 Pro thing.dreame.vacuum.p2036 = Trouver Robot LDS Vacuum-Mop Finder thing.dreame.vacuum.p2041o = Mi Robot Vacuum-Mop 2 Pro+ thing.dreame.vacuum.p2156o = MOVA Z500 Robot Vacuum and Mop Cleaner @@ -775,6 +776,49 @@ ch.dreame.vacuum.p2028-miot.voice_packet_id = Audio - Voice Packet Id ch.dreame.vacuum.p2028-miot.volume = Audio - Volume ch.dreame.vacuum.p2028-miot.waterbox_status = Vacuum Extend - Waterbox Status ch.dreame.vacuum.p2028-miot.work_mode = Vacuum Extend - Work Mode +ch.dreame.vacuum.p2029-miot.actions = Actions +ch.dreame.vacuum.p2029-miot.battery_level = Battery - Battery Level +ch.dreame.vacuum.p2029-miot.break_point_restart = Vacuum Extend - Break Point Restart +ch.dreame.vacuum.p2029-miot.brush_left_time = Main Cleaning Brush - Brush Left Time +ch.dreame.vacuum.p2029-miot.brush_left_time1 = Side Cleaning Brush - Brush Left Time +ch.dreame.vacuum.p2029-miot.brush_life_level = Main Cleaning Brush - Brush Life Level +ch.dreame.vacuum.p2029-miot.brush_life_level1 = Side Cleaning Brush - Brush Life Level +ch.dreame.vacuum.p2029-miot.carpet_press = Vacuum Extend - Carpet Press +ch.dreame.vacuum.p2029-miot.charging_state = Battery - Charging State +ch.dreame.vacuum.p2029-miot.clean_extend_data = Vacuum Extend - Clean Extend Data +ch.dreame.vacuum.p2029-miot.clean_rags_tip = Vacuum Extend - Clean Rags Tip +ch.dreame.vacuum.p2029-miot.cleaning_area = Vacuum Extend - Cleaning Area +ch.dreame.vacuum.p2029-miot.cleaning_mode = Vacuum Extend - Cleaning Mode +ch.dreame.vacuum.p2029-miot.cleaning_time = Vacuum Extend - Cleaning Time +ch.dreame.vacuum.p2029-miot.enable = Do Not Disturb - Enable +ch.dreame.vacuum.p2029-miot.end_time = Do Not Disturb - End Time +ch.dreame.vacuum.p2029-miot.fault = Robot Cleaner - Device Fault +ch.dreame.vacuum.p2029-miot.faults = Vacuum Extend - Faults +ch.dreame.vacuum.p2029-miot.filter_left_time = Filter - Filter Left Time +ch.dreame.vacuum.p2029-miot.filter_life_level = Filter - Filter Life Level +ch.dreame.vacuum.p2029-miot.first_clean_time = Clean Logs - First Clean Time +ch.dreame.vacuum.p2029-miot.frame_info = Map - Frame Info +ch.dreame.vacuum.p2029-miot.keep_sweeper_time = Vacuum Extend - Keep Sweeper Time +ch.dreame.vacuum.p2029-miot.map_extend_data = Map - Map Extend Data +ch.dreame.vacuum.p2029-miot.mop_mode = Vacuum Extend - Mop Mode +ch.dreame.vacuum.p2029-miot.mult_map_info = Map - Mult Map Info +ch.dreame.vacuum.p2029-miot.mult_map_state = Map - Mult Map State +ch.dreame.vacuum.p2029-miot.remote_state = Vacuum Extend - Remote State +ch.dreame.vacuum.p2029-miot.serial_number = Vacuum Extend - Serial Number +ch.dreame.vacuum.p2029-miot.set_voice = Audio - Set Voice +ch.dreame.vacuum.p2029-miot.start_time = Do Not Disturb - Start Time +ch.dreame.vacuum.p2029-miot.status = Robot Cleaner - Status +ch.dreame.vacuum.p2029-miot.task_status = Vacuum Extend - Task Status +ch.dreame.vacuum.p2029-miot.time_zone = Time - Time Zone +ch.dreame.vacuum.p2029-miot.timer_clean = Time - Timer Clean +ch.dreame.vacuum.p2029-miot.total_clean_area = Clean Logs - Total Clean Area +ch.dreame.vacuum.p2029-miot.total_clean_time = Clean Logs - Total Clean Time +ch.dreame.vacuum.p2029-miot.total_clean_times = Clean Logs - Total Clean Times +ch.dreame.vacuum.p2029-miot.voice_change_state = Audio - Voice Change State +ch.dreame.vacuum.p2029-miot.voice_packet_id = Audio - Voice Packet Id +ch.dreame.vacuum.p2029-miot.volume = Audio - Volume +ch.dreame.vacuum.p2029-miot.waterbox_status = Vacuum Extend - Waterbox Status +ch.dreame.vacuum.p2029-miot.work_mode = Vacuum Extend - Work Mode ch.dreame.vacuum.p2156o-miot.actions = Actions ch.dreame.vacuum.p2156o-miot.battery_level = Battery - Battery Level ch.dreame.vacuum.p2156o-miot.break_point_restart = Vacuum Extend - Break Point Restart @@ -2381,6 +2425,43 @@ option.dreame.vacuum.p2028-miot.task_status-3 = SelectAreanClean option.dreame.vacuum.p2028-miot.task_status-4 = SpotArea option.dreame.vacuum.p2028-miot.waterbox_status-0 = Status 0 option.dreame.vacuum.p2028-miot.waterbox_status-1 = Status 1 +option.dreame.vacuum.p2029-miot.actions-audio-play-sound = Audio Play Sound +option.dreame.vacuum.p2029-miot.actions-audio-position = Audio Position +option.dreame.vacuum.p2029-miot.actions-battery-start-charge = Battery Start Charge +option.dreame.vacuum.p2029-miot.actions-brush-cleaner-reset-brush-life = Brush Cleaner Reset Brush Life +option.dreame.vacuum.p2029-miot.actions-filter-reset-filter-life = Filter Reset Filter Life +option.dreame.vacuum.p2029-miot.actions-map-map-req = Map Map Req +option.dreame.vacuum.p2029-miot.actions-map-update-map = Map Update Map +option.dreame.vacuum.p2029-miot.actions-time-delete-timer = Time Delete Timer +option.dreame.vacuum.p2029-miot.actions-vacuum-extend-start-clean = Vacuum Extend Start Clean +option.dreame.vacuum.p2029-miot.actions-vacuum-extend-stop-clean = Vacuum Extend Stop Clean +option.dreame.vacuum.p2029-miot.actions-vacuum-start-sweep = Vacuum Start Sweep +option.dreame.vacuum.p2029-miot.actions-vacuum-stop-sweeping = Vacuum Stop Sweeping +option.dreame.vacuum.p2029-miot.break_point_restart-0 = Off +option.dreame.vacuum.p2029-miot.break_point_restart-1 = On +option.dreame.vacuum.p2029-miot.carpet_press-0 = On +option.dreame.vacuum.p2029-miot.carpet_press-1 = Off +option.dreame.vacuum.p2029-miot.charging_state-1 = Charging +option.dreame.vacuum.p2029-miot.charging_state-2 = Not Charging +option.dreame.vacuum.p2029-miot.charging_state-5 = Go Charging +option.dreame.vacuum.p2029-miot.cleaning_mode-0 = Silent +option.dreame.vacuum.p2029-miot.cleaning_mode-1 = Standard +option.dreame.vacuum.p2029-miot.cleaning_mode-2 = Strong +option.dreame.vacuum.p2029-miot.cleaning_mode-3 = Turbo +option.dreame.vacuum.p2029-miot.mop_mode-1 = Low +option.dreame.vacuum.p2029-miot.mop_mode-2 = Medium +option.dreame.vacuum.p2029-miot.mop_mode-3 = High +option.dreame.vacuum.p2029-miot.mult_map_state-0 = Close +option.dreame.vacuum.p2029-miot.mult_map_state-1 = Open +option.dreame.vacuum.p2029-miot.status-1 = Sweeping +option.dreame.vacuum.p2029-miot.status-2 = Idle +option.dreame.vacuum.p2029-miot.status-3 = Paused +option.dreame.vacuum.p2029-miot.status-4 = Error +option.dreame.vacuum.p2029-miot.status-5 = Go Charging +option.dreame.vacuum.p2029-miot.status-6 = Charging +option.dreame.vacuum.p2029-miot.status-7 = Mopping +option.dreame.vacuum.p2029-miot.waterbox_status-0 = Removed +option.dreame.vacuum.p2029-miot.waterbox_status-1 = Installed option.dreame.vacuum.p2156o-miot.actions-audio-play-sound = Audio Play Sound option.dreame.vacuum.p2156o-miot.actions-audio-position = Audio Position option.dreame.vacuum.p2156o-miot.actions-battery-start-charge = Start Charge diff --git a/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2029-miot.json b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2029-miot.json new file mode 100644 index 0000000000000..c581c8424738f --- /dev/null +++ b/bundles/org.openhab.binding.miio/src/main/resources/database/dreame.vacuum.p2029-miot.json @@ -0,0 +1,1072 @@ +{ + "deviceMapping": { + "id": [ + "dreame.vacuum.p2029" + ], + "propertyMethod": "get_properties", + "maxProperties": 1, + "channels": [ + { + "property": "", + "friendlyName": "Actions", + "channel": "actions", + "type": "String", + "stateDescription": { + "options": [ + { + "value": "vacuum-start-sweep", + "label": "Vacuum Start Sweep" + }, + { + "value": "vacuum-stop-sweeping", + "label": "Vacuum Stop Sweeping" + }, + { + "value": "battery-start-charge", + "label": "Battery Start Charge" + }, + { + "value": "brush-cleaner-reset-brush-life", + "label": "Brush Cleaner Reset Brush Life" + }, + { + "value": "brush-cleaner-reset-brush-life", + "label": "Brush Cleaner Reset Brush Life" + }, + { + "value": "filter-reset-filter-life", + "label": "Filter Reset Filter Life" + }, + { + "value": "vacuum-extend-start-clean", + "label": "Vacuum Extend Start Clean" + }, + { + "value": "vacuum-extend-stop-clean", + "label": "Vacuum Extend Stop Clean" + }, + { + "value": "map-map-req", + "label": "Map Map Req" + }, + { + "value": "map-update-map", + "label": "Map Update Map" + }, + { + "value": "audio-position", + "label": "Audio Position" + }, + { + "value": "audio-play-sound", + "label": "Audio Play Sound" + }, + { + "value": "time-delete-timer", + "label": "Time Delete Timer" + } + ] + }, + "refresh": false, + "actions": [ + { + "command": "action", + "parameterType": "EMPTY", + "siid": 2, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-start-sweep" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 2, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-stop-sweeping" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 3, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "battery-start-charge" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 9, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "brush-cleaner-reset-brush-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 10, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "brush-cleaner-reset-brush-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 11, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "filter-reset-filter-life" + } + ] + } + }, + { + "command": "action", + "parameterType": "UNKNOWN", + "parameters": [ + 10.0 + ], + "siid": 4, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-extend-start-clean" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 4, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "vacuum-extend-stop-clean" + } + ] + } + }, + { + "command": "action", + "parameterType": "UNKNOWN", + "parameters": [ + 2.0 + ], + "siid": 6, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "map-map-req" + } + ] + } + }, + { + "command": "action", + "parameterType": "UNKNOWN", + "parameters": [ + 4.0 + ], + "siid": 6, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "map-update-map" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 7, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "audio-position" + } + ] + } + }, + { + "command": "action", + "parameterType": "EMPTY", + "siid": 7, + "aiid": 2, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "audio-play-sound" + } + ] + } + }, + { + "command": "action", + "parameterType": "UNKNOWN", + "parameters": [ + 3.0 + ], + "siid": 8, + "aiid": 1, + "condition": { + "name": "matchValue", + "parameters": [ + { + "matchValue": "time-delete-timer" + } + ] + } + } + ] + }, + { + "property": "status", + "siid": 2, + "piid": 1, + "friendlyName": "Robot Cleaner - Status", + "channel": "status", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "Sweeping" + }, + { + "value": "2", + "label": "Idle" + }, + { + "value": "3", + "label": "Paused" + }, + { + "value": "4", + "label": "Error" + }, + { + "value": "5", + "label": "Go Charging" + }, + { + "value": "6", + "label": "Charging" + }, + { + "value": "7", + "label": "Mopping" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"Sweeping\",\"2\"\u003d\"Idle\",\"3\"\u003d\"Paused\",\"4\"\u003d\"Error\",\"5\"\u003d\"Go Charging\",\"6\"\u003d\"Charging\",\"7\"\u003d\"Mopping\"]" + }, + { + "property": "fault", + "siid": 2, + "piid": 2, + "friendlyName": "Robot Cleaner - Device Fault", + "channel": "fault", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "battery-level", + "siid": 3, + "piid": 1, + "friendlyName": "Battery - Battery Level", + "channel": "battery_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "charging-state", + "siid": 3, + "piid": 2, + "friendlyName": "Battery - Charging State", + "channel": "charging_state", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "1", + "label": "Charging" + }, + { + "value": "2", + "label": "Not Charging" + }, + { + "value": "5", + "label": "Go Charging" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"1\"\u003d\"Charging\",\"2\"\u003d\"Not Charging\",\"5\"\u003d\"Go Charging\"]" + }, + { + "property": "brush-left-time", + "siid": 9, + "piid": 1, + "friendlyName": "Main Cleaning Brush - Brush Left Time", + "channel": "brush_left_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 300, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-life-level", + "siid": 9, + "piid": 2, + "friendlyName": "Main Cleaning Brush - Brush Life Level", + "channel": "brush_life_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-left-time1", + "siid": 10, + "piid": 1, + "friendlyName": "Side Cleaning Brush - Brush Left Time", + "channel": "brush_left_time1", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 200, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "brush-life-level1", + "siid": 10, + "piid": 2, + "friendlyName": "Side Cleaning Brush - Brush Life Level", + "channel": "brush_life_level1", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-life-level", + "siid": 11, + "piid": 1, + "friendlyName": "Filter - Filter Life Level", + "channel": "filter_life_level", + "type": "Number:Dimensionless", + "unit": "percentage", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "filter-left-time", + "siid": 11, + "piid": 2, + "friendlyName": "Filter - Filter Left Time", + "channel": "filter_left_time", + "type": "Number:Time", + "unit": "hours", + "stateDescription": { + "minimum": 0, + "maximum": 150, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "work-mode", + "siid": 4, + "piid": 1, + "friendlyName": "Vacuum Extend - Work Mode", + "channel": "work_mode", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 50, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-time", + "siid": 4, + "piid": 2, + "friendlyName": "Vacuum Extend - Cleaning Time", + "channel": "cleaning_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": 0, + "maximum": 32767, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-area", + "siid": 4, + "piid": 3, + "friendlyName": "Vacuum Extend - Cleaning Area", + "channel": "cleaning_area", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 32767, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "cleaning-mode", + "siid": 4, + "piid": 4, + "friendlyName": "Vacuum Extend - Cleaning Mode", + "channel": "cleaning_mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Silent" + }, + { + "value": "1", + "label": "Standard" + }, + { + "value": "2", + "label": "Strong" + }, + { + "value": "3", + "label": "Turbo" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Silent\",\"1\"\u003d\"Standard\",\"2\"\u003d\"Strong\",\"3\"\u003d\"Turbo\"]" + }, + { + "property": "mop-mode", + "siid": 4, + "piid": 5, + "friendlyName": "Vacuum Extend - Mop Mode", + "channel": "mop_mode", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "1", + "label": "Low" + }, + { + "value": "2", + "label": "Medium" + }, + { + "value": "3", + "label": "High" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ], + "readmeComment": "Value mapping [\"1\"\u003d\"Low\",\"2\"\u003d\"Medium\",\"3\"\u003d\"High\"]" + }, + { + "property": "waterbox-status", + "siid": 4, + "piid": 6, + "friendlyName": "Vacuum Extend - Waterbox Status", + "channel": "waterbox_status", + "type": "Number", + "stateDescription": { + "readOnly": true, + "options": [ + { + "value": "0", + "label": "Removed" + }, + { + "value": "1", + "label": "Installed" + } + ] + }, + "refresh": true, + "actions": [], + "readmeComment": "Value mapping [\"0\"\u003d\"Removed\",\"1\"\u003d\"Installed\"]" + }, + { + "property": "task-status", + "siid": 4, + "piid": 7, + "friendlyName": "Vacuum Extend - Task Status", + "channel": "task_status", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 20, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "clean-extend-data", + "siid": 4, + "piid": 10, + "friendlyName": "Vacuum Extend - Clean Extend Data", + "channel": "clean_extend_data", + "type": "String", + "refresh": false, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "break-point-restart", + "siid": 4, + "piid": 11, + "friendlyName": "Vacuum Extend - Break Point Restart", + "channel": "break_point_restart", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Off" + }, + { + "value": "1", + "label": "On" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Off\",\"1\"\u003d\"On\"]" + }, + { + "property": "carpet-press", + "siid": 4, + "piid": 12, + "friendlyName": "Vacuum Extend - Carpet Press", + "channel": "carpet_press", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "On" + }, + { + "value": "1", + "label": "Off" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"On\",\"1\"\u003d\"Off\"]" + }, + { + "property": "serial-number", + "siid": 4, + "piid": 14, + "friendlyName": "Vacuum Extend - Serial Number", + "channel": "serial_number", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "remote-state", + "siid": 4, + "piid": 15, + "friendlyName": "Vacuum Extend - Remote State", + "channel": "remote_state", + "type": "String", + "refresh": false, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "clean-rags-tip", + "siid": 4, + "piid": 16, + "friendlyName": "Vacuum Extend - Clean Rags Tip", + "channel": "clean_rags_tip", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": 0, + "maximum": 120, + "step": 1, + "pattern": "%.0f %unit%" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "keep-sweeper-time", + "siid": 4, + "piid": 17, + "friendlyName": "Vacuum Extend - Keep Sweeper Time", + "channel": "keep_sweeper_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": -1, + "maximum": 1000000, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "faults", + "siid": 4, + "piid": 18, + "friendlyName": "Vacuum Extend - Faults", + "channel": "faults", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "enable", + "siid": 5, + "piid": 1, + "friendlyName": "Do Not Disturb - Enable", + "channel": "enable", + "type": "Switch", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "ONOFFBOOL" + } + ] + }, + { + "property": "start-time", + "siid": 5, + "piid": 2, + "friendlyName": "Do Not Disturb - Start Time", + "channel": "start_time", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "end-time", + "siid": 5, + "piid": 3, + "friendlyName": "Do Not Disturb - End Time", + "channel": "end_time", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "frame-info", + "siid": 6, + "piid": 2, + "friendlyName": "Map - Frame Info", + "channel": "frame_info", + "type": "String", + "refresh": false, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "map-extend-data", + "siid": 6, + "piid": 4, + "friendlyName": "Map - Map Extend Data", + "channel": "map_extend_data", + "type": "String", + "refresh": false, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "mult-map-state", + "siid": 6, + "piid": 7, + "friendlyName": "Map - Mult Map State", + "channel": "mult_map_state", + "type": "Number", + "stateDescription": { + "options": [ + { + "value": "0", + "label": "Close" + }, + { + "value": "1", + "label": "Open" + } + ] + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ], + "readmeComment": "Value mapping [\"0\"\u003d\"Close\",\"1\"\u003d\"Open\"]" + }, + { + "property": "mult-map-info", + "siid": 6, + "piid": 8, + "friendlyName": "Map - Mult Map Info", + "channel": "mult_map_info", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "volume", + "siid": 7, + "piid": 1, + "friendlyName": "Audio - Volume", + "channel": "volume", + "type": "Number", + "stateDescription": { + "minimum": 0, + "maximum": 100, + "step": 1, + "pattern": "%.0f" + }, + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "NUMBER" + } + ] + }, + { + "property": "voice-packet-id", + "siid": 7, + "piid": 2, + "friendlyName": "Audio - Voice Packet Id", + "channel": "voice_packet_id", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "voice-change-state", + "siid": 7, + "piid": 3, + "friendlyName": "Audio - Voice Change State", + "channel": "voice_change_state", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "set-voice", + "siid": 7, + "piid": 4, + "friendlyName": "Audio - Set Voice", + "channel": "set_voice", + "type": "String", + "refresh": false, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "time-zone", + "siid": 8, + "piid": 1, + "friendlyName": "Time - Time Zone", + "channel": "time_zone", + "type": "String", + "stateDescription": { + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "timer-clean", + "siid": 8, + "piid": 2, + "friendlyName": "Time - Timer Clean", + "channel": "timer_clean", + "type": "String", + "refresh": true, + "actions": [ + { + "command": "set_properties", + "parameterType": "STRING" + } + ] + }, + { + "property": "first-clean-time", + "siid": 12, + "piid": 1, + "friendlyName": "Clean Logs - First Clean Time", + "channel": "first_clean_time", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-time", + "siid": 12, + "piid": 2, + "friendlyName": "Clean Logs - Total Clean Time", + "channel": "total_clean_time", + "type": "Number:Time", + "unit": "minutes", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f %unit%", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-times", + "siid": 12, + "piid": 3, + "friendlyName": "Clean Logs - Total Clean Times", + "channel": "total_clean_times", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + }, + { + "property": "total-clean-area", + "siid": 12, + "piid": 4, + "friendlyName": "Clean Logs - Total Clean Area", + "channel": "total_clean_area", + "type": "Number", + "stateDescription": { + "minimum": 0, + "step": 1, + "pattern": "%.0f", + "readOnly": true + }, + "refresh": true, + "actions": [] + } + ], + "experimental": false + } +} From f184e4e88f73ae7b6b8af3b84abc4ebba73e4339 Mon Sep 17 00:00:00 2001 From: basse04 Date: Sat, 5 Nov 2022 13:50:49 +0100 Subject: [PATCH 25/55] [kostalinverter] Fix for the Kostal inverter binding to work with different firmware releases regarded to PIKO 10-20 V. 221004 (#13490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [kostalinverter] Fix for the Kostal inverter binding to work with different firmware releases regarded to PIKO 10-20 Version 221004 This is the next version regarded to Closed PR #13464 * [kostalinverter] Changes done 20221019 Regarded to Thread..... * Proper handling of InterruptedException Also removbe a useless call to return Signed-off-by: Örjan Backsell Co-authored-by: Laurent Garnier --- .../README.md | 2 +- .../SecondGenerationHandler.java | 267 +++++++++--------- 2 files changed, 135 insertions(+), 134 deletions(-) diff --git a/bundles/org.openhab.binding.kostalinverter/README.md b/bundles/org.openhab.binding.kostalinverter/README.md index 5e1d192253d97..c2cabb09aa0a7 100644 --- a/bundles/org.openhab.binding.kostalinverter/README.md +++ b/bundles/org.openhab.binding.kostalinverter/README.md @@ -291,7 +291,7 @@ String SolarStatus "Solar status [%s]" { channel="kostalinverter:kostal demo.items: ``` -Number:Power GridOutputPower "PV Output Power" { channel="kostalinverter:piko1020:mypiko1020:gridOutputPower" } +Number:Power GridOutputPower "Grid Output Power" { channel="kostalinverter:piko1020:mypiko1020:gridOutputPower" } Number:Energy YieldDaySecondGen "PV Output Power Day" { channel="kostalinverter:piko1020:mypiko1020:yieldDaySecondGen" } Number:Energy YieldTotalSecondGen "PV Output Power Total" { channel="kostalinverter:piko1020:mypiko1020:yieldTotalSecondgen" } Number:Dimensionless OperatingStatus "Operating Status" { channel="kostalinverter:piko1020:mypiko1020:operatingStatus" } diff --git a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationHandler.java b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationHandler.java index 3b873b8110645..771f424b904e4 100644 --- a/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationHandler.java +++ b/bundles/org.openhab.binding.kostalinverter/src/main/java/org/openhab/binding/kostalinverter/internal/secondgeneration/SecondGenerationHandler.java @@ -187,147 +187,142 @@ public void dispose() { } private void refresh() { - try { - // Build posts for dxsEntries part - String dxsEntriesCall = inverterConfig.url + "/api/dxs.json?dxsEntries=" + channelConfigs.get(0).dxsEntries; - for (int i = 1; i < channelConfigs.size(); i++) { - dxsEntriesCall += ("&dxsEntries=" + channelConfigs.get(i).dxsEntries); - } - String jsonDxsEntriesResponse = callURL(dxsEntriesCall, httpClient); - SecondGenerationDxsEntriesContainerDTO dxsEntriesContainer = gson.fromJson(jsonDxsEntriesResponse, - SecondGenerationDxsEntriesContainerDTO.class); - - String[] channelPosts = new String[23]; - int channelPostsCounter = 0; - for (SecondGenerationDxsEntries dxsentries : dxsEntriesContainer.dxsEntries) { - channelPosts[channelPostsCounter] = dxsentries.getName(); - channelPostsCounter++; - } - channelPostsTemp = List.of(channelPosts); + // Build posts for dxsEntries part + String dxsEntriesCall = inverterConfig.url + "/api/dxs.json?dxsEntries=" + channelConfigs.get(0).dxsEntries; + for (int i = 1; i < channelConfigs.size(); i++) { + dxsEntriesCall += ("&dxsEntries=" + channelConfigs.get(i).dxsEntries); + } + String jsonDxsEntriesResponse = callURL(dxsEntriesCall, httpClient); + SecondGenerationDxsEntriesContainerDTO dxsEntriesContainer = gson.fromJson(jsonDxsEntriesResponse, + SecondGenerationDxsEntriesContainerDTO.class); + + String[] channelPosts = new String[23]; + int channelPostsCounter = 0; + for (SecondGenerationDxsEntries dxsentries : dxsEntriesContainer.dxsEntries) { + channelPosts[channelPostsCounter] = dxsentries.getName(); + channelPostsCounter++; + } + channelPostsTemp = List.of(channelPosts); + + // Build posts for dxsEntriesExt part + String dxsEntriesCallExt = inverterConfig.url + "/api/dxs.json?dxsEntries=" + + channelConfigsExt.get(0).dxsEntries; + for (int i = 1; i < channelConfigs.size(); i++) { + dxsEntriesCallExt += ("&dxsEntries=" + channelConfigsExt.get(i).dxsEntries); + } + String jsonDxsEntriesResponseExt = callURL(dxsEntriesCallExt, httpClient); + SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerExt = gson.fromJson(jsonDxsEntriesResponseExt, + SecondGenerationDxsEntriesContainerDTO.class); + String[] channelPostsExt = new String[23]; + int channelPostsCounterExt = 0; + for (SecondGenerationDxsEntries dxsentriesExt : dxsEntriesContainerExt.dxsEntries) { + channelPostsExt[channelPostsCounterExt] = dxsentriesExt.getName(); + channelPostsCounterExt++; + } + channelPostsTempExt = List.of(channelPostsExt); + + // Build posts for dxsEntriesExtExt part + String dxsEntriesCallExtExt = inverterConfig.url + "/api/dxs.json?dxsEntries=" + + channelConfigsExtExt.get(0).dxsEntries; + for (int i = 1; i < channelConfigsExtExt.size(); i++) { + dxsEntriesCallExtExt += ("&dxsEntries=" + channelConfigsExtExt.get(i).dxsEntries); + } + String jsonDxsEntriesResponseExtExt = callURL(dxsEntriesCallExtExt, httpClient); + SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerExtExt = gson.fromJson(jsonDxsEntriesResponseExtExt, + SecondGenerationDxsEntriesContainerDTO.class); + String[] channelPostsExtExt = new String[3]; + int channelPostsCounterExtExt = 0; + for (SecondGenerationDxsEntries dxsentriesExtExt : dxsEntriesContainerExtExt.dxsEntries) { + channelPostsExtExt[channelPostsCounterExtExt] = dxsentriesExtExt.getName(); + channelPostsCounterExtExt++; + } + channelPostsTempExtExt = List.of(channelPostsExtExt); + + // Concatenate posts for all parts except configurable channels + channelPostsTempAll = combinePostsLists(channelPostsTemp, channelPostsTempExt, channelPostsTempExtExt); + String[] channelPostsTempAll1 = channelPostsTempAll.toArray(new String[0]); - // Build posts for dxsEntriesExt part - String dxsEntriesCallExt = inverterConfig.url + "/api/dxs.json?dxsEntries=" - + channelConfigsExt.get(0).dxsEntries; - for (int i = 1; i < channelConfigs.size(); i++) { - dxsEntriesCallExt += ("&dxsEntries=" + channelConfigsExt.get(i).dxsEntries); + // Build posts for dxsEntriesConfigureable part + String[] channelPostsConfigurable = new String[5]; + if (inverterConfig.hasBattery) { + String dxsEntriesCallConfigurable = inverterConfig.url + "/api/dxs.json?dxsEntries=" + + channelConfigsConfigurable.get(0).dxsEntries; + for (int i = 1; i < channelConfigsConfigurable.size(); i++) { + dxsEntriesCallConfigurable += ("&dxsEntries=" + channelConfigsConfigurable.get(i).dxsEntries); } - String jsonDxsEntriesResponseExt = callURL(dxsEntriesCallExt, httpClient); - SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerExt = gson.fromJson(jsonDxsEntriesResponseExt, - SecondGenerationDxsEntriesContainerDTO.class); - String[] channelPostsExt = new String[23]; - int channelPostsCounterExt = 0; - for (SecondGenerationDxsEntries dxsentriesExt : dxsEntriesContainerExt.dxsEntries) { - channelPostsExt[channelPostsCounterExt] = dxsentriesExt.getName(); - channelPostsCounterExt++; + String jsonDxsEntriesResponseConfigurable = callURL(dxsEntriesCallConfigurable, httpClient); + SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerConfigurable = gson + .fromJson(jsonDxsEntriesResponseConfigurable, SecondGenerationDxsEntriesContainerDTO.class); + int channelPostsCounterConfigurable = 0; + for (SecondGenerationDxsEntries dxsentriesConfigurable : dxsEntriesContainerConfigurable.dxsEntries) { + channelPostsConfigurable[channelPostsCounterConfigurable] = dxsentriesConfigurable.getName(); + channelPostsCounterConfigurable++; } - channelPostsTempExt = List.of(channelPostsExt); + } - // Build posts for dxsEntriesExtExt part - String dxsEntriesCallExtExt = inverterConfig.url + "/api/dxs.json?dxsEntries=" - + channelConfigsExtExt.get(0).dxsEntries; - for (int i = 1; i < channelConfigsExtExt.size(); i++) { - dxsEntriesCallExtExt += ("&dxsEntries=" + channelConfigsExtExt.get(i).dxsEntries); - } - String jsonDxsEntriesResponseExtExt = callURL(dxsEntriesCallExtExt, httpClient); - SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerExtExt = gson - .fromJson(jsonDxsEntriesResponseExtExt, SecondGenerationDxsEntriesContainerDTO.class); - String[] channelPostsExtExt = new String[3]; - int channelPostsCounterExtExt = 0; - for (SecondGenerationDxsEntries dxsentriesExtExt : dxsEntriesContainerExtExt.dxsEntries) { - channelPostsExtExt[channelPostsCounterExtExt] = dxsentriesExtExt.getName(); - channelPostsCounterExtExt++; + // Create and update actual values for non-configurable channels + if (!inverterConfig.hasBattery) { + channelConfigsAll = combineChannelConfigLists(channelConfigs, channelConfigsExt, channelConfigsExtExt); + int channelValuesCounterAll = 0; + for (SecondGenerationChannelConfiguration cConfig : channelConfigsAll) { + String channel = cConfig.id; + updateState(channel, getState(channelPostsTempAll1[channelValuesCounterAll], cConfig.unit)); + channelValuesCounterAll++; } - channelPostsTempExtExt = List.of(channelPostsExtExt); - - // Concatenate posts for all parts except configurable channels - channelPostsTempAll = combinePostsLists(channelPostsTemp, channelPostsTempExt, channelPostsTempExtExt); - String[] channelPostsTempAll1 = channelPostsTempAll.toArray(new String[0]); - - // Build posts for dxsEntriesConfigureable part - String[] channelPostsConfigurable = new String[5]; - if (inverterConfig.hasBattery) { - String dxsEntriesCallConfigurable = inverterConfig.url + "/api/dxs.json?dxsEntries=" - + channelConfigsConfigurable.get(0).dxsEntries; - for (int i = 1; i < channelConfigsConfigurable.size(); i++) { - dxsEntriesCallConfigurable += ("&dxsEntries=" + channelConfigsConfigurable.get(i).dxsEntries); - } - String jsonDxsEntriesResponseConfigurable = callURL(dxsEntriesCallConfigurable, httpClient); - SecondGenerationDxsEntriesContainerDTO dxsEntriesContainerConfigurable = gson - .fromJson(jsonDxsEntriesResponseConfigurable, SecondGenerationDxsEntriesContainerDTO.class); - int channelPostsCounterConfigurable = 0; - for (SecondGenerationDxsEntries dxsentriesConfigurable : dxsEntriesContainerConfigurable.dxsEntries) { - channelPostsConfigurable[channelPostsCounterConfigurable] = dxsentriesConfigurable.getName(); - channelPostsCounterConfigurable++; - } + } + // Create and update actual values for all channels + if (inverterConfig.hasBattery) { + // Part for updating non-configurable channels + channelConfigsAll = combineChannelConfigLists(channelConfigs, channelConfigsExt, channelConfigsExtExt); + // Update the non-configurable channels + int channelValuesCounterAll = 0; + for (SecondGenerationChannelConfiguration cConfig : channelConfigsAll) { + String channel = cConfig.id; + updateState(channel, getState(channelPostsTempAll1[channelValuesCounterAll], cConfig.unit)); + channelValuesCounterAll++; } - // Create and update actual values for non-configurable channels - if (!inverterConfig.hasBattery) { - channelConfigsAll = combineChannelConfigLists(channelConfigs, channelConfigsExt, channelConfigsExtExt); - int channelValuesCounterAll = 0; - for (SecondGenerationChannelConfiguration cConfig : channelConfigsAll) { - String channel = cConfig.id; - updateState(channel, getState(channelPostsTempAll1[channelValuesCounterAll], cConfig.unit)); - channelValuesCounterAll++; + // Part for updating configurable channels + int channelValuesCounterConfigurable = 0; + for (SecondGenerationChannelConfiguration cConfig : channelConfigsConfigurable) { + String channel = cConfig.id; + String value = channelPostsConfigurable[channelValuesCounterConfigurable]; + int dxsEntriesCheckCounter = 3; + if (cConfig.dxsEntries.equals("33556484")) { + dxsEntriesCheckCounter = 1; } - } - // Create and update actual values for all channels - if (inverterConfig.hasBattery) { - // Part for updating non-configurable channels - channelConfigsAll = combineChannelConfigLists(channelConfigs, channelConfigsExt, channelConfigsExtExt); - // Update the non-configurable channels - int channelValuesCounterAll = 0; - for (SecondGenerationChannelConfiguration cConfig : channelConfigsAll) { - String channel = cConfig.id; - updateState(channel, getState(channelPostsTempAll1[channelValuesCounterAll], cConfig.unit)); - channelValuesCounterAll++; + if (cConfig.dxsEntries.equals("33556482")) { + dxsEntriesCheckCounter = 2; } - - // Part for updating configurable channels - int channelValuesCounterConfigurable = 0; - for (SecondGenerationChannelConfiguration cConfig : channelConfigsConfigurable) { - String channel = cConfig.id; - String value = channelPostsConfigurable[channelValuesCounterConfigurable]; - int dxsEntriesCheckCounter = 3; - if (cConfig.dxsEntries.equals("33556484")) { - dxsEntriesCheckCounter = 1; - } - if (cConfig.dxsEntries.equals("33556482")) { - dxsEntriesCheckCounter = 2; - } - switch (dxsEntriesCheckCounter) { - case 1: - if ("false".equals(value)) { - updateState(channel, OnOffType.OFF); - } - if ("true".equals(value)) { - updateState(channel, OnOffType.ON); - } - channelValuesCounterConfigurable++; - break; - case 2: - if ("false".equals(value)) { - State stateFalse = new StringType("0"); - updateState(channel, stateFalse); - } - if ("true".equals(value)) { - State stateTrue = new StringType("1"); - updateState(channel, stateTrue); - } - channelValuesCounterConfigurable++; - break; - case 3: - State stateOther = getState(channelPostsConfigurable[channelValuesCounterConfigurable], - cConfig.unit); - updateState(channel, stateOther); - channelValuesCounterConfigurable++; - break; - } + switch (dxsEntriesCheckCounter) { + case 1: + if ("false".equals(value)) { + updateState(channel, OnOffType.OFF); + } + if ("true".equals(value)) { + updateState(channel, OnOffType.ON); + } + channelValuesCounterConfigurable++; + break; + case 2: + if ("false".equals(value)) { + State stateFalse = new StringType("0"); + updateState(channel, stateFalse); + } + if ("true".equals(value)) { + State stateTrue = new StringType("1"); + updateState(channel, stateTrue); + } + channelValuesCounterConfigurable++; + break; + case 3: + State stateOther = getState(channelPostsConfigurable[channelValuesCounterConfigurable], + cConfig.unit); + updateState(channel, stateOther); + channelValuesCounterConfigurable++; + break; } } - } catch (final RuntimeException e) { - logger.debug("Updating inverter status failed: ", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } @@ -338,7 +333,10 @@ private final void preSetExecuteConfigurationChanges(HttpClient httpClientHandle try { SecondGenerationConfigurationHandler.executeConfigurationChanges(httpClientHandleCommand, url, username, password, dxsEntriesConf, valueConfiguration); - } catch (InterruptedException | ExecutionException | TimeoutException | NoSuchAlgorithmException e) { + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.debug("Connection to inverter interrupted during configuration"); + } catch (ExecutionException | TimeoutException | NoSuchAlgorithmException e) { logger.debug("Connection to inverter disturbed during configuration"); } } @@ -347,8 +345,11 @@ private final void preSetExecuteConfigurationChanges(HttpClient httpClientHandle private final String callURL(String dxsEntriesCall, HttpClient httpClient) { String jsonDxsResponse = ""; try { - jsonDxsResponse = httpClient.GET(dxsEntriesCall).getContentAsString(); - } catch (InterruptedException | ExecutionException | TimeoutException e2) { + jsonDxsResponse = httpClient.GET(dxsEntriesCall).getContentAsString().replace("null", "0.000000"); + } catch (InterruptedException e2) { + Thread.currentThread().interrupt(); + logger.debug("Connection to inverter interrupted during scrape"); + } catch (ExecutionException | TimeoutException e2) { logger.debug("Connection to inverter disturbed during scrape"); } return jsonDxsResponse; From c81790cebf3f9675659648b7a12ba2926f592f15 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sat, 5 Nov 2022 14:42:01 +0100 Subject: [PATCH 26/55] [verisure] Avoid updates duplication after communication errors (#13652) Avoid registering several times the same device status listener. It is called by the thing handler each time the bridge status changed to ONLINE. Signed-off-by: Laurent Garnier --- .../openhab/binding/verisure/internal/VerisureSession.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java index 1b313c0ac0b14..712d4c773545c 100644 --- a/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java +++ b/bundles/org.openhab.binding.verisure/src/main/java/org/openhab/binding/verisure/internal/VerisureSession.java @@ -27,8 +27,8 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; @@ -82,7 +82,7 @@ public class VerisureSession { private final Map> verisureHandlers = new ConcurrentHashMap<>(); private final Logger logger = LoggerFactory.getLogger(VerisureSession.class); private final Gson gson = new Gson(); - private final List> deviceStatusListeners = new CopyOnWriteArrayList<>(); + private final Set> deviceStatusListeners = ConcurrentHashMap.newKeySet(); private final Map verisureInstallations = new ConcurrentHashMap<>(); private static final List APISERVERLIST = Arrays.asList("https://m-api01.verisure.com", "https://m-api02.verisure.com"); From bbc744e3ffc714204f6f03bbc3cc7a6a3559af4b Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 5 Nov 2022 08:23:02 -0600 Subject: [PATCH 27/55] [jrubyscripting] don't manually filter presets. (#13548) simply just don't overwrite any constants that already exist refs https://github.com/openhab/openhab-core/pull/3113 Signed-off-by: Cody Cutrer --- .../internal/JRubyScriptEngineFactory.java | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java index 326ffbc594b62..5d74d7b2fd2a1 100644 --- a/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java +++ b/bundles/org.openhab.automation.jrubyscripting/src/main/java/org/openhab/automation/jrubyscripting/internal/JRubyScriptEngineFactory.java @@ -15,7 +15,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -47,26 +46,12 @@ public class JRubyScriptEngineFactory extends AbstractScriptEngineFactory { private final JRubyScriptEngineConfiguration configuration = new JRubyScriptEngineConfiguration(); - // Filter out the File entry to prevent shadowing the Ruby File class which breaks Ruby in spectacularly - // difficult ways to debug. - private static final Set FILTERED_PRESETS = Set.of("File", "Files", "Path", "Paths"); - private static final Set INSTANCE_PRESETS = Set.of(); - private final javax.script.ScriptEngineFactory factory = new org.jruby.embed.jsr223.JRubyEngineFactory(); private final List scriptTypes = Stream .concat(factory.getExtensions().stream(), factory.getMimeTypes().stream()) .collect(Collectors.toUnmodifiableList()); - // Adds @ in front of a set of variables so that Ruby recognizes them as instance variables - private static Map.Entry mapInstancePresets(Map.Entry entry) { - if (INSTANCE_PRESETS.contains(entry.getKey())) { - return Map.entry("@" + entry.getKey(), entry.getValue()); - } else { - return entry; - } - } - // Adds $ in front of a set of variables so that Ruby recognizes them as global variables private static Map.Entry mapGlobalPresets(Map.Entry entry) { if (Character.isLowerCase(entry.getKey().charAt(0)) && !(entry.getValue() instanceof Class) @@ -102,8 +87,6 @@ public void scopeValues(ScriptEngine scriptEngine, Map scopeValu scopeValues // .entrySet() // .stream() // - .filter(map -> !FILTERED_PRESETS.contains(map.getKey())) // - .map(JRubyScriptEngineFactory::mapInstancePresets) // .map(JRubyScriptEngineFactory::mapGlobalPresets) // .collect(Collectors.toMap(map -> map.getKey(), map -> map.getValue())); // @@ -126,7 +109,7 @@ public void scopeValues(ScriptEngine scriptEngine, Map scopeValu private void importClassesToRuby(ScriptEngine scriptEngine, Map objects) { try { scriptEngine.put("__classes", objects); - final String code = "__classes.each { |(name, klass)| Object.const_set(name, klass.ruby_class) }"; + final String code = "__classes.each { |(name, klass)| Object.const_set(name, klass.ruby_class) unless Object.const_defined?(name, false) }"; scriptEngine.eval(code); // clean up our temporary variable scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE).remove("__classes"); From 51d3fc211a5577feb6f384e2e839d5f2fe926a1a Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Sat, 5 Nov 2022 15:26:46 +0100 Subject: [PATCH 28/55] [jsscripting] Reimplement timer polyfills to conform standard JS (#13623) * [jsscripting] Reimplement timers to conform standard JS * [jsscripting] Name scheduled jobs by loggerName + id * [jsscripting] Update timer identifiers * [jsscripting] Update identifiers for scheduled jobs * [jsscripting] Synchronize method that is called when the script is reloaded * [jsscripting] Cancel all scheduled jobs when the engine is closed * [jsscripting] Ensure that a timerId is never reused by a subsequent call & Use long primitive type instead of Integer * [jsscripting] Use an abstraction class to inject features into the JS runtime * [jsscripting] Make ThreadsafeTimers threadsafe for concurrent access to the class itself * [jsscripting] Move the locking for `invokeFunction` to `OpenhabGraalJSScriptEngine` Signed-off-by: Florian Hotze --- .../internal/JSRuntimeFeatures.java | 56 +++++ .../internal/OpenhabGraalJSScriptEngine.java | 20 +- .../internal/threading/ThreadsafeTimers.java | 231 ++++++++++++------ .../node_modules/@jsscripting-globals.js | 55 +---- 4 files changed, 240 insertions(+), 122 deletions(-) create mode 100644 bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/JSRuntimeFeatures.java diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/JSRuntimeFeatures.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/JSRuntimeFeatures.java new file mode 100644 index 0000000000000..790b464168fe3 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/JSRuntimeFeatures.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jsscripting.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers; + +/** + * Abstraction layer to collect all features injected into the JS runtime during the context creation. + * + * @author Florian Hotze - Initial contribution + */ +@NonNullByDefault +public class JSRuntimeFeatures { + /** + * All elements of this Map are injected into the JS runtime using their key as the name. + */ + private final Map features = new HashMap<>(); + public final ThreadsafeTimers threadsafeTimers; + + JSRuntimeFeatures(Object lock) { + this.threadsafeTimers = new ThreadsafeTimers(lock); + + features.put("ThreadsafeTimers", threadsafeTimers); + } + + /** + * Get the features that are to be injected into the JS runtime during context creation. + * + * @return the runtime features + */ + public Map getFeatures() { + return features; + } + + /** + * Un-initialization hook, called when the engine is closed. + * Use this method to clean up resources or cancel operations that were created by the JS runtime. + */ + public void close() { + threadsafeTimers.clearAll(); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java index 8a1282c8fd1c5..f810e2375e7b2 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java @@ -46,7 +46,6 @@ import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel; import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker; import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable; -import org.openhab.automation.jsscripting.internal.threading.ThreadsafeTimers; import org.openhab.core.automation.module.script.ScriptExtensionAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +57,8 @@ * * @author Jonathan Gilbert - Initial contribution * @author Dan Cunningham - Script injections - * @author Florian Hotze - Create lock object for multi-thread synchronization + * @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures} + * into the JS context */ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable { @@ -71,6 +71,7 @@ public class OpenhabGraalJSScriptEngine // shared lock object for synchronization of multi-thread access private final Object lock = new Object(); + private final JSRuntimeFeatures jsRuntimeFeatures = new JSRuntimeFeatures(lock); // these fields start as null because they are populated on first use private String engineIdentifier; @@ -209,7 +210,7 @@ protected void beforeInvocation() { delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn); // Injections into the JS runtime delegate.put("require", wrapRequireFn.apply((Function) delegate.get("require"))); - delegate.put("ThreadsafeTimers", new ThreadsafeTimers(lock)); + jsRuntimeFeatures.getFeatures().forEach((key, obj) -> delegate.put(key, obj)); initialized = true; @@ -220,6 +221,19 @@ protected void beforeInvocation() { } } + @Override + public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException { + // Synchronize multi-thread access to avoid exceptions when reloading a script file while the script is running + synchronized (lock) { + return super.invokeFunction(s, objects); + } + } + + @Override + public void close() { + jsRuntimeFeatures.close(); + } + /** * Tests if this is a root node directory, `/node_modules`, `C:\node_modules`, etc... * diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeTimers.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeTimers.java index 9583ca57c12b0..8b88ac3aad4d9 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeTimers.java +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeTimers.java @@ -12,126 +12,209 @@ */ package org.openhab.automation.jsscripting.internal.threading; +import java.time.Duration; import java.time.ZonedDateTime; -import java.util.concurrent.TimeUnit; +import java.time.temporal.Temporal; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; -import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.model.script.ScriptServiceUtil; -import org.openhab.core.model.script.actions.Timer; import org.openhab.core.scheduler.ScheduledCompletableFuture; import org.openhab.core.scheduler.Scheduler; -import org.openhab.core.scheduler.SchedulerRunnable; +import org.openhab.core.scheduler.SchedulerTemporalAdjuster; /** - * A replacement for the timer functionality of {@link org.openhab.core.model.script.actions.ScriptExecution - * ScriptExecution} which controls multithreaded execution access to the single-threaded GraalJS contexts. + * A polyfill implementation of NodeJS timer functionality (setTimeout(), setInterval() and + * the cancel methods) which controls multithreaded execution access to the single-threaded GraalJS contexts. * * @author Florian Hotze - Initial contribution + * @author Florian Hotze - Reimplementation to conform standard JS setTimeout and setInterval */ public class ThreadsafeTimers { private final Object lock; + private final Scheduler scheduler; + // Mapping of positive, non-zero integer values (used as timeoutID or intervalID) and the Scheduler + private final Map> idSchedulerMapping = new ConcurrentHashMap<>(); + private AtomicLong lastId = new AtomicLong(); + private String identifier = "noIdentifier"; public ThreadsafeTimers(Object lock) { this.lock = lock; + this.scheduler = ScriptServiceUtil.getScheduler(); } - public Timer createTimer(ZonedDateTime instant, Runnable callable) { - return createTimer(null, instant, callable); + /** + * Set the identifier base string used for naming scheduled jobs. + * + * @param identifier identifier to use + */ + public void setIdentifier(String identifier) { + this.identifier = identifier; } - public Timer createTimer(@Nullable String identifier, ZonedDateTime instant, Runnable callable) { - Scheduler scheduler = ScriptServiceUtil.getScheduler(); - - return new TimerImpl(scheduler, instant, () -> { + /** + * Schedules a callback to run at a given time. + * + * @param id timerId to append to the identifier base for naming the scheduled job + * @param zdt time to schedule the job + * @param callback function to run at the given time + * @return a {@link ScheduledCompletableFuture} + */ + private ScheduledCompletableFuture createFuture(long id, ZonedDateTime zdt, Runnable callback) { + return scheduler.schedule(() -> { synchronized (lock) { - callable.run(); + callback.run(); } + }, identifier + ".timeout." + id, zdt.toInstant()); + } - }, identifier); + /** + * setTimeout() polyfill. + * Sets a timer which executes a given function once the timer expires. + * + * @param callback function to run after the given delay + * @param delay time in milliseconds that the timer should wait before the callback is executed + * @return Positive integer value which identifies the timer created; this value can be passed to + * clearTimeout() to cancel the timeout. + */ + public long setTimeout(Runnable callback, Long delay) { + return setTimeout(callback, delay, new Object()); } - public Timer createTimerWithArgument(ZonedDateTime instant, Object arg1, Runnable callable) { - return createTimerWithArgument(null, instant, arg1, callable); + /** + * setTimeout() polyfill. + * Sets a timer which executes a given function once the timer expires. + * + * @param callback function to run after the given delay + * @param delay time in milliseconds that the timer should wait before the callback is executed + * @param args + * @return Positive integer value which identifies the timer created; this value can be passed to + * clearTimeout() to cancel the timeout. + */ + public long setTimeout(Runnable callback, Long delay, Object... args) { + long id = lastId.incrementAndGet(); + ScheduledCompletableFuture future = createFuture(id, ZonedDateTime.now().plusNanos(delay * 1000000), + callback); + idSchedulerMapping.put(id, future); + return id; } - public Timer createTimerWithArgument(@Nullable String identifier, ZonedDateTime instant, Object arg1, - Runnable callable) { - Scheduler scheduler = ScriptServiceUtil.getScheduler(); - return new TimerImpl(scheduler, instant, () -> { + /** + * clearTimeout() polyfill. + * Cancels a timeout previously created by setTimeout(). + * + * @param timeoutId The identifier of the timeout you want to cancel. This ID was returned by the corresponding call + * to setTimeout(). + */ + public void clearTimeout(long timeoutId) { + ScheduledCompletableFuture scheduled = idSchedulerMapping.remove(timeoutId); + if (scheduled != null) { + scheduled.cancel(true); + } + } + + /** + * Schedules a callback to run in a loop with a given delay between the executions. + * + * @param id timerId to append to the identifier base for naming the scheduled job + * @param delay time in milliseconds that the timer should delay in between executions of the callback + * @param callback function to run + */ + private void createLoopingFuture(long id, Long delay, Runnable callback) { + ScheduledCompletableFuture future = scheduler.schedule(() -> { synchronized (lock) { - callable.run(); + callback.run(); } - - }, identifier); + }, identifier + ".interval." + id, new LoopingAdjuster(Duration.ofMillis(delay))); + idSchedulerMapping.put(id, future); } /** - * This is an implementation of the {@link Timer} interface. - * Copy of {@link org.openhab.core.model.script.internal.actions.TimerImpl} as this is not accessible from outside - * the - * package. + * setInterval() polyfill. + * Repeatedly calls a function with a fixed time delay between each call. * - * @author Kai Kreuzer - Initial contribution + * @param callback function to run + * @param delay time in milliseconds that the timer should delay in between executions of the callback + * @return Numeric, non-zero value which identifies the timer created; this value can be passed to + * clearInterval() to cancel the interval. */ - @NonNullByDefault - public static class TimerImpl implements Timer { - - private final Scheduler scheduler; - private final ZonedDateTime startTime; - private final SchedulerRunnable runnable; - private final @Nullable String identifier; - private ScheduledCompletableFuture future; - - public TimerImpl(Scheduler scheduler, ZonedDateTime startTime, SchedulerRunnable runnable) { - this(scheduler, startTime, runnable, null); - } - - public TimerImpl(Scheduler scheduler, ZonedDateTime startTime, SchedulerRunnable runnable, - @Nullable String identifier) { - this.scheduler = scheduler; - this.startTime = startTime; - this.runnable = runnable; - this.identifier = identifier; + public long setInterval(Runnable callback, Long delay) { + return setInterval(callback, delay, new Object()); + } - future = scheduler.schedule(runnable, identifier, startTime.toInstant()); - } + /** + * setInterval() polyfill. + * Repeatedly calls a function with a fixed time delay between each call. + * + * @param callback function to run + * @param delay time in milliseconds that the timer should delay in between executions of the callback + * @param args + * @return Numeric, non-zero value which identifies the timer created; this value can be passed to + * clearInterval() to cancel the interval. + */ + public long setInterval(Runnable callback, Long delay, Object... args) { + long id = lastId.incrementAndGet(); + createLoopingFuture(id, delay, callback); + return id; + } - @Override - public boolean cancel() { - return future.cancel(true); - } + /** + * clearInterval() + * polyfill. + * Cancels a timed, repeating action which was previously established by a call to setInterval(). + * + * @param intervalID The identifier of the repeated action you want to cancel. This ID was returned by the + * corresponding call to setInterval(). + */ + public void clearInterval(long intervalID) { + clearTimeout(intervalID); + } - @Override - public synchronized boolean reschedule(ZonedDateTime newTime) { - future.cancel(false); - future = scheduler.schedule(runnable, identifier, newTime.toInstant()); - return true; - } + /** + * Cancels all timed actions (i.e. timeouts and intervals) that were created with this instance of + * {@link ThreadsafeTimers}. + * Should be called in a de-initialization/unload hook of the script engine to avoid having scheduled jobs that are + * running endless. + */ + public void clearAll() { + idSchedulerMapping.forEach((id, future) -> future.cancel(true)); + idSchedulerMapping.clear(); + } - @Override - public @Nullable ZonedDateTime getExecutionTime() { - return future.isCancelled() ? null : ZonedDateTime.now().plusNanos(future.getDelay(TimeUnit.NANOSECONDS)); - } + /** + * This is a temporal adjuster that takes a single delay. + * This adjuster makes the scheduler run as a fixed rate scheduler from the first time adjustInto was called. + * + * @author Florian Hotze - Initial contribution + */ + private static class LoopingAdjuster implements SchedulerTemporalAdjuster { - @Override - public boolean isActive() { - return !future.isDone(); - } + private Duration delay; + private @Nullable Temporal timeDone; - @Override - public boolean isCancelled() { - return future.isCancelled(); + LoopingAdjuster(Duration delay) { + this.delay = delay; } @Override - public boolean isRunning() { - return isActive() && ZonedDateTime.now().isAfter(startTime); + public boolean isDone(Temporal temporal) { + // Always return false so that a new job will be scheduled + return false; } @Override - public boolean hasTerminated() { - return future.isDone(); + public Temporal adjustInto(Temporal temporal) { + Temporal localTimeDone = timeDone; + Temporal nextTime; + if (localTimeDone != null) { + nextTime = localTimeDone.plus(delay); + } else { + nextTime = temporal.plus(delay); + } + timeDone = nextTime; + return nextTime; } } } diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js b/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js index e3b3399147560..88d31c686412b 100644 --- a/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js @@ -1,3 +1,4 @@ +// ThreadsafeTimers is injected into the JS runtime (function (global) { 'use strict'; @@ -5,8 +6,9 @@ // Append the script file name OR rule UID depending on which is available const defaultIdentifier = "org.openhab.automation.script" + (globalThis["javax.script.filename"] ? ".file." + globalThis["javax.script.filename"].replace(/^.*[\\\/]/, '') : globalThis["ruleUID"] ? ".ui." + globalThis["ruleUID"] : ""); const System = Java.type('java.lang.System'); - const ZonedDateTime = Java.type('java.time.ZonedDateTime'); const formatRegExp = /%[sdj%]/g; + // Pass the defaultIdentifier to ThreadsafeTimers to enable naming of scheduled jobs + ThreadsafeTimers.setIdentifier(defaultIdentifier); function createLogger(name = defaultIdentifier) { return Java.type("org.slf4j.LoggerFactory").getLogger(name); @@ -162,61 +164,24 @@ }, // Allow user customizable logging names + // Be aware that a log4j2 required a logger defined for the logger name, otherwise messages won't be logged! set loggerName(name) { log = createLogger(name); this._loggerName = name; + ThreadsafeTimers.setIdentifier(name); }, get loggerName() { - return this._loggerName || defaultLoggerName; + return this._loggerName || defaultIdentifier; } }; - function setTimeout(cb, delay) { - const args = Array.prototype.slice.call(arguments, 2); - return ThreadsafeTimers.createTimerWithArgument( - defaultIdentifier + '.setTimeout', - ZonedDateTime.now().plusNanos(delay * 1000000), - args, - function (args) { - cb.apply(global, args); - } - ); - } - - function clearTimeout(timer) { - if (timer !== undefined && timer.isActive()) { - timer.cancel(); - } - } - - function setInterval(cb, delay) { - const args = Array.prototype.slice.call(arguments, 2); - const delayNanos = delay * 1000000 - let timer = ThreadsafeTimers.createTimerWithArgument( - defaultIdentifier + '.setInterval', - ZonedDateTime.now().plusNanos(delayNanos), - args, - function (args) { - cb.apply(global, args); - if (!timer.isCancelled()) { - timer.reschedule(ZonedDateTime.now().plusNanos(delayNanos)); - } - } - ); - return timer; - } - - function clearInterval(timer) { - clearTimeout(timer); - } - // Polyfill common NodeJS functions onto the global object globalThis.console = console; - globalThis.setTimeout = setTimeout; - globalThis.clearTimeout = clearTimeout; - globalThis.setInterval = setInterval; - globalThis.clearInterval = clearInterval; + globalThis.setTimeout = ThreadsafeTimers.setTimeout; + globalThis.clearTimeout = ThreadsafeTimers.clearTimeout; + globalThis.setInterval = ThreadsafeTimers.setInterval; + globalThis.clearInterval = ThreadsafeTimers.clearInterval; // Support legacy NodeJS libraries globalThis.global = globalThis; From 969fef8612dccf0f3aff362171e4591a4759b877 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Sat, 5 Nov 2022 16:11:06 +0100 Subject: [PATCH 29/55] [tellstick] Avoid updates duplication after communication errors (#13479) * [tellstick] Avoid updates duplication after communication errors Fix #13453 Do not register the same device handler many times as listener in the bridge handler Unregister the device handler from the bridge handler when disposing device handler HTTP timeout set to 15s Remove the retry mechanism related to the timeout Check HTTP status code Fix discovery service unregistration Log statistics about request/refresh durations and number of timeouts/errors Change logging in case of exception Also change few logs level (remove usage of logger.error) Execute one refresh at bridge initialization and not 2 Small enhancement of the bridge/things status management implement discovery service unregistration Fix few code analysis findings Signed-off-by: Laurent Garnier * Use a set for deviceStatusListeners to avoid duplications Review comment: @NonNullByDefault for TellstickHandlerFactory Review comment: use ThingStatusDetail.CONFIGURATION_ERROR if no bridge is defined Review comment: use 1_000_000 instead of 1000000 Review comment: use Instant instead of LocalDateTime Review comment: Thread.currentThread().interrupt() Signed-off-by: Laurent Garnier --- .../internal/TellstickHandlerFactory.java | 49 ++++++++-- .../core/TelldusCoreBridgeHandler.java | 6 +- .../discovery/TellstickDiscoveryService.java | 9 ++ .../handler/TelldusDevicesHandler.java | 39 +++++--- .../live/TelldusLiveBridgeHandler.java | 44 +++++++-- .../live/TelldusLiveDeviceController.java | 91 ++++++++++++++----- .../internal/live/xml/TellstickNetDevice.java | 5 +- .../internal/live/xml/TellstickNetSensor.java | 5 +- .../local/TelldusLocalBridgeHandler.java | 8 +- .../local/TelldusLocalDeviceController.java | 1 - 10 files changed, 185 insertions(+), 72 deletions(-) diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java index f39ff2a91ea98..975cc8e5c53a4 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/TellstickHandlerFactory.java @@ -16,6 +16,8 @@ import java.util.Hashtable; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.tellstick.internal.core.TelldusCoreBridgeHandler; import org.openhab.binding.tellstick.internal.discovery.TellstickDiscoveryService; @@ -31,6 +33,7 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.framework.ServiceRegistration; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -43,12 +46,15 @@ * * @author Jarle Hjortland - Initial contribution * @author Jan Gustafsson - Adding support for local API + * @author Laurent Garnier - fix discovery service registering/unregistering */ @Component(service = ThingHandlerFactory.class, configurationPid = "binding.tellstick") +@NonNullByDefault public class TellstickHandlerFactory extends BaseThingHandlerFactory { private final Logger logger = LoggerFactory.getLogger(TellstickHandlerFactory.class); - private TellstickDiscoveryService discoveryService = null; private final HttpClient httpClient; + private @Nullable TellstickDiscoveryService discoveryService = null; + private @Nullable ServiceRegistration discoveryServiceRegistration = null; @Activate public TellstickHandlerFactory(@Reference HttpClientFactory httpClientFactory) { @@ -60,18 +66,38 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); } - private void registerDeviceDiscoveryService(TelldusBridgeHandler tellstickBridgeHandler) { - if (discoveryService == null) { - discoveryService = new TellstickDiscoveryService(tellstickBridgeHandler); - discoveryService.activate(); - bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()); + private synchronized void registerDeviceDiscoveryService(TelldusBridgeHandler tellstickBridgeHandler) { + TellstickDiscoveryService service = discoveryService; + if (service == null) { + service = new TellstickDiscoveryService(tellstickBridgeHandler); + service.activate(); + discoveryService = service; + discoveryServiceRegistration = (ServiceRegistration) bundleContext + .registerService(DiscoveryService.class.getName(), service, new Hashtable<>()); } else { - discoveryService.addBridgeHandler(tellstickBridgeHandler); + service.addBridgeHandler(tellstickBridgeHandler); + } + } + + private synchronized void unregisterDeviceDiscoveryService(TelldusBridgeHandler tellstickBridgeHandler) { + TellstickDiscoveryService service = discoveryService; + if (service != null) { + if (service.isOnlyForOneBridgeHandler()) { + service.deactivate(); + discoveryService = null; + ServiceRegistration serviceReg = discoveryServiceRegistration; + if (serviceReg != null) { + serviceReg.unregister(); + discoveryServiceRegistration = null; + } + } else { + service.removeBridgeHandler(tellstickBridgeHandler); + } } } @Override - protected ThingHandler createHandler(Thing thing) { + protected @Nullable ThingHandler createHandler(Thing thing) { if (thing.getThingTypeUID().equals(TELLDUSCOREBRIDGE_THING_TYPE)) { TelldusCoreBridgeHandler handler = new TelldusCoreBridgeHandler((Bridge) thing); registerDeviceDiscoveryService(handler); @@ -91,4 +117,11 @@ protected ThingHandler createHandler(Thing thing) { return null; } } + + @Override + protected void removeHandler(ThingHandler thingHandler) { + if (thingHandler instanceof TelldusBridgeHandler) { + unregisterDeviceDiscoveryService((TelldusBridgeHandler) thingHandler); + } + } } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java index 2c1ed8e41c07e..03b9cf046cca7 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/core/TelldusCoreBridgeHandler.java @@ -16,8 +16,9 @@ import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Set; import java.util.Vector; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ConcurrentHashMap; import org.openhab.binding.tellstick.internal.conf.TellstickBridgeConfiguration; import org.openhab.binding.tellstick.internal.handler.DeviceStatusListener; @@ -66,7 +67,7 @@ public TelldusCoreBridgeHandler(Bridge br) { private List sensorList = new Vector<>(); private TellstickEventHandler eventHandler; private static boolean initialized = false; - private List deviceStatusListeners = new CopyOnWriteArrayList<>(); + private Set deviceStatusListeners = ConcurrentHashMap.newKeySet(); @Override public void handleCommand(ChannelUID channelUID, Command command) { @@ -160,7 +161,6 @@ public void rescanTelldusDevices() { } } - } catch (SupportedMethodsException e) { logger.error("Failed to get devices ", e); } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java index 41c48c7955264..5f3e5f5e9af56 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/discovery/TellstickDiscoveryService.java @@ -205,4 +205,13 @@ public void addBridgeHandler(TelldusBridgeHandler tellstickBridgeHandler) { telldusBridgeHandlers.add(tellstickBridgeHandler); tellstickBridgeHandler.registerDeviceStatusListener(this); } + + public void removeBridgeHandler(TelldusBridgeHandler tellstickBridgeHandler) { + telldusBridgeHandlers.remove(tellstickBridgeHandler); + tellstickBridgeHandler.unregisterDeviceStatusListener(this); + } + + public boolean isOnlyForOneBridgeHandler() { + return telldusBridgeHandlers.size() == 1; + } } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java index 133cb3a809238..c0618a928bd59 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/handler/TelldusDevicesHandler.java @@ -151,24 +151,26 @@ private void refreshDevice(Device dev) { public void initialize() { Configuration config = getConfig(); logger.debug("Initialize TelldusDeviceHandler {}. class {}", config, config.getClass()); - final Object configDeviceId = config.get(TellstickBindingConstants.DEVICE_ID); - if (configDeviceId != null) { - deviceId = configDeviceId.toString(); - } else { - logger.debug("Initialized TellStick device missing serialNumber configuration... troubles ahead"); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR); - } final Boolean isADimmer = (Boolean) config.get(TellstickBindingConstants.DEVICE_ISDIMMER); if (isADimmer != null) { - this.isDimmer = isADimmer; + isDimmer = isADimmer; } final BigDecimal repeatCount = (BigDecimal) config.get(TellstickBindingConstants.DEVICE_RESEND_COUNT); if (repeatCount != null) { resend = repeatCount.intValue(); } - Bridge bridge = getBridge(); - if (bridge != null) { - bridgeStatusChanged(bridge.getStatusInfo()); + final Object configDeviceId = config.get(TellstickBindingConstants.DEVICE_ID); + if (configDeviceId != null) { + deviceId = configDeviceId.toString(); + Bridge bridge = getBridge(); + if (bridge != null) { + bridgeStatusChanged(bridge.getStatusInfo()); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No bridge defined"); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Missing serialNumber configuration"); } } @@ -180,7 +182,7 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { Bridge localBridge = getBridge(); if (localBridge != null) { TelldusBridgeHandler telldusBridgeHandler = (TelldusBridgeHandler) localBridge.getHandler(); - logger.debug("Init bridge for {}, bridge:{}", deviceId, telldusBridgeHandler); + logger.debug("Init device {}, bridge:{}", deviceId, telldusBridgeHandler); if (telldusBridgeHandler != null) { this.bridgeHandler = telldusBridgeHandler; this.bridgeHandler.registerDeviceStatusListener(this); @@ -208,12 +210,21 @@ public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) { } } } catch (Exception e) { - logger.error("Failed to init bridge for {}", deviceId, e); + logger.warn("Failed to init device {}", deviceId, e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR); } } else { - updateStatus(ThingStatus.OFFLINE, bridgeStatusInfo.getStatusDetail()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE); + } + } + + @Override + public void dispose() { + TelldusBridgeHandler bridgeHandler = getTellstickBridgeHandler(); + if (bridgeHandler != null) { + bridgeHandler.unregisterDeviceStatusListener(this); } + super.dispose(); } private Device getDevice(TelldusBridgeHandler tellHandler, String deviceId) { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveBridgeHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveBridgeHandler.java index 53814ff95d797..ed9fcc6b8bcbf 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveBridgeHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveBridgeHandler.java @@ -12,8 +12,11 @@ */ package org.openhab.binding.tellstick.internal.live; +import java.time.Duration; +import java.time.Instant; import java.util.List; -import java.util.Vector; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -51,14 +54,19 @@ */ public class TelldusLiveBridgeHandler extends BaseBridgeHandler implements TelldusBridgeHandler { + private static final int REFRESH_DELAY = 10; + private final Logger logger = LoggerFactory.getLogger(TelldusLiveBridgeHandler.class); private TellstickNetDevices deviceList = null; private TellstickNetSensors sensorList = null; private TelldusLiveDeviceController controller = new TelldusLiveDeviceController(); - private List deviceStatusListeners = new Vector<>(); + private Set deviceStatusListeners = ConcurrentHashMap.newKeySet(); - private static final int REFRESH_DELAY = 10; + private int nbRefresh; + private long sumRefreshDuration; + private long minRefreshDuration = 1_000_000; + private long maxRefreshDuration; public TelldusLiveBridgeHandler(Bridge bridge) { super(bridge); @@ -88,9 +96,8 @@ public void initialize() { this.controller = new TelldusLiveDeviceController(); this.controller.connectHttpClient(configuration.publicKey, configuration.privateKey, configuration.token, configuration.tokenSecret); + updateStatus(ThingStatus.UNKNOWN); startAutomaticRefresh(configuration.refreshInterval); - refreshDeviceList(); - updateStatus(ThingStatus.ONLINE); } private synchronized void startAutomaticRefresh(long refreshInterval) { @@ -112,17 +119,36 @@ private void scheduleImmediateRefresh() { } synchronized void refreshDeviceList() { + Instant start = Instant.now(); try { updateDevices(deviceList); updateSensors(sensorList); updateStatus(ThingStatus.ONLINE); - } catch (TellstickException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - logger.error("Failed to update", e); } catch (Exception e) { + logger.warn("Failed to update", e); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - logger.error("Failed to update", e); } + monitorAdditionalRefresh(start, Instant.now()); + } + + private void monitorAdditionalRefresh(Instant start, Instant end) { + if (!logger.isDebugEnabled()) { + return; + } + long duration = Duration.between(start, end).toMillis(); + sumRefreshDuration += duration; + nbRefresh++; + if (duration < minRefreshDuration) { + minRefreshDuration = duration; + } + if (duration > maxRefreshDuration) { + maxRefreshDuration = duration; + } + logger.debug( + "{} refresh avg = {} ms min = {} max = {} (request avg = {} ms min = {} max = {}) ({} timeouts {} errors)", + nbRefresh, nbRefresh == 0 ? 0 : sumRefreshDuration / nbRefresh, minRefreshDuration, maxRefreshDuration, + controller.getAverageRequestDuration(), controller.getMinRequestDuration(), + controller.getMaxRequestDuration(), controller.getNbTimeouts(), controller.getNbErrors()); } private synchronized void updateDevices(TellstickNetDevices previouslist) throws TellstickException { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveDeviceController.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveDeviceController.java index b78c60947ee8e..253ee6d3d1bae 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveDeviceController.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/TelldusLiveDeviceController.java @@ -14,6 +14,8 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Duration; +import java.time.Instant; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -35,6 +37,7 @@ import org.asynchttpclient.oauth.ConsumerKey; import org.asynchttpclient.oauth.OAuthSignatureCalculator; import org.asynchttpclient.oauth.RequestToken; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.tellstick.internal.TelldusBindingException; import org.openhab.binding.tellstick.internal.handler.TelldusDeviceController; import org.openhab.binding.tellstick.internal.live.xml.TelldusLiveResponse; @@ -66,7 +69,7 @@ public class TelldusLiveDeviceController implements DeviceChangeListener, Sensor private final Logger logger = LoggerFactory.getLogger(TelldusLiveDeviceController.class); private long lastSend = 0; public static final long DEFAULT_INTERVAL_BETWEEN_SEND = 250; - static final int REQUEST_TIMEOUT_MS = 5000; + private static final int REQUEST_TIMEOUT_MS = 15000; private AsyncHttpClient client; static final String HTTP_API_TELLDUS_COM_XML = "http://api.telldus.com/xml/"; static final String HTTP_TELLDUS_CLIENTS = HTTP_API_TELLDUS_COM_XML + "clients/list"; @@ -77,7 +80,13 @@ public class TelldusLiveDeviceController implements DeviceChangeListener, Sensor static final String HTTP_TELLDUS_DEVICE_DIM = HTTP_API_TELLDUS_COM_XML + "device/dim?id=%d&level=%d"; static final String HTTP_TELLDUS_DEVICE_TURNOFF = HTTP_API_TELLDUS_COM_XML + "device/turnOff?id=%d"; static final String HTTP_TELLDUS_DEVICE_TURNON = HTTP_API_TELLDUS_COM_XML + "device/turnOn?id=%d"; - private static final int MAX_RETRIES = 3; + + private int nbRequest; + private long sumRequestDuration; + private long minRequestDuration = 1_000_000; + private long maxRequestDuration; + private int nbTimeouts; + private int nbErrors; public TelldusLiveDeviceController() { } @@ -87,7 +96,7 @@ public void dispose() { try { client.close(); } catch (Exception e) { - logger.error("Failed to close client", e); + logger.debug("Failed to close client", e); } } @@ -101,7 +110,7 @@ void connectHttpClient(String publicKey, String privateKey, String token, String Response response = client.prepareGet(HTTP_TELLDUS_CLIENTS).execute().get(); logger.debug("Response {} statusText {}", response.getResponseBody(), response.getStatusText()); } catch (InterruptedException | ExecutionException e) { - logger.error("Failed to connect", e); + logger.warn("Failed to connect", e); } } @@ -114,7 +123,7 @@ private AsyncHttpClientConfig createAsyncHttpClientConfig() { @Override public void handleSendEvent(Device device, int resendCount, boolean isdimmer, Command command) throws TellstickException { - logger.info("Send {} to {}", command, device); + logger.debug("Send {} to {}", command, device); if (device instanceof TellstickNetDevice) { if (command == OnOffType.ON) { turnOn(device); @@ -276,37 +285,71 @@ public void onRequest(TellstickDeviceEvent newDevices) { } T callRestMethod(String uri, Class response) throws TelldusLiveException { + Instant start = Instant.now(); T resultObj = null; try { - for (int i = 0; i < MAX_RETRIES; i++) { - try { - resultObj = innerCallRest(uri, response); - break; - } catch (TimeoutException e) { - logger.warn("TimeoutException error in get", e); - } catch (InterruptedException e) { - logger.warn("InterruptedException error in get", e); - } - } - } catch (JAXBException e) { - logger.warn("Encoding error in get", e); + resultObj = innerCallRest(uri, response); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); logResponse(uri, e); + monitorAdditionalRequest(start, Instant.now(), e); throw new TelldusLiveException(e); - } catch (XMLStreamException e) { - logger.warn("Communication error in get", e); + } catch (TimeoutException | ExecutionException | JAXBException | XMLStreamException e) { logResponse(uri, e); - throw new TelldusLiveException(e); - } catch (ExecutionException e) { - logger.warn("ExecutionException error in get", e); + monitorAdditionalRequest(start, Instant.now(), e); throw new TelldusLiveException(e); } + monitorAdditionalRequest(start, Instant.now(), null); return resultObj; } + private void monitorAdditionalRequest(Instant start, Instant end, @Nullable Throwable exception) { + if (!logger.isDebugEnabled()) { + return; + } + long duration = Duration.between(start, end).toMillis(); + sumRequestDuration += duration; + nbRequest++; + if (duration < minRequestDuration) { + minRequestDuration = duration; + } + if (duration > maxRequestDuration) { + maxRequestDuration = duration; + } + if (exception instanceof TimeoutException) { + nbTimeouts++; + } else if (exception != null) { + nbErrors++; + } + } + + public long getAverageRequestDuration() { + return nbRequest == 0 ? 0 : sumRequestDuration / nbRequest; + } + + public long getMinRequestDuration() { + return minRequestDuration; + } + + public long getMaxRequestDuration() { + return maxRequestDuration; + } + + public int getNbTimeouts() { + return nbTimeouts; + } + + public int getNbErrors() { + return nbErrors; + } + private T innerCallRest(String uri, Class response) throws InterruptedException, ExecutionException, TimeoutException, JAXBException, FactoryConfigurationError, XMLStreamException { Future future = client.prepareGet(uri).execute(); Response resp = future.get(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (resp.getStatusCode() != 200) { + throw new ExecutionException("HTTP request returned status code " + resp.getStatusCode(), null); + } // TelldusLiveHandler.logger.info("Devices" + resp.getResponseBody()); JAXBContext jc = JAXBContext.newInstance(response); XMLInputFactory xif = XMLInputFactory.newInstance(); @@ -324,8 +367,6 @@ private T innerCallRest(String uri, Class response) throws InterruptedExc } private void logResponse(String uri, Exception e) { - if (e != null) { - logger.warn("Request [{}] Failure:{}", uri, e.getMessage()); - } + logger.warn("Request [{}] failed: {} {}", uri, e.getClass().getSimpleName(), e.getMessage()); } } diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetDevice.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetDevice.java index 2a98601b0a0d6..ea4ac9392127f 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetDevice.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetDevice.java @@ -151,10 +151,7 @@ public boolean equals(Object obj) { return false; } TellstickNetDevice other = (TellstickNetDevice) obj; - if (deviceId != other.deviceId) { - return false; - } - return true; + return deviceId == other.deviceId; } public int getMethods() { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetSensor.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetSensor.java index bdb670ea86ec0..799e051a877ee 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetSensor.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/live/xml/TellstickNetSensor.java @@ -143,10 +143,7 @@ public boolean equals(Object obj) { return false; } TellstickNetSensor other = (TellstickNetSensor) obj; - if (deviceId != other.deviceId) { - return false; - } - return true; + return deviceId == other.deviceId; } public void setUpdated(boolean b) { diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java index 7a4f184bc8327..be0d8d2d1f55a 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalBridgeHandler.java @@ -13,9 +13,9 @@ package org.openhab.binding.tellstick.internal.local; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -50,7 +50,7 @@ * to the framework. All {@link TelldusDevicesHandler}s use the * {@link TelldusLocalDeviceController} to execute the actual commands. * - * @author Jan Gustafsson- Initial contribution + * @author Jan Gustafsson - Initial contribution */ public class TelldusLocalBridgeHandler extends BaseBridgeHandler implements TelldusBridgeHandler { @@ -59,7 +59,7 @@ public class TelldusLocalBridgeHandler extends BaseBridgeHandler implements Tell private TellstickLocalDevicesDTO deviceList = null; private TellstickLocalSensorsDTO sensorList = null; private TelldusLocalDeviceController controller = null; - private List deviceStatusListeners = Collections.synchronizedList(new ArrayList<>()); + private Set deviceStatusListeners = ConcurrentHashMap.newKeySet(); private final HttpClient httpClient; private ScheduledFuture pollingJob; /** diff --git a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java index ea459f1bc40e3..4f1dbecf29b7f 100644 --- a/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java +++ b/bundles/org.openhab.binding.tellstick/src/main/java/org/openhab/binding/tellstick/internal/local/TelldusLocalDeviceController.java @@ -56,7 +56,6 @@ public class TelldusLocalDeviceController implements DeviceChangeListener, Senso private final Logger logger = LoggerFactory.getLogger(TelldusLocalDeviceController.class); private long lastSend = 0; public static final long DEFAULT_INTERVAL_BETWEEN_SEND_SEC = 250; - static final int REQUEST_TIMEOUT_MS = 5000; private final HttpClient httpClient; private final Gson gson = new Gson(); private String localApiUrl; From db0ca281ca92a6bfacb6340f6ff3981232774657 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Sat, 5 Nov 2022 09:57:06 -0600 Subject: [PATCH 30/55] [mqtt.homeassistant] support non-RGB lights (#13413) * [mqtt.homeassistant] support non-RGB lights dynamically decide which type of channel to expose. also send "down-typed" commands to the proper topic. this also sets the groundwork for supporting template and JSON schemas Signed-off-by: Cody Cutrer --- .../README.md | 3 +- .../internal/ComponentChannel.java | 12 +- .../internal/HomeAssistantChannelState.java | 15 +- .../internal/component/ComponentFactory.java | 2 +- .../component/DefaultSchemaLight.java | 350 ++++++++++++++++++ .../internal/component/Light.java | 332 +++++++++++------ .../component/AbstractComponentTests.java | 14 + .../component/DefaultSchemaLightTests.java | 282 ++++++++++++++ .../internal/component/LightTests.java | 94 ----- 9 files changed, 893 insertions(+), 211 deletions(-) create mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java create mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLightTests.java delete mode 100644 bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/README.md b/bundles/org.openhab.binding.mqtt.homeassistant/README.md index 409a788753570..09dd009cfb681 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/README.md +++ b/bundles/org.openhab.binding.mqtt.homeassistant/README.md @@ -22,7 +22,8 @@ These can be installed under `Settings` → `Addon` → `Transformations` * The HomeAssistant Fan Components only support ON/OFF. * The HomeAssistant Cover Components only support OPEN/CLOSE/STOP. -* The HomeAssistant Light Component only supports RGB color changes. +* The HomeAssistant Light Component only support on/off, brightness, and RGB. + Other color spaces, color temperature, effects, and white channel may work, but are untested. * The HomeAssistant Climate Components is not yet supported. ## Tasmota auto discovery diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java index 62828ad41a064..5e603b8432178 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/ComponentChannel.java @@ -136,6 +136,8 @@ public static class Builder { private @Nullable String templateIn; private @Nullable String templateOut; + private String format = "%s"; + public Builder(AbstractComponent component, String channelID, Value valueState, String label, ChannelStateUpdateListener channelStateUpdateListener) { this.component = component; @@ -206,6 +208,11 @@ public Builder commandFilter(@Nullable Predicate commandFilter) { return this; } + public Builder withFormat(String format) { + this.format = format; + return this; + } + public ComponentChannel build() { return build(true); } @@ -222,11 +229,10 @@ public ComponentChannel build(boolean addToComponent) { channelUID.getGroupId() + "_" + channelID); channelState = new HomeAssistantChannelState( ChannelConfigBuilder.create().withRetain(retain).withQos(qos).withStateTopic(stateTopic) - .withCommandTopic(commandTopic).makeTrigger(trigger).build(), + .withCommandTopic(commandTopic).makeTrigger(trigger).withFormatter(format).build(), channelUID, valueState, channelStateUpdateListener, commandFilter); - String localStateTopic = stateTopic; - if (localStateTopic == null || localStateTopic.isBlank() || this.trigger) { + if (this.trigger) { type = ChannelTypeBuilder.trigger(channelTypeUID, label) .withConfigDescriptionURI(URI.create(MqttBindingConstants.CONFIG_HA_CHANNEL)) .isAdvanced(isAdvanced).build(); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java index d908791ef551e..4ce52a8f18637 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/HomeAssistantChannelState.java @@ -56,9 +56,18 @@ public HomeAssistantChannelState(ChannelConfig config, ChannelUID channelUID, Va @Override public CompletableFuture publishValue(Command command) { - if (commandFilter != null && !commandFilter.test(command)) { - logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID, command); - return CompletableFuture.completedFuture(false); + if (commandFilter != null) { + try { + if (!commandFilter.test(command)) { + logger.trace("Channel {} updates are disabled by command filter, ignoring command {}", channelUID, + command); + return CompletableFuture.completedFuture(false); + } + } catch (IllegalArgumentException e) { + CompletableFuture f = new CompletableFuture<>(); + f.completeExceptionally(e); + return f; + } } return super.publishValue(command); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java index cb755ac0fef7c..98264e322e84c 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/ComponentFactory.java @@ -66,7 +66,7 @@ public static AbstractComponent createComponent(ThingUID thingUID, HaID haID, case "climate": return new Climate(componentConfiguration); case "light": - return new Light(componentConfiguration); + return Light.create(componentConfiguration); case "lock": return new Lock(componentConfiguration); case "sensor": diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java new file mode 100644 index 0000000000000..e1b2355d0999f --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLight.java @@ -0,0 +1,350 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant.internal.component; + +import java.math.BigDecimal; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; +import org.openhab.binding.mqtt.generic.mapping.ColorMode; +import org.openhab.binding.mqtt.generic.values.ColorValue; +import org.openhab.binding.mqtt.generic.values.TextValue; +import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification. + * + * Specifically, the default schema. This class will present a single channel for color, brightness, + * or on/off as appropriate. Additional attributes are still exposed as dedicated channels. + * + * @author Cody Cutrer - Initial contribution + */ +@NonNullByDefault +public class DefaultSchemaLight extends Light { + protected static final String HS_CHANNEL_ID = "hs"; + protected static final String RGB_CHANNEL_ID = "rgb"; + protected static final String RGBW_CHANNEL_ID = "rgbw"; + protected static final String RGBWW_CHANNEL_ID = "rgbww"; + protected static final String XY_CHANNEL_ID = "xy"; + protected static final String WHITE_CHANNEL_ID = "white"; + + protected @Nullable ComponentChannel hsChannel; + protected @Nullable ComponentChannel rgbChannel; + protected @Nullable ComponentChannel xyChannel; + + public DefaultSchemaLight(ComponentFactory.ComponentConfiguration builder) { + super(builder); + } + + @Override + protected void buildChannels() { + ComponentChannel localOnOffChannel; + localOnOffChannel = onOffChannel = buildChannel(ON_OFF_CHANNEL_ID, onOffValue, "On/Off State", this) + .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate) + .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .commandFilter(this::handleRawOnOffCommand).build(false); + + @Nullable + ComponentChannel localBrightnessChannel = null; + if (channelConfiguration.brightnessStateTopic != null || channelConfiguration.brightnessCommandTopic != null) { + localBrightnessChannel = brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, brightnessValue, + "Brightness", this) + .stateTopic(channelConfiguration.brightnessStateTopic, + channelConfiguration.brightnessValueTemplate) + .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .withFormat("%.0f").commandFilter(this::handleBrightnessCommand).build(false); + } + + if (channelConfiguration.whiteCommandTopic != null) { + buildChannel(WHITE_CHANNEL_ID, brightnessValue, "Go directly to white of a specific brightness", this) + .commandTopic(channelConfiguration.whiteCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .isAdvanced(true).build(); + } + + if (channelConfiguration.colorModeStateTopic != null) { + buildChannel(COLOR_MODE_CHANNEL_ID, new TextValue(), "Current color mode", this) + .stateTopic(channelConfiguration.colorModeStateTopic, channelConfiguration.colorModeValueTemplate) + .build(); + } + + if (channelConfiguration.colorTempStateTopic != null || channelConfiguration.colorTempCommandTopic != null) { + buildChannel(COLOR_TEMP_CHANNEL_ID, colorTempValue, "Color Temperature", this) + .stateTopic(channelConfiguration.colorTempStateTopic, channelConfiguration.colorTempValueTemplate) + .commandTopic(channelConfiguration.colorTempCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(); + } + + if (channelConfiguration.effectStateTopic != null || channelConfiguration.effectCommandTopic != null) { + buildChannel(EFFECT_CHANNEL_ID, effectValue, "Lighting effect", this) + .stateTopic(channelConfiguration.effectStateTopic, channelConfiguration.effectValueTemplate) + .commandTopic(channelConfiguration.effectCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(); + } + + if (channelConfiguration.rgbStateTopic != null || channelConfiguration.rgbCommandTopic != null) { + hasColorChannel = true; + hiddenChannels.add(rgbChannel = buildChannel(RGB_CHANNEL_ID, new ColorValue(ColorMode.RGB, null, null, 100), + "RGB state", this) + .stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate) + .commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(false)); + } + + if (channelConfiguration.rgbwStateTopic != null || channelConfiguration.rgbwCommandTopic != null) { + hasColorChannel = true; + hiddenChannels.add(buildChannel(RGBW_CHANNEL_ID, new TextValue(), "RGBW state", this) + .stateTopic(channelConfiguration.rgbwStateTopic, channelConfiguration.rgbwValueTemplate) + .commandTopic(channelConfiguration.rgbwCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(false)); + } + + if (channelConfiguration.rgbwwStateTopic != null || channelConfiguration.rgbwwCommandTopic != null) { + hasColorChannel = true; + hiddenChannels.add(buildChannel(RGBWW_CHANNEL_ID, new TextValue(), "RGBWW state", this) + .stateTopic(channelConfiguration.rgbwwStateTopic, channelConfiguration.rgbwwValueTemplate) + .commandTopic(channelConfiguration.rgbwwCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(false)); + } + + if (channelConfiguration.xyStateTopic != null || channelConfiguration.xyCommandTopic != null) { + hasColorChannel = true; + hiddenChannels.add( + xyChannel = buildChannel(XY_CHANNEL_ID, new ColorValue(ColorMode.XYY, null, null, 100), "XY State", + this).stateTopic(channelConfiguration.xyStateTopic, channelConfiguration.xyValueTemplate) + .commandTopic(channelConfiguration.xyCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(false)); + } + + if (channelConfiguration.hsStateTopic != null || channelConfiguration.hsCommandTopic != null) { + hasColorChannel = true; + hiddenChannels.add(this.hsChannel = buildChannel(HS_CHANNEL_ID, new TextValue(), "Hue and Saturation", this) + .stateTopic(channelConfiguration.hsStateTopic, channelConfiguration.hsValueTemplate) + .commandTopic(channelConfiguration.hsCommandTopic, channelConfiguration.isRetain(), + channelConfiguration.getQos()) + .build(false)); + } + + if (hasColorChannel) { + hiddenChannels.add(localOnOffChannel); + if (localBrightnessChannel != null) { + hiddenChannels.add(localBrightnessChannel); + } + buildChannel(COLOR_CHANNEL_ID, colorValue, "Color", this) + .commandTopic(DUMMY_TOPIC, channelConfiguration.isRetain(), channelConfiguration.getQos()) + .commandFilter(this::handleColorCommand).build(); + } else if (localBrightnessChannel != null) { + hiddenChannels.add(localOnOffChannel); + channels.put(BRIGHTNESS_CHANNEL_ID, localBrightnessChannel); + } else { + channels.put(ON_OFF_CHANNEL_ID, localOnOffChannel); + } + } + + // all handle*Command methods return false if they've been handled, + // or true if default handling should continue + + // The commandFilter for onOffChannel + private boolean handleRawOnOffCommand(Command command) { + // on_command_type of brightness is not allowed to send an actual on command + if (command.equals(OnOffType.ON) && channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_BRIGHTNESS)) { + // No prior state (or explicit off); set to 100% + if (brightnessValue.getChannelState() instanceof UnDefType + || brightnessValue.getChannelState().equals(PercentType.ZERO)) { + brightnessChannel.getState().publishValue(PercentType.HUNDRED); + } else { + brightnessChannel.getState().publishValue((Command) brightnessValue.getChannelState()); + } + return false; + } + + return true; + } + + // The helper method the other commandFilters call + private boolean handleOnOffCommand(Command command) { + if (!handleRawOnOffCommand(command)) { + return false; + } + + // OnOffType commands to go the regular command topic + if (command instanceof OnOffType) { + onOffChannel.getState().publishValue(command); + return false; + } + + boolean needsOn = !onOffValue.getChannelState().equals(OnOffType.ON); + if (command.equals(PercentType.ZERO) || command.equals(HSBType.BLACK)) { + needsOn = false; + } + if (needsOn) { + if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_FIRST)) { + onOffChannel.getState().publishValue(OnOffType.ON); + } else if (channelConfiguration.onCommandType.equals(ON_COMMAND_TYPE_LAST)) { + // TODO: schedule the ON publish for after this is sent + } + } + return true; + } + + private boolean handleBrightnessCommand(Command command) { + // if it's OnOffType, it'll get handled by this; otherwise it'll return + // true and PercentType will be handled as normal + return handleOnOffCommand(command); + } + + private boolean handleColorCommand(Command command) { + if (!handleOnOffCommand(command)) { + return false; + } else if (command instanceof HSBType) { + HSBType color = (HSBType) command; + if (channelConfiguration.hsCommandTopic != null) { + // If we don't have a brightness channel, something is probably busted + // but don't choke + if (channelConfiguration.brightnessCommandTopic != null) { + brightnessChannel.getState().publishValue(color.getBrightness()); + } + String hs = String.format("%d,%d", color.getHue().intValue(), color.getSaturation().intValue()); + hsChannel.getState().publishValue(new StringType(hs)); + } else if (channelConfiguration.rgbCommandTopic != null) { + rgbChannel.getState().publishValue(command); + // } else if (channelConfiguration.rgbwCommandTopic != null) { + // TODO + // } else if (channelConfiguration.rgbwwCommandTopic != null) { + // TODO + } else if (channelConfiguration.xyCommandTopic != null) { + PercentType[] xy = color.toXY(); + // If we don't have a brightness channel, something is probably busted + // but don't choke + if (channelConfiguration.brightnessCommandTopic != null) { + brightnessChannel.getState().publishValue(color.getBrightness()); + } + String xyString = String.format("%f,%f", xy[0].doubleValue(), xy[1].doubleValue()); + xyChannel.getState().publishValue(new StringType(xyString)); + } + } else if (command instanceof PercentType) { + if (channelConfiguration.brightnessCommandTopic != null) { + brightnessChannel.getState().publishValue(command); + } else { + // No brightness command topic?! must be RGB only + // so re-calculatate + State color = colorValue.getChannelState(); + if (color instanceof UnDefType) { + color = HSBType.WHITE; + } + HSBType existingColor = (HSBType) color; + HSBType newCommand = new HSBType(existingColor.getHue(), existingColor.getSaturation(), + (PercentType) command); + // re-process + handleColorCommand(newCommand); + } + } + return false; + } + + @Override + public void updateChannelState(ChannelUID channel, State state) { + ChannelStateUpdateListener listener = this.channelStateUpdateListener; + switch (channel.getIdWithoutGroup()) { + case ON_OFF_CHANNEL_ID: + if (hasColorChannel) { + HSBType newOnState = colorValue.getChannelState() instanceof HSBType + ? (HSBType) colorValue.getChannelState() + : HSBType.WHITE; + if (state.equals(OnOffType.ON)) { + colorValue.update(newOnState); + } + + listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID), + state.equals(OnOffType.ON) ? newOnState : HSBType.BLACK); + } else if (brightnessChannel != null) { + listener.updateChannelState(new ChannelUID(channel.getThingUID(), BRIGHTNESS_CHANNEL_ID), + state.equals(OnOffType.ON) ? brightnessValue.getChannelState() : PercentType.ZERO); + } else { + listener.updateChannelState(channel, state); + } + return; + case BRIGHTNESS_CHANNEL_ID: + onOffValue.update(Objects.requireNonNull(state.as(OnOffType.class))); + if (hasColorChannel) { + if (colorValue.getChannelState() instanceof HSBType) { + HSBType hsb = (HSBType) (colorValue.getChannelState()); + colorValue.update(new HSBType(hsb.getHue(), hsb.getSaturation(), + (PercentType) brightnessValue.getChannelState())); + } else { + colorValue.update(new HSBType(DecimalType.ZERO, PercentType.ZERO, + (PercentType) brightnessValue.getChannelState())); + } + listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID), + colorValue.getChannelState()); + } else { + listener.updateChannelState(channel, state); + } + return; + case COLOR_TEMP_CHANNEL_ID: + case EFFECT_CHANNEL_ID: + // Real channels; pass through + listener.updateChannelState(channel, state); + return; + case HS_CHANNEL_ID: + case XY_CHANNEL_ID: + if (brightnessValue.getChannelState() instanceof UnDefType) { + brightnessValue.update(PercentType.HUNDRED); + } + String[] split = state.toString().split(","); + if (split.length != 2) { + throw new IllegalArgumentException(state.toString() + " is not a valid string syntax"); + } + float x = Float.parseFloat(split[0]); + float y = Float.parseFloat(split[1]); + PercentType brightness = (PercentType) brightnessValue.getChannelState(); + if (channel.getIdWithoutGroup().equals(HS_CHANNEL_ID)) { + colorValue.update(new HSBType(new DecimalType(x), new PercentType(new BigDecimal(y)), brightness)); + } else { + HSBType xyColor = HSBType.fromXY(x, y); + colorValue.update(new HSBType(xyColor.getHue(), xyColor.getSaturation(), brightness)); + } + listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID), + colorValue.getChannelState()); + return; + case RGB_CHANNEL_ID: + colorValue.update((HSBType) state); + listener.updateChannelState(new ChannelUID(getGroupUID(), COLOR_CHANNEL_ID), + colorValue.getChannelState()); + break; + case RGBW_CHANNEL_ID: + case RGBWW_CHANNEL_ID: + // TODO: update color value + break; + } + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java index e9f2681863fef..32c0308debc01 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Light.java @@ -12,7 +12,10 @@ */ package org.openhab.binding.mqtt.homeassistant.internal.component; +import java.math.BigDecimal; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.stream.Stream; @@ -22,28 +25,56 @@ import org.openhab.binding.mqtt.generic.ChannelStateUpdateListener; import org.openhab.binding.mqtt.generic.mapping.ColorMode; import org.openhab.binding.mqtt.generic.values.ColorValue; +import org.openhab.binding.mqtt.generic.values.NumberValue; +import org.openhab.binding.mqtt.generic.values.OnOffValue; +import org.openhab.binding.mqtt.generic.values.PercentageValue; +import org.openhab.binding.mqtt.generic.values.TextValue; import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration; +import org.openhab.binding.mqtt.homeassistant.internal.exception.UnsupportedComponentException; import org.openhab.core.io.transport.mqtt.MqttBrokerConnection; +import org.openhab.core.library.unit.Units; import org.openhab.core.thing.ChannelUID; import org.openhab.core.types.Command; -import org.openhab.core.types.State; import com.google.gson.annotations.SerializedName; /** - * A MQTT light, following the https://www.home-assistant.io/components/light.mqtt/ specification. + * A MQTT light, following the + * https://www.home-assistant.io/components/light.mqtt/ specification. * - * This class condenses the three state/command topics (for ON/OFF, Brightness, Color) to one - * color channel. + * Individual concrete classes implement the differing semantics of the + * three different schemas. + * + * As of now, only on/off, brightness, and RGB are fully implemented and tested. + * HS and XY are implemented, but not tested. Color temp and effect are only + * implemented (but not tested) for the default schema. * * @author David Graeff - Initial contribution + * @author Cody Cutrer - Re-write for (nearly) full support */ @NonNullByDefault -public class Light extends AbstractComponent implements ChannelStateUpdateListener { - public static final String SWITCH_CHANNEL_ID = "light"; // Randomly chosen channel "ID" - public static final String BRIGHTNESS_CHANNEL_ID = "brightness"; // Randomly chosen channel "ID" - public static final String COLOR_CHANNEL_ID = "color"; // Randomly chosen channel "ID" +public abstract class Light extends AbstractComponent + implements ChannelStateUpdateListener { + protected static final String DEFAULT_SCHEMA = "default"; + protected static final String JSON_SCHEMA = "json"; + protected static final String TEMPLATE_SCHEMA = "template"; + + protected static final String STATE_CHANNEL_ID = "state"; + protected static final String ON_OFF_CHANNEL_ID = "on_off"; + protected static final String BRIGHTNESS_CHANNEL_ID = "brightness"; + protected static final String COLOR_MODE_CHANNEL_ID = "color_mode"; + protected static final String COLOR_TEMP_CHANNEL_ID = "color_temp"; + protected static final String EFFECT_CHANNEL_ID = "effect"; + // This channel is a synthetic channel that may send to other channels + // underneath + protected static final String COLOR_CHANNEL_ID = "color"; + + protected static final String DUMMY_TOPIC = "dummy"; + + protected static final String ON_COMMAND_TYPE_FIRST = "first"; + protected static final String ON_COMMAND_TYPE_BRIGHTNESS = "brightness"; + protected static final String ON_COMMAND_TYPE_LAST = "last"; /** * Configuration class for MQTT component @@ -53,155 +84,238 @@ static class ChannelConfiguration extends AbstractChannelConfiguration { super("MQTT Light"); } - @SerializedName("brightness_scale") - protected int brightnessScale = 255; - protected boolean optimistic = false; - @SerializedName("effect_list") - protected @Nullable List effectList; + /* Attributes that control the basic structure of the light */ - // Defines when on the payload_on is sent. Using last (the default) will send any style (brightness, color, etc) - // topics first and then a payload_on to the command_topic. Using first will send the payload_on and then any - // style topics. Using brightness will only send brightness commands instead of the payload_on to turn the light + protected String schema = DEFAULT_SCHEMA; + protected @Nullable Boolean optimistic; // All schemas + protected boolean brightness = false; // JSON schema only + @SerializedName("color_mode") + protected boolean colorMode = false; // JSON schema only + @SerializedName("supported_color_modes") + protected @Nullable List supportedColorModes; // JSON schema only + // Defines when on the payload_on is sent. Using last (the default) will send + // any style (brightness, color, etc) + // topics first and then a payload_on to the command_topic. Using first will + // send the payload_on and then any + // style topics. Using brightness will only send brightness commands instead of + // the payload_on to turn the light // on. @SerializedName("on_command_type") - protected String onCommandType = "last"; + protected String onCommandType = ON_COMMAND_TYPE_LAST; // Default schema only + + /* Basic control attributes */ @SerializedName("state_topic") - protected @Nullable String stateTopic; - @SerializedName("command_topic") - protected @Nullable String commandTopic; + protected @Nullable String stateTopic; // All Schemas @SerializedName("state_value_template") - protected @Nullable String stateValueTemplate; + protected @Nullable String stateValueTemplate; // Default schema only + @SerializedName("state_template") + protected @Nullable String stateTemplate; // Template schema only + @SerializedName("payload_on") + protected String payloadOn = "ON"; // Default schema only + @SerializedName("payload_off") + protected String payloadOff = "OFF"; // Default schema only + @SerializedName("command_topic") + protected @Nullable String commandTopic; // All schemas + @SerializedName("command_on_template") + protected @Nullable String commandOnTemplate; // Template schema only; required + @SerializedName("command_off_template") + protected @Nullable String commandOffTemplate; // Template schema only; required + + /* Brightness attributes */ + @SerializedName("brightness_scale") + protected int brightnessScale = 255; // Default, JSON schemas only @SerializedName("brightness_state_topic") - protected @Nullable String brightnessStateTopic; - @SerializedName("brightness_command_topic") - protected @Nullable String brightnessCommandTopic; + protected @Nullable String brightnessStateTopic; // Default schema only @SerializedName("brightness_value_template") - protected @Nullable String brightnessValueTemplate; + protected @Nullable String brightnessValueTemplate; // Default schema only + @SerializedName("brightness_template") + protected @Nullable String brightnessTemplate; // Template schema only + @SerializedName("brightness_command_topic") + protected @Nullable String brightnessCommandTopic; // Default schema only + @SerializedName("brightness_command_template") + protected @Nullable String brightnessCommandTemplate; // Default schema only + /* White value attributes */ + + @SerializedName("white_scale") + protected int whiteScale = 255; // Default, JSON schemas only + @SerializedName("white_command_topic") + protected @Nullable String whiteCommandTopic; // Default schema only + + /* Color mode attributes */ + + @SerializedName("color_mode_state_topic") + protected @Nullable String colorModeStateTopic; // Default schema only + @SerializedName("color_mode_value_template") + protected @Nullable String colorModeValueTemplate; // Default schema only + + /* Color temp attributes */ + + @SerializedName("min_mireds") + protected @Nullable Integer minMireds; // All schemas + @SerializedName("max_mireds") + protected @Nullable Integer maxMireds; // All schemas @SerializedName("color_temp_state_topic") - protected @Nullable String colorTempStateTopic; - @SerializedName("color_temp_command_topic") - protected @Nullable String colorTempCommandTopic; + protected @Nullable String colorTempStateTopic; // Default schema only @SerializedName("color_temp_value_template") - protected @Nullable String colorTempValueTemplate; + protected @Nullable String colorTempValueTemplate; // Default schema only + @SerializedName("color_temp_template") + protected @Nullable String colorTempTemplate; // Template schema only + @SerializedName("color_temp_command_topic") + protected @Nullable String colorTempCommandTopic; // Default schema only + @SerializedName("color_temp_command_template") + protected @Nullable String colorTempCommandTemplate; // Default schema only - @SerializedName("effect_command_topic") - protected @Nullable String effectCommandTopic; + /* Effect attributes */ + @SerializedName("effect_list") + protected @Nullable List effectList; // All schemas @SerializedName("effect_state_topic") - protected @Nullable String effectStateTopic; + protected @Nullable String effectStateTopic; // Default schema only @SerializedName("effect_value_template") - protected @Nullable String effectValueTemplate; + protected @Nullable String effectValueTemplate; // Default schema only + @SerializedName("effect_template") + protected @Nullable String effectTemplate; // Template schema only + @SerializedName("effect_command_topic") + protected @Nullable String effectCommandTopic; // Default schema only + @SerializedName("effect_command_template") + protected @Nullable String effectCommandTemplate; // Default schema only - @SerializedName("rgb_command_topic") - protected @Nullable String rgbCommandTopic; + /* HS attributes */ + @SerializedName("hs_state_topic") + protected @Nullable String hsStateTopic; // Default schema only + @SerializedName("hs_value_template") + protected @Nullable String hsValueTemplate; // Default schema only + @SerializedName("hs_command_topic") + protected @Nullable String hsCommandTopic; // Default schema only + + /* RGB attributes */ @SerializedName("rgb_state_topic") - protected @Nullable String rgbStateTopic; + protected @Nullable String rgbStateTopic; // Default schema only @SerializedName("rgb_value_template") - protected @Nullable String rgbValueTemplate; + protected @Nullable String rgbValueTemplate; // Default schema only + @SerializedName("red_template") + protected @Nullable String redTemplate; // Template schema only + @SerializedName("green_template") + protected @Nullable String greenTemplate; // Template schema only + @SerializedName("blue_template") + protected @Nullable String blueTemplate; // Template schema only + @SerializedName("rgb_command_topic") + protected @Nullable String rgbCommandTopic; // Default schema only @SerializedName("rgb_command_template") - protected @Nullable String rgbCommandTemplate; + protected @Nullable String rgbCommandTemplate; // Default schema only + + /* RGBW attributes */ + @SerializedName("rgbw_state_topic") + protected @Nullable String rgbwStateTopic; // Default schema only + @SerializedName("rgbw_value_template") + protected @Nullable String rgbwValueTemplate; // Default schema only + @SerializedName("rgbw_command_topic") + protected @Nullable String rgbwCommandTopic; // Default schema only + @SerializedName("rgbw_command_template") + protected @Nullable String rgbwCommandTemplate; // Default schema only - @SerializedName("white_value_command_topic") - protected @Nullable String whiteValueCommandTopic; - @SerializedName("white_value_state_topic") - protected @Nullable String whiteValueStateTopic; - @SerializedName("white_value_template") - protected @Nullable String whiteValueTemplate; + /* RGBWW attributes */ + @SerializedName("rgbww_state_topic") + protected @Nullable String rgbwwStateTopic; // Default schema only + @SerializedName("rgbww_value_template") + protected @Nullable String rgbwwValueTemplate; // Default schema only + @SerializedName("rgbww_command_topic") + protected @Nullable String rgbwwCommandTopic; // Default schema only + @SerializedName("rgbww_command_template") + protected @Nullable String rgbwwCommandTemplate; // Default schema only + /* XY attributes */ @SerializedName("xy_command_topic") - protected @Nullable String xyCommandTopic; + protected @Nullable String xyCommandTopic; // Default schema only @SerializedName("xy_state_topic") - protected @Nullable String xyStateTopic; + protected @Nullable String xyStateTopic; // Default schema only @SerializedName("xy_value_template") - protected @Nullable String xyValueTemplate; - - @SerializedName("payload_on") - protected String payloadOn = "ON"; - @SerializedName("payload_off") - protected String payloadOff = "OFF"; + protected @Nullable String xyValueTemplate; // Default schema only } - protected ComponentChannel colorChannel; - protected ComponentChannel switchChannel; - protected ComponentChannel brightnessChannel; - private final @Nullable ChannelStateUpdateListener channelStateUpdateListener; + protected final boolean optimistic; + protected boolean hasColorChannel = false; + + protected @Nullable ComponentChannel onOffChannel; + protected @Nullable ComponentChannel brightnessChannel; + + // State has to be stored here, in order to mux multiple + // MQTT sources into single OpenHAB channels + protected OnOffValue onOffValue; + protected PercentageValue brightnessValue; + protected final NumberValue colorTempValue; + protected final TextValue effectValue = new TextValue(); + protected final ColorValue colorValue = new ColorValue(ColorMode.HSB, null, null, 100); + + protected final List hiddenChannels = new ArrayList<>(); + protected final ChannelStateUpdateListener channelStateUpdateListener; + + public static Light create(ComponentFactory.ComponentConfiguration builder) throws UnsupportedComponentException { + String schema = builder.getConfig(ChannelConfiguration.class).schema; + switch (schema) { + case DEFAULT_SCHEMA: + return new DefaultSchemaLight(builder); + default: + throw new UnsupportedComponentException( + "Component '" + builder.getHaID() + "' of schema '" + schema + "' is not supported!"); + } + } - public Light(ComponentFactory.ComponentConfiguration builder) { + protected Light(ComponentFactory.ComponentConfiguration builder) { super(builder, ChannelConfiguration.class); this.channelStateUpdateListener = builder.getUpdateListener(); - ColorValue value = new ColorValue(ColorMode.RGB, channelConfiguration.payloadOn, - channelConfiguration.payloadOff, 100); - - // Create three MQTT subscriptions and use this class object as update listener - switchChannel = buildChannel(SWITCH_CHANNEL_ID, value, channelConfiguration.getName(), this) - .stateTopic(channelConfiguration.stateTopic, channelConfiguration.stateValueTemplate, - channelConfiguration.getValueTemplate()) - .commandTopic(channelConfiguration.commandTopic, channelConfiguration.isRetain(), - channelConfiguration.getQos()) - .build(false); - - colorChannel = buildChannel(COLOR_CHANNEL_ID, value, channelConfiguration.getName(), this) - .stateTopic(channelConfiguration.rgbStateTopic, channelConfiguration.rgbValueTemplate) - .commandTopic(channelConfiguration.rgbCommandTopic, channelConfiguration.isRetain(), - channelConfiguration.getQos()) - .build(false); - - brightnessChannel = buildChannel(BRIGHTNESS_CHANNEL_ID, value, channelConfiguration.getName(), this) - .stateTopic(channelConfiguration.brightnessStateTopic, channelConfiguration.brightnessValueTemplate) - .commandTopic(channelConfiguration.brightnessCommandTopic, channelConfiguration.isRetain(), - channelConfiguration.getQos()) - .build(false); - - channels.put(COLOR_CHANNEL_ID, colorChannel); + + @Nullable + Boolean optimistic = channelConfiguration.optimistic; + if (optimistic != null) { + this.optimistic = optimistic; + } else { + this.optimistic = (channelConfiguration.stateTopic == null); + } + + onOffValue = new OnOffValue(channelConfiguration.payloadOn, channelConfiguration.payloadOff); + brightnessValue = new PercentageValue(null, new BigDecimal(channelConfiguration.brightnessScale), null, null, + null); + @Nullable + BigDecimal min = null, max = null; + if (channelConfiguration.minMireds != null) { + min = new BigDecimal(channelConfiguration.minMireds); + } + if (channelConfiguration.maxMireds != null) { + max = new BigDecimal(channelConfiguration.maxMireds); + } + colorTempValue = new NumberValue(min, max, BigDecimal.ONE, Units.MIRED); + + buildChannels(); } + protected abstract void buildChannels(); + @Override public CompletableFuture<@Nullable Void> start(MqttBrokerConnection connection, ScheduledExecutorService scheduler, int timeout) { - return Stream.of(switchChannel, brightnessChannel, colorChannel) // + return Stream.concat(channels.values().stream(), hiddenChannels.stream()) // .map(v -> v.start(connection, scheduler, timeout)) // .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v)); } @Override public CompletableFuture<@Nullable Void> stop() { - return Stream.of(switchChannel, brightnessChannel, colorChannel) // - .map(v -> v.stop()) // + return Stream.concat(channels.values().stream(), hiddenChannels.stream()) // + .filter(Objects::nonNull) // + .map(ComponentChannel::stop) // .reduce(CompletableFuture.completedFuture(null), (f, v) -> f.thenCompose(b -> v)); } - /** - * Proxy method to condense all three MQTT subscriptions to one channel - */ - @Override - public void updateChannelState(ChannelUID channelUID, State value) { - ChannelStateUpdateListener listener = channelStateUpdateListener; - if (listener != null) { - listener.updateChannelState(colorChannel.getChannelUID(), value); - } - } - - /** - * Proxy method to condense all three MQTT subscriptions to one channel - */ @Override public void postChannelCommand(ChannelUID channelUID, Command value) { - ChannelStateUpdateListener listener = channelStateUpdateListener; - if (listener != null) { - listener.postChannelCommand(colorChannel.getChannelUID(), value); - } + throw new UnsupportedOperationException(); } - /** - * Proxy method to condense all three MQTT subscriptions to one channel - */ @Override public void triggerChannel(ChannelUID channelUID, String eventPayload) { - ChannelStateUpdateListener listener = channelStateUpdateListener; - if (listener != null) { - listener.triggerChannel(colorChannel.getChannelUID(), eventPayload); - } + throw new UnsupportedOperationException(); } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java index cfc1df26d8b4c..2336b80afe3f4 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java @@ -51,6 +51,7 @@ import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.types.Command; import org.openhab.core.types.State; /** @@ -244,6 +245,19 @@ protected boolean publishMessage(String mqttTopic, byte[] payload) { return false; } + /** + * Send command to a thing's channel + * + * @param component component + * @param channelId channel + * @param command command to send + */ + protected void sendCommand(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component, + String channelId, Command command) { + var channel = Objects.requireNonNull(component.getChannel(channelId)); + thingHandler.handleCommand(channel.getChannelUID(), command); + } + protected static class LatchThingHandler extends HomeAssistantThingHandler { private @Nullable CountDownLatch latch; private @Nullable AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> discoveredComponent; diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLightTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLightTests.java new file mode 100644 index 0000000000000..9fda31b5fcfaa --- /dev/null +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/DefaultSchemaLightTests.java @@ -0,0 +1,282 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mqtt.homeassistant.internal.component; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mqtt.generic.values.ColorValue; +import org.openhab.binding.mqtt.generic.values.OnOffValue; +import org.openhab.binding.mqtt.generic.values.PercentageValue; +import org.openhab.binding.mqtt.homeassistant.internal.ComponentChannel; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.HSBType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; + +/** + * Tests for {@link Light} confirming to the default schema + * + * @author Anton Kharuzhy - Initial contribution + */ +@NonNullByDefault +public class DefaultSchemaLightTests extends AbstractComponentTests { + public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt"; + + @Test + public void testRgb() throws InterruptedException { + // @formatter:off + var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), + "{ " + + " \"availability\": [ " + + " { " + + " \"topic\": \"zigbee2mqtt/bridge/state\" " + + " } " + + " ], " + + " \"device\": { " + + " \"identifiers\": [ " + + " \"zigbee2mqtt_0x0000000000000000\" " + + " ], " + + " \"manufacturer\": \"Lights inc\", " + + " \"model\": \"light v1\", " + + " \"name\": \"Light\", " + + " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " + + " }, " + + " \"name\": \"light\", " + + " \"state_topic\": \"zigbee2mqtt/light/state\", " + + " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + + " \"state_value_template\": \"{{ value_json.power }}\", " + + " \"payload_on\": \"ON_\", " + + " \"payload_off\": \"OFF_\", " + + " \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " + + " \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " + + " \"rgb_value_template\": \"{{ value_json.rgb }}\", " + + " \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " + + " \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " + + " \"brightness_value_template\": \"{{ value_json.br }}\" " + + "}"); + // @formatter:on + + assertThat(component.channels.size(), is(1)); + assertThat(component.getName(), is("light")); + + assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class); + + @Nullable + ComponentChannel onOffChannel = component.onOffChannel; + assertThat(onOffChannel, is(notNullValue())); + if (onOffChannel != null) { + assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State", + OnOffValue.class); + } + @Nullable + ComponentChannel brightnessChannel = component.brightnessChannel; + assertThat(brightnessChannel, is(notNullValue())); + if (brightnessChannel != null) { + assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness", + "Brightness", PercentageValue.class); + } + + publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}"); + assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE); + publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}"); + assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(10, 20, 30)); + publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}"); + assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE); + + sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE); + assertPublished("zigbee2mqtt/light/set/rgb", "0,0,255"); + + // Brightness commands should route to the correct topic + sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50)); + assertPublished("zigbee2mqtt/light/set/brightness", "128"); + + // OnOff commands should route to the correct topic + sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF); + assertPublished("zigbee2mqtt/light/set/state", "OFF_"); + } + + @Test + public void testRgbWithoutBrightness() throws InterruptedException { + // @formatter:off + var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), + "{ " + + " \"name\": \"light\", " + + " \"state_topic\": \"zigbee2mqtt/light/state\", " + + " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + + " \"state_value_template\": \"{{ value_json.power }}\", " + + " \"payload_on\": \"ON_\", " + + " \"payload_off\": \"OFF_\", " + + " \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " + + " \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " + + " \"rgb_value_template\": \"{{ value_json.rgb }}\"" + + "}"); + // @formatter:on + + publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}"); + assertState(component, Light.COLOR_CHANNEL_ID, HSBType.WHITE); + + // Brightness commands should route to the correct topic, converted to RGB + sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50)); + assertPublished("zigbee2mqtt/light/set/rgb", "127,127,127"); + + // OnOff commands should route to the correct topic + sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF); + assertPublished("zigbee2mqtt/light/set/state", "OFF_"); + } + + @Test + public void testHsb() throws InterruptedException { + // @formatter:off + var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), + "{ " + + " \"name\": \"light\", " + + " \"state_topic\": \"zigbee2mqtt/light/state\", " + + " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + + " \"state_value_template\": \"{{ value_json.power }}\", " + + " \"payload_on\": \"ON_\", " + + " \"payload_off\": \"OFF_\", " + + " \"hs_state_topic\": \"zigbee2mqtt/light/hs\", " + + " \"hs_command_topic\": \"zigbee2mqtt/light/set/hs\", " + + " \"hs_value_template\": \"{{ value_json.hs }}\", " + + " \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " + + " \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " + + " \"brightness_value_template\": \"{{ value_json.br }}\" " + + "}"); + // @formatter:on + + assertThat(component.channels.size(), is(1)); + assertThat(component.getName(), is("light")); + + assertChannel(component, Light.COLOR_CHANNEL_ID, "", "dummy", "Color", ColorValue.class); + + @Nullable + ComponentChannel onOffChannel = component.onOffChannel; + assertThat(onOffChannel, is(notNullValue())); + if (onOffChannel != null) { + assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State", + OnOffValue.class); + } + @Nullable + ComponentChannel brightnessChannel = component.brightnessChannel; + assertThat(brightnessChannel, is(notNullValue())); + if (brightnessChannel != null) { + assertChannel(brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness", + "Brightness", PercentageValue.class); + } + + publishMessage("zigbee2mqtt/light/hs", "{\"hs\": \"180,50\"}"); + publishMessage("zigbee2mqtt/light/brightness", "{\"br\": \"128\"}"); + assertState(component, Light.COLOR_CHANNEL_ID, new HSBType(new DecimalType(180), new PercentType(50), + new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128)))); + + sendCommand(component, Light.COLOR_CHANNEL_ID, HSBType.BLUE); + assertPublished("zigbee2mqtt/light/set/brightness", "255"); + assertPublished("zigbee2mqtt/light/set/hs", "240,100"); + + // Brightness commands should route to the correct topic + sendCommand(component, Light.COLOR_CHANNEL_ID, new PercentType(50)); + assertPublished("zigbee2mqtt/light/set/brightness", "128"); + + // OnOff commands should route to the correct topic + sendCommand(component, Light.COLOR_CHANNEL_ID, OnOffType.OFF); + assertPublished("zigbee2mqtt/light/set/state", "OFF_"); + } + + @Test + public void testBrightnessAndOnOff() throws InterruptedException { + // @formatter:off + var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), + "{ " + + " \"name\": \"light\", " + + " \"state_topic\": \"zigbee2mqtt/light/state\", " + + " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + + " \"state_value_template\": \"{{ value_json.power }}\", " + + " \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " + + " \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " + + " \"payload_on\": \"ON_\", " + + " \"payload_off\": \"OFF_\" " + + "}"); + // @formatter:on + + assertThat(component.channels.size(), is(1)); + assertThat(component.getName(), is("light")); + + assertChannel(component, Light.BRIGHTNESS_CHANNEL_ID, "zigbee2mqtt/light/brightness", + "zigbee2mqtt/light/set/brightness", "Brightness", PercentageValue.class); + @Nullable + ComponentChannel onOffChannel = component.onOffChannel; + assertThat(onOffChannel, is(notNullValue())); + if (onOffChannel != null) { + assertChannel(onOffChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "On/Off State", + OnOffValue.class); + } + + publishMessage("zigbee2mqtt/light/brightness", "128"); + assertState(component, Light.BRIGHTNESS_CHANNEL_ID, + new PercentType(new BigDecimal(128 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128))); + publishMessage("zigbee2mqtt/light/brightness", "64"); + assertState(component, Light.BRIGHTNESS_CHANNEL_ID, + new PercentType(new BigDecimal(64 * 100).divide(new BigDecimal(255), MathContext.DECIMAL128))); + + sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.OFF); + assertPublished("zigbee2mqtt/light/set/state", "OFF_"); + + sendCommand(component, Light.BRIGHTNESS_CHANNEL_ID, OnOffType.ON); + assertPublished("zigbee2mqtt/light/set/state", "ON_"); + } + + @Test + public void testOnOffOnly() throws InterruptedException { + // @formatter:off + var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), + "{ " + + " \"name\": \"light\", " + + " \"state_topic\": \"zigbee2mqtt/light/state\", " + + " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + + " \"state_value_template\": \"{{ value_json.power }}\", " + + " \"payload_on\": \"ON_\", " + + " \"payload_off\": \"OFF_\" " + + "}"); + // @formatter:on + + assertThat(component.channels.size(), is(1)); + assertThat(component.getName(), is("light")); + + assertChannel(component, Light.ON_OFF_CHANNEL_ID, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", + "On/Off State", OnOffValue.class); + assertThat(component.brightnessChannel, is(nullValue())); + + publishMessage("zigbee2mqtt/light/state", "{\"power\": \"ON_\"}"); + assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.ON); + publishMessage("zigbee2mqtt/light/state", "{\"power\": \"OFF_\"}"); + assertState(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF); + + sendCommand(component, Light.ON_OFF_CHANNEL_ID, OnOffType.OFF); + assertPublished("zigbee2mqtt/light/set/state", "OFF_"); + } + + @Override + protected Set getConfigTopics() { + return Set.of(CONFIG_TOPIC); + } +} diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java deleted file mode 100644 index 219b6c34b9919..0000000000000 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LightTests.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright (c) 2010-2022 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.mqtt.homeassistant.internal.component; - -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; - -import java.util.Set; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.junit.jupiter.api.Test; -import org.openhab.binding.mqtt.generic.values.ColorValue; -import org.openhab.core.library.types.HSBType; -import org.openhab.core.library.types.OnOffType; - -/** - * Tests for {@link Light} - * The current {@link Light} is non-compliant with the Specification and must be rewritten from scratch. - * - * @author Anton Kharuzhy - Initial contribution - */ -@NonNullByDefault -public class LightTests extends AbstractComponentTests { - public static final String CONFIG_TOPIC = "light/0x0000000000000000_light_zigbee2mqtt"; - - @Test - public void test() throws InterruptedException { - // @formatter:off - var component = (Light) discoverComponent(configTopicToMqtt(CONFIG_TOPIC), - "{ " + - " \"availability\": [ " + - " { " + - " \"topic\": \"zigbee2mqtt/bridge/state\" " + - " } " + - " ], " + - " \"device\": { " + - " \"identifiers\": [ " + - " \"zigbee2mqtt_0x0000000000000000\" " + - " ], " + - " \"manufacturer\": \"Lights inc\", " + - " \"model\": \"light v1\", " + - " \"name\": \"Light\", " + - " \"sw_version\": \"Zigbee2MQTT 1.18.2\" " + - " }, " + - " \"name\": \"light\", " + - " \"state_topic\": \"zigbee2mqtt/light/state\", " + - " \"command_topic\": \"zigbee2mqtt/light/set/state\", " + - " \"state_value_template\": \"{{ value_json.power }}\", " + - " \"payload_on\": \"ON_\", " + - " \"payload_off\": \"OFF_\", " + - " \"rgb_state_topic\": \"zigbee2mqtt/light/rgb\", " + - " \"rgb_command_topic\": \"zigbee2mqtt/light/set/rgb\", " + - " \"rgb_value_template\": \"{{ value_json.rgb }}\", " + - " \"brightness_state_topic\": \"zigbee2mqtt/light/brightness\", " + - " \"brightness_command_topic\": \"zigbee2mqtt/light/set/brightness\", " + - " \"brightness_value_template\": \"{{ value_json.br }}\" " + - "}"); - // @formatter:on - - assertThat(component.channels.size(), is(1)); - assertThat(component.getName(), is("light")); - - assertChannel(component, Light.COLOR_CHANNEL_ID, "zigbee2mqtt/light/rgb", "zigbee2mqtt/light/set/rgb", "light", - ColorValue.class); - - assertChannel(component.switchChannel, "zigbee2mqtt/light/state", "zigbee2mqtt/light/set/state", "light", - ColorValue.class); - assertChannel(component.brightnessChannel, "zigbee2mqtt/light/brightness", "zigbee2mqtt/light/set/brightness", - "light", ColorValue.class); - - publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"255,255,255\"}"); - assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(255, 255, 255)); - publishMessage("zigbee2mqtt/light/rgb", "{\"rgb\": \"10,20,30\"}"); - assertState(component, Light.COLOR_CHANNEL_ID, HSBType.fromRGB(10, 20, 30)); - - component.switchChannel.getState().publishValue(OnOffType.OFF); - assertPublished("zigbee2mqtt/light/set/state", "0,0,0"); - } - - @Override - protected Set getConfigTopics() { - return Set.of(CONFIG_TOPIC); - } -} From 2c30756fdda5fdc0e6d70eee2c8dd46251a82aae Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 5 Nov 2022 20:27:38 +0100 Subject: [PATCH 31/55] [jdbc] Fix MySQL deprecation notice on startup (#13660) Signed-off-by: Jacob Laursen --- .../main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java | 2 +- .../main/java/org/openhab/persistence/jdbc/db/JdbcMysqlDAO.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index 074530b953810..6bc19dc7ab16e 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -165,7 +165,7 @@ private void initSqlTypes() { * HSQLDB: org.hsqldb.jdbcDriver * Jaybird: org.firebirdsql.jdbc.FBDriver * MariaDB: org.mariadb.jdbc.Driver - * MySQL: com.mysql.jdbc.Driver + * MySQL: com.mysql.cj.jdbc.Driver * MaxDB: com.sap.dbtech.jdbc.DriverSapDB * PostgreSQL: org.postgresql.Driver * SyBase: com.sybase.jdbc3.jdbc.SybDriver diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcMysqlDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcMysqlDAO.java index b73f216fd9e44..361b177995efd 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcMysqlDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcMysqlDAO.java @@ -34,7 +34,7 @@ */ @NonNullByDefault public class JdbcMysqlDAO extends JdbcBaseDAO { - private static final String DRIVER_CLASS_NAME = com.mysql.jdbc.Driver.class.getName(); + private static final String DRIVER_CLASS_NAME = com.mysql.cj.jdbc.Driver.class.getName(); @SuppressWarnings("unused") private static final String DATA_SOURCE_CLASS_NAME = com.mysql.cj.jdbc.MysqlDataSource.class.getName(); From 162e1461b0da4cc2e9feb980f1eae09b8c9f2680 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 6 Nov 2022 10:03:51 +0100 Subject: [PATCH 32/55] [miio] Fix warning messages and discovery on newer robots (#13111) also includes update to latest devicenames list Signed-off-by: Marcel Verpaalen --- .../binding/miio/internal/MiIoDevices.java | 11 + .../miio/internal/robot/RRMapFileParser.java | 19 +- .../src/main/resources/misc/device_names.json | 1267 ++++++++++++++++- 3 files changed, 1256 insertions(+), 41 deletions(-) diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java index 593524bbb7780..0a1dc9787a87a 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/MiIoDevices.java @@ -186,6 +186,17 @@ public enum MiIoDevices { ROBOROCK_VACUUM_A15("roborock.vacuum.a15", "Roborock S7", THING_TYPE_VACUUM), ROBOROCK_VACUUM_A19("roborock.vacuum.a19", "Roborock S4 Max", THING_TYPE_VACUUM), ROBOROCK_VACUUM_A23("roborock.vacuum.a23", "Roborock T7S Plus", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A26("roborock.vacuum.a26", "Roborock G10S Pro", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A27("roborock.vacuum.a27", "Roborock S7 MaxV", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A29("roborock.vacuum.a29", "Roborock G10", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A30("roborock.vacuum.a30", "Roborock G10", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A34("roborock.vacuum.a34", "Roborock Q5", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A37("roborock.vacuum.a37", "Roborock T8", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A38("roborock.vacuum.a38", "Roborock Q7 Max", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A40("roborock.vacuum.a40", "Roborock Q7", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A46("roborock.vacuum.a46", "Roborock G10S", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A52("roborock.vacuum.a52", "Roborock T8 Plus", THING_TYPE_VACUUM), + ROBOROCK_VACUUM_A62("roborock.vacuum.a62", "Roborock S7 Pro Ultra", THING_TYPE_VACUUM), ROBOROCK_VACUUM_C1("roborock.vacuum.c1", "Xiaowa C1", THING_TYPE_VACUUM), ROBOROCK_VACUUM_E2("roborock.vacuum.e2", "Roborock Xiaowa E Series Vacuum v2", THING_TYPE_UNSUPPORTED), ROBOROCK_VACUUM_M1S("roborock.vacuum.m1s", "Mi Robot Vacuum 1S", THING_TYPE_VACUUM), diff --git a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapFileParser.java b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapFileParser.java index 245bb984ffb9e..3656161af01cd 100644 --- a/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapFileParser.java +++ b/bundles/org.openhab.binding.miio/src/main/java/org/openhab/binding/miio/internal/robot/RRMapFileParser.java @@ -60,7 +60,14 @@ public class RRMapFileParser { public static final int CARPET_MAP = 17; public static final int MOP_PATH = 18; public static final int CARPET_FORBIDDEN_AREA = 19; - + public static final int SMART_ZONES_PATH_TYPE = 20; + public static final int SMART_ZONES = 21; + public static final int CUSTOM_CARPET = 22; + public static final int CL_FORBIDDEN_ZONES = 23; + public static final int FLOOR_MAP = 24; + public static final int FURNITURES = 25; + public static final int DOCK_TYPE = 26; + public static final int ENEMIES = 27; public static final int DIGEST = 1024; public static final int HEADER = 0x7272; @@ -272,6 +279,16 @@ public RRMapFileParser(byte[] raw) { int blocksPairs = getUInt16(header, 0x08); blocks = getBytes(data, 0, blocksPairs); break; + case SMART_ZONES_PATH_TYPE: + case SMART_ZONES: + case CUSTOM_CARPET: + case CL_FORBIDDEN_ZONES: + case FLOOR_MAP: + case FURNITURES: + case DOCK_TYPE: + case ENEMIES: + // new blocktypes not yet decoded + break; default: logger.info("Unknown blocktype {} (pls report to author)", blocktype); printBlockDetails = true; diff --git a/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json b/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json index 43a0ac9da1deb..5316fe681c90d 100644 --- a/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json +++ b/bundles/org.openhab.binding.miio/src/main/resources/misc/device_names.json @@ -1,29 +1,40 @@ { + "090615.aircondition.ktf": "SmartAircondition (VRF)", "090615.aircondition.lnk": "PTX central air conditioning controller", + "090615.bhf_light.ptxyb": "PTXZN intelligent Yuba A1", "090615.curtain.1mcu01": "One meter intelligent curtain", "090615.curtain.bofud1": "Bofu intelligent rolling shutter motor", + "090615.curtain.crli82": "Intelligent lithium battery motor (Mesh)", "090615.curtain.jldj03": "PTX Rolling curtain", "090615.curtain.jxdj02": " PTX mechanical roller motor", "090615.curtain.kcz82d": "KCZ intelligent curtain motor", "090615.curtain.mehe82": "MEEHE Intelligent curtain motor", "090615.curtain.mt800w": "MEEYA intelligent curtain", "090615.curtain.nmg82": "Lemon fruit intelligent curtain motor (WiFi)", + "090615.curtain.osh82d": "Oshiheng intelligent curtain motor (WiFi)", "090615.curtain.p01": "PTX Intelligent Curtain Switch", "090615.curtain.pipacr": "Pipa intelligent curtain motor", + "090615.curtain.ptcu02": "Intelligent voice curtain motor", "090615.curtain.ptx82": "PTX intelligent curtain motor", + "090615.curtain.s2mesh": "PTX smart curtain (mesh)", "090615.curtain.sidt82": "WiFi intelligent curtain motor", "090615.curtain.sumi82": "SUMI intelligent curtain motor", "090615.curtain.ws9856": "WDS Intelligent motor curtain", "090615.curtain.wsjl01": "WSERD Smart roller shutter motor ", + "090615.curtain.ycur01": "YCZNWL Intelligent curtain motor", "090615.curtain.ykgc82": "Intelligent curtain motor of Youke workshop(WiFi)", "090615.curtain.zsdj35": "ZS roller shutter motor", "090615.curtain.zsdj82": "ZS intelligent curtain motor", + "090615.gateway.ktvrf": "PTXZN air conditioning gateway (VRF)", + "090615.light.cxlg01": "PTX intelligent magnetic suction lamp (mesh)", "090615.light.demo2": "PTX intelligent heating and cooling lamp belt (mesh)", "090615.light.hylg01": "Haoyi intelligent dimming lamp (mesh)", + "090615.light.hylg03": "Haoyi intelligent magnetic suction lamp (mesh)", "090615.light.kylg02": "Haoyi intelligent two color light belt (mesh)", "090615.light.mdd02": "PTX intelligent two tone light strip (mesh)", "090615.light.mlig01": "PTX intelligent downlight (mesh)", "090615.light.mlig02": "PTX Intelligent deep dimming lamp (mesh)", + "090615.light.pblg01": "PTXZN intelligent flat lamp", "090615.light.pipadd": "Crackle smart light band (mesh)", "090615.light.pipatd": "Crackle smart anti glare lamp (mesh)", "090615.light.xrlig1": "Cactus intelligent spotlight (mesh)", @@ -31,6 +42,8 @@ "090615.plug.pipa86": "Crackle intelligent switch two three plug", "090615.plug.plus01": "Intelligent 86 socket(WIFI)", "090615.plug.plus02": "Intelligent Mobile Plug (WIFI)", + "090615.remote.x6s4q": "X6 intelligent four scene switch(PTXZN)", + "090615.remote.yk6sw": "PTXZN six key wireless switch", "090615.switch.azsw01": "Azena mesh intelligent zero fire one switch", "090615.switch.azsw02": "Azena mesh intelligent zero fire two switch", "090615.switch.azsw03": "Azena mesh intelligent zero fire three switch", @@ -43,6 +56,9 @@ "090615.switch.hmb012": "H intelligent single key switch (mesh)", "090615.switch.hmb022": "H intelligent double key switch (mesh)", "090615.switch.hmb032": "H intelligent three key switch (mesh)", + "090615.switch.hmsw01": "HOMRAY intelligent switch one button", + "090615.switch.hmsw02": "HOMRAY intelligent switch two keys", + "090615.switch.hmsw03": "HOMRAY intelligent switch three keys", "090615.switch.mesh01": "PTX single fire touch switch one button (mesh)", "090615.switch.mesh02": "PTX single fire touch switch two keys (mesh)", "090615.switch.mesh03": "PTX single fire touch switch three keys (mesh) ", @@ -57,13 +73,17 @@ "090615.switch.mets3": "PTX three key intelligent switch (mesh)", "090615.switch.mhrsw1": "PTX Sle Bluetooth intelligent switch moduleing", "090615.switch.mhrsw2": "PTX dual Bluetooth intelligent switch module", + "090615.switch.pi16sw": "Pipa 16 switch controler", "090615.switch.piak01": "Piak intelligent one switch", "090615.switch.piak02": "Piak intelligent two switch", "090615.switch.piak03": "Piak intelligent three switch", "090615.switch.ptxtc1": "PTX intelligent touch switch on (mesh)", "090615.switch.ptxtc2": "PTX intelligent touch two on (mesh)", "090615.switch.ptxtc3": "PTX intelligent touch switch three on (mesh)", + "090615.switch.qksw01": "PTXZN smart card power on switch (mesh)", "090615.switch.qksw32": "PTX card access switch (WiFi)", + "090615.switch.sk4k": "PTXZN intelligent four key switch", + "090615.switch.sk6k": "PTXZN intelligent six key switch", "090615.switch.smb1s1": "Sumi B1 Bluetooth mesh switch (single key)", "090615.switch.smb1s2": "Sumi B1 Bluetooth mesh switch (double key)", "090615.switch.smb1s3": "Sumi B1 Bluetooth mesh switch (three key)", @@ -73,6 +93,7 @@ "090615.switch.switch01": "One Intelligent switch(WIFI)", "090615.switch.switch02": "Two Intelligent Switch (WIFI)", "090615.switch.switch03": "Three Intelligent switch(WIFI)", + "090615.switch.tftcmb": "Knob intelligent switch", "090615.switch.xswitch01": "PTX OneKey Switch (WIFI)", "090615.switch.xswitch02": "PTX Twokey switch(wifi)", "090615.switch.xswitch03": "PTX ThreeKey Switch (WIFI)", @@ -82,10 +103,12 @@ "123888.stb.1101": "机顶盒", "123888.tv.xhzn11": "电视", "1245.airfresh.mini2": "Potato Mini", + "1245.airfresh.super2": "TUDOU PRO", "1245.airpurifier.dl01": "DATUDOU SUPER", "17216.desk.lt001": "Leband Electric Sit-to-Stand Desk", "17216.magic_touch.d150": "MagicTouch Warm", "17216.magic_touch.d152": "MagicTouch-Go", + "17216.massage.6261d": "Skyper double-robot 6 sense smart king chair", "17216.massage.6602a": "joypal Fascia Massage Chair", "17216.massage.e560": "joypal Royal Rhythm Massage Chair", "17216.massage.ec1266a": "Easewell AI Massage Chair", @@ -98,6 +121,8 @@ "360sdj.vacuum.s": "360AICleanRobotics", "364656.light.sm01a": "科技生活", "397.light.hallight": "Stars LED Light", + "445.toilet.cnlzmt": "Diiib super energy ion intelligent toilet dream Moon silver AIOT", + "445.toilet.xmt015": "diiib double engine supercharged intelligent toilet AIOT model", "666.curtain.em75": "A-OK Curtain Controller", "666.curtain.gt01": "Smart Curtain Motor", "666.curtain.gt01m": "Genwits Smart Curtain", @@ -114,29 +139,38 @@ "881878.light.znxyd": "Smart Night Light", "881878.switch.ddkgznljq": "Smart Module for Electric Switch", "a24234.plug.89987": "创建云接入", + "actek.curtain.500": "窗帘电机", + "actek.light.400": "调光灯", "aden.airc.a6": "VINO Inverter Air Conditioner 3P(New Energy Label Level 2)", "aden.aircondition.a1": "VINO Inverter Air Conditioner(Energy Label Level 3)", "aden.aircondition.a2": "VINO Inverter Air Conditioner(Energy Label Level 1)", "aden.aircondition.a4": "VINO Inverter Air Conditioner", "aden.aircondition.a7": "VINO Inverter Air Conditioner 2P(New Energy Label Level 2)", + "adx.curtain.curt": "窗帘", "adx.switch.s1": "开关", "afour.s_lamp.001": "ZIWOOO Intelligent germicidal lamp", - "aice.motor.kzmu3": "KMZ intelligent shutter door controller", + "afu.switch.p1": "智能开关", + "ai123.magic_touch.xjb1a": "TefeshiAPP intelligent cervical massage instrument", + "aice.motor.kzmu3": "KZM intelligent shutter door controller", "aihome.light.m85": "light", "aihome.light.m93": "FanLight", "aiicn.curtain.r51": "智能窗帘控制器", "aiicn.light.pwm2": "LED驱动器", "aiicn.light.r01": "单路接收器", "aimore.curtain.curt01": "大于智能窗帘", + "aimore.light.cw3201": "smart light", "aimore.light.led01": "大于智能灯", "aimore.plug.switch": "大于智能插座", "aimore.switch.double": "大于智能双键开关", "aimore.switch.switch": "大于智能开关", "aimore.switch.three": "大于智能三键开关", + "air.airp.fm3rob": "AD63", "air.blanket.tq24": "智能水暖床", "air.fan.18123": "AD55", "air.fan.23110": "AD19", "air.fan.30140": "AD39", + "air.fan.ad63": "AD63", + "air.fan.ad70": "AD70", "air.fan.air4": "Air4-S", "air.fan.ca23ad9": "Airmate air circulation fan", "air.heater.wda14": "Airmate mobile floor heating", @@ -144,9 +178,31 @@ "airdog.airpurifier.x5": "Airdog X3(M) air purifier", "airdog.airpurifier.x7": "Airdog X7(M) air purifier", "airdog.airpurifier.x7sm": "Airdog X7S(M) air purifier", + "akeeta.light.bulb": "彩光灯", + "akeeta.light.double": "白光灯", + "akeeta.light.dstrip": "双色灯带", + "akeeta.light.fan": "风扇灯", + "akeeta.light.strip": "彩光灯带", + "akeeta.switch.relay": "通断器", + "akt.derh.2008": "FLE-2008EW", "alive.aircondition.air1": "智能空调", "alive.curtain.cmd82": "智能窗帘", "alive.light.alive1": "智能灯", + "aliyun.aircondition.22": "红外控制器(空调)", + "aliyun.aircondition.45": "红外控制器二代(空调)", + "aliyun.aircondition.57": "红外控制器三代(空调)", + "aliyun.curtain.060001": "窗帘电机", + "aliyun.curtain.6": "窗帘电机", + "aliyun.light.15": "二路开关", + "aliyun.light.39": "调光开关(0-10v)", + "aliyun.light.52": "三线二路开关", + "aliyun.light.65": "调光开关二代(0-10v)", + "aliyun.plug.1": "智能插座", + "aliyun.plug.23": "大功率开关", + "aliyun.plug.25": "智能开关(软件虚拟)", + "aliyun.plug.36": "RS232控制器", + "alsmai.hood.a32201": "6.86TFT烟机", + "alsmai.oven.a02202": "6.86TFT嵌入式自洁烤箱", "amtt.light.r": "灯", "anlin.curtain.1": "电机控制", "anlin.light.22": "智能LED灯", @@ -166,33 +222,57 @@ "asdds.light.wyfpj1": "Feipujia Intelligent lamps and lanterns", "asp.treadmill.a9": "Lijiujia treadmill", "asp.treadmill.pbj": "Lijiujia M1 treadmill", + "asu.projector.cronus": "Mi Laser Projector 2", "asunga.curtain.ct01s": "smartcurtain", "ateai.mosq.dakuo": "Dakuo Mosquito Repellent", + "aupu.bhf_light.p3328e": "AUPU-AOXIN", "aupu.bhf_light.s368m": "AUPU S368M", "aux.aircondition.hc1": "AUX Smart Air Conditioner", "aux.aircondition.test1": "aux-air condition", "aux.aircondition.test2": "aux-air condition", "aux.aircondition.v1": "AUX Smart Air Conditioner", + "avail.heater.7801": "地暖智能温控", + "avail.switch.7b01": "漏水保护器", + "avail.waterheater.7a01": "零冷水", + "axent.tow_w.a804": "AXENT COZY heated towel rack", "ayzn.aircondition.i1": "air conditioning", "ayzn.fan.irfan": "Fan", "ayzn.stb.irstb": "stb", "ayzn.tv.irtv": "television", "babai.airer.hc01b": "Hrlchure Intelligent Drying Rack s63", "babai.airer.lyj01": "Ai Smart Airer", + "babai.bhf_light.kmdm01": "Comotomo intelligent bath heater", + "babai.bhf_light.mk885": "Mico intelligent bath heater", "babai.curtain.190812": "Smart Curtain Motor", "babai.curtain.ad5810": "Curtain Motor (WIFI)", "babai.curtain.at5810": "AUTORAIL Curtain Motor (WIFI)", "babai.curtain.bb82mj": "Babuy Smart Curtain", + "babai.curtain.bb82mm": "Ai Smart Curtain (Mesh)", + "babai.curtain.cy850": "CANY Smart Curtain", "babai.curtain.kyx850": "KYX Smart Curtain", "babai.curtain.lb100a": "LANBOO Smart Curtain", + "babai.curtain.lm860": "LanMi Smart curtain", "babai.curtain.m515e": "Zemismart smart curtain motor", "babai.curtain.mtx850": "MTX Smart Curtain", "babai.curtain.nh5810": "NanHoo Smart Curtain", + "babai.curtain.osh850": "Oshiheng Intelligent Curtain", + "babai.curtain.ql850": "LD Smart Curtain", + "babai.curtain.xy850": "XiYan Smart curtain motor", "babai.curtain.yilc3": "Yi-LOCK Smart Curtain C3", "babai.curtain.ym25e": "YS Roller Motor", "babai.light.tf31a": "TAIFOO Light Controler", "babai.plug.sk01a": "Babuy Wi-Fi Smart Plug", "babai.plug.sk86a": "Babuy Wall Outlet", + "babai.sofa.kh01": "Kingdee Yaju Smart Sofa", + "babai.switch.201m": "Ai BLE-Mesh Wall Switch M1", + "babai.switch.201ml": "Ai BLE-Mesh Wall Switch ML1 (No Neutral)", + "babai.switch.201s": "Ai WiFi Wall Switch S1(Combo)", + "babai.switch.202m": "Ai BLE-Mesh Wall Switch M2", + "babai.switch.202ml": "Ai BLE-Mesh Wall Switch ML2 (No Neutral)", + "babai.switch.202s": "Ai WiFi Wall Switch S2(Combo)", + "babai.switch.203m": "Ai BLE-Mesh Wall Switch M3", + "babai.switch.203ml": "Ai BLE-Mesh Wall Switch ML3 (No Neutral)", + "babai.switch.203s": "Ai WiFi Wall Switch S3(Combo)", "babai.switch.bb101s": "WiFi Wall Switch A1", "babai.switch.bb102s": "WiFi Wall Switch A2", "babai.switch.bb103s": "WiFi Wall Switch A3", @@ -203,23 +283,47 @@ "bangbo.airer.y6": "Mans Cooper", "baomi.airpurifier.450a": "Baomi Air Purifier 2S", "baomi.airpurifier.bmi": "baomi-air-purifier", + "bc1869.lock.t002": "Yi-Lock", "bdx.i_stove.a1xs": "BIGDIPPER Internet Integrated Cooker Appliance (model S of A1 disinfection cabinet)", "bdx.i_stove.a1z": "BIGDIPPER Internet Integrated Cooker Appliance ( A1/C2 steam box)", "bdx.i_stove.c1x": "BIGDIPPER Internet Integrated Cooker Appliance (C1 disinfection cabinet)", "bdx.i_stove.c2x": "BIGDIPPER Internet Integrated Cooker Appliance (C2 disinfection cabinet)", + "bdx.i_stove.c5zk": "BIGDIPPER Internet Integrated Cooker Appliance ( C2/C3 steam box)", + "bean.switch.bl01": "Single button Fire Mesh switch", + "bean.switch.bl02": "Two buttons fire Mesh switch", + "bean.switch.bl03": "Three buttons fire Mesh switch", + "bean.switch.bln01": "Single button Mesh switch", + "bean.switch.bln02": "Two buttons Mesh switch", + "bean.switch.bln03": "Three buttons Mesh switch", + "bean.switch.bln04": "Four buttons Mesh switch", "beihao.airer.airer3": "xiangyang clothes-horse", "beihao.airer.bd": "Lanyou clothes-horse", "beihao.airer.l9": "Lisheng clothes-horse", "beihao.airer.l9xf": "JIajueshi clothes-horse", "beihao.airer.lyj08": "Ai Smart Clothes Dryer", - "beiyun.switch.250c": "Seebest Smart Universal Dimmer Switch (BLE-Mesh)", + "beiyun.light.260c": "Zhao Yun LED Ceiling Lamp", + "beiyun.light.chanyi": "Epoch", + "beiyun.light.hane": "Han E Sound Chandelier", + "beiyun.switch.250c": "seebest Smart Dimmer Switch (BLE-Mesh)", "bemfa.aircondition.air2": "空调", "bemfa.aircondition.air4": "空调", + "bemfa.curtain.ai2009": "窗帘电机", "bemfa.fan.fan": "风扇", + "bemfa.light.be002": "智能灯", "bemfa.light.be01": "巴法", "bemfa.light.bemfa": "巴法云", "bemfa.plug.be002": "巴法插座", "bemfa.sensor_ht.dht11": "传感器", + "bemfa.switch.air006": "开关", + "best.aircondition.1": "空调", + "best.curtain.1": "窗帘", + "best.fan.1": "风扇", + "best.light.1": "灯", + "best.plug.1": "插座", + "best.projector.2": "投影仪", + "best.stb.1": "机顶盒", + "best.switch.1": "开关", + "best.tv.1": "电视", "bgdz.light.test3": "light", "bgdz.plug.chazuo": "outlet", "binthe.curtain.bcm": "BINTHEN Smart Curtain", @@ -242,8 +346,18 @@ "blink.fan.bf0001": "blinker风扇", "blink.light.bl0001": "BlinkerSmartLight", "blink.plug.bp0001": "BlinkerSmartPlug", + "blst.magic_touch.f2pro": "Neck Massager F2pro", + "bofu.curtain.bfmh": "Bofu Vertical Curtain", + "bofu.curtain.ey25": "BOFU Smart motor", "bofu.curtain.normal": "BOFU Electric Curtain", "bofu.curtain.q301": "QinLinkQ301", + "bofu.curtain.sp10xm": "SHIDIAN Dream Curtain", + "bolian.curtain.c00001": "窗帘", + "bolian.light.l00001": "灯-W", + "bolian.light.l00003": "灯-CW", + "bolian.light.l00004": "灯-RGB", + "bolian.light.light": "灯", + "bolian.switch.s00001": "开关", "bright.aircondition.air": "空调", "bright.curtain.curtai": "窗帘电机", "bright.light.light": "灯", @@ -254,6 +368,9 @@ "bright.switch.switch": "二路开关", "bright.switch.v100": "一路开关", "bright.tv.tv": "电视", + "brrx.switch.qst1c1": "Submarine Smart One-keys Wall Switch", + "brrx.switch.qst1c2": "Submarine Smart Two-keys Wall Switch", + "brrx.switch.qst1c3": "Submarine Smart Three-keys Wall Switch", "btiot.aircondition.air": "aricondition", "btiot.aircondition.air2": "空调插", "btiot.plug.outlet": "智能墙插", @@ -269,36 +386,107 @@ "btzn.switch.1key": "比特单键弱电开关", "btzn.switch.2key": "比特双键弱电开关", "btzn.switch.4key": "比特四键弱电开关", + "btzn.switch.bh8b1": "bittel Mesh Single fire switch", + "btzn.switch.bh8b2": "bittel Mesh Single fire switch double bond", + "btzn.switch.bh8b3": "Bit Mesh Single-fire switch three-key", "btzn.switch.cardv1": "比特插卡取电开关", + "bull.airer.pid091": "晾衣架GJ051", + "bull.airer.pid092": "晾衣架GJ056", + "bull.airer.pid129": "晾衣机J221", + "bull.airer.pid130": "晾衣机J222", + "bull.airer.pid131": "晾衣机J226", "bull.curtain.cw11": "智能窗帘", + "bull.curtain.gdc025": "智能窗帘GDC025", + "bull.curtain.gds015": "智能窗帘GDS015", + "bull.curtain.pid056": "智能窗帘GDS015", + "bull.curtain.pid058": "智能窗帘GDC025", + "bull.curtain.pid059": "智能窗帘GDZ026(Mesh)", + "bull.curtain.pid060": "智能窗帘GDS016(Mesh)", + "bull.light.0167": "颢智餐吊灯(蓝牙MESH版)", + "bull.light.3988": "品轩客厅吸顶灯(蓝牙MESH版)", + "bull.light.4873": "T02低压灯带(蓝牙MESH版)", + "bull.light.8264": "品轩卧室吸顶灯(蓝牙MESH版)", + "bull.light.9901": "T023寸筒灯(蓝牙MESH版)", + "bull.light.l042": "T02-3寸筒灯(MESH版)", + "bull.light.pid076": "T05智能明装筒灯", + "bull.light.pid077": "T03智能迷你格栅灯", + "bull.light.pid078": "S03智能磁吸线条灯", + "bull.light.pid079": "S03智能磁吸格栅灯", + "bull.light.pid080": "S03智能磁吸可调射灯", + "bull.light.pid081": "S03智能磁吸吊线灯", + "bull.light.pid082": "T05智能格栅灯", + "bull.light.pid083": "T02 Pro智能嵌装筒灯", + "bull.light.pid084": "T05智能嵌装筒灯", + "bull.light.pid127": "明皓圆形卧室灯", + "bull.magnet.pid068": "智能门窗传感器", "bull.plug.27s223": "G27智能插座_10A五孔", "bull.plug.gn003": "G27_智能插座_16A三孔", + "bull.plug.s053": "电源净化插座智联版", "bull.plug.uy2162": "WiFi插座(16A电量统计版)", "bull.plug.y2012": "WiFi插座", "bull.plug.y201g": "WiFi插座(电量统计版)", + "bull.remote.s115y": "Bull smart one-key wireless switch", + "bull.remote.s215y": "Bull smart two-keys wireless switch", + "bull.remote.s315y": "Bull smart three-keys wireless switch", + "bull.sensor_ht.pid067": "智能温湿度传感器", "bull.switch.001": "WiFi插座_非计量版", + "bull.switch.102pw1": "G55智能三位零火开关-左", + "bull.switch.102pw2": "G55智能三位零火开关-中", + "bull.switch.102pw3": "G55智能三位零火开关-右", + "bull.switch.103pw1": "G55智能二位零火开关-左", + "bull.switch.103pw2": "G55智能二位零火开关-右", + "bull.switch.32pw1": "智慧屏网关左键", + "bull.switch.32pw2": "智慧屏网关中键", + "bull.switch.32pw3": "智慧屏网关右键", + "bull.switch.39pw1": "智慧屏开关左键", + "bull.switch.39pw2": "智慧屏开关中键", + "bull.switch.39pw3": "智慧屏开关右键", "bull.switch.bull01": "WiFi插座_计量版", "bull.switch.gn004": "G27_智能插座_10A五孔", + "bull.switch.pid104": "G55智能一位零火开关", + "bull.switch.s034": "G55零火一开(Mesh版)", + "bull.switch.s035a": "G55零火二开_左(Mesh版)", + "bull.switch.s035b": "G55零火二开_右(Mesh版)", + "bull.switch.s036a": "G55零火三开_左(Mesh版)", + "bull.switch.s036b": "G55零火三开_中(Mesh版)", + "bull.switch.s036c": "G55零火三开_右(Mesh版)", "bull.switch.s049": "G55智能一位单火开关", + "bull.switch.s050a": "G55智能二位单火开关-左", + "bull.switch.s050b": "G55智能二位单火开关-右", "bull.switch.s051a": "G55智能三位单火开关-左", + "bull.switch.s051b": "G55智能三位单火开关-中", + "bull.switch.s051c": "G55智能三位单火开关-右", "bull.switch.s112": "G27_智能开关_一开", + "bull.switch.s112y": "Bull smart one-key switch(No Neutral)", "bull.switch.s212": "G27_智能开关_二开_上键", "bull.switch.s212b": "G27_智能开关_二开_下键", + "bull.switch.s212y": "Bull smart two-keys switch(No Neutral)", "bull.switch.s312a": "G27_智能开关_三开_上键", "bull.switch.s312b": "G27_智能开关_三开_中键", "bull.switch.s312c": "G27_智能开关_三开_下键", + "bull.switch.s312y": "Bull smart three-keys switch(No Neutral)", "bymiot.aircondition.ir2": "未来居空调控制器(红外版)", "bymiot.aircondition.v1": "未来居中央空调控制器", "bymiot.aircondition.wl2": "未来居无线中央空调控制器", "bymiot.cs.wlv1": "未来居无线强电插卡取电", + "bymiot.curtain.m2w": "Bymiot curtain m2w", "bymiot.curtain.v2": "未来居电动窗帘", "bymiot.curtain.wlv2": "未来居无线窗帘电机(M1开合帘)", + "bymiot.gateway.rcuv1": "未来居一体式RCU网关", "bymiot.gateway.v1": "企业有线智能网关", "bymiot.gateway.v2": "企业有线智能网关2.0版", "bymiot.gateway.v2pro": "未来居有线智能网关2.0升级版", + "bymiot.gateway.v3pro": "未来居有线智能网关3.0增强版", "bymiot.gateway.wlv1": "未来居无线智能网关", "bymiot.ir.wlv1": "未来居无线万能遥控器", + "bymiot.light.ctwlv1": "未来居无线筒射灯调光驱动器", + "bymiot.light.dcwlv1": "未来居无线单路控制器", + "bymiot.light.dmwlv1": "未来居无线灯带调光驱动器", "bymiot.motion.v1": "未来居人体移动传感器", + "bymiot.plug.wlv1": "未来居无线墙壁插座(电流检测)", + "bymiot.remote.4kwlv2": "未来居无线四键场景开关", + "bymiot.remote.6kwlv1": "未来居无线六键场景开关", + "bymiot.remote.lockv2": "未来居控锁器", "bymiot.sensor_occupy.v3": "未来居强电插卡取电", "bymiot.switch.1keyv1": "未来居单键强电开关", "bymiot.switch.1kwlv2": "未来居单键强电无线开关", @@ -313,11 +501,14 @@ "bzhome.curtain.sz050": "窗帘电机", "bzhome.plug.sz070": "插座转换器", "bzhome.switch.sz010": "开关", + "calvi.curtain.cam01": "CALVI curtain motor", "candor.wine_cool.a": "Candor Wine Cooler(80L/275L)", "careli.fryer.maf01": "Mi air frying pan", "careli.fryer.maf02": "Mi Smart Air Fryer (3.5L)", "careli.fryer.maf03": "Mi air frying pan", + "careli.fryer.maf04": "Mijia Smart Air Fryer Pro 4L", "careli.fryer.maf07": "Mi Smart Air Fryer (3.5L)", + "careli.fryer.ybaf01": "Upany Air Fryer YB-2208DTW", "cchip.airer.a2": "manskubo P series", "cchip.light.l1": "Air Surface ceiling light", "cchome.curtain.khl01": "Curtain motor", @@ -327,9 +518,16 @@ "cchome.switch.86l3v1": "Smart Switch(Three)", "cchome.tow_w.wyj001": "Bathroom Rack", "cchome.wopener.tcq001": "Motor Controller", + "cdn.bhf_light.zjd606": "CDN intelligent heater", + "cdn.light.w00a02": "Seton monochrome light strip", "cdn.light.wy0a01": "Xidun 26 Series Smart Spotlight", "cdn.light.wy0a03": "Xidun 36 Series Smart Spotlight", - "cdn.light.wy0a04": "Xidun Yueshang Smart Spotlight", + "cdn.light.wy0a04": "Xidun Constant Current Smart Drive", + "cdn.light.wy0a05": "Seton two-color light strip", + "cdn.remote.x1md2": "SJ 2-button scene switch", + "cdn.remote.x1md3": "SJ 3-button scene switch", + "cdn.switch.x1msl2": "SJ Smart Switch(2 keys)", + "cdn.switch.x1msl3": "SJ Smart Switch(3 keys)", "cgllc.airm.cgdn1": "Qingping Air Monitor Lite", "cgllc.airmonitor.b1": "Mi Multifunction Air Monitor", "cgllc.airmonitor.s1": "Qingping Air Monitor", @@ -345,6 +543,7 @@ "cgzn.aircondition.air": "air", "cgzn.airer.airer": "晾衣架", "cgzn.airer.airer2": "晾衣架", + "cgzn.airer.airer5": "晾衣架", "cgzn.airpurifier.ai": "ai", "cgzn.camera.camera": "摄像头", "cgzn.curtain.curtain": "curtain", @@ -356,9 +555,12 @@ "cgzn.switch.onoff": "switch", "cgzn.tv.tv": "tv", "cgzn.vacuum.vacuum": "vacuum", + "chint.light.chint1": "Chint Smart Light", "chuan.light.0800": "Light", "chuan.plug.0100": "Outlet", "chuan.switch.0300": "Switch", + "chuangmi.aircondition.a": "Air Conditioner", + "chuangmi.airer.7l0a02": "IMILAB Clothes Drying Rack X1 PRO", "chuangmi.camera.019e04": "IMILAB Home Security Camera A1 (Mom\u0026 Baby Caring Version)", "chuangmi.camera.021a04": "Mi 360° Home Security Camera 2K Pro", "chuangmi.camera.021e02": "IMILAB C40", @@ -367,7 +569,13 @@ "chuangmi.camera.029a02": "Mi 360° Home Security Camera 2K", "chuangmi.camera.036a02": "IMILAB Home Security Camera Y2", "chuangmi.camera.038a02": "IMILAB C21", - "chuangmi.camera.042a02": "IMILAB EC3 Pro Outdoor Security Camera", + "chuangmi.camera.039a01": "Xiaomi Smart Camera C400", + "chuangmi.camera.040a02": "IMILAB EC6 Outdoor Camera", + "chuangmi.camera.042a02": "IMILAB Security Camera EC3 Pro", + "chuangmi.camera.046a01": "Mi 360° Camera (1080p)", + "chuangmi.camera.046b02": "IMILAB Home Security Camera Y2", + "chuangmi.camera.049a01": "Xiaomi 360° Home Security Camera 2", + "chuangmi.camera.055a02": "IMILAB EC5 Floodlight Camera", "chuangmi.camera.27a02": "IMILAB C10", "chuangmi.camera.ip026c": "Mi 360° Home Security Camera 1080p Essential", "chuangmi.camera.ip029a": "Mi 360° Home Security Camera 2K", @@ -394,11 +602,17 @@ "chuangmi.cateye.i023a01": "imilab video doorbell", "chuangmi.cateye.ipc018": "IMILAB Digital Door Viewer", "chuangmi.cateye.ipc508": "IMILAB Intelligent Door Visual System H1", + "chuangmi.curtain.711a02": "IMILAB Smart Curtain Motor L2", "chuangmi.curtain.h209": "IMILAB Smart Curtain Motor Bluetooth Gateway L1", + "chuangmi.door.515a01": "Imilab Smart Door Blade Star Series", "chuangmi.door.hmi508": "IMILAB Digital Door Looc H1", + "chuangmi.gateway.720": "Xiaobai Smart Central Air Conditioner Controller", "chuangmi.gateway.ipc011": "IMILAB EC2 Wireless Camera Gateway", "chuangmi.ir.v2": "Mi Universal Remote", "chuangmi.light.028a01": "IMILAB Eye-caring Table Lamp", + "chuangmi.light.716a02": "Xiaobai Smart Downlight", + "chuangmi.light.717a02": "Xiaobai Smart Downlight", + "chuangmi.light.718a02": "Xiaobai Smart Magnetic Lamp", "chuangmi.lock.hmi501": "IMI Security Smart Lock C1", "chuangmi.lock.hmi501b01": "小白智能门锁G1", "chuangmi.lock.hmi503a01": "IMI Home Security Smart Lock J16", @@ -415,15 +629,23 @@ "chuangmi.plug.vtl_v1": "Mi Smart Power Plug(Virtual)", "chuangmi.radio.v1": "Mi Network Radio", "chuangmi.radio.v2": "Mi Network Radio Plus", + "chuangmi.remote.732a02": "Xiaobai Bluetooth wireless switch K2 (single key)", + "chuangmi.remote.732b02": "Xiaobai Bluetooth wireless switch K2 (double keys)", "chuangmi.remote.h102a03": "IMI Remote", "chuangmi.remote.h102c01": "IMI Remote", "chuangmi.remote.v2": "Mi Remote", + "chuangmi.switch.733a02": "Xiaobai smart wall switch K2(zero fire single key)", + "chuangmi.switch.733b02": "Xiaobai smart wall switch K2(zero fire double key)", + "chuangmi.switch.733c02": "Xiaobai smart wall switch K2(zero fire three key)", "chuangmi.switch.mesh": "IMILAB Smart switch K1-A", "chuangmi.switch.meshb01": "IMILAB Smart switch K1-B", "chuangmi.switch.meshc01": "IMILAB Smart switch K1-C", + "chuangmi.vacuum.707b02": "IMILAB V2 Vacuuming Robot", + "chuangmi.vacuum.719a02": "IMILAB V3 Vacuuming Robot", "chuangmi.vacuum.hmi707": "IMILAB V1 Vacuuming Robot", "chuangmi.wifi.v1": "小米随身WIFI", "chunmi.cooker.eh1": "Mi Smart MultiCooker 1.6L", + "chunmi.cooker.eh3": "Mijia Smart Rice Cooker Mini 2", "chunmi.cooker.eh402": "Mi Smart Multicooker 4L", "chunmi.cooker.k1pro1": "Mi Induction Heating Pressure Rice Cooker 1S", "chunmi.cooker.naeg1": "Mi IH Rice Cooker", @@ -437,10 +659,20 @@ "chunmi.cooker.normalcd2": "Mi IH Rice Cooker 4L", "chunmi.cooker.press1": "Mi IH Pressure Rice Cooker", "chunmi.cooker.press2": "Mi IH Pressure Rice Cooker", + "chunmi.cooker.r1": "joyami Smart Rice Cooker S1", + "chunmi.cooker.r401": "joyami Smart Rice Cooker (S1/L1)", "chunmi.cooker.tw1": "Mi Smart Rice Cooker (1.6L)", "chunmi.cooker.wy3": "Xiaomi Smart Rice Cooker 3L", + "chunmi.cooker.wy4": "Xiaomi Smart Rice Cooker 4L", + "chunmi.cooker.ztw02": "zhiwuzhu Rice Cooker 1.6L", "chunmi.cooker.zwz01": "ZHIWUZHU Cooking Intelligent Rice Cooker 4L", + "chunmi.cooker.zwz02": "ZHIWUZHU Rice Cooker 1.6L", + "chunmi.cooker.zwz04": "ZHIWUZHU Smart Rice Cooker 1.6L", "chunmi.health_pot.a1": "Mi Smart Multi-functional Kettle", + "chunmi.health_pot.zwza1": "ZHIWUZHU Smart Multi-functional Kettle", + "chunmi.i_stove.s1": "Mijia Multifunction Kitchen Range S1", + "chunmi.ihcooker.cb": "TOKIT Ultra Thin Smart Induction Cooker-AI Edition", + "chunmi.ihcooker.cbhw": "TOKIT Razor Cooktop Pro", "chunmi.ihcooker.chefnic": "Mi Induction Cooker Set Chef Nic Edition", "chunmi.ihcooker.double": "Mi Dual Burner Induction Cooker", "chunmi.ihcooker.eg1": "Mi Induction Cooker", @@ -452,21 +684,37 @@ "chunmi.ihcooker.tkv1": "TOKIT-Smart induction Cooker Entry-chunmi", "chunmi.ihcooker.tw1": "Mi Induction Cooker", "chunmi.ihcooker.v1": "Mi Induction Cooker", + "chunmi.ihcooker.v1joy": "joyami Smart Induction Cooker", + "chunmi.ihcooker.v2": "Mi Induction Cooker 2", + "chunmi.ihcooker.zwz03": "ZHIWUZHU Induction Cooker", "chunmi.juicer.a1": "Mi High-speed Smart Blender", + "chunmi.juicer.jym1": "joyami Smart Blender", + "chunmi.juicer.twa1": "Xiaomi Smart Blender", "chunmi.microwave.n20l01": "Mi Smart Microwave Oven", + "chunmi.microwave.n20l02": "Mi Smart Microwave Oven", "chunmi.microwave.n23l01": "Mi Smart Microwave Oven with Grill", + "chunmi.oven.lite01": "TOKIT Smart Oven 32L", + "chunmi.oven.s1": "Mijia Smart Multifunction Wall Oven S1", "chunmi.oven.steam30lv1": "Mi Smart Steam Oven", "chunmi.oven.x02": "Mi Smart Steam Oven (12L)", "chunmi.pre_cooker.dylg5": "Mi Smart Pressure Cooker 5L", "chunmi.pre_cooker.eh1": "Mi Smart Pressure Cooker", "chunmi.pre_cooker.mini1": "Mi Smart Pressure Cooker 2.5L", "chunmi.waterpuri.600f2": "Mi Water Purifier H600G", + "chunmi.waterpuri.600j6": "Mi Water Filtration System Q600 (Hot and Cold)", "chunmi.waterpuri.800f3": "Mi Water Purifier H800G", + "chunmi.waterpuri.800f3p": "Xiaomi Water Purifier H800G Pro", + "chunmi.waterpuri.qc1200": "QCOOKER CS-CJ01-1200G reverse osmosis water purifier", + "chunmi.waterpuri.qc1600": "QCOOKER CS-CJ01-1600G reverse osmosis water purifier", + "chunmi.waterpuri.tkj10": "TOKIT Water Filtration System (Hot and Cold)", "chunmi.ysj.tsa1": "Mi Countertop Filtered Water Dispenser", + "chunmi.ysj.tsj7": "Mijia Smart Filtered Water Dispenser (Hot and Cold)", "cleargrass.sensor_ht.dk1": "Mi Temperature and Humidity Monitor", "clk.light.basket": "WuZuoTiLan", "cmcc.aircondition.x12": "中国移动空调伴侣CMCC-X12", "cmcc.plug.x11": "中国移动智能插座CMCC-X11", + "cmjd.curtain.cm82": "XM smart curtains", + "cmjd.curtain.cm83": "Deyu smart curtain", "coc.airpurifier.tk": "离子塔/静音无耗材空气净化器-骑士", "coc.airpurifier.tk2": "CoClean云对接", "coc.dryer.fdpsm": "清蜓智能便携干衣机", @@ -474,6 +722,8 @@ "codoon.watch.s1": "CODOON GPS SPORT WATCH S1", "coli.waterpuri.nf4645": "Nanofiltration machine", "coolki.aircondition.yk7": "遥控大师-wifi转红外-空调", + "coolki.curtain.iid165": "多功能双通道开关_支持电机模式", + "coolki.curtain.uid126": "多功能双通道电量检测开关", "coolki.curtain.uid131": "2.4G门控遥控器", "coolki.curtain.uid132": "2.4G窗帘遥控器", "coolki.curtain.uiid11": "cutain", @@ -487,6 +737,7 @@ "coolki.light.uid135": "博流冷暖双色灯", "coolki.light.uid136": "博流RGB五色灯", "coolki.light.uid137": "律动灯带蓝牙版", + "coolki.light.uid166": "2.4G轻智能五色球泡灯", "coolki.light.uid34": "风扇灯", "coolki.light.uid44": "智能调光器(uiid44)", "coolki.light.uid57": "单色球泡灯(57)", @@ -504,6 +755,7 @@ "coolki.plug.uiid83": "outletGsm83", "coolki.plug.uiid84": "outletGsm84", "coolki.sensor_ht.ui1770": "zigbee子设备温湿度传感器(1770)", + "coolki.sensor_ht.uid181": "TH_R3 恒温恒湿改装件_支持历史记录", "coolki.stb.yk0001": "遥控大师-wifi转红外-电视机顶盒", "coolki.switch.four": "plug_four", "coolki.switch.t3uk1c": "Wi-Fi智能墙面开关", @@ -545,19 +797,42 @@ "cubee.airrtc.th123w": "Heatcold UFH Thermostat", "cubee.airrtc.th125a": "Heatcold FCU thermostat", "cubee.airrtc.th125t": "Heatcold Heat Pump Thermostat", - "cuco.light.sl4": "Gosund Smart Light SL4", - "cuco.light.sl4a": "NiteBird Smart Light Strip SL4", + "cuco.acpartner.cp6": "DianXiaoKu AC Partner CP6", + "cuco.light.sl4": "Smart Light SL4", + "cuco.light.sl4a": "Smart Light Strip SL4", + "cuco.light.wb2m": "Gosund Smart White Light WB2-M", + "cuco.light.wb2mn": "WB2-M Smart White Bulb", + "cuco.light.wb5m": "Gosund Smart Lantern WB5-M", + "cuco.light.wb5mn": "WB5-M Smart Color Bulb", "cuco.plug.co1": "Gosund Smart Wall Socket CO1-M", + "cuco.plug.co1d": "Dianxiaoku Smart Wall Socket CO1-M", "cuco.plug.cp1": "Gosund Smart Plug CP1", + "cuco.plug.cp1d": "Dianxiaoku Smart Plug CP1", "cuco.plug.cp1m": "Gosund Smart Socket CP1-AM (with battery count)", "cuco.plug.cp2": "Gosund Smart Plug CP2", "cuco.plug.cp2a": "Gosund Smart Socket CP2-A", + "cuco.plug.cp2ad": "Dianxiaoku Smart Socket CP2-A", + "cuco.plug.cp2d": "Dianxiaoku Smart Socket CP2", "cuco.plug.cp3a": "Gosund Smart Socket CP3-A", + "cuco.plug.cp3ad": "Dianxiaoku Smart Socket CP3-AM", + "cuco.plug.cp5d": "Dianxiaoku Smart Socket CP5", + "cuco.plug.cp5prd": "Dianxiaoku smart socket cp5 pro", + "cuco.plug.cp5pro": "Gosund smart socket cp5 pro", + "cuco.plug.p2": "Gosund Smart Socket P2", "cuco.plug.sp5": "Gosund smart socket CP5", "cuco.plug.wp5": "Cuco Smart Plug (wifi) Basic", "cuco.switch.cs1": "Gosund Smart Switch CS1 (Single-Key Version of Zero Fire Wire)", + "cuco.switch.cs1d": "Dianxiaoku Smart Switch CS1 (Single-Key Version of Zero Fire Wire)", "cuco.switch.cs2": "Gosund Smart Switch CS2 (Zero Fire Wire Double Button Version)", + "cuco.switch.cs2d": "Dianxiaoku Smart Switch CS2 (Zero Fire Wire Double Button Version)", "cuco.switch.cs3": "Gosund Smart Switch CS3 (Zero FireWire three-key version)", + "cuco.switch.cs3d": "Dianxiaoku Smart Switch CS3 (Zero FireWire three-key version)", + "cuco.switch.s4amtd": "Dianxiaoku Smart Switch S4AM", + "cuco.switch.s4amts": "Gosund Smart Switch S4AM", + "cuco.switch.s5amtd": "Dianxiaoku Smart Switch S5AM", + "cuco.switch.s5amts": "Gosund Smart Switch S5AM", + "cuco.switch.s6amtd": "Dianxiaoku Smart Switch S6AM", + "cuco.switch.s6amts": "Gosund Smart Switch S6AM", "cxds.light.wymz01": "Muzi Intelligent Light", "cydj.light.rgb": "RGB Bulb", "cydj.light.v2": "RGB LED Controller", @@ -567,11 +842,15 @@ "cydj.light.v8": "RGBCW Bulb", "cydj.plug.v5": "Switch", "cykj.hood.yjc901": "cleadeep hood", + "dadou.curtain.one": "大豆智能窗帘", + "dadou.light.color": "大豆智能彩灯", + "dadou.light.one": "大豆智能灯", "daolai.light.dlb001": "LED Scene Lamp", "daolai.light.qp320": "GLITTERING WORD LED lamp", "daolai.light.qp350": "GLITTERING WORD Color lamp", "daolai.light.sd330": "QL-LIGHT", "daolai.light.xd340": "Ceiling lamp", + "daolai.motor.jdts": "Laser TV adjustment platform", "daolai.switch.k1": "Single switch", "davell.airrtc.dv323x": "Heating thermostat", "dayang.mosq.dyt16s": "Mosquito lamp", @@ -607,8 +886,9 @@ "dec.plug.faxian": "发现云智控插座", "dec.switch.faxian": "发现云智控开关", "deerma.humidifier.ct500": "Evaporative Humidifier", + "deerma.humidifier.cz300": "DEERMA Thermal Distillation Humidifier", "deerma.humidifier.jsq": "Mi Smart Antibacterial Humidifier", - "deerma.humidifier.jsq1": "Mi S Smart humidifer ", + "deerma.humidifier.jsq1": "MI Smart Humidifier S", "deerma.humidifier.jsq3": "MI Smart humidifer", "deerma.humidifier.jsq4": "Mi Smart Evaporative Humidifer ", "deerma.humidifier.jsq5": "Mi Smart Antibacterial Humidifier", @@ -617,6 +897,9 @@ "deerma.humidifier.jsqs": "Mi Smart Humidifer S", "deerma.humidifier.mjjsq": "Mi Smart Humidifier", "deerma.humidifier.rz300": "Thermal Distillation Humidifier", + "deerma.mfcp.v1": "Mijia Smart Multi-functional Cooker (1.5L)", + "deerma.mfcp.v2": "Mijia Smart Multi-functional Cooker (1.5L)", + "deerma.mfcp.v3": "Mijia Smart Multi-functional Cooker (1.5L)", "degree.lunar.smh013": "37 Degree Sleep Tracking Strap SMH013", "delian.aircondition.de": "delianAir", "delian.stb.delian": "delian_stb", @@ -644,10 +927,18 @@ "dikair.light.wy0a02": "Xuan Yi Smart Light", "dikair.light.wy0a03": "Yan Tai Scene Smart Llight", "dikair.light.wy0a04": "Yi Tang Smart Light", + "disen.airfresh.ds300": "Devotion Air Fresh", + "disen.waterheater.slm6": "Squirrel Heater", + "dlx.light.eps118": "DLX Smart Lighting", + "dlx.light.eps119": "DLX Smart modern Lighting", "dmaker.airfresh.a1": "Mi Fresh Air Ventilator A1-150", "dmaker.airfresh.dm2018": "DreamMaker Fresh Air Ventilator", "dmaker.airfresh.t2017": "Mi Fresh Air Ventilator", + "dmaker.airp.swift": "Mi Smart Circulating Air Purifier", + "dmaker.airp.swift2": "Xiaomi Smart Circulating Air Purifier", "dmaker.airpurifier.f20": "Mi Air Purifier F1", + "dmaker.derh.22ht": "Xiaomi Smart Dehumidifier", + "dmaker.derh.22l": "Mi Smart Dehumidifier (22L)", "dmaker.fan.01": "造梦者体感落地扇", "dmaker.fan.02": "Dream Maker Feel Fan Plus", "dmaker.fan.1c": "Mi Smart Standing Fan 2 Lite", @@ -657,6 +948,11 @@ "dmaker.fan.p15": "Mi Smart Standing Fan Pro", "dmaker.fan.p18": "Mi Smart Standing Fan 2", "dmaker.fan.p220": "Mi Smart Standing Fan (with Battery)", + "dmaker.fan.p23": "Xiaomi Smart Cool and Hot Fan Heater", + "dmaker.fan.p28": "Mijia Smart Standing Air Circulation Fan", + "dmaker.fan.p30": "Xiaomi Smart Standing Fan 2", + "dmaker.fan.p33": "Xiaomi Smart Standing Fan 2 Pro", + "dmaker.fan.p39": "Xiaomi Smart Tower Fan", "dmaker.fan.p5": "Mi Smart Standing Fan 1X", "dmaker.fan.p8": "Mi Smart Standing Fan 1C", "dmaker.fan.p9": "Mi Smart Tower Fan", @@ -678,6 +974,9 @@ "dong.light.wy0a01": "Dongdong simple smart living room lamp", "dooya.airer.mjlyj": "PanPan Smart Clothes Rack", "dooya.curtain.c1": "DooyaCurtainController", + "dooya.curtain.d1xc": "Dooya Curtain D1-XC", + "dooya.curtain.d8xwb": "Dooya Curtain d8xwb", + "dooya.curtain.m03": "DOOYA Smart Curtain M3", "dooya.curtain.m1": "DooyaSmartCurtain", "dooya.curtain.m2": "Dooya Curtain", "dooya.curtain.m5": "Dooya Curtain", @@ -685,9 +984,15 @@ "dotdot.light.dian24": "Diandian24w", "dotdot.light.dian45": "Diandian45w", "dotdot.light.dian90": "Diandian90w", + "dreame.fan.l2146": "Xiaomi Smart Purifying Fan", + "dreame.fan.p2018": "Xiaomi Smart Purifying Fan", + "dreame.scooter.epro": "Xiaomi Electric Scooter 3 Lite", + "dreame.scooter.p2223": "NAVEE Electric Scooter S65", + "dreame.scooter.t2185": "Xiaomi Electric Scooter 3 Lite", "dreame.vacuum.ma1808": "Mi Robot Vacuum-Mop", "dreame.vacuum.mb1808": "Mi Robot Vacuum-Mop", "dreame.vacuum.mc1808": "Mi Robot Vacuum-Mop", + "dreame.vacuum.md1808": "Xiaomi Robot Vacuum-Mop 2C", "dreame.vacuum.p2008": "Dreame Robot Vacuum-Mop F9", "dreame.vacuum.p2009": "Dreame Robot Vacuum D9 ", "dreame.vacuum.p2027": "Dreame Bot W10", @@ -697,23 +1002,65 @@ "dreame.vacuum.p2036": "Trouver Robot LDS Vacuum-Mop Finder", "dreame.vacuum.p2041": "Mi Robot Vacuum-Mop 1T", "dreame.vacuum.p2041o": "Mi Robot Vacuum-Mop 2 Pro+", + "dreame.vacuum.p2114a": "Xiaomi Robot Vacuum X10+", + "dreame.vacuum.p2114o": "Mijia Omni Robot Vacuum-Mop", "dreame.vacuum.p2140": "Mijia Robot Vacuum-Mop 2C", + "dreame.vacuum.p2140a": "Mi Robot Vacuum-Mop 2C", + "dreame.vacuum.p2140o": "Mi Robot Vacuum-Mop 2", + "dreame.vacuum.p2140p": "Mi Robot Vacuum-Mop 2", + "dreame.vacuum.p2140q": "Mi Robot Vacuum-Mop 2", "dreame.vacuum.p2148o": "Mijia Robot Vacuum-Mop Ultra Slim", + "dreame.vacuum.p2149o": "Mijia Self-Cleaning Robot Vacuum-Mop Pro", + "dreame.vacuum.p2150a": "Mi Robot Vacuum-Mop 2 Ultra", + "dreame.vacuum.p2150b": "Mi Robot Vacuum-Mop 2 Ultra Set", "dreame.vacuum.p2150o": "Mijia Robot Vacuum-Mop Dirt Disposal", + "dreame.vacuum.p2156": "追觅扫地机", "dreame.vacuum.p2156o": "MOVA Z500 Robot Vacuum and Mop Cleaner", "dreame.vacuum.p2157": "MOVA L600 Robot Vacuum and Mop Cleaner", "dreame.vacuum.p2187": "Dreame Bot D9 Pro", "dreame.vacuum.p2259": "Dreame Bot D9 Max", + "dreame.vacuum.r2104": "Dreame Bot W10 Pro Self-Cleaning Robot Vacuum and Mop", + "dreame.vacuum.r2205": "Dreame Bot D10 Plus", + "dreame.vacuum.r2228": "DreameBot S10", + "dreame.vacuum.r2228o": "DreameBot L10s Ultra", + "dreame.vacuum.r2233": "DreameBot S10 Pro", "dsm.lock.h3": "DESSMANN smart lock-Di H3", "dsm.lock.q3": "Q3", "dsm.lock.r5": "DESSMANN Facial recognition smart lock-Di R5", + "dsrn.airrtc.dswkq": "小松鼠温控器", + "dsrn.waterheater.slm6": "小松鼠壁挂炉", "dsuper.bed.esleep": "索菲莉尔智能电动床", "dtr.magic_touch.211mgr": "PGG Neck Massager P5B", "dtr.magic_touch.p6b": "PGG Neck Massager P6", + "dtr.magic_touch.p6c": "PGG intelligent neck massage P6", "dtr.magic_touch.p7": "PGG Neck Massager P7", "dun.cateye.nknk500": "DUN Smart Doorbell", "duoqin.safe.pbfv01": "Privacy box for finger vein identification", - "dwdz.switch.sw0a01": "Scene mesh breaker DBS", + "duwi.aircondition.50102": "亿林中央空调", + "duwi.aircondition.50103": "德姆瑞中央空调", + "duwi.airfresh.050302": "博乐新风", + "duwi.airfresh.050303": "净养新风", + "duwi.curtain.040102": "窗帘", + "duwi.fan.010201": "风扇", + "duwi.fan.010501": "风扇(不带电量)", + "duwi.heater.050202": "亿林地暖", + "duwi.light.010101": "灯", + "duwi.light.010401": "灯(不带电量)", + "duwi.light.030101": "调光灯", + "duwi.light.030201": "色温灯", + "duwi.light.030301": "调光调色温灯", + "duwi.light.030401": "RGBW灯", + "duwi.switch.010301": "开关", + "duwi.switch.010601": "开关(不带电量)", + "dwdz.switch.sw0a01": "Scenario mesh breaker DBS version", + "e11035.airer.1208a": "Heichuang HRLCHURE intelligent clothes hanger", + "e11035.airer.1208b": "LFK intelligent clothes hanger", + "e11035.airer.9000x": "TAITAILE TES Single function Clothes dryer", + "e11035.airer.9003sx": "TAITAILE TES Full function Clothes dryer", + "e11035.airer.a6": "JOMOO A6090 Electric Clothes Dryer", + "e11035.airer.t10l": "JL Single Function Smart Clothes Dryer", + "e11035.airer.t10xfh": "JL Full Function Smart Clothes Dryer", + "eairpo.bed.mi12": "iGROX Air circle temperature control mattress", "easc.alarm.biv1": "Building Incline Vibration Safety Monitor", "ecloud.airc.eq": "智能空调", "ecloud.airfresh.eq": "新风", @@ -724,6 +1071,7 @@ "ecloud.plug.eq": "智能插座", "ecloud.switch.eq": "智能开关", "ecloud.tv.eq": "电视", + "eco.airp.z1": "科沃斯沁宝Z1空气净化器", "eco.vacuum.deebot": "科沃斯扫地机器人", "eco.vacuum.dk45": "科沃斯智能扫地机器人", "eco.vacuum.dqcneu": "科沃斯智能扫地机器人T9", @@ -736,6 +1084,9 @@ "eco.vacuum.n8pink": "科沃斯智能扫地机器人", "eco.vacuum.n8wh": "科沃斯智能扫地机器人", "eco.vacuum.n9": "科沃斯智能扫地机器人N9", + "eco.vacuum.t10": "科沃斯智能扫地机器人T10", + "eco.vacuum.t10om": "科沃斯智能扫地机器人T10 OMNI", + "eco.vacuum.t10t": "科沃斯智能扫地机器人T10 TURBO", "eco.vacuum.t5wh": "科沃斯智能扫地机器人", "eco.vacuum.t8aivi": "科沃斯智能扫地机器人", "eco.vacuum.t8mop": "科沃斯智能扫地机器人", @@ -743,6 +1094,10 @@ "eco.vacuum.t9max": "科沃斯智能扫地机器人T9MAX", "eco.vacuum.t9pmax": "科沃斯智能扫地机器人T9+", "eco.vacuum.t9pow": "科沃斯智能扫地机器人T9Power", + "eco.vacuum.turbo": "科沃斯智能扫地机器人X1 TURBO", + "eco.vacuum.x1": "科沃斯智能扫地机器人X1", + "eco.vacuum.x1omni": "科沃斯智能扫地机器人X1 OMNI", + "ecozy.magic_touch.b029": "Mini Thermal Fascia Gun", "eda.switch.1sfsw": "Earda Mesh Switch L1", "eda.switch.2sfsw": "Earda Mesh Switch L2", "eda.switch.2sw": "Earda Mesh Switch 2", @@ -759,7 +1114,12 @@ "eide.curtain.curtai": "窗帘", "eide.light.adds21": "普通灯光", "eide.light.tiaog": "调光灯", + "enjia1.bed.ej001": "恩嘉智能床", + "enjia1.sofa.ej002": "恩嘉智能沙发", "era.airp.cwb03": "EraClean Intelligent deodorizer", + "era.diffuser.ws01": "Smart Odor Eliminator", + "era.steriliser.gc01": "UVC sterilizing ultrasonic cleaner", + "erazl.curtain.curt": "窗帘", "erazl.switch.s1": "开关", "espush.light.es02": "智云物联水草灯", "espush.sensor_ht.es03": "空气检测仪", @@ -793,6 +1153,7 @@ "ezhome.switch.zhyapp06": "Reset-Switch", "ezhome.tv.yy1002": "云接入电视", "fawad.airrtc.30011": "FOWAD VRF Thermostat", + "fawad.airrtc.30012": "FOWAD VRF Thermostat - Enjoy", "fawad.airrtc.40011": "FOWAD Floorheating Thermostat", "fawad.airrtc.fwd20011": "FOWAD thermostat controller", "fbs.airmonitor.pth02": "AIR QUALITY TESTER", @@ -803,10 +1164,14 @@ "feibit.plug.tskt106w": "Metering socket", "feibit.sensor_ht.fzb870": "Temperature and humidity sensor", "feibit.switch.tzsw21hb": "one gang smart switch", + "fengmi.projector.055fc": "Formovie R1 Nano UST Laser Projector", + "fengmi.projector.146fc": "Formovie V10 4K Projector", "fengmi.projector.c015": "Xming Q1 mini projector", + "fengmi.projector.c025": "Xming Q1 Pro Smart Projector", "fengmi.projector.fm05": "Fengmi Young", "fengmi.projector.fm15": "Mijia Laser TV", "fengmi.projector.fm154k": "Mijia Laser Projection TV 4K", + "fengmi.projector.l065f": "Formovie X1 Laser Projector", "fengmi.projector.l115fx": "Formovie Laser Projector R1", "fengmi.projector.l166": "A300 Splendid Laser Projector", "fengmi.projector.l166f": "Formovie Laser TV Cinema 2", @@ -814,7 +1179,10 @@ "fengmi.projector.l176fp": "Formovie Laser Projector 4K Cinema Pro ", "fengmi.projector.l176jp": "Mi Laser Projector 1S 4K", "fengmi.projector.l185": "Mi Laser Projector", + "fengmi.projector.l185j2": "xiaomi Laser Projector 1S", "fengmi.projector.l206a": "Appotronics Home Laser Projector A300", + "fengmi.projector.l206fx": "Formovie Laser TV T1", + "fengmi.projector.l206j": "Mi Laser Projector2 4K", "fengmi.projector.l246": "C700 4K Home Theater Projector", "fengmi.projector.l306a": "Appotronics D30 Laser Projector", "fengmi.projector.l406f": "Formovie Laser Cinema 4K Max", @@ -830,6 +1198,16 @@ "fengyu.intercom.litev1": "Mi Walkie-Talkie Lite", "fengyu.intercom.sealv1": "Mi Walkie-Talkie 1S", "fengyu.intercom.sharkv1": "Mi Walkie-Talkie 2", + "fest.aircondition.cac": "中央空调", + "fest.aircondition.cc": "空调", + "fest.curtain.pw": "窗帘", + "fest.light.light": "普通灯", + "fest.light.rgb": "变色灯", + "fest.light.spd": "调光灯", + "fest.plug.pp": "插座", + "fest.switch.scene2": "场景开关", + "fest.tv.tv": "电视", + "fgj.bed.rcctgc": "Smart bedside table", "fimi.camera.c1": "Mi Action Camera 4K", "fimi.camera.c1b": "Mi Action Camera 4K", "fine.aircondition.cl": "空调", @@ -840,7 +1218,13 @@ "fine.plug.dc1": "单孔插座", "fine.sensor_ht.th": "温湿度传感器", "fine.switch.switch": "开关", + "fjrh.massage.rh7600": "RH multifunctional massage chair", "fotile.hood.emd1tmi": "CXW-200-EMD1T.MI", + "frfox.switch.bl01": "Single button fire Mesh switch", + "frfox.switch.bl02": "Two buttons fire Mesh switch", + "frfox.switch.bl03": "Three buttons fire Mesh switch", + "fsl.curtain.c00001": "窗帘", + "fsl.light.l00002": "灯-CW", "ftds.light.wyft1": "Fantong Intelligent Lamp", "ftnet.switch.xh01s": "云开关", "future.aircondition.ac": "空调", @@ -854,11 +1238,19 @@ "future.tv.tv": "电视机", "future.wifispeaker.amp02": " 瑞立音乐播放器", "fyw.switch.sw0a01": "Xiaowei Scene MESH Switch", + "galime.curtain.gm46": "Dinton Smart Curtain", + "galime.curtain.gp72": "Dinton Smart Sliding Window Opener", + "galime.curtain.gz45": "Ding Dong intelligent rolling curtain motor", + "gdds.light.wy0a01": "High Light Master Smart Light", + "gdls.switch.t00002": "插座网关", + "gdxdkj.blanket.01": "XD-SNT01", + "gdxdkj.blanket.smxdr1": "Xiaoda Graphene smart electric blanket", "gdyimu.vacuum.g1m": "Lydsto inertial navigation sweep and drag integrated robot G1M", "gdyimu.vacuum.r1": "Lydsto Lydsto R1 A Intelligent Sweep\u0026Mop Integrated Robot", "gdyimu.vacuum.r1da": "Lydsto Sweeping And Mopping Robot R1DA", "ge.light.mono1": "X-Bulb智能灯泡", "gerwin.curtain.gm25xm": "GM Smart Curtain", + "gerwin.curtain.gm35xm": "GM Smart Curtain Motor", "ghome.curtain.sf004": "窗帘电机", "ghome.fan.sf008": "风扇", "ghome.kettle.sf009": "热水壶", @@ -871,25 +1263,33 @@ "ghome.tv.sf005": "电视红外遥控", "ghome.vacuum.sf013": "扫地机器人", "giec.light.sl1501": "智能护眼台灯", + "giot.light.aise01": "intelligence Ai scene WIFI two-color light", "girt.light.light": "light", "girt.light.light5ch": "light5ch", "girt.plug.socket": "socket", "girt.switch.switch": "switch", "girt.vacuum.cleanrobot": "vaccum", + "girt.vacuum.vacuum": "Grit扫地机系列", "giz.aircondition.kt001": "空调", + "giz.airrtc.thermo": "温控器", "giz.derh.csj001": "除湿机", "giz.light.clight": "light", "giz.light.light": "色温灯", "giz.plug.outlet": "socket", "giz.switch.switch": "开关", + "gmair.airfresh.gm2201": "果麦GM280", + "gmair.airfresh.gm280": "果麦新风机280", "gmair.airfresh.gm320": "果麦新风机320", "gmair.airfresh.gm320p": "果麦新风320Pro", "gmair.airfresh.gm420": "果麦新风机420", "gmair.airfresh.gm420s": "果麦新风机420S", "gmair.airfresh.gm500": "果麦新风机500", "gmair.airfresh.gm520": "果麦新风机520", + "gmair.airfresh.gmxfp1": "果麦", "gmair.fan.wy100": "果麦冷暖风扇", "gmair.fan.wy101": "果麦冷风扇", + "gmm.light.wy0a01": "Dual Erotic Scene Wisdom Lamp", + "gmm.light.wy0a02": "LED smart light mesh", "gmn.bhf_light.yb1": "Gomani bath heater", "gmn.light.wy0a01": "Gomanni G1 series ceiling lamp", "gmn.light.wy0a02": "Gomanni Scene two color downlight", @@ -903,18 +1303,31 @@ "golden.curtain.c01": "curtain", "golden.kettle.wd01ei": "drinking", "golden.light.m002": "bulb", + "gosvn.switch.sw0a01": "Gosvn smart valve controller (WiFi)", + "gosvn.switch.sw1a01": "Gosvn one key smart switch (WiFi)", + "gosvn.switch.sw2a01": "Gosvn two key smart switch (WiFi)", + "gosvn.switch.sw3a01": "Gosvn three key smart switch (WiFi)", + "gran.switch.four": "Wireless IoT zero fire four key switch", + "gran.switch.one": "Wireless IoT zero fire single key switch", + "gran.switch.three": "Wireless IOT zero fire triple key switch", + "gran.switch.two1": "Wireless IoT zero fire double key switch", "gsd.light.wy0a01": "QTUN Mesh Light", + "gtop.light.c02": "ZY Intelligent Light Lamp", "gtop.light.eps119": "ZY Smart Ceiling Lamp", + "gtop.light.eps120": "ZhiYue Smart Lighting", "gtop.light.gtop02": "ZhiYue Smart Spotlight", "gtop.light.xl01": "ZhiYue Smart Light", "gtop.light.xl6601": "智能灯具", + "gtop.switch.yd01": "ZhiYue Smart switch", "guoshi.other.sem01": "Hi+ Intelligent Steam Eyeshade", + "gxdz.curtain.dg168": "SiaerSingleRail", "gxdz.curtain.gx168": "Integrated Electric Curtain", "gxhl.switch.7zds11": "7z One-way Smart Dimmable Switch", "gxhl.switch.7zds12": "7z Two-way Smart Dimmable Switch", "gxhl.switch.7zsw11": "7z One-key Smart Switch", "gxhl.switch.7zsw12": "7z Two-key Smart Switch", "gxhl.switch.7zsw13": "7z Three-key Smart Switch", + "gzht.plug.s1": "万象天引", "h2t.aircondition.ac": "红外智能控制终端", "h2t.plug.2": "分布式电源2.0", "h2t.projector.projet": "投影仪智能控制模块", @@ -922,9 +1335,12 @@ "hannto.printer.anise": "Mi Wireless Inkjet Printer", "hannto.printer.basil": "Mi Portable Photo Printer", "hannto.printer.honey": "Mi Wireless Photo Printer", - "hannto.printer.honey1s": "Mi Wireless Photo Printer 1S", + "hannto.printer.honey1s": "Xiaomi Instant Photo Printer 1S", + "hannto.printer.lager": "Mi All-in-One Laser Printer K200", + "hannto.printer.rmy": "Mi Wireless All-in-One Inkjet Printer", "hanyi.airpurifier.kj550": "MiWhole Air Purifier Mix", "hanyi.vacuum.m7pro": "MI WHOLE M7 PRO", + "hanyi.vacuum.m8pro": "MIWHOLE M8", "hbc.light.wy0a01": "Huangde Smart Lamp", "hbkj1.aircondition.v1": "空调", "hbkj1.curtain.v1": "窗帘", @@ -957,12 +1373,23 @@ "here8.light.dimmer": "调光灯", "here8.switch.here8": "利麦开关", "here8.tv.tv": "电视", + "hey.switch.x100": "IOT-BASED MICRO CIRCUIT BREAKER", + "hfjh.fishbowl.600": "Desgeo intelligent modular eco-fish tank", "hfjh.fishbowl.c500": "Desgeo amphibious ecological bottom filter fish tank", "hfjh.fishbowl.v1": "Desgeo smart fishbowl", "hfjh.fishbowl.v2": "Desgeo C series smart fish tank", "hhcc.bleflowerpot.v2": "Ropot", "hhcc.plantmonitor.v1": "Flower Care", + "hipc.switch.k1": "HiPC远程开机卡", + "hisens.aircondition.nb": "空调", + "hisens.curtain.cur1": "单开窗帘", + "hisens.plug.socket": "插座", + "hisens.switch.sw1": "一键开关", + "hisens.switch.sw2": "两键开关", + "hisens.switch.sw3": "三键开关", "hith.foot_bath.q2": "HITH wireless foot bath Q2", + "hle.light.light": "照明灯", + "hle.plug.smartsocket": "Smart Socket", "hmpace.bracelet.v3nfc": "小米手环3 NFC版", "hmpace.bracelet.v4": "Mi Smart Band 4 NFC", "hmpace.scales.mibfs": "Mi Body Composition Scale 2", @@ -971,14 +1398,19 @@ "hope.curtain.473": "窗帘电机", "hope.gateway.host3": "智慧屏网关", "hope.switch.switch": "开关", + "hope.switch.w1": "Smart screen switch", "hope.wifispeaker.box3x": "Background Music", "hope.wifispeaker.m9": "Hope M9", - "hope.wifispeaker.mini3a": "HOPE Music Box Mini 3A", + "hope.wifispeaker.mini3a": "HOPE Mini 3A", + "hope.wifispeaker.mini3s": "Single source BM", "hope.wifispeaker.pad3s": "Double source BM", "hopo.wopener.si200": "窗户", "hopo.wopener.si300": "智能窗控", + "hopo.wopener.si500": "电动窗", "horow.airer.p90": "Xijian smart clothes rack", + "horow.toilet.s10": "Horow S10", "horow.toilet.s4": "Horow S4", + "horow.toilet.s4p": "Horow S4p", "hosjoy.aircondition.airfh": "空调地暖面板", "hosjoy.aircondition.hos": "空调", "hosjoy.aircondition.tt": "空调", @@ -998,8 +1430,13 @@ "hotata.switch.ls013": "科徕尼三路灯控面板", "hotata.switch.ls022": "科徕尼二路灯控面板", "hotata.switch.ls023": "科徕尼三路灯控面板", + "hoto.etool.cjyusa": "HOTO Smart Laser Measure", "hoto.etool.finder": "HOTO Smart Laser Measure", + "hoto.etool.mf01": "Mi Smart Laser Measure", + "hoto.etool.mf02": "Xiaomi Smart Laser Measure", + "hoto.etool.plus01": "HOTO Smart Laser Measure Plus", "hoto.k_scale.qwcfc": "HOTO Smart Kitchen Scale", + "hoto.k_scale.qwcfc2": "HOTO Smart Kitchen Scale", "hsgczx.toilet.ctv1": "Health Closestool", "hsmart.aircondition.hsa": "空调面板", "hsmart.aircondition.irc": "空调", @@ -1015,6 +1452,8 @@ "hti.gateway.123": "123", "htwl.dryer.v1": "Shoes dryer", "huachu.dryer.xqd100": "Smart clothing", + "huadn.blanket.123456": "Huanding water heating blanket", + "huadn.blanket.abcdef": "GIPIN Water heating blanket", "huayi.bhf_light.libra": "HUIZUO LIBRA BATH HEATER Pro", "huayi.light.aqu114": "HUIZUO AQUARIUS Bulb", "huayi.light.ari013": "HUIZUO ARIES For Bedroom", @@ -1040,14 +1479,27 @@ "huayi.light.wy202": "HUIZUO Bulb (BLE Mesh)", "huayi.light.wy203": "HUIZUO YONG Downlight (BLE Mesh)", "huayi.light.wy208": "HUIZUO Lucky Star For Children", + "huayi.light.wy211": "HUAYI Universal Light", + "huayi.light.wy212": "Huayi Intelligent Ceiling Lamp", "huayi.light.wyheat": "HUIZUO Heating Lamp", "huayi.light.zw131": "HUIZUO ZIWEI Ceiling Lamp", "huayi.switch.v0001": "HUIZUO Switch (Single Key)", "huayi.switch.v0002": "HUIZUO Switch (Double Key)", + "huayi1.aircondition.a01": "空调", + "huayi1.curtain.wc10": "窗帘", + "huayi1.light.dd01": "灯带", + "huayi1.light.dm10": "二路调光面板", + "huayi1.light.li20": "灯光", + "huayi1.switch.st00": "开关", + "huayi1.switch.st10": "场景", + "huayi1.tv.ds10": "电视", "huazhu.airc.v2": "空调控制器", "huazhu.curtain.v2": "窗帘电机", "huazhu.switch.1keyv2": "单键开关", "huazhu.tv.ir1": "电视控制器", + "huca.switch.dh1": "H+ single fire switch", + "huca.switch.dh2": "H+ single ignition double key switch", + "huca.switch.dh3": "H+ Single fire button switch", "huida.toilet.et65q": "HDET65-Q Pro", "huihe.switch.plug": "smart pulg", "huohe.lock.m1": "M1Lock", @@ -1059,11 +1511,22 @@ "hxrcj.oven.e5": "蒸箱", "hyd.airer.h51a": "Hooeasy smart clothes dryer-Simplicity2", "hyd.airer.h51c": "Hooeasy smart clothes dryer-UV2", + "hyd.airer.hx1": "Hooeasy smart clothes dryer", + "hyd.airer.hx2": "Hooeasy smart clothes dryer", + "hyd.airer.hx3": "Hooeasy smart clothes dryer", + "hyd.airer.lyjpro": "Mijia Smart Clothes Heat Drying Rack (Ceiling Mounted)", + "hyd.airer.m36a": "Smart Clothes Dryer-Dual Models", "hyd.airer.znlyj1": "MI Home smart clothes dryer", "hyd.airer.znlyj2": "Mijia Smart Clothes Drying Rack", "hyd.airer.znlyj3": "Mijia Smart Clothes Drying Rack", "hyd.airer.znlyj4": "Mijia Smart Clothes Drying Rack", + "hyd.airer.znlyj5": "Mijia Smart Clothes Drying Rack", + "hyyx.massage.s518": "XIWEI Multifunctional Microcomputer Control Massgae Chair JY-740", + "hyyx.massage.xwboo1": "XIWEI Intelligent massage chair", "hyzm.light.wymy1": "Lifan Nordic Intelligent Lamp", + "hyzngc.switch.hy0601": "One-key switch", + "hyzngc.switch.hy0602": "Two-key switch", + "hyzngc.switch.hy0603": "Three-key switch", "hzft.hood.em52i": "油烟机EM52.i", "hzft.waterheater.d1301i": "热水器D1301i", "idelan.aircondition.g1": "Jinxing Smart Air Conditioner", @@ -1080,8 +1543,16 @@ "ihome.sensor_ht.esth": "温湿度传感器", "ihome.switch.switch": "开关", "ijai.vacuum.v1": "Mi Robot Vacuum-Mop Pro", + "ijai.vacuum.v10": "Mi Robot Vacuum-Mop 2 Lite", + "ijai.vacuum.v13": "Mijia Robot Vacuum-Mop 2Pro", + "ijai.vacuum.v14": "Mijia Self-Cleaning Robot Vacuum-Mop", + "ijai.vacuum.v18": "Mijia Robot Vacuum-Mop 3C", + "ijai.vacuum.v19": "Xiaomi Robot Vacuum-Mop 2S", "ijai.vacuum.v2": "Mi Robot Vacuum-Mop 2", + "ijai.vacuum.v3": "Mi Robot Vacuum-Mop 2 Pro", "ijai.vacuum.v19": "Mi Robot Vacuum-Mop 2S", + "ijomoo.airer.mja6": "JOMOO smart clothes dryer -A6090", + "ijomoo.bhf_light.jd071": "JOMOO Smart Bath Heater JD071", "ijomoo.toilet.i90": "JOMOO Smart Toilet I90", "ijomoo.toilet.zs320": "Smart Toilet Lid-ZS320T", "ikea.light.led1536g5": "IKEA E14 white spectrum", @@ -1095,6 +1566,7 @@ "ikecin.airfresh.26": "新风控制器", "ikecin.airfresh.95": "智能新风控制", "ikecin.airrtc.103": "空调温控器", + "ikecin.airrtc.107": "智能温控器", "ikecin.airrtc.25": "智能温控器", "ikecin.airrtc.29": "智能温控器", "ikecin.airrtc.76": "智能温控器", @@ -1119,12 +1591,21 @@ "ileja.hud.ccm": "Carrobot C2-Mini", "ileja.hud.ccms": "Carrobot C2-Mini Pro", "ilife.vacuum.x900": "ILIFE Robot", + "ilvsd.heater.dl": "ilvsd.heater", + "ilvsd.humidifier.jl": "Leavston humidifier", "imibar.cooker.mbihr3": "Cooking robot", "imou99.camera.tp2": "IMOU Smart Camera 360°", "innolinks.plug.ap3200": "Airconditon Smart Plug", "inovel.projector.me2": "inovel projector", "inshow.watch.w1": "Mi Quartz Watch", "insistek.tracker.wa620": "米哇定位豆", + "insona.curtain.cur": "窗帘", + "insona.light.ctl": "双色温灯", + "insona.light.hsl": "RGB灯", + "insona.light.level": "调光灯", + "insona.light.onoff": "开关灯", + "insona.light.rgb": "RGBCW灯", + "insona.switch.scene": "场景开关", "intre.light.sf1": "儿童伴读智慧台灯", "ipuray.switch.e108": "e108", "ipuray.switch.e112": "e112", @@ -1135,26 +1616,40 @@ "isa.camera.hl5": "XiaoYuan Smart Camera", "isa.camera.hlc6": "Mi Home Security Camera 1080p (Magnetic Mount)", "isa.camera.hlc7": "Mi Camera 2K (Magnetic Mount)", + "isa.camera.hlv3c": "Hualai small square outdoor camera 2K version", "isa.camera.isc5": "Xiao Fang SMART CAMERA", "isa.camera.isc5c1": "Xiao Fang SMART CAMERA", + "isa.camera.panjd1": "Xiao Fang Smart Camera Pan", "isa.camera.qf3": "Hualai Xiaofang smart camera (dual camera version)", "isa.camera.virtual": "小方智能摄像机组", "isa.cateye.hldb6": "XiaoFang Smart Video Doorbell 2 Pro", + "isa.lock.hlsl01": "Hualai Padlock", "isa.magnet.dw2hl": "Mi Door and Window Sensor 2", "isa.router.mr01hl": "HuaLai Xiao Fang Mesh router gateway", "isa.switch.kg01hl": "Mi Smart Single One Way Wall Switch with Display", "isa.switch.kg02hl": "Mi Smart Dual One Way Wall Switch with Display", "isa.switch.kg03hl": "Mi Smart Triple One Way Wall Switch with Display", "isl.light.wy0a01": "Aslan Smart Lamp", + "isl.light.wy0a02": "Aislan Smart Mesh Downlight", "isleep.blanket.hs2001": "LETSLEEP Water Heating Mattress HS2001", "isleep.blanket.hs2201": "LETSLEEP water heated blanket‘HS2201’", + "isleep.blanket.hs2205": "LETSLEEP water heated blanket‘HS2205’", "iwarm.aircondition.gt": "小沃精灵", "iwarm.waterheater.l1p24": "小沃壁挂炉", "janshi.magic_touch.g2": "G2 Spinal comfort neck massager", + "janshi.magic_touch.g20": "G20 Jeeback Neck massager", + "jare.massage.jre6": "Jiaren Massage Chair JR-E6", + "jare.massage.jrq81u": "Kangjia Massage Chair KZ-RH8800", + "jare.massage.rh6687": "Konka Massage Chair KZ-RH6687", + "jieman.magic_touch.fbv1": "GAX Bianstone waist and abdomen massager", + "jieman.magic_touch.g771": "Yubaina Multifunctional Neck Massager", + "jieman.magic_touch.gax1": "GAX Smart Cervical Massage Apparatus", + "jieman.magic_touch.gax2": "GAX Multifunctional Neck Massager", "jieman.magic_touch.js01": "Jishu Intelligent cervical spine massage instrument", "jieman.magic_touch.js03": "Jishu Smart Massager V1", "jieman.magic_touch.js78": "Jishu Intelligent Cervical Massage", "jieman.magic_touch.kdl0": "Kangduoli Smart Massager", + "jieman.magic_touch.kdl2": "Condoli Multifunctional Neck Massager", "jieman.magic_touch.ms9": "Intelligent cervical massage instrument", "jieman.magic_touch.newf": "New for Smart Massager", "jieman.magic_touch.tfs2": "Tefeishi Smart Massager", @@ -1177,11 +1672,15 @@ "jiqid.mistory.zjj": "Kids Smart Storyteller", "jiqid.mistudy.v2": "Mi Card Learning Robot", "jiqid.robot.cube": "Mi Smart Magic Cube", + "jiqid.sxb.v1": "Xiaomi LCD Writing Tablet 13.5\u0027\u0027 (Bluetooth Edition)", "jisi.heater.is30w": "硅晶烯电暖器", "jiwu.lock.jwp01": "Berson Digital Door Lock JW-P ", "jksx.light.mzx": "MZXlight", "jksx.light.mzx01": "MZXlight", + "jldj.airer.80036h": "H smart clothes drying rack", + "jmkj.light.wy0a01": "Jimi Smart Scenario Light", "jmls.light.ls001": "colourful ceiling lamp", + "jns.airer.1": "Intelligent clothes hanger", "jrnet.plug.out2": "金软网智能插座2", "jrnet.sensor_ht.senws1": "温湿度传感器", "jrnet.switch.sw1": "金软网智能开关盒1", @@ -1189,6 +1688,9 @@ "jsc.light.wydfn1": "Danfino Intelligent Roof Suction Lamp", "jsc.light.wyls02": "Scene Ble mesh light", "jsc.light.wyyt01": "Yantai Intelligent Lamp", + "jstar.curtain.jcd25": "JC Smart Rolling Shutters", + "jstar.curtain.jcv24": "JC Smart Blinds", + "jstar.curtain.stew": "JC Smart Curtain Motor", "jtxiot.curtain.jtxwin": "窗帘电机", "jtxiot.switch.switch": "开关", "juhl.aircondition.a11": "aircondition", @@ -1219,6 +1721,8 @@ "jya.light.sla1": "Jya Smart Cordless LED Desk Lamp ", "jyaiot.cm.ccj01": "Solista Automatic Cooker", "jyaiot.cm.jycj01": "Cooking Machine SD-CJ01", + "jymc.light.jmlmp2": "Smart Lamp CW", + "jyst.light.wy0a01": "JinYun intelligent scene lamp", "jyx.aircondition.016301": "空调", "jyx.airm.030901": "PM传感器", "jyx.curtain.020201": "窗帘", @@ -1231,6 +1735,8 @@ "jyx.tv.016301": "电视", "k0918.toothbrush.kid01": "DR.BEI Kids Sonic Electric Toothbrush", "k0918.toothbrush.t700": "mijia E-toothbrush t700", + "k0918.toothbrush.t700i": "Xiaomi Electric Toothbrush T700", + "kaadas.airer.km01": "kaadas basal hanger", "kadeer.fan.30ar": "Zhifan Fs-30arl electric fan", "kadeer.fan.flp01": "Philips Water Tower Fan", "kadeer.fan.l15br": "Zhifan FLS-L15BRL leafless cooling fan", @@ -1241,6 +1747,7 @@ "kadeer.heater.15b6rw": "JO FOND Wall mounted electric oiling", "kadeer.heater.20yrw": "JO FOND Indoor heater (wall mounted heater)", "kadeer.heater.b73brg": "JO FOND Indoor heater (convection electric heater)", + "kadeer.heater.l200mr": "JO FOND heater", "kadeer.heater.l220ir": "JO FOND Indoor heater (skirting heater)", "keeson.bed.ks001": "麒盛科技智能床", "kejia.airer.jjs": "GARJOSS Smart Airer", @@ -1260,7 +1767,16 @@ "kiwik.switch.kt2": "智能触摸开关双键", "kiwik.switch.kt3": "智能触摸开关三键", "kiwik.tv.irtv": "电视红外遥控器", + "kkzn.aircondition.ac": "空调", + "kkzn.airfresh.konke": "新风", + "kkzn.curtain.konke": "窗帘电机", + "kkzn.fan.konke": "风扇", + "kkzn.heater.konke": "地暖", + "kkzn.light.light": "调光灯类设备", + "kkzn.plug.konke": "插座", "kkzn.switch.light": "灯控面板", + "kkzn.tv.konke": "电视", + "kkzn.wopener.konke": "开窗器", "knx.aircondition.kac": "VRV Gateway", "knx.airfresh.kts": "Air-fresh Gateway", "knx.curtain.ktexxc": "Smart Curtain Control", @@ -1289,6 +1805,7 @@ "konke2.tv.rmdvd": "红外DVD", "konke2.tv.rmtv": "红外电视", "ksmb.treadmill.k12": "KingSmith K12 Treadmill", + "ksmb.treadmill.m1v1": "Mi Treadmill", "ksmb.treadmill.v1": "KingSmith Treadmill", "ksmb.treadmill.v2": "KingSmith K15 Treadmill", "ksmb.walkingpad.s1": "KingSmith WalkingPad S1", @@ -1318,6 +1835,7 @@ "kuju02.plug.23": "大功率开关", "kuju02.plug.25": "智能开关(软件虚拟)", "kuju02.plug.36": "RS232控制器", + "kunton.plug.skwsg1": "CHLOROP Smart Wall Socket", "kxf321.mop.mo001": "sawadika robot", "kxf321.mop.mo002": "ZDG300s", "lambot.vacuum.lambot": "Lambot Vacuum Cleaner", @@ -1325,6 +1843,7 @@ "landib.light.kd85": "灯", "lbest.airer.lm01": "Clothes Drying Rack", "lcrmcr.safe.20mini": "Camerick Mini under screen fingerprint safe deposit box", + "lcrmcr.safe.25z": "CRMCR Anno Fingerprint Safe Deposit Box 25Z", "lcrmcr.safe.30mk": "CRMCR Miike fingerprint safe deposit box", "lcrmcr.safe.an35sidz": "CRMCR iRon electronic safe", "lcrmcr.safe.an35sizw": "CRMCR iRon fingerprint safe", @@ -1333,6 +1852,8 @@ "lcrmcr.safe.ms30mp": "CRMCR Annuo Smart Safe PRO", "lcrmcr.safe.ms55kn": "CRMCR Kanuo Smart Safe", "lcrmcr.safe.ms80b": "Carberry Face Recognition Safe", + "lcrmcr.safe.sd003": "CRMCR Water-drop smart safe deposit box", + "lcrmcr.safe.x142": "CRMCR smart retro safe deposit box", "ldsn.curtain.awcc10": "智能窗帘电机", "ldsn.curtain.awcc11": "智能窗帘电机", "ldsn.light.2bpd18": "抱朴(DIM PP-有线版)", @@ -1346,6 +1867,9 @@ "ldsn.light.2ygd05": "摇光(DIM大版-遥控版)", "ldsn.light.3ygd05": "摇光(DIM大版-遥控版)", "ldsn.light.bd01": "壁灯 固定底座版", + "ldsn.light.c4yd1": "C4 大尺寸", + "ldsn.light.c4yd2": "C4 中尺寸", + "ldsn.light.c4yd3": "C4 小尺寸", "ldsn.light.dd01": "智能灯带(单色)", "ldsn.light.dd02": "智能灯带(双色)", "ldsn.light.dd03": "智能灯带(彩色)", @@ -1354,23 +1878,34 @@ "ldsn.light.fc01": "方寸-壁灯中版-高度825mm", "ldsn.light.fy01": "扶摇-立灯-深咖版", "ldsn.light.hg01": "吸顶灯C2(合光 吸顶灯)", + "ldsn.light.hg2": "C2 小版灯", "ldsn.light.hsxd01": "怀素吸顶灯", "ldsn.light.hsxd02": "怀素吸顶灯", "ldsn.light.hy01": "合意-壁灯-触控版", "ldsn.light.ml01": "米来-单头-PP-遥控版", + "ldsn.light.ml2": "米来-单头-陶瓷-遥控版", "ldsn.light.ng01": "凝光-吊灯", + "ldsn.light.ng2": "凝光12头", + "ldsn.light.ng3": "凝光16头", "ldsn.light.qp01": "智能灯泡(双色)", "ldsn.light.qp02": "智能灯泡(彩色)", "ldsn.light.qp12": "智能灯泡(彩色)", "ldsn.light.ry01": "如翼-吊灯-触控版", "ldsn.light.tc01": "天长-吊灯-长度1335mm", + "ldsn.light.ty1": "天元吊灯", + "ldsn.light.xd1": "超薄吸顶灯", + "ldsn.light.xd2": "超薄吸顶灯", "ldsn.light.ygxd01": "摇光吸顶灯", "ldsn.light.ygxd11": "摇光吸顶灯", + "ldsn.light.ys1": "R1灯", + "ldsn.light.ys12": "R2灯", "ldsn.light.yz01": "映烛-吊灯", "ldws.light.04001d": "LED台灯", "ldws.light.04001e": "卧室吸顶灯", "ledv.light.wy0a01": "LEDVANCE Light 01", "ledv.light.wy0a02": "LEDVANCE Light 02", + "ledvan.light.cw0001": "Smart Ceiling", + "ledvan.light.cw0002": "smart ceiling2", "leishi.airer.lyj001": "NVC Smart Airer PRO", "leishi.airer.lyj002": "NVC Smart Airer", "leishi.bhf_light.ls109": "NVC Smart Bath-Heater", @@ -1382,8 +1917,12 @@ "leishi.light.eps116": "NVC Pendant lamp", "leishi.light.eps117": "NVC Spotlight", "leishi.light.eps118": "NVC LED Smart Ceiling Lamp", + "leishi.light.eps119": "NVC Projection LED Ceiling Lamp", + "leishi.light.eps120": "NVC Smart Auxiliary Lighting", + "leishi.light.eps121": "NVC Fresh Fan Light", "leishi.light.esp114": "NVC DieYing LED ceiling lamp", "leishi.light.fan01": "NVC Fan Light", + "leishi.light.lstd01": "NVC LED table lamp", "leishi.light.nest": "NVC LED Nest Ceiling Lamp", "leishi.light.rgba01": "NVC Intelligent Bulb", "leishi.light.rgba02": "NVC Smart color light strip", @@ -1396,14 +1935,17 @@ "leishi.light.wy0a06": "NVC Intelligent Lighting", "leishi.light.wy0a07": "NVC Smart Strip", "leishi.light.wy0a08": "NVC smart ceiling lamp", + "leishi.light.wy0a09": "NVC Smart Light Source Module", "leishi.light.wy0b01": "NVC Smart Desk lamp", "leishi.light.wy0c01": "NVC Smart Ceiling Lamp(ZhiRui)", "leishi.light.wy0c02": "NVC Smart Ceiling Lamp(ZhiZhen)", "leishi.light.wyfa02": "NVC Smart fan lamp(ZhiYi)", + "leishi.light.wyfa03": "NVC Smart Fan Light", "leishi.light.wyfao1": "NVC Smart fan lamp(ZhiYa)", "leishi.light.yying": "NVC YueYing LED ceiling lamp", "lejia.light.33301": "Scene Ble mesh light A", "lejia.light.wy02": "Scene mesh color temperature lamp C", + "lemesh.curtain.cura01": "Scene mesh curtain motor", "lemesh.light.w00a01": "Scene Mesh monochrome light", "lemesh.light.w00d01": "Scene mesh lamp K", "lemesh.light.wy": "Scene Color Light WIFI X", @@ -1413,14 +1955,19 @@ "lemesh.light.wy0c05": "Scenario mesh color temperature lamp E series", "lemesh.light.wy0c07": "Scene mesh color temperature lamp TM series", "lemesh.light.wy0c08": "Scene mesh color temperature lamp", + "lemesh.light.wy0c09": "Scenario mesh color temperature lamp V2 series", "lemesh.switch.sw0a01": "Scene mesh breaker", "lemesh.switch.sw0a02": "Scene mesh breaker TM", "lemesh.switch.sw1a02": "One-click Smart Switch Mesh version", "lemesh.switch.sw2a02": "Two-button Smart Switch(Mesh)", "lemesh.switch.sw3a02": "Three-button Smart Switch(Mesh)", + "lemesh.switch.sw3b01": "Three-button smart single fire switch mesh version", + "lemesh.switch.sw4a02": "Four-button smart switch mesh version", + "lesdn.light.wy0a01": "Leishitun intelligent lights", "leshi.curtain.v0001": "Scene Curtain WIFI X", "leshi.light.wy0b01": "Scenario WIFI dual color light", "leshi.light.wy0b03": "Scene WIFI color temperature light", + "leshi.light.wyfa01": "Scenario Smart Fan Light Universal Model", "leshi.light.wyfan": "Scene intelligent fan lamp WiFi", "leshi.switch.sw1b01": "One Key Touch Switch X1(WiFi)", "leshi.switch.sw2b01": "Two Key Touch Switch X1(WiFi)", @@ -1435,18 +1982,22 @@ "leshow.fan.ss4": "leshow fan", "leshow.heater.bs1": "Mi Smart Baseboard Heater", "leshow.heater.bs1s": "Mi Smart Baseboard Heater 1S", + "leshow.heater.bs3": "Xiaomi Graphene Baseboard Heater", "leshow.heater.nfj1lx": "Mijia vertical heater", "leshow.humidifier.is2": "leshow humidifier is2", "leshow.humidifier.jsq1": "Mi Smart Evaporative Humidifer Pro", + "let.light.light": "Lettin智能灯具", "lettin.light.bulb2": "Lettin Chroma Bulb", "lettin.light.chromabr": "Lettin Chroma BR30", "lettin.light.downlight": "Lettin Essential Downlight", "lettin.light.essenbulb": "Lettin Essential Bulb", "lettin.light.lightstrip": "Lettin Chroma Lightstrip", "lfsmt.aircondition.s1": "AC Remote Controller", + "lfsmt.curtain.ls133": "智能窗帘电机", "lfsmt.light.ls024": "Smart Plug", "lfsmt.light.ls034sl": "SPOT", "lfsmt.light.ls065": "Smart Light Strip", + "lfsmt.light.ls220": "调光调色灯", "lfsmt.plug.ls002": "Smart Plug", "lfsmt.plug.ls060": "Smart Plug (Wi-Fi)", "lfsmt.sensor_ht.ls063wh": "CUBE Environmental Sensor", @@ -1455,10 +2006,19 @@ "lfsmt.switch.ls056": "Smart Light Switch (2-way)", "lfsmt.switch.ls057": "Smart Light Switch (3-way)", "lfsmt.tv.spot001": "TV Remote Controller", - "line.curtain.8m1": "Line Curtain 8m1", - "line.curtain.dm1": "Line Curtain DM1", + "line.curtain.8m1": "Line curtain 8m1", + "line.curtain.azm01": "Line curtain azm01", + "line.curtain.dm1": "Line curtain dm1", + "line.switch.kgk1": "LineHope Smart Switch K1(Single Rocker)", + "line.switch.kgk12": "LineHope Smart Switch K1(Double Rocker)", + "line.switch.kgk13": "LineHope Smart Switch K1(Triple Rocker)", + "lingli.curtain.v1": "智能窗帘", + "lingli.switch.v1": "智能开关", "linked.aircondition.st1": "QJ_WK", + "linked.aircondition.st2": "青春版温控器zwave", "linked.aircondition.st4": "青春版温控器(蓝牙版)", + "linked.aircondition.st5": "单地暖温控器", + "linked.aircondition.st7": "芬尼直流风盘温控器(蓝牙版)", "linked.aircondition.st8": "青春版温控器", "linked.airfresh.air8": "linked-air8", "linked.airpurifier.800fac": "新风控制器青春版", @@ -1475,19 +2035,32 @@ "linp.doorbell.g03": "Self-Powered Wirelss Doorbell", "linp.doorbell.g04": "Self-powered WiFi doorbell 2", "linp.gateway.n2": "Linptech RF Gateway", + "linp.light.lmcw01": "Linptech Smart Magnet Lamp", + "linp.magnet.m1": "Linptech Door and Window Sensor", + "linp.motion.h1": "Linptech Human Sensor", "linp.remote.k9b": "Linptech Bluetooth Wireless Switch (3 Keys)", + "linp.remote.k9b01": "Linptech wireless switch K11", "linp.remote.k9b1": "Linptech Bluetooth Wireless Switch (1 Keys)", "linp.remote.k9b11": "Linptech Bluetooth Wireless Switch (2 Keys)", + "linp.remote.kh1bb1": "Linptech Wireless Switch KH1(1 key)", + "linp.remote.kh1bb2": "Linptech Wireless Switch KH1(2 keys)", + "linp.remote.kh1bb3": "Linptech Wireless Switch KH1(3 keys)", + "linp.remote.kh1bb4": "Linptech Wireless Switch KH1(4 keys)", "linp.switch.q31": "Linptech Smart Wall Switch(1 Key)", + "linp.switch.q31s": "Linptech Q3 Smart Wall Switch(1 Key)", "linp.switch.q32": "Linptech Smart Wall Switch(2 Keys)", + "linp.switch.q32s": "Linptech Q3 Smart Wall Switch(2 Key)", "linp.switch.q33": "Linptech Smart Wall Switch(3 Keys)", + "linp.switch.q33s": "Linptech Q3 Smart Wall Switch(3 Keys)", "linp.switch.q3s1": "Linptech Mesh Wall Switch (1 Key)", "linp.switch.q3s2": "Linptech Mesh Wall Switch(2 Keys)", "linp.switch.q3s3": "Linptech Mesh Wall Switch (3 Keys)", - "linp.switch.q4s1": "Linptech Q4 Mesh Wall Switch(1 Keys)", - "linp.switch.q4s2": "Linptech Q4 Mesh Wall Switch(2 Keys)", - "linp.switch.q4s3": "Linptech Q4 Mesh Wall Switch (3 Keys)", + "linp.switch.q4s1": "Linptech Mesh Wall Switch (1 Keys)", + "linp.switch.q4s2": "Linptech Mesh Wall Switch(2 Keys)", + "linp.switch.q4s3": "Linptech Mesh Wall Switch (3 Keys)", + "linp.switch.s1": "Linptech full screen switch", "linqi.projector.td01lq": "Mi Cinema Headset", + "lmdq.airp.dtha02": "LOMEDIQI DTH-A02 Air disinfector", "lmds.light.wy0a01": "Lemeng lighting living room ceiling lamp", "lmds.light.wy0a02": "Lemeng lighting bedroom ceiling lamp", "lndq.light.wy0a01": "LUNO Lvneng Mijia intelligent lamp", @@ -1497,16 +2070,25 @@ "lonink.switch.ln084": "LONINK One key switch (WiFi)", "lonink.switch.ln085": "LONINK Two key switch (WiFi)", "lonink.switch.ln086": "LONINK Three key switch(WiFi)", + "lonsam.curtain.001": "LS Smart Curtain Motor", "lonsam.curtain.lscl": "LS intelligent curtain motor", "loock.cateye.v01": "Loock CatY", "loock.cateye.v02": "Mi Smart Video Doorbell with Monitor", + "loock.cateye.v06": "Mi Smart Video Doorbell with Monitor 1S", + "loock.cateye.v07": "Lockin Smart Video Doorbell with Monitor CatX", "loock.lock.cc2s": "Loock Smart Lock Classic 2", "loock.lock.cc2xpro": "Loock Smart Lock Classic 2X Pro", + "loock.lock.fvl109": "Lockin Smart Door Lock S50M", "loock.lock.ojjz1": "OJJ Z1", + "loock.lock.p50": "Lockin Finger Vein Smart Lock P50", + "loock.lock.pfvl10": "Lockin smart lock R1M", "loock.lock.s30": "Loock Smart Lock S30 Pro", "loock.lock.s30v2": "Loock Smart Lock S30", + "loock.lock.s50f": "Lockin FaceID Automatic Smart Door Lock S50F", "loock.lock.t1": "Mi Automatic Smart Door Lock", "loock.lock.t1pro": "Mi Automatic Smart Door Lock Pro", + "loock.lock.t1v2": "Xiaomi Automatic Smart Door Lock", + "loock.lock.t2v1": "Xiaomi Smart Door Lock M20", "loock.lock.v1": "Loock Classic", "loock.lock.v14": "Loock Spider-Man Series Limited Edition", "loock.lock.v15": "Lockin Smart Lock X1", @@ -1518,6 +2100,7 @@ "loock.lock.v7": "LOOCK Classic 2X", "loock.lock.v8": "LOOCK Classic 2S", "loock.lock.v9": "Loock Smart", + "loock.lock.xfvl10": "Lockin Finger Vein Automatic Smart Door Lock S50", "loock.safe.v1": "Mi Smart Safe Box", "lsds.light.wy0a01": "Shengxin Intelligent lamp", "ltcn.controller.prov2": "场景控制面板", @@ -1533,10 +2116,14 @@ "lumi.aircondition.aq1": "空调温控器", "lumi.aircondition.cn05": "空调伴侣P3", "lumi.airer.02": "晾衣机", + "lumi.airer.acn000": "智能晾衣机H1", "lumi.airer.acn01": "Aqara Smart Clothes Drying Rack", "lumi.airer.acn02": "Aqara Smart Clothes Drying Rack Lite", + "lumi.airrtc.model": "智能温控器S3", + "lumi.airrtc.scene": "温控器S3-scenepanel", "lumi.airrtc.tcpecn01": "Thermostat", "lumi.airrtc.tcpecn02": "Thermostat S2", + "lumi.airrtc.ther": "温控器S3", "lumi.airrtc.vrfegl01": "VRF Air Conditioning Controller", "lumi.camera.aq1": "Camera Hub", "lumi.camera.gwagl01": "Camera Hub G2", @@ -1548,7 +2135,9 @@ "lumi.ctrl_ln2.v1": "Aqara Wall Switch (With Neutral, Double Rocker)", "lumi.ctrl_neutral1.v1": "Aqara Wall Switch(No Neutral, Single Rocker)", "lumi.ctrl_neutral2.v1": "Aqara Wall Switch (No Neutral, Double Rocker)", + "lumi.curtain.acn": "智能窗帘伴侣E1", "lumi.curtain.acn002": "Aqara Blind Engine E1", + "lumi.curtain.acn006": "Mi Smart Battery Powered Curtain", "lumi.curtain.aq2": "Aqara Roller Shade Controller", "lumi.curtain.fix": "智能窗帘电机", "lumi.curtain.fix2": "智能窗帘电机(百分比控制)", @@ -1559,6 +2148,8 @@ "lumi.curtain.hagl05": "Xiaomiyoupin Curtain Controller (Wi-Fi)", "lumi.curtain.hagl08": "Aqara Curtain Controller A1", "lumi.curtain.hmcn01": "Mi Smart Motorized Curtain", + "lumi.curtain.hmcn02": "Xiaomi Smart Curtain Motor", + "lumi.curtain.hmcn04": "Xiaomi Smart Curtain Motor Group", "lumi.curtain.hocn01": "好博推窗器", "lumi.curtain.l07": "智能窗帘电机C2", "lumi.curtain.naq2": "智能窗帘", @@ -1566,8 +2157,10 @@ "lumi.curtain.nv1": "智能窗帘", "lumi.curtain.other": "智能窗帘电机", "lumi.curtain.v1": "Aqara Curtain Controller", + "lumi.dry.acn001": "毛巾架", "lumi.flood.acn001": "Aqara Water Leak Sensor E1", "lumi.flood.bmcn01": "Mi Flood Detector", + "lumi.gateway.acn004": "Aqara Hub M1S 2022", "lumi.gateway.acn01": "Aqara Hub M1S", "lumi.gateway.aeu01": "Aqara Hub M1S", "lumi.gateway.aqcn02": "Aqara Hub E1", @@ -1583,6 +2176,12 @@ "lumi.gateway.v1": "Mi Control Hub", "lumi.gateway.v2": "Mi Control Hub", "lumi.gateway.v3": "Mi Control Hub", + "lumi.light.acn003": "Aqara Ceiling Light L1-350", + "lumi.light.acn014": "Aqara LED Light Bulb T1(Turnable White)", + "lumi.light.acn123": "射灯 T2(15度)", + "lumi.light.acn125": "射灯 T2(36度)", + "lumi.light.acn126": "筒灯 T2", + "lumi.light.acn127": "射灯 T2(24度)", "lumi.light.aeu01": "Aqara网关M1S海外版", "lumi.light.aqcn02": "Aqara LED Light Bulb (Tunable White)", "lumi.light.bacn1": "Aqara智能恒流驱动器T1-1", @@ -1612,17 +2211,21 @@ "lumi.lock.bmcn02": "Mi Smart Door Lock", "lumi.lock.bmcn03": "Mi Smart Door Lock E", "lumi.lock.bmcn04": "Mi Smart Door Lock 1S", + "lumi.lock.bmcn05": "Mi Smart Door Lock with Face Unlock X", "lumi.lock.bzacn1": "Aqara smart door lock N200", "lumi.lock.bzacn2": "Aqara smart door lock N100", + "lumi.lock.eicn02": "Smart Door Lock J1", "lumi.lock.mcn01": "Mi Smart Door Lock", "lumi.lock.v1": "Door lock", "lumi.lock.wbmcn1": "Mi Smart Door Lock Pro", "lumi.magnet.acn001": "Aqara Door and Window Sensor E1", + "lumi.motion.acn001": "Aqara Motion Sensor E1", "lumi.motion.bmgl01": "Mi Motion Sensor 2", "lumi.plug.acn01": "智能插座T1(国标)", "lumi.plug.acn02": "智能墙壁插座T1", "lumi.plug.aq1": "墙壁插座(Zigbee版)", "lumi.plug.aus01": "智能插座(Zigbee版)", + "lumi.plug.cn03": "智能墙壁插座H1", "lumi.plug.es1": "墙壁插座(Zigbee版)", "lumi.plug.gaq1": "智能插座(Zigbee版)", "lumi.plug.ges1": "智能插座(Zigbee版)", @@ -1638,6 +2241,7 @@ "lumi.relay.c2acn01": "Aqara Wireless Relay Controller(2 Channels)", "lumi.remote.acn003": "Aqara Wireless Remote Switch E1 (Single Rocker)", "lumi.remote.acn004": "Aqara Wireless Remote Switch E1 (Double Rocker)", + "lumi.remote.acn007": "Aqara Wireless Mini Switch E1", "lumi.remote.b186acn01": "Aqara Wireless Remote Switch (Single Rocker)", "lumi.remote.b186acn02": "Aqara Wireless Remote Switch D1 (Single Rocker)", "lumi.remote.b1acn01": "Aqara Wireless Mini Switch", @@ -1653,6 +2257,7 @@ "lumi.sensor_cube.aqgl01": "Aqara Cube", "lumi.sensor_cube.v1": "Mi Cube", "lumi.sensor_dlock.v1": "门锁传感器", + "lumi.sensor_gas.acn02": "Aqara Natural Gas Detector", "lumi.sensor_gas.mcn02": "Mi Natural Gas Detector", "lumi.sensor_ht.erv1": "温湿度传感器", "lumi.sensor_ht.gl02": "温湿度传感器", @@ -1667,6 +2272,7 @@ "lumi.sensor_motion.v2": "Mi Motion Sensor", "lumi.sensor_natgas.v1": "Mi Smart Natural Gas Detector", "lumi.sensor_smoke.acn01": "Aqara Smart Smoke Detector(NB-IoT)", + "lumi.sensor_smoke.acn03": "Aqara Smoke Detector", "lumi.sensor_smoke.mcn02": "Mi Smoke Detector", "lumi.sensor_smoke.v1": "Mi Smart Smoke Detector", "lumi.sensor_switch.aq2": "Aqara Wireless Mini Switch", @@ -1692,6 +2298,9 @@ "lumi.switch.4acn3": "智能场景面板开关S1 双开版", "lumi.switch.4acn4": "智能场景面板开关S1 三开版", "lumi.switch.acn01": "单路控制模块", + "lumi.switch.acn016": "Xiaomi Smart Wall Switch (1 Gang, Neutral Wire)", + "lumi.switch.acn017": "Xiaomi Smart Wall Switch (2 Gang, Neutral Wire)", + "lumi.switch.acn018": "Xiaomi Smart Wall Switch (3 Gang, Neutral Wire)", "lumi.switch.acn02": "智能墙壁开关D1(零火线单健版)", "lumi.switch.acn1": "智能墙壁开关H1(零火线三键版)", "lumi.switch.acn3": "智能墙壁开关D1(零火线三键版)", @@ -1748,6 +2357,7 @@ "lumi.vibration.aq1": "Aqara Vibration Sensor", "lumi.weather.v1": "Aqara Temperature and Humidity Sensor", "lumi.wopener.hocn02": "好博开窗器", + "lvl.light.wy0a01": "LVL Air Surface ceiling light", "lwkj.bed.5110": "智能床", "lwkj.curtain.2a10": "智能窗帘", "lwkj.light.2010": "彩色灯", @@ -1769,18 +2379,26 @@ "lxk.switch.sjkg": "双键开关", "lxk.waterpuri.jsq": "净水器", "madv.alarm.winlock1": "Dling window security alarm smart model.", + "madv.cateye.dlc5jz": "Dling Smart Video Doorbell C5", "madv.cateye.dle3jz": "Dling Smart Video Doorbell E3", + "madv.cateye.dle4jz": "Dling Video Doorbell E6", "madv.cateye.dlowl": "dling Smart Video Doorbell", "madv.cateye.dlowle": "Dling Smart Video Doorbell E3", "madv.cateye.dlowlplus": "Dling Smart Video Doorbell Plus", "madv.cateye.dlowlse": "Dling Smart Video Doorbell C3", "madv.cateye.dlowlse2": "Dling Smart Video Doorbell C5", "madv.cateye.mi2gt": "Mi Smart Video Doorbell 2", + "madv.cateye.mic2jz": "Mi Smart VIdeo Doorbell 2", "madv.cateye.miowl": "Mi Smart Video Doorbell", + "madv.cateye.miowl3": "Xiaomi Smart Doorbell 3", "madv.cateye.miowlv2": "Mi Smart Video Doorbell 2", "madv.cateye.miowlv2l": "Mi Smart Video Doorbell 2 Lite", "madv.cateye.miv2jz": "Mi Smart Video Doorbell 2", + "madv.cateye.mowl3g": "Xiaomi Smart Doorbell 3", + "madv.cateye.owlg2": "Madv Smart Video Doorbell 2M", + "magao.light.wy0a01": "MG smart light (Bluetooth Mesh version)", "magene.walkingpad.g83": "顽鹿游戏功率健身车", + "maijia.ajh.mj9001": "MyJoy Intelligent moxibustion instrument", "manka.light.light": "照明灯", "manka.plug.socket": "智能插座", "mate.humidifier.cs01": "MATE air cube", @@ -1792,7 +2410,12 @@ "maxway.light.mx101": "maxway_light", "mbzn.curtain.m20051": "智能窗帘", "mcl.switch.mcid15": "智能开关", + "mcosu.bhf_light.pipam1": "Pipa smart Yuba M1", + "mcosu.switch.pipam1": "PIPA intelligent switch one keys MESH", + "mcosu.switch.pipam2": "PIPA intelligent switch two keys MESH", + "mcosu.switch.pipam3": "PIPA intelligent switch three keys MESH", "mengye.plug.rmtsw": "电脑远程开关机卡", + "mgzn.light.wy0a01": "C09MG smart light", "mhaq.aircondition.miair": "三菱重工海尔·智能空调", "mhaq.airfresh.miwind": "三菱重工海尔·恒温新风机", "mhiot.light.me27w": "WiFi智能灯头", @@ -1800,23 +2423,39 @@ "mhiot.plug.ms86w": "WiFi智能墙壁插座", "mhiot.switch.mc01w": "WiFi智能开关", "miaomiaoce.airm.air01": "ZenMeasure Air Quality Monitor", + "miaomiaoce.airm.air02": "ZenMeasure Air Quality Monitor (HCHO)", "miaomiaoce.blanket.d02": "Smart Low Voltage Blanket Double", "miaomiaoce.blanket.s02": "Smart Low Voltage Blanket Single", "miaomiaoce.clock.ht02": "ZenMeasure Smart Temperature Clock", "miaomiaoce.sensor_ht.h1": "ZenMeasure Bluetooth Hygrometer Thermometer", + "miaomiaoce.sensor_ht.o2": "Xiaomi Temperature and Humidity Monitor Clock", "miaomiaoce.sensor_ht.t1": "Mi Temperature and Humidity Monitor Digital Clock", "miaomiaoce.sensor_ht.t2": "Mi Temperature and Humidity Monitor 2", + "miaomiaoce.sensor_ht.t6": "Xiaomi Digital Temperature and Humidity Monitor", "miaomiaoce.thermo.t01": "Zenmeasure smart thermometer", "miaomiaoce.thermo.t10": "Mi Digital Thermometer", "miaomiaoce.thermo.t11": "ZenMeasure Bluetooth Electronic Thermometer", + "mibe.aircondition.ac": "miBEE空调", + "mibe.airfresh.xf": "miBEE新风", + "mibe.airm.airbox": "miBEE空气盒子", + "mibe.airrtc.heat": "miBEE地暖温控", + "mibe.curtain.cur": "miBEE窗帘", + "mibe.light.dim": "miBEE调光灯", + "mibe.light.rpl4": "miBEE灯光", "mibx2.fridge.496": "Mi Internet Refrigerator 496L", "mibx2.fridge.v1": "Mi Internet Refrigerator 450L", "mibx2.washer.v1": "Mi Internet Direct Drive Washer Dryer 10kg", "mibx2.washer.v2": "Mi Direct Drive Washer Dryer 10kg", "mibx2.washer.v3": "Mi Wave Washer 10kg", + "mibx2.washer.v5": "Mijia Mini washing machine 1kg", "mibx2.washer.v6": "Mi Wave Washer 10kg m2", + "mibx2.washer.v7": "Direct drive | Mijia washing and drying machine Pro10kg", "mibx5.dry.v2": "Mi Smart Heat Pump Dryer 10kg", "mibx5.dry.v3": "Mi Smart Dryer 10kg", + "mibx5.washer.v1": "MI wave washer Premium 10kg", + "mibx5.washer.v4": "MI Direct Drive Washer Premium 10Kg", + "mibx5.washer.v6": "Mijia Direct Drive Washer 10Kg", + "mibx5.washer.v7": "Mijia DD Combination Washer Dryer 10kg", "micoe.airer.hz001h": "Mocie Mies 10 Pro- Drying Style", "micoe.airer.hz001z": "Mocie Mies 10S lighting", "micoe.airer.hz002h": "Rise10 Mix- Ring pole retainer (drying flagship)", @@ -1826,8 +2465,16 @@ "midea.aircondition.v1": "Midea AC-i Youth", "midea.aircondition.xa1": "Midea AC-Cool Golden", "midea.aircondition.xa2": "美的空调 - 酷金", - "midjd6.fridge.v1": " Mi Internet Refrigerator 540L", + "midjd6.fridge.536": "Mijia Side Mounted Refrigerator 536L (Carbon Black)", + "midjd6.fridge.540pro": "Mijia Double Door Refrigerator 540L Pro", + "midjd6.fridge.5501": "Mijia Refrigerator Premium 550L cross four doors", + "midjd6.fridge.610": "Mijia Side Mounted Refrigerator 610L (Carbon Black)", + "midjd6.fridge.v1": "Mijia Internet Refrigerator 540L", "midjd7.fridge.4851": "Mijia Double Door Refrigerator 485L", + "midjd7.fridge.4852": "Mijia Double Door Refrigerator 485L", + "midjd7.fridge.5021": "Mijia Side Mounted Refrigerator 502L (Glass Door)", + "midjd7.fridge.5022": "Mijia Side Mounted Refrigerator 502L (Carbon Black)", + "midjd8.fridge.630": "Mijia Refrigerator Double Door 630L", "midr.bike.x1": "70mai Smart E- A1 Pro", "midr.cardvr.m1": "Mi rear-view mirror recorder", "midr.cardvr.mv2": "Mi Dash Cam 2 2K", @@ -1889,12 +2536,23 @@ "minij.washer.v5": "Mi Smart Combo Washer Dryer Pro 10kg", "minij.washer.v8": "Mi Smart Combo Washer Dryer 10kg (or 1S 10kg Golden/Silver)", "minuo.tracker.lm001": "RANRES smart tracker", + "miot.aircondition.cl801": "1", + "miot.aircondition.v1": "auto aircondition", "miot.clock.mijia": "米家App自动化RN的model", + "miot.lock.qqaly": "Qqara 蓝牙智能门锁", + "miot.lock.x2": "jianeng", + "miot.lock.x3": "佳能蓝牙智能门锁(海外)", + "miot.pettoy.bbb": "test-llj", "miot.plug.v1640": "test update name", "miot.switch.mswl1": "Mesh无线开关测试(单键)", "miot.switch.mswl2": "Mesh无线开关测试(双键)", "miot.switch.mswl3": "Mesh无线开关测试(三键)", "miot.switch.y0715": "2", + "mirsz.flood.wa500": "LOVYA Water leakage sensor", + "mirsz.magnet.mc500": "LOVYA Door sensor", + "mirsz.motion.ir500": "LOVYA PIR motion sensor", + "mirsz.sensor_ht.te110": "LOVYA temperature and humidity sensor (without screen)", + "mirsz.sensor_ht.te500": "LOVYA temperature and humidity sensor", "miyooo.light.x32": "美攸智能", "mjj.curtain.mode2": "窗帘", "mjj.light.mode3": "灯控", @@ -1911,12 +2569,18 @@ "mls.light.mls001": "MLS LED Smart Ceiling Lamp1", "mls.light.mls002": "MLS LED Smart Ceiling Lamp2", "mls.light.mls003": "MLS LED Smart Ceiling Lamp3", + "mly.curtain.cura01": "Magic cloud smart curtain motor", + "mly.light.wy0a01": "Magic Cloud Smart Lamp", + "mly.switch.sw0a01": "Magic cloud intelligent lighting controller", "mmgg.feeder.bowl": "Pawbby smart pet bowl", + "mmgg.feeder.fi1": "Xiaomi Smart Pet Food Feeder", + "mmgg.feeder.inland": "Mijia Smart Pet Feeder", "mmgg.feeder.petfeeder": "Mijia Smart Pet Feeder", "mmgg.feeder.snack": "Pawbby Dog Camera \u0026 Treat Cam", "mmgg.feeder.spec": "XIAOWAN Smart Pet Feeder", "mmgg.pet_waterer.s1": "Mijia Smart Pet Water Dispenser", "mmgg.pet_waterer.s4": "XIAOWAN Smart Pet Water Dispenser", + "mmgg.pet_waterer.wi11": "Xiaomi Smart Pet Fountain", "mnc.light.wy0a01": "Mini Smart Light", "morfun.kettle.mf809": "MORFUN Smart Instant Heating Water Dispenser", "morfun.ysj.mf208": "MORFUN Smart Instant Heating Water Dispenser MF208", @@ -1933,6 +2597,7 @@ "mpkx.switch.030001": "美规开关", "mpkx.switch.030002": "双键墙壁开关", "mpkx.switch.030003": "三键墙壁开关", + "mpmiot.pettoy.v1": "Merrypet Smart Ball for Pets", "mrbond.airer.c1x": "MrBond smart clothes dryer", "mrbond.airer.m0": "MR.BOND", "mrbond.airer.m1pro": "MR.BOND", @@ -1948,24 +2613,34 @@ "mrbond.airer.m31c2": "MrBond smart clothes dryer-UV2", "mrbond.airer.m33a": "MrBond smart clothes dryer-Simple3", "mrbond.airer.m33c": "MrBond smart clothes dryer-UV3", + "mrbond.airer.m50f": "MrBond smart clothes dryer-LA", "mrbond.airer.m53c": "MrBond smart clothes dryer-Air Drying", + "mrbond.airer.m53cpr": "MrBond smart clothes dryer-LDAH", "mrbond.airer.m53pro": "MrBond smart clothes dryer-Drying", + "mrbond.airer.m65": "MrBond smart clothes air dryer Pro", "mrbond.airp.h1pro": "MrBond intelligent deodorizer", "mrbond.curtain.rac03": "MR.BOND Smart Curtain", "msj.f_washer.m1": "Sink cleaning machine", "msj.f_washer.m1pro": "Food cleaning machine", "msj.f_washer.m2": "Smart sink washing machine", + "msj.f_washer.v3": "Integrated sink3.0", "mskub.airer.k6": "Manskubo K model smart clothes dryer", "mxiang.camera.mwc10": "Mi Wireless Outdoor Security Camera 1080p Indoor Receiver", "mxiang.camera.mwc11": "Mi Wireless Outdoor Security Camera 1080p", "mxiang.cateye.mdb10": "Xiaomo Smart Video Doorbell", "mxiang.cateye.xmcatt1": "Xiaomo Smart Peep Hole", + "nancii.toothbrush.t600": "Nancii Smart Electric Toothbrush T600", "nbczwl.airer.airer": "Smart Airer", "nbczwl.airer.kfe01": "KFE", + "nbczwl.airer.krqu40": "Courage Airer", + "nbczwl.airer.wtt001": "WTT Airer", "nbczwl.airer.zbs01": "ZBS", + "nbfdzn.derh.cs01": "forest life dehumidifier", "nbym.curtain.yznv1": "Cloud intelligent electric curtain", "nhy.airrtc.v1": "Golan Denmark Heating", "nhy.rtc.pexrtc730": "丹麦格澜空调", + "nineam.desk.hoo01": "9am Standing Desk", + "nineam.desk.hoo021": "9am Standing Desk Model L", "ninebot.balscooter.v1": "Ninebot", "ninebot.scooter.v1": "Electric Scooter", "ninebot.scooter.v2": "Mi Electric Scooter Pro", @@ -1974,6 +2649,7 @@ "ninebot.scooter.v5": "Mi Electric Scooter Essential", "ninebot.scooter.v6": "Mi Electric Scooter 1S", "ninebot.scooter.v7": "Mi Electric Scooter 3", + "nnleaf.light.glbulp": "Nanoleaf Shapes (dual mode)", "nnleaf.light.panels": "Light Panels", "nnleaf.light.strips": "Nanoleaf Lightstrip", "nnleaf.light.ulp": "Nanoleaf ULP", @@ -1982,6 +2658,7 @@ "novo.airer.g30": "NOVO smart luxury motorized clothes rack", "novo.airer.g31": "NOVO basic smart clothes rack", "novo.curtain.n21": "NOVO Smart Curtain Motor", + "npu.light.npu001": "NPU Smart Ceiling Lamp", "nuwa.robot.minikiwi": "Nuwa Robotics Danny Robot", "nuwa.robot.nb1": "KebbiAir", "nvc.light.c209": "雷士吸顶灯", @@ -2006,10 +2683,26 @@ "nvcls.switch.030001": "美规开关", "nvcls.switch.030002": "双键墙壁开关", "nvcls.switch.030003": "三键墙壁开关", + "nvcsmt.light.wzj201": "pendant light", "nwt.aircondition.26eaw1": "NWT Internet Portable Air Conditioner", "nwt.derh.312en": "NEW WIDETECH Internet Dehumidifier 12L", "nwt.derh.330ef": "NWT Internet Dehumidifier 30L", "nwt.derh.wdh318efw1": "NWT Internet Dehumidifier 18L", + "obbo.aircondition.kt001": "空调", + "obbo.curtain.cl001": "智能窗帘", + "obbo.fan.dfs001": "电风扇", + "obbo.light.hcdd01": "灯带", + "obbo.light.qpd001": "球泡灯", + "obbo.light.qpd002": "球泡灯", + "obbo.light.sbd001": "书本灯", + "obbo.light.sd001": "书本灯", + "obbo.light.sd002": "扇灯", + "obbo.plug.cz002": "RGB插座", + "obbo.plug.cz003": "面板插座", + "obbo.switch.kgr001": "一键开关", + "obbo.switch.kgr002": "二键开关", + "obbo.switch.kgr003": "三键开关", + "obbo.tv.tv001": "电视", "odds.light.wy0a01": "Zhanxiu Crystal Love intelligent lamp", "oeco.light.rgbc": "RGBC Lighting", "oeco.light.rgbcw": "RGBCW Lighting", @@ -2025,10 +2718,20 @@ "ohh.plug.sk01": "Ohh smart socket", "ohh.switch.slc03w": "smart lighting panel", "ola.lock.i3": "Ola Mars", + "ondawn.aircondition.ar": "昂道智能空调", + "ondawn.light.light": "昂道智能灯光", + "ondawn.switch.switch": "昂道智能开关", + "ondawn.tvbox.2": "昂道机顶盒", "onemore.soundbox.sm001": "Mi Music Alarm Clock", "onemore.wifispeaker.sm4": "xiaomiaibox", "opple.bhf_light.acmoto": "Opple Bath Heater", + "opple.bhf_light.ct88": "Opple Bath Heater Mode C", "opple.bhf_light.dcmoto": "Opple Bath Heater", + "opple.bhf_light.fhbath": "Opple Bath Heater BPF12i", + "opple.bhf_light.fx88a": "Opple Bath Heater Mode D", + "opple.bhf_light.yuba": "欧普智能浴霸", + "opple.bhf_light.yuba2": "欧普智能浴霸2", + "opple.fan.fan": "风扇灯", "opple.light.barelp": "Opple Smart Lamp Board Mode A", "opple.light.bydceiling": "Opple BEYOND Ceiling", "opple.light.ceiling": "ceilinglight", @@ -2045,30 +2748,43 @@ "opple.switch.lbs01a": "OPPLE Q4 Mesh Wall Switch(1 Keys)", "opple.switch.lbs02a": "OPPLE Q4 Mesh Wall Switch(2 Keys)", "opple.switch.lbs03a": "OPPLE Q4 Mesh Wall Switch(3 Keys)", + "opple.ven_fan.liba": "欧普智能凉霸", "orion.wifispeaker.cm1": "小豹AI音箱", + "orlant.airer.b": "Orlant Intelligent Clothes Hanger B Series", + "orlant.airer.c": "Orlant Intelligent drying machine C series", + "orrgi.light.wy0a01": "Qiyuan two-color lamp", "orvibo.aircondition.ir1": "Air conditioner", "orvibo.airer.csv1": "欧瑞博晾衣架", "orvibo.curtain.v01": "smart curtains", "orvibo.fan.ir01": "Fan", + "orvibo.light.light": "调光灯", "orvibo.plug.s20c": "S20c Smart Socket", "orvibo.plug.s30": "S30c Smart Socket", "orvibo.stb.irstbv1": "STB", "orvibo.switch.mixpad": "MixPad Mini 超级智能开关", "orvibo.switch.switch": "开关", "orvibo.tv.ltv1": "TV", + "orvibo.vacuum.robot": "欧瑞博扫地机器人", "orz.airer.hanger": "奥创晾衣架", "orz.airer.orzhg2": "奥创晾衣架2", "orz.bhf_light.bh02": "奥创浴霸面板", "orz.bhf_light.bh03": "奥创浴霸面板", "orz.fan.ygs": "月光扇", + "orz.heater.orztr3": "奥创电暖器", "orz.light.clight": "奥创调光灯", "orz.light.light": "奥创灯", + "ouchen.airer.m01": "Smart clothes hangers MOMO", "ougn.light.wy0a01": "Ougenuo Intelligent lamp", + "oujing.derh.137e": "OUJIN Dehumidifier OJ-137E", + "oujing.derh.225e": "Eurgeen Dehumidifier OJ-225E", + "outes.plug.outes1": "智能插座", "ows.heater.pdeh1a": "Focus Heater", "ows.heater.pdeh2a": "OWS Heater 3T", "ows.heater.pdeh3a": "OWS Heater Pro", + "ows.heater.pro2": "OWS Heater Pro2", "ows.heater.yudi20": "OWSYUDI", "ows.tow_w.mjj20a": "OWS NEX Towel rail", + "ows.tow_w.mjjs1": "OWS-S1 towel-rack", "ows.towel_w.mj1x0": "Intelligent electric heating towel rack", "ozner.airpurifier.u1": "ozner air purifier u1", "p2c.aircondition.air": "空调", @@ -2082,16 +2798,30 @@ "pair.light.4": "调光灯", "pair.light.5": "可调双色温灯", "pair.switch.scene": "场景", + "pak.bhf_light.pak11": "Pak bath heater", "pak.light.pak002": "pak ceiling lamp", "pak.light.pak005": "Pak LED Smart Home Ceiling Lamp", "pak.light.pak006": "Pak LED BULB", + "pak.light.pak007": "Pak LED Smart Ceiling Lamp2", "pak.light.pak01": "Pak LED Smart Ceiling Lamp", + "pak.light.pak08": "Pak Home Smart Light", + "pak.light.pak10": "GUANGDONG PAK Smart home chandelier", + "pak.light.pak12": "Pak Smart Spot Light", "panpan.airer.y18": "PANPAN smart clothes dryer-Y18", "park.switch.fp509": "车位来智能地锁", "permay.switch.kgkgk": "开关", "permay.switch.pmswch": "开关5", + "pgkj.aircondition.znkt": "智能空调", + "pgkj.curtain.zncl": "智能窗帘", + "pgkj.light.znzm": "智能灯光", + "pgkj.switch.znkg": "智能开关", + "philips.light.aibed": "Xiaomi Smart Sleep and Wake-up Light", + "philips.light.bceil": "Philips Zhirui Ceiling Lamp Living room 80W", + "philips.light.bceil1": "Philips Zhirui Ceiling Lamp Bedroom 40W", + "philips.light.bceil2": "Philips Zhirui Ceiling Lamp Bedroom 28W", "philips.light.bceiling1": "Philips Zhirui Ceiling Lamp Bedroom 40W", "philips.light.bceiling2": "Philips Zhirui Ceiling Lamp Bedroom 28W", + "philips.light.blue": "Philips BlueSky Lamp", "philips.light.boc1": "Philips ZhiYi Ceiling lamp FL2 80W", "philips.light.boc2": "Philips ZhiYi Ceiling lamp FL2 40W", "philips.light.boc3": "Philips ZhiYi Ceiling lamp FL2 28W", @@ -2130,7 +2860,10 @@ "philips.light.sread2": "Mijia Philips Desk Lamp 2S", "philips.light.sread3": "Mijia Philips Desk Lamp 3", "philips.light.sread4": "ZhiRui read \u0026 write Desk Lamp Wi-Fi ", + "philips.light.sread6": "Philips Zhirui Desk Lamp 2S", + "philips.light.sread8": "Philips Zhixiu Desk Lamp", "philips.light.strip2": "ZhiRui RGB strip", + "philips.light.strip3": "Xiaomi Smart Lightstrip Pro", "philips.light.virtual": "Philips connected lights", "philips.light.xzceil": "Zhirui Ceiling Lamp Gorgeous 80W", "philips.light.xzceim": "Zhirui Ceiling Lamp Gorgeous 40W", @@ -2144,11 +2877,18 @@ "philips.light.zystrip": "Philips ZhiYi strip", "phnix.waterheater.sf": "PHNIX Heat Pump Water Heater", "pinelc.switch.001": "松果WiFi开机卡", + "pingke.aircondition.81": "空调", "pingke.heater.7801": "地暖智能温控", "pingke.switch.7b01": "漏水保护器", "pingke.waterheater.7a01": "苹可智能", + "pma.magic_touch.jb01": "KULAX Neck Support", + "pmfbj.light.lz8321": "Panasonic modern fan light", "pmfbj.light.xsx330": "Panasonic Mingpan Ceiling Light", "pmfbj.light.xsx332": "Panasonic Songtian Ceiling Light", + "pmfbj.light.xsx340": "Panasonic modern ceiling light(living room)", + "pmfbj.light.xsx341": "Panasonic modern ceiling light(bedroom)", + "pmfbj.light.xsx344": "Panasonic Shiyuguang ceiling light(bedroom)", + "pmfbj.light.xsx345": "Panasonic Shiyuguang chandelier", "pmfbj.light.xsx505": "Panasonic Yinglun Ceiling Light", "poer19.airrtc.ptc10": "PTC10", "poer19.airrtc.ptc20": "PTC20", @@ -2157,23 +2897,36 @@ "poer19.heater.ptc20": "PTC20-电暖器", "poer19.heater.ptv30": "PTV30-电暖气", "poer19.plug.pth10": "PTH10", + "povit.jmprope.p231": "Puweite Smart Bluetooth Skipping Rope", "puppy.vacuum.r3x": "puppy vacuum", "purest.derh.bossp": "浦力适智能除湿机干衣机", "purest.derh.bossp1": "浦力适智能除湿干衣机", - "pwzn.light.apple": "LEDStrip controller", + "pwzn.light.apple": "LED Strip controller", "pwzn.plug.banana": "Intelligent wireless wall charger", "pwzn.relay.apple": "16 relays module", "pwzn.relay.banana": "16 relays module modbus version", "pze.light.wy0a01": "Puzhuoer Smart Light", "qcy.camera.c1pro": "QCY IPC CC1S", + "qdhkl.aircondition.ac": "VRF Air Conditioner", + "qdhkl.aircondition.b23": "AC Indoor Unit Controller", "qdhkl.aircondition.md01": "Sumi central air conditioning controller", + "qdhkl.gateway.b18p": "VRF Air Conditioner Manager", + "qdhkl.gateway.b19": "VRF Central Air Conditioner Controller", + "qdhkl.gateway.lonink": "LONINK VRF Central Air Conditioner Controller", + "qdhkl.gateway.sim": "SYMI VRF Central Air Conditioner Controller", + "qdhkl.gateway.sm": "SUMI VRF Air Conditioner Gateway", + "qdjszk.aircondition.1": "约克VRF中央空调", "qdmyk.beauty.mry01": "SeeMagic Smart Fair Removal", "qhzm.light.wy0a01": "Qinghe Smart Light", "qicyc.bike.tdp02z": "qicybike", "qicyc.bike.xmdzlzxc01qj": "Mi Smart Electric Folding Bike", "qike.bhf_light.qk201801": "Smart Bath(Basic)", + "qindao.blanket.1905": "Qindao Smart Electric Blanket", + "qmi.plug.2a1c1": "Xiaomi Smart Power Strip 20W Fast Charge (2A1C)", "qmi.plug.psv1": "aigo smart powerstrip", + "qmi.plug.psv3": "Xiaomi Smart Power Strip 2 (5 Outlet)", "qmi.powerstrip.v1": "CHINGMI Smart Power Strip", + "qshui.blanket.d1": "Saiwen intelligent water heating mattress", "quhwa.aircondition.0602": "空调", "quhwa.curtain.0109": "窗帘", "quhwa.light.0133": "灯", @@ -2184,17 +2937,33 @@ "qushui.bed.002": "8H Smart Adjustable Mattress", "qushui.bed.003": "8H Milan Smart Electric Bed Pro", "qushui.bed.004": "8h artificial intelligence sleeping mattress Damo", + "qushui.bed.007": "8H Feel leather intelligent electric bed x Pro", + "qushui.bed.008": "8H Feel leather intelligent electric bed x", "qushui.bed.8hai1": "8H AI Smart mattress", "qushui.bed.dt4": "8H Milan Smart leather electric bed S", + "qushui.blanket.g2": "8h graphene intelligent temperature control bed warm pad", + "qushui.blanket.g3": "8h graphene intelligent temperature control mattress", "qushui.dryer.8gxg1": "8h free Intelligent nursing, sterilization and deodorization shoe cabinet", + "qushui.pillow.p2": "8h intelligent sleeping natural latex pillow x", "qushui.sofa.001": "8H Master Smart Electric Modular Sofa", + "qushui.sofa.003": "8H COZY massage couch", + "qushui.sofa.004": "8H kola smart electric leisure recliner", + "qushui.sofa.b7": "8H Le relaxed intelligent electric slow rocking sofa", + "qwliot.fishbowl.38000": "Black Engine", "qyds.light.wy0a01": "Qianzhongyi intelligent lamp", "qykj.curtain.qycm60": "骐远开合帘电机", + "raile.light.zhzm1": "Intelligent lighting", "raymo.switch.000101": "雷莫智能开关", "rbe.light.wy0a01": "RBE wisdow living room lamp", "rbe.light.wy0b01": "Ruiboer Intelligent Lamp", "really.heater.f1": "Really Heater", + "renx.airer.dilai": "Laidi smart clothes dryer", + "renx.airer.pinsh": "PISN smart clothes dryer", + "repor.magic_touch.r1": "Repro intelligent neck pillow", + "rjkj.switch.b103": "Intelligent Circuit Breaker", "rjkj.switch.b105": "B105断路器", + "rjxn20.light.rjss01": "Ai color temperature light", + "rjxn20.light.sw01": "Songwei intelligent chandelier", "rmt.bed.zhsbed": "CHEERS Smart Bed", "roborock.vacuum.a01": "Roborock E Series", "roborock.vacuum.a08": "Roborock S6 Pure", @@ -2205,6 +2974,17 @@ "roborock.vacuum.a15": "Roborock S7", "roborock.vacuum.a19": "Roborock S4 Max", "roborock.vacuum.a23": "Roborock T7S Plus", + "roborock.vacuum.a26": "Roborock G10S Pro", + "roborock.vacuum.a27": "Roborock S7 MaxV", + "roborock.vacuum.a29": "Roborock G10", + "roborock.vacuum.a30": "Roborock G10", + "roborock.vacuum.a34": "Roborock Q5", + "roborock.vacuum.a37": "Roborock T8", + "roborock.vacuum.a38": "Roborock Q7 Max", + "roborock.vacuum.a40": "Roborock Q7", + "roborock.vacuum.a46": "Roborock G10S", + "roborock.vacuum.a52": "Roborock T8 Plus", + "roborock.vacuum.a62": "Roborock S7 Pro Ultra", "roborock.vacuum.c1": "Xiaowa C1", "roborock.vacuum.cloud": "石头智能", "roborock.vacuum.e2": "Xiaowa E Series", @@ -2225,8 +3005,12 @@ "roidmi.cleaner.v1": "ROIDMI NEX S", "roidmi.cleaner.v2": "ROIDMI NEX VX", "roidmi.cleaner.v382": "ROIDMI Cordless Vacuum Cleaner NEX2 Pro", - "roidmi.vacuum.v1": "ROIDMI Handheld Vacuum Cleaner", + "roidmi.cleaner.v58": "ROIDMI Cordless Vacuum Cleaner", + "roidmi.mop.xdj01": "ROIDMI Wet Dry Vacuum Cleaner NEO", + "roidmi.vacuum.r1b": "ROIDMI EVE", + "roidmi.vacuum.v1": "ROIDMI Cordless Vacuum Cleaner", "roidmi.vacuum.v60": "ROIDMI EVE", + "roidmi.vacuum.v66": "ROIDMI EVA", "rokid.robot.alien": "Rokid Alien", "rokid.robot.alien2": "Rokid Alien", "rokid.robot.me": "Rokid Me", @@ -2234,6 +3018,7 @@ "rokid.robot.panda0": "Rokid Panda", "rokid.robot.pebble": "Rokid 月石", "rokid.robot.pebble2": "Rokid Pebble", + "ronco.curtain.rc120": "RONCO Intelligent curtain", "roome.bhf_light.1036f": "ARROW Smart Bath-Heater X1", "roome.bhf_light.1037fx": "ARROW Smart Bath-Heater X2", "roome.bhf_light.yf6002": "M-YF6002 Smart Bathroom Heater", @@ -2244,9 +3029,23 @@ "roome.switch.v2": "智如易云对接开关", "roome.switch.v3": "智如易云对接开关", "roome.switch.v4": "智如易云对接开关", + "roomin.curtain.rlcma1": "如影智能窗帘", + "roomin.switch.klpd01": "单火单键(白色版)", + "roomin.switch.klpd02": "单火双键开关(白色版)", + "roomin.switch.klpd03": "单火三键(白色版)", + "roomin.switch.klpj01": "零火单键开关(黑色版)", + "roomin.switch.klpj02": "零火双键开关(黑色版)", + "roomin.switch.klpj03": "零火三键开关(黑色版)", + "roomin.switch.klpj1d": "如影智能开关面板 单火单键(黑色版)", + "roomin.switch.klpj1w": "零火单键开关(白色版)", + "roomin.switch.klpj2d": "如影智能开关面板 单火双键(黑色版)", + "roomin.switch.klpjd3": "单火三键黑色版", + "roomin.switch.klpjw2": "如影智能开关面板 零火双键(白色版)", + "roomin.switch.klpjw3": "如影智能开关面板 零火三键(白色版)", "rotai.magic_touch.sx300": "Intelligent plantar massager", "rotai.magic_touch.sx315": "MOMODA Head Massager", "rotai.massage.m700": "Momoda M700 AI Massage Chair", + "rotai.massage.m700a": "Momoda π Smart Massage Chair", "rotai.massage.rt5728": "MOMODA RT5728", "rotai.massage.rt5850": "Rongtai 5850", "rotai.massage.rt5850s": "Rongtai 5850s", @@ -2257,7 +3056,13 @@ "ryeex.bracelet.sake": "Hey+ Band", "san.airrtc.s89h": "Sancory Thermostat", "scds.light.wy0a01": "Dengbushangshu Picturesque lamp", + "schmzn.aircondition.q6c": "schmzn.air-conditioner.chq6c", + "schmzn.fridge.505q3s": "505冰箱", + "schmzn.fridge.ml3xxw": "美菱BCD-3xxUPC/U冰箱", + "schmzn.fridge.ml45xw": "美菱BCD-4/5xxWPU9CX冰箱", "schmzn.fridge.ml48x": "MeiLing-Fridge", + "schmzn.fridge.ml505x": "schmzn.fridge.ml2xwp3cx", + "schmzn.fridge.ml56xw": "美菱BCD-5/6xxWPUCX冰箱", "schyrl.switch.sw12": "华宇瑞联智能开关", "scinan.aircondition.online": "上线-空调插", "scinan.curtain.mc01": "智能窗帘", @@ -2291,13 +3096,16 @@ "scmkcz.switch.crspv3": "调光开关", "scmkcz.switch.scene": "场景", "scmkcz.switch.scene2": "情景", + "sdzy.curtain.dt170": "ShiDianSmartCurtain", "sfl.light.aaas": "FSL Cloud-controlled smart lamps", "sfl.light.fsl001": "FSL FIHAMS model", "sfl.light.fsl002": "FSL elegance model", + "sfl.light.fsl003": "FSL smart lighting series", "sfl.light.wl215": "5w调光调色筒灯", "shanhe.switch.1": "善禾物联", "shanhe.switch.v1": "善禾物联", "shanu.light.oreo": "OREO", + "shhf.light.mhcw01": "zhimei Smart Ceiling Light", "shibei.light.wy0a01": "Seebest Smart Living Room Ceiling Light", "shibei.light.wy0a02": "Seebest smart bedroom ceiling light", "shjszn.gateway.c1": "Zelkova smart gateway", @@ -2313,6 +3121,7 @@ "sig.curtain.welink": "微联小智-窗帘", "silen.fryer.sck501": "Silencare AirFryer 1.8L", "silen.fryer.sck505": "Silencare AirFryer", + "silen.fryer.sck5o8": "Silencare AirFryer SC-K508W", "silen.mfcp.sck307": "Silencare Grill", "simai.switch.srd01": "继电器模块", "simon.aircondition.ac": "空调", @@ -2329,9 +3138,11 @@ "simon.curtain.e2cur": "2位窗帘开关", "simon.curtain.i1cur": "1位窗帘开关", "simon.curtain.i2cur": "2位窗帘开关", + "simon.curtain.zigcur": "Zigbee窗帘", "simon.light.1light": "1位调光开关", "simon.light.2light": "2位调光开关", "simon.light.bvls": "Bus调光开关", + "simon.light.dcls": "调光调色开关", "simon.light.e1lsw": "1位调光开关", "simon.light.e2lsw": "2位调光开关", "simon.plug.bvso": "Bus插座", @@ -2346,15 +3157,50 @@ "simon.switch.sw2": "2位智能开关", "simon.switch.sw3": "3位智能开关", "sini.switch.495246": "欣易联Sinilink", + "sinl.tow_w.s1": "intelligent touch screen towel dryer", "skg.magic_touch.skgw50": "SKG Waist Massager W5 TE", + "skxc.aircondition.sair": "空调", + "skxc.curtain.smcu": "窗帘", + "skxc.fan.sfan": "风扇", + "skxc.light.lamp01": "联了么智能", + "skxc.plug.smss": "插座", + "skxc.switch.smsw": "开关", "skyrc.airp.su001": "Petoneer Smart Odor Eliminator-Pro", "skyrc.airpurifier.pur": "Petoneer AirMaster", + "skyrc.bed.nest": "Petoneer Cozy Sofa", "skyrc.feeder.dfeed": "Petoneer Two-Meal Feeder", "skyrc.feeder.fed": " Petoneer Nutri Mini Feeder", + "skyrc.feeder.mini2": "Petoneer Nutri Mini 2", "skyrc.feeder.mmfed": "Petoneer NutriSpin 6 Meal Pet Feeder", "skyrc.feeder.vdfeed": "Nutri Vision Mini", "skyrc.feeder.vfed": " Petoneer Nutri Vision", "skyrc.pet_waterer.fre1": "Fresco Mini Plus", + "skyrc.pet_waterer.fres2": "Petoneer Fresco Mini Pro", + "sld001.walkingpad.s2": "Sulida S2 intelligent walking machine", + "sld001.walkingpad.s2pro": "Sulida S2 Pro intelligent walking machine", + "slf.aircondition.air03": "Smart central air conditioning controller D8C07", + "slf.aircondition.air04": "Smart central air conditioning controller HR7C07", + "slf.airfresh.airf01": "Smart Airfresh controller D8C07", + "slf.airfresh.airf02": "Smart Airfresh controller HR7C07", + "slf.airrtc.air02": "Smart Floor heating controller D8C07", + "slf.airrtc.air05": "Smart Floor heating controller HR7C07", + "slf.plug.sw0a01": "Chi Rafi two or three plugs", + "slf.remote.rem01": "Chi Rafi Freshman Open - Scene Switch", + "slf.remote.rem02": "Chi Rafi Sophomore Open - Scene Switch", + "slf.remote.rem03": "Chi Rafi Junior Open - Scene Switch", + "slf.remote.rem04": "Smart Zero Fire Four Button Scene Panel HR7C07", + "slf.remote.rem05": "Chi Rafi Three Open Six - Scene Switch", + "slf.remote.rem06": "Chi Rafi Two Open Four - Scene Switch", + "slf.remote.sen01": "Intelligent emergency call button HR7C07", + "slf.remote.sen02": "Intelligent emergency call button D8C07", + "slf.switch.sw1a01": "Zhilafei freshman opening -1 street lamp control", + "slf.switch.sw1b01": "Smart Zero Fire One Key Switch HR7C07", + "slf.switch.sw2a01": "Zhilafei sophomore opening -2 street lamp control", + "slf.switch.sw2b01": "Smart Zero Fire Double Button Switch HR7C07", + "slf.switch.sw3a01": "Zhilafei junior high school -3 street lamp control", + "slf.switch.sw3b01": "Smart Zero Fire Three Button Switch HR7C07", + "slf.switch.sw4a01": "Zhi Lafei two open four-four street light control", + "slf.switch.sw4b01": "Smart Zero Fire Four Button Switch HR7C07", "smartj.curtain.sjcmns": "SmartJoy Zhizhen Curtain Motor(WiFi)", "smartj.curtain.sjd82p": "SmartJoy Intelligent curtain motor (WiFi Pro)", "smartj.curtain.sjdt82": "SmartJoy intelligent curtain motor", @@ -2376,6 +3222,7 @@ "smartj.switch.sjlh2s": "SmartJoy Zhizhen wall switch (two key Mesh)", "smartj.switch.sjlh3e": "SmartJoy wisdom at wall switch (Three key WiFi)", "smartj.switch.sjlh3s": "SmartJoy Zhizhen wall switch (three key Mesh)", + "smartj.switch.sjlh4s": "SmartJoy Zhizhen Wall Switch (Zero Fire Four-Button Bluetooth Mesh Version)", "smith.blanket.cxma1": "Chanitex Water-heated mattress CXM-A1", "smith.w_soften.cxs05ta1": "CHANITEX WATER SOFTENER", "smith.waterheater.cxea1": "Chanitex Electric Water Heater CXE-A1", @@ -2390,6 +3237,9 @@ "smyoo.light.music": "音乐灯", "smyoo.plug.plugin02": "Smyoo Switch", "smyoo.switch.syn020": "开关带重启", + "snowyi.aircondition.bp4": "雪意4按键带负载三合一温控面板", + "snowyi.aircondition.ht8": "雪意采暖温控器", + "snowyi.airfresh.sivs": "雪意一体式新风控制器(附带PM2.5/VOC/温度", "snowyi.curtain.cre04": "雪意4路导轨式窗帘电机控制模块", "snowyi.light.cct04": "雪意4路LED调光模块(0-10v)", "snowyi.light.rel04": "雪意4路导轨式继电器控制模块", @@ -2400,14 +3250,23 @@ "soocare.toothbrush.mc1": "Mi Kids Electric Toothbrush", "soocare.toothbrush.x3": "Soocare Electric Toothbrush", "sperll.light.fairy": "灯带控制器", + "sps.aircondition.ac": "空调", + "sps.airfresh.newwin": "新风机", + "sps.airm.aircon": "空气质量探测器", + "sps.curtain.curta": "窗帘", + "sps.light.light": "灯", + "sps.switch.switch": "开关", "srkj.aircondition.v001": "空调", "srkj.curtain.cl01": "窗帘", + "srkj.fan.fs01": "风扇", "srkj.light.test": "调光灯", "srkj.switch.sw1": "开关", "srkj.waterheater.re01": "热水器", "stds.light.wyst01": "Shengteng Lighting Ceiling Light", "stds.light.wyst02": "Shengteng Ceiling Light", + "suis.controller.pad01": "FMS Control Panel", "suittc.aircondition.46": "智能空调系列", + "suittc.airrtc.111": "智能空调二合一镜面", "suittc.airrtc.29": "DCN-A系列", "suittc.airrtc.72": "智能空调二合一系列", "suittc.airrtc.76": "DCN-B系列", @@ -2431,6 +3290,7 @@ "sxds.blanket.jssncd": "JESIF Smart Plumbing Mattress(single)", "sxds.pillow.pillow02": "Natural latex sleep pillow", "sxmiot.sensor_ht.th0001": "迅鸣温湿度计", + "syi.airer.d08": "dengdi electric clothes hangers", "syi.airer.d16": "KaBei electric clothes rack", "syi.airer.hkg001": "Haohaka electric clothes hangers", "syi.airer.sy1": "KaRuiQi electric drying rack", @@ -2460,6 +3320,8 @@ "sykj.switch.030002": "双键墙壁开关", "sykj.switch.030003": "三键墙壁开关", "sykj.switch.030009": "通断器", + "symi.light.wy0a01": "Kaomi downlight mesh version", + "symi.light.wy0a02": "Kaomi magnetic suction lamp mesh version", "syniot.airer.l2": "smart airer", "syniot.curtain.m1": "synIOT rolling motor", "syniot.curtain.syc1": "Syniot Curtain", @@ -2471,8 +3333,13 @@ "syniot.switch.t2": "synIOT switch 2 key", "syniot.switch.t3": "synIOT switch 3 key", "szdy.airfresh.n80": "Bijia Fresh Air Ventilator", + "szkj.mop.mjhvc1": "Mijia Hi-temperature 3-in-1 Cordless Floor Washing Vacuum Cleaner", + "szkj.mop.x1": "Shunzao HotClean Wet Dry Vacuum Cleaner", + "szkj.vacuum.fc01eu": "Xiaomi Robot Vacuum S10T", + "szkj.vacuum.mjfc01": "Mijia Robot Vacuum-Mop Tangle-free Brush", "szlxz.light.v1": "电灯", "szsy.curtain.050001": "智能窗帘", + "szsy.curtain.1": "窗帘", "szsy.light.040004": "CW球泡灯", "szsy.light.04000a": "灯带", "szsy.light.04000e": "CWRGB球泡灯", @@ -2485,6 +3352,8 @@ "szsy.light.04001d": "朗德万斯led台灯", "szsy.light.04001e": "儿童卧室吸顶灯", "szsy.light.090006": "雷士风扇灯", + "szsy.light.cw": "CW灯", + "szsy.light.cwrgb": "CWRGB灯", "szsy.plug.010001": "墙壁插座", "szsy.plug.010002": "慎勇转插插座", "szsy.switch.030001": "美规开关", @@ -2493,6 +3362,7 @@ "szsy.switch.030009": "通断器", "szsy.switch.090005": "八键情景开关", "tab.vacuum.robot": "Tatajia Vacuum", + "tata.milkheat.bl1108": "BABY BOTTLE WARMER", "tbsy.aircondition.123": "空调", "tbsy.curtain.123": "智能窗帘", "tbsy.light.1221": "智能灯", @@ -2546,10 +3416,16 @@ "thing.motor.smart2": "智能窗帘电机", "thing.plug.socket": "智能插座", "thinks.lunar.xm1": "Smart Sleeping Sensor -- Lunar", + "tianyi.aircondition.v2": "空调", + "tianyi.curtain.v2": "窗帘", + "tianyi.fan.v2": "风扇", + "tianyi.light.v2": "灯", + "tianyi.switch.v2": "开关", "tih.aircondition.a12345": "Maid White 冷气机", "tinymu.toilet.ailid": "TINYMU Smart Toilet AI Version", "tinymu.toilet.wmt15": "TINYMU Smart Toilet Ultimate V Shape", "tinymu.toiletlid.v1": "TINYMU Smart Toilet", + "tnkq.airfresh.tnkq01": "TNKQ Multifunction Air Fresh", "tokit.cooker.press1": "TOKIT Pressure IH Smart Rice Cooker", "tokit.cooker.tk20l01": "Mini Rice Cooker", "tokit.cooker.tk4001": "TOKIT Smart IH Rice Cooker", @@ -2559,6 +3435,10 @@ "tokit.oven.tk32pro1": "TOKIT Smart Oven", "tokit.pre_cooker.tkih1": "TOKIT Smart IH Pressure Cooker", "tokit.waterpuri.tkj1": "TOKIT Smart Water Purifier 600G", + "topwit.bhf_light.rz01": "Smart Bath Heater Switch", + "topwit.switch.bln01": "Single way BLE Mesh zero fire switch", + "topwit.switch.bln02": "Two way BLE Mesh zero fire switch", + "topwit.switch.bln03": "Three way BLE Mesh zero fire switch", "topwit.switch.rzw01": "Smart Switch 1 gang (Not Required N Wire)", "topwit.switch.rzw02": "Smart Switch 2 gang (Not Required N Wire)", "topwit.switch.rzw03": "Smart Switch 2 gang (Not Required N Wire)", @@ -2571,6 +3451,8 @@ "tospo1.light.bpzp02": "Tospo A70 Bulb ", "tospo1.light.rpc01": "TOSPO LED Module", "trios.bleshoes.v1": "米家智能跑鞋", + "trios1.bleshoes.rftcdl": "L-ChiYi Temperature Control Shoes", + "trios1.bleshoes.rftcdr": "R-ChiYi Temperature Control Shoes", "trios1.bleshoes.v02": "FreeTie Smart Sports Core", "tsd.light.amn001": "Topstar Smart Bulb", "tsd.light.amn002": "Topstar Smart Light Strip", @@ -2612,6 +3494,8 @@ "tyzhjt.stb.stb003": "机顶盒", "tyzhjt.switch.000001": "开关", "tyzhjt.tv.ds0001": "电视", + "uaiot.light.fsl001": "佛照坐姿灯", + "uehome.sofa.imi1": "UE IMI-intelligent massage sofa", "ufun.light.board": "灯控板", "ufun.switch.board": "开关控制板", "uiot.aircondition.ai11": "Infrared Air Conditioner", @@ -2635,10 +3519,21 @@ "umiot.curtain.curt": "AllCenter窗帘系列", "umiot.light.ums": "AllCenter灯系列", "umiot.switch.ums": "AllCenter开关系列", + "uvfive.dryer.panda": "FIVE Smart Shoes Bedding Dryer", "uvfive.s_lamp.slmap2": "Five germicidal lamp", + "uvfive.steriliser.maine": "FIVE sterilization rack(Wall Mounted)", "uvfive.steriliser.tiger": "FIVE sterilization rack", "uvfive.tow_w.berry": "FIVE Smart Hot Towel Machine", + "uxkeji.curtain.gmkhl": "GM curtain", + "uze.hud.jl101": "JiuLu HUD GX", "vanco.toilet.pro1": "Vancoco Smart Toilet Pro", + "vcontr.aircondition.ac": "空调", + "vcontr.airp.aircle": "空气净化器", + "vcontr.curtain.curta": "窗帘", + "vcontr.light.lig01": "灯光", + "vcontr.light.lig05": "卫生间灯", + "vcontr.light.lig06": "廊灯", + "vcontr.tv.vcon02": "电视", "viewx.light.101001": "inncap smart bulb distribution network", "viomi.airc.m1": "meekee 1P", "viomi.airc.m2": "meekee 1.5P", @@ -2659,12 +3554,18 @@ "viomi.aircondition.v33": "King 1X 1.5P", "viomi.aircondition.v34": " King 1A 1P", "viomi.aircondition.v35": " King 1A 1.5P", + "viomi.aircondition.v37": "Crown-72A3", + "viomi.aircondition.v38": "Crown-26A3", + "viomi.aircondition.v39": "Crown-35A3", + "viomi.aircondition.v40": "Crown-50A3", "viomi.aircondition.v6": "King(1P)", "viomi.aircondition.v7": "King", "viomi.aircondition.v8": "King 1S", "viomi.aircondition.v9": "Royal", "viomi.aircondition.y1": "Milano (Rou Feng) 2P", "viomi.aircondition.y10": "Milano 1.5P", + "viomi.aircondition.y101": "Milano 2S 1.5P", + "viomi.aircondition.y103": "Milano 2T 1.5P", "viomi.aircondition.y11": "iCool 1C 1P", "viomi.aircondition.y12": "iCool 1C 1.5P", "viomi.aircondition.y13": "iCool 1P", @@ -2733,11 +3634,20 @@ "viomi.aircondition.y88": "Smart AC A1-12K", "viomi.aircondition.y89": "Smart AC A1-18K", "viomi.aircondition.y9": "Milano 1P", + "viomi.aircondition.y99": "Smart 2S 1.5P", "viomi.airer.ich108": "Viomi electric drying rack Lite 1C", "viomi.airer.ich109": "Viomi Internet e-drying rack(Aing)", + "viomi.airer.ich111": "Viomi electric drying rack Lite 1C", "viomi.airer.nch102": "Viomi Internet e-drying rack Sunny 2S", + "viomi.airer.vch100": "Viomi AI e-drying rack Lite2(Space)", + "viomi.airer.vch103": "Viomi Internet e-drying rack Sunny 2Pro", + "viomi.airer.vch104": "Viomi AI e-drying rack Warm 2Pro", + "viomi.airer.vch109": "Viomi AI e-drying rack Super 2S", + "viomi.airer.vch110": "Viomi AI e-drying rack Super 2Y", "viomi.airer.vcy102": "Viomi Internet e-drying rack Sunny 2A", - "viomi.airer.xy108": "Viomi Internet e-drying rack(Suny)", + "viomi.airer.vcy104": "Viomi Internet e-drying rack Sunny 2A", + "viomi.airer.xy108": "Viomi Internet e-drying rack(Sunny)", + "viomi.airer.xy108b": "Viomi Internet e-drying rack(Sunny)", "viomi.airp.v3": "Smart Air Purifier", "viomi.airpurifier.v1": "Airman Pro", "viomi.airpurifier.v2": "Airman", @@ -2754,22 +3664,27 @@ "viomi.cooker.v5": "Viomi Smart Rice Cooker 3L", "viomi.curtain.v1": "Viomi Internet curtain motor (Wi-Fi)", "viomi.curtain.v2": "Viomi Internet smart curtain Auto 1A", + "viomi.curtain.v3": "Viomi AI smart curtain Auto 1S", "viomi.dishwasher.m01": "Mi Smart Built-in Dishwasher (8 Dining Sets)", "viomi.dishwasher.m02": "Mi Smart Dishwasher (4 Dining Sets)", + "viomi.dishwasher.m03": "Mijia Smart FS\u0026BI Dishwasher S1", "viomi.dishwasher.v01": "Viomi Dishwasher", "viomi.dishwasher.v03": "Viomi Smart Dish Washer(Build-in)", "viomi.dishwasher.v05": "Viomi Smart sink dishwasher", "viomi.dishwasher.v06": "Viomi Smart Built-in Dishwasher", "viomi.dishwasher.v07": "Viomi Smart Protable dishwasher", - "viomi.dishwasher.v08": "Viomi Smart dishwahser 8 settings 0803A", - "viomi.dishwasher.v09": "Viomi Smart dishwahser 4 settings 0402", + "viomi.dishwasher.v08": "Viomi smart dishwasher 0803A", + "viomi.dishwasher.v09": "Viomi Smart dishwahser", "viomi.dishwasher.v10": "Viomi Smart dishwasher 10 settings 0805", "viomi.dishwasher.v11": "Viomi Smart dishwasher 10 settings 0804", - "viomi.dishwasher.v12": "viomi smart dishwasher 8 settings 0803B", + "viomi.dishwasher.v12": "Viomi smart dishwasher 0803B", "viomi.dishwasher.v14": "viomi smart dishwasher 10 settings building 0807", + "viomi.dishwasher.v15": "Viomi AI Dishwasher Milano 15sets", + "viomi.fan.v13": "Viomi Smart Circulation Fan", "viomi.fan.v5": "Viomi ButterflyFan DC", "viomi.fan.v6": "VIOMI AI Air-circulating Fan", "viomi.fan.v7": "Viomi AI DC Inverter floor fan 3", + "viomi.fan.v8": "VIOMI SMART TOWER FAN", "viomi.fridge.b1": "Viomi Smart Refrigerator(SBS 380L)", "viomi.fridge.b3": "Viomi Refrigerator(Cross Door 408L)", "viomi.fridge.b4": "Viomi Refrigerator (SBS458L)", @@ -2809,6 +3724,9 @@ "viomi.fridge.u45": "Viomi Refrigerator (French Door 508L)", "viomi.fridge.u46": "Viomi Refrigerator (SBS 456L) plus", "viomi.fridge.u47": "Viomi Refrigerator (SBS 590L)", + "viomi.fridge.u49": "Viomi Refrigerator iLive2(Cross Door 450L)", + "viomi.fridge.u50": "Viomi Refrigerator iLive2(Cross Door 518L)", + "viomi.fridge.u51": "Viomi Refrigerator(SBS 636L)", "viomi.fridge.u6": "Viomi Smart Refrigerator iLive (4-door 486L)", "viomi.fridge.u7": "Viomi Internet refrigerator iLive (side by side 545L)", "viomi.fridge.u8": "Viomi Smart Refrigerator iLive(SBS 603L)", @@ -2833,13 +3751,21 @@ "viomi.fridge.x29": "Viomi Smart Refrigerator 21Face(505L)", "viomi.fridge.x35": "Viomi Refrigerator (SBS610L)", "viomi.fridge.x37": "Viomi AI Refrigerator 21Face2(525L)", + "viomi.fridge.x38": "Viomi AI Refrigerator 21Face2(Cross Door 506L)", "viomi.fridge.x4": "Viomi Internet refrigerator 21 face (side by side 450L)", + "viomi.fridge.x44": "Viomi AI Refrigerator 21Face2(525L)", + "viomi.fridge.x48": "VIOMI AI Refrigerator 21Face2 S(SBS 640)", + "viomi.fridge.x49": "VIOMI AI Refrigerator 21Face2 (Quad 502)", + "viomi.fridge.x50": "VIOMI AI Refrigerator 21Face 2PRO(T 515)", "viomi.fridge.x7": "Viomi Smart refrigerator 21 face (428L)", "viomi.health_pot.c2": "Viomi Smart Health Pot Honey Pro", "viomi.health_pot.v1": "Mi Smart Multipurpose Kettle", "viomi.heater.v3": "Viomi Tower type heater", "viomi.heater.v4": "Viomi Kick-Line heater Pro2", "viomi.heater.v5": "Viomi Smart Heater Pro 2", + "viomi.heater.v6": "Viomi Smart Baseboard Heater", + "viomi.heater.v8": "Viomi Smart Convection Panel Heater", + "viomi.heater.v9": "Viomi Smart Heater Pro2 (Bluetooth Version)", "viomi.heater.vxtj01": "Viomi Kick-Line heater Pro", "viomi.hood.a10": "Viomi Smart Hood Free 3", "viomi.hood.a11": "Viomi Smart Hood (VC302)", @@ -2851,7 +3777,9 @@ "viomi.hood.c15": "Viomi Smart Hood Cross3 A1", "viomi.hood.c16": "Viomi Smart Hood Wing 1C", "viomi.hood.c17": "Viomi Smart Hood Free 3 Pro", + "viomi.hood.c19": "Viomi AI Hood Wing A1", "viomi.hood.c2": "Viomi Smart Hood Cross 2", + "viomi.hood.c21": "Viomi AI Range Hood Cross2 A1", "viomi.hood.c3": "Viomi Smart Hood (VK501)", "viomi.hood.c4": "Viomi Internet Hood Flash Pro", "viomi.hood.c5": "Viomi Smart Hood Cross 3", @@ -2863,23 +3791,41 @@ "viomi.hood.h5": "Viomi Internet Hood Flash", "viomi.hood.v1": "Exhaust Hood \u0026 Stovetop", "viomi.hood.v2": "Mi Smart Side-Draft Range Hood", + "viomi.hood.v5": "Mi Crossover Range Hood S1 ", + "viomi.hood.v6": "Mi Side Suction Range Hood S1", + "viomi.hood.v8": "Mijia Smart Automatic Multi-frequency Range Hood S1", "viomi.humidifier.h1": "Humidifier King", "viomi.i_stove.v1": "Viomi Smart Integrated Stove", "viomi.i_stove.v3": "Viomi Smart Integrated Stove", "viomi.i_stove.v4": "Viomi smart integrated cooker (2 in 1)", "viomi.i_stove.v5": "Viomi smart steaming oven", "viomi.i_stove.v6": "Viomi AI Integrated Stove", + "viomi.i_stove.v7": "Viomi AI Integrated Stove-VJ502", + "viomi.i_stove.v8": "Viomi AI Integrated Stove-VJ503", "viomi.juicer.v1": "Mi High-speed Smart Blender", "viomi.juicer.v2": "Viomi High Speed Blender(Quiet Version)", "viomi.lock.lbt14a": "Viomi Internet smart lock eLink 2A", + "viomi.lock.lbt41e": "Viomi AI Smart lock eyeLink 2F Pro", + "viomi.lock.lbt48a": "Viomi AI Smart lock eyeLink 2S", + "viomi.lock.lbt51a": "Viomi AI intelligent door lock Smart 2", "viomi.lock.link1": "VIOMI door lock Link (Bluetooth)", "viomi.lock.link2": "Viomi Door Lock Link (Long endurance version)", "viomi.lock.link2p": "Viomi Internet smart lock eLink 2Pro", "viomi.lock.link2v": "Viomi Internet smart lock eyeLink 2V", + "viomi.lock.link3": "Viomi AI intelligent door lock eLink 2C", + "viomi.lock.link4s": "Viomi Internet Smart Lock eyeLink 2F", + "viomi.mfcp.m1": "Xiaomi Smart IH Multi-functional Cooker", "viomi.oven.so1": "VIOMI Internet steaming and baking all-in-one machine king (embedded)", "viomi.oven.so2": "VIOMI Internet steaming and baking all-in-one machine queen (embedded)", "viomi.oven.so3": "Viomi Smart Steaming and Baking Machine King Pro(Build-in)", + "viomi.oven.so5": "Viomi Smart Steam \u0026 Baking Oven Face", "viomi.oven.so6": "Viomi Smart Steaming and Baking Machine Face(Build-in)", + "viomi.oven.so8": "Viomi Smart Steam oven Face A1(Build-in)", + "viomi.oven.so9": "Viomi Smart Steam and Baking Oven-Face (Taiwan)", + "viomi.plug.w1": "云米AI智能插座inkRock 1S(10A)", + "viomi.plug.w2": "云米AI智能插座inkRock 1S(10A/USB版)", + "viomi.plug.w3": "云米AI智能插座inkRock 1S(16A)", + "viomi.projector.l2": "coKiing AI巨幕影院Royal(4K激光)", "viomi.steriliser.v1": "Viomi Disinfection Cabinet (Build-in)", "viomi.switch.mlc1": "云米智能开关1C(单火一键)", "viomi.switch.mlc2": "云米智能开关1C(单火二键)", @@ -2888,6 +3834,13 @@ "viomi.switch.mlc5": "云米智能开关1C(零火二键)", "viomi.switch.mlc6": "云米智能开关1C(零火三键)", "viomi.switch.s1": "云米无线开关inkRock(单键版)", + "viomi.switch.w1": "云米AI智能开关inkRock 1S(单火一键)", + "viomi.switch.w2": "云米AI智能开关inkRock 1S(单火二键)", + "viomi.switch.w3": "云米AI智能开关inkRock 1S(单火三键)", + "viomi.switch.w4": "云米AI智能开关inkRock 1S(零火一键)", + "viomi.switch.w5": "云米AI智能开关inkRock 1S(零火二键)", + "viomi.switch.w6": "云米AI智能开关inkRock 1S(零火三键)", + "viomi.toilet.m06": "Viomi AI health check toilet Air", "viomi.vacuum.v11": "Viomi V-SLAM Robot Vacuum", "viomi.vacuum.v12": "Viomi Cleanning Robot X2", "viomi.vacuum.v13": "Viomi V3", @@ -2897,12 +3850,20 @@ "viomi.vacuum.v19": "Viomi SE", "viomi.vacuum.v20": "VIOMI FIERCE-UV", "viomi.vacuum.v21": "VIOMI ALPHA-UV", + "viomi.vacuum.v22": "VIOMI Alpha 2 Pro", "viomi.vacuum.v23": "VIOMI V3 Max", "viomi.vacuum.v24": "VIOMI 5G IOT LingLi 2", "viomi.vacuum.v25": "VIOMI X3", "viomi.vacuum.v27": "VIOMI Alpha 2 Plus", "viomi.vacuum.v29": "VIOMI Eagle", "viomi.vacuum.v3": "Viomi sweeper PRO", + "viomi.vacuum.v35": "Viomi V2 Max", + "viomi.vacuum.v36": "Viomi V5", + "viomi.vacuum.v37": "Viomi Alpha Lite", + "viomi.vacuum.v38": "Viomi V5 Pro", + "viomi.vacuum.v40": "Viomi Alpha 2 Pro(WH)", + "viomi.vacuum.v41": "Viomi Alpha 2 Max", + "viomi.vacuum.v45": "VIOMI Auto Cleaning Alpha 3", "viomi.vacuum.v6": "Viomi Cleaning Robot", "viomi.vacuum.v7": "Mi Robot Vacuum-Mop P", "viomi.vacuum.v8": "Mi Robot Vacuum-Mop P", @@ -2937,7 +3898,8 @@ "viomi.washer.v38": "Viomi Washer\u0026Dryer Master2Mix (10kg)", "viomi.washer.v4": "Viomi Washing Drying Machine (8KG)", "viomi.washer.v40": "Viomi Washer\u0026Dryer Neo3 Eyebot(10kg)", - "viomi.washer.v41": "Viomi Washer\u0026Dryer Neo3 Fresh Dry(10kg)", + "viomi.washer.v41": "Viomi Washer\u0026Dryer Fresh Dry(10kg)", + "viomi.washer.v43": "Viomi Washer and Dryer Master 2", "viomi.washer.v5": "Viomi Washing Drying Machine (10KG)", "viomi.washer.v6": "Viomi Washer\u0026Dryer Rose 9kg", "viomi.washer.v7": "Viomi Smart Washer \u0026 Dryer Combo Neo (8kg OTA )", @@ -2957,10 +3919,15 @@ "viomi.waterheater.e25": "Viomi Electric Water Heater VEW6018", "viomi.waterheater.e26": "Viomi Electric Water Heater 5019", "viomi.waterheater.e27": "Viomi Electric Water Heater 6019", + "viomi.waterheater.e28": "Viomi Electric Water Heater 5020", + "viomi.waterheater.e29": "Viomi Electric Water Heater 6020", "viomi.waterheater.e3": "Viomi Smart Electric Water Heater Air(60L Dual-Tank Voice-Contorl)", + "viomi.waterheater.e33": "Viomi Electric Water Heater 6022", + "viomi.waterheater.e38": "VIOMI AI Electric Water Heater Blue", "viomi.waterheater.e4": "VIOMI Internet electric water heater 50L(Premium Version)", "viomi.waterheater.e7": "Viomi Smart Electric Water Heater Air (60L Dual-Tank Excellent)", "viomi.waterheater.e8": "Viomi Smart Electric Water Heater Air (60L Dual-Tank Wisdom)", + "viomi.waterheater.m1": "Mijia Smart Tankless Gas Heater S1 (18L)", "viomi.waterheater.u1": "Yunmi Internet gas water heater 1A (13L)", "viomi.waterheater.u10": "Viomi Internet Gas Water Heater Zero (13L Zero Cold Water)", "viomi.waterheater.u11": "Viomi Smart Gas Water Heater Zero 13L", @@ -2996,21 +3963,27 @@ "viomi.waterheater.u49": "Viomi Smart Gas Water Heater VGW1821 18L", "viomi.waterheater.u50": "Viomi Smart Gas Water Heater VGW1622 16L", "viomi.waterheater.u51": "Viomi Smart Gas Water Heater VGW1822 18L", + "viomi.waterheater.u52": "Viomi AI Gas Water Heater VGW1623 16L", "viomi.waterheater.u53": "Viomi AI Gas Water Heater VGW1823 18L", + "viomi.waterheater.u55": "Viomi AI Gas Water Heater Blue", + "viomi.waterheater.u57": "Viomi AI Tankless Gas Water Heater Super", "viomi.waterheater.u6": "Viomi Internet gas water heater 16L(Premium Version)", + "viomi.waterheater.u62": "Viomi AI Tankless Gas Water Heater Super 2 Max", "viomi.waterheater.u7": "Viomi Smart Gas Water Heater Zero 16L", "viomi.waterheater.u8": "Viomi Smart Gas Water Heater Zero(18L)", "wainft.light.wy0a01": "Xinguang intelligent bulb", "wainft.light.wy0a02": "XG magnet lamp", "wainft.light.wyra01": "XG Color Light Strip", "wainft.switch.sw0a01": "Xinguang Bluetooth mesh switch", - "wainft.switch.sw0a02": "XG Bluetooth Mesh Breaker", + "wainft.switch.sw0a02": "XG Bluetooth Mesh Switch", + "wainft.switch.sw3a01": "XG Bluetooth Mesh Smart Switch", "wanai.fan.t118": "DEMULLER Fan(Single Cold Tower Fan)", "wanhe.waterheater.dr": "vanward-ElectricWaterHeater", "wanhe.waterheater.rr2": "vanward-gasheater", "wanhe.waterheater.rr3": "vanward-gasheater", "wanhe.waterheater.rr4": "vanward_gasheater", "wanhe.waterheater.s5": "VANWARD Water Heater S5", + "wanrui.blanket.psn2a": "Zhiling Intelligent Water Warm Mat", "wanye.plug.zcz00": "智能插座", "wanye.plug.zcz004": "红外插座", "wanye.plug.zcz005": "红外插座", @@ -3025,6 +3998,14 @@ "wfiot.switch.switch": "开关", "whp.washer.wgf80870bi": "whirlpool_waster_WG-F80870BI", "wintom.curtain.230xm": "WinTom Curtain", + "wise.wifispeaker.x7": "WISE SMART CONTROL PAD", + "wiser.aircondition.irac": "红外空调", + "wiser.curtain.wscur": "窗帘", + "wiser.light.tgq01": "调光器", + "wiser.switch.kg01": "普通开关", + "wiser.tv.irtv1": "红外电视", + "wiz.light.tunablecolor": "Tunable Color", + "wiz.light.tunablewhite": "Tunable White", "wlank.aircondition.001": "空调", "wlank.curtain.001": "窗帘", "wlank.light.001": "调光灯", @@ -3033,11 +4014,16 @@ "wln.light.cw": "灯", "wln.plug.cz": "插座", "worth.fan.18123": "艾美特风扇", + "worth.light.0zgz": "新云二路灯", "worth.light.270": "二路球泡灯", + "worth.light.aei0": "新云面板灯", + "worth.light.ieqbp": "一路面板灯", + "worth.light.oam9qo": "RGBCW灯", "worth.light.ws1b8z": "五路球泡灯", "worth.light.wsomqn": "三路灯带", "worth.light.wsoqsx": "康佳吸顶灯", "worth.light.wspk9a": "五路球泡灯", + "worth.light.xp4x": "新云三路灯带", "worth.plug.787076": "智能插座", "wowoto.projector.all": "小窝投影", "wxzn.switch.801438": "开关面板", @@ -3054,9 +4040,14 @@ "xckj.waterheater.int02": "Circle kitchen ultra-thin Internet electric water heater", "xckj.waterpuri.ihwp01": "Ocooker Water Purifier", "xckj.waterpuri.js01": "Circle kitchen household water purifier", + "xfocus.wifispeaker.dual": "Dual source backgound musice", "xfocus.wifispeaker.xfos": "Background Music", "xgds.light.wy0a01": "Romne Intelligent lamp", "xgds.light.wy0a02": "Romne smart ceiling lamp", + "xhuan.aircondition.ac01": "Sumi VRF Controller", + "xhuan.aircondition.x1": "苏米VRF控制器2", + "xhuan.curtain.cura02": "Sumi Smart Curtain Motor T1", + "xhuan.curtain.cura03": "Sumi Smart Curtain Motor T1 Pro", "xhuan.light.wy0a01": "Sumi Ruying Light Strip", "xhuan.light.wy0a02": "Sumi Downlight Lite", "xhuan.light.wy0a03": "Sumi Magnetic Floodlight", @@ -3066,6 +4057,9 @@ "xhuan.light.wy0a07": "Sumi Ruguang Spotlight", "xhuan.light.wy0a08": "Sumi Bulb Light Lite", "xhuan.light.wy0a10": "Sumi smart lamp", + "xhuan.switch.sw1a01": "Sumi t 1 intelligent switch (zero fire single key)", + "xhuan.switch.sw2a01": "Sumi t 1 intelligent switch (Zero Fire Two keys)", + "xhuan.switch.sw3a01": "Sumi t 1 intelligent switch (zero-fire triple bond)", "xhzm.light.wy0a01": "Xinhong ceiling lamp", "xiaomi.aircondition.c10": "Mi Smart Ultra Electricity Saving Vertical Air Conditioner (2HP/Inverter/New China Energy Label Level 1)", "xiaomi.aircondition.c11": "Mi Smart Ultra Electricity Saving Vertical Air Conditioner (3HP/Inverter/New China Energy Label Level 1)", @@ -3076,6 +4070,14 @@ "xiaomi.aircondition.c16": "Mi Smart Air Conditioner Energy Saving (Sleep Optimization 1.5HP)", "xiaomi.aircondition.c17": "Mi Smart AC with Ventilation (1.5HP)", "xiaomi.aircondition.c19": "Mi Smart AC with Ventilation (1HP)", + "xiaomi.aircondition.c20": "Mi Smart Vertical AC with Ventilation (3HP New China Energy Label Level 1)", + "xiaomi.aircondition.c21": "Mi Smart Electricity Saving Air Conditioner(1HP/New China Energy Label Level3)", + "xiaomi.aircondition.c22": "Mi Smart Electricity Saving Air Conditioner Pro (1.5 HP/New China Energy Label )", + "xiaomi.aircondition.c24": "Mi Smart Vertical AC with Ventilation (2HP New China Energy Label Level 1)", + "xiaomi.aircondition.c25": "Mi Smart AC Natural Breeze Vertical 3HP (New China Energy Label Level 1)", + "xiaomi.aircondition.c26": "Mi Smart AC Natural Breeze Vertical 2HP (New China Energy Label Level 1)", + "xiaomi.aircondition.k01": "Mi Smart Air Conditioner Straight Cool (1HP)", + "xiaomi.aircondition.m4": "Mi Smart Electricity Saving Air Conditioner (1HP/New China Energy Label )", "xiaomi.aircondition.ma1": "Mi Inverter Air Conditioner (1.5HP)", "xiaomi.aircondition.ma2": "Mi Inverter Air Conditioner (1.5HP, China Energy Label Level 1)", "xiaomi.aircondition.ma4": "Mi Vertical Air Conditioner (2HP)", @@ -3095,6 +4097,7 @@ "xiaomi.aircondition.mh3": "Mi Smart Ultra Electricity Saving Air Conditioner(1HP/Inverter/New China Energy Label Level 3)", "xiaomi.aircondition.mh4": "Mi Smart Air Conditioner Energy Saving (2HP)", "xiaomi.aircondition.mh6": "Mi Smart Air Conditioner Energy Saving (Gold Edition 3HP Standing)", + "xiaomi.aircondition.mt0": "Mi Smart AC with Ventilation (Elite Edition 1.5HP)", "xiaomi.aircondition.mt1": "Mi Smart Air Conditioner X (1HP / Inverter / New China Energy Label Level 1)", "xiaomi.aircondition.mt2": "Mi Smart Air Conditioner X (1.5HP / Inverter / New China Energy Label Level 1)", "xiaomi.aircondition.mt3": "Mi Smart Gentle Breeze Air Conditioner (1HP / Inverter / New China Energy Label Level 1)", @@ -3103,8 +4106,11 @@ "xiaomi.aircondition.mt6": "Mi Smart AC with Ventilation (Elite Edition 1.5HP)", "xiaomi.aircondition.mt7": "Mi Smart Ultra Electricity Saving Air Conditioner (1HP/Inverter/New China Energy Label Level 1)", "xiaomi.aircondition.mt8": "Mi Smart Ultra Electricity Saving Air Conditioner (1.5HP/Inverter/New China Energy Label Level 1)", + "xiaomi.aircondition.t13": "Mi Smart Ultra Electricity Saving AC (1.5HP/Inverter/New China Energy Label L1)", + "xiaomi.camera.c01a02": "Mi 360° Home Security Camera 2K", "xiaomi.demo.v1": "Beta build", "xiaomi.demo.v2": "Beta build", + "xiaomi.gateway.hub1": "Xiaomi Home Hub", "xiaomi.phone_ir.t1": "手机红外遥控器", "xiaomi.phone_ir.v1": "IR remote", "xiaomi.plc.v1": "Mi electricity WiFi extender", @@ -3113,7 +4119,11 @@ "xiaomi.repeater.v2": "Mi Wi-Fi Repeater 2", "xiaomi.repeater.v3": "Mi Wi-Fi Range Extender Pro", "xiaomi.repeater.v6": "Mi WiFi Range Extender AC1200", + "xiaomi.router.cr5508": "小米路由器CR5508", "xiaomi.router.cr6606": "小米路由器CR6606", + "xiaomi.router.cr8806": "小米路由器CR8806", + "xiaomi.router.cr8808": "小米路由器CR8808", + "xiaomi.router.cr8809": "小米路由器CR8809", "xiaomi.router.d01": "Mi Router Mesh", "xiaomi.router.lv1": "Mi Wi-Fi Nano", "xiaomi.router.lv3": "小米路由器R3C", @@ -3139,40 +4149,53 @@ "xiaomi.router.ra67u": "小米路由器RA67", "xiaomi.router.ra69": "Redmi路由器AX6", "xiaomi.router.ra70": "Xiaomi Router AX9000", + "xiaomi.router.ra71": "Redmi路由器AX1800", "xiaomi.router.ra72": "小米路由器AX6000", + "xiaomi.router.ra74": "Redmi Router AX5400", "xiaomi.router.ra80": "小米路由器AX3000", "xiaomi.router.ra81": "Redmi路由器AX3000", + "xiaomi.router.ra82": "Xiaomi Mesh System AX3000", + "xiaomi.router.rb01": "Xiaomi Router AX3200", + "xiaomi.router.rb02": "Xiaomi Router AC1200", + "xiaomi.router.rb03": "Redmi路由器AX6S", + "xiaomi.router.rb04": "Redmi电竞路由器AX5400", + "xiaomi.router.rb06": "Redmi Router AX6000", + "xiaomi.router.rb08": "Xiaomi HomeWiFi三频Mesh路由器", "xiaomi.router.rm1800": "小米路由器AX1800", "xiaomi.router.rm2100": "Redmi 路由器AC2100", "xiaomi.router.rmo15": "小米路由器", + "xiaomi.router.tr606": "小米路由器TR606", "xiaomi.router.v1": "Mi Wi-Fi", "xiaomi.router.v2": "Mi Wi-Fi", "xiaomi.router.v3": "小米路由器3", "xiaomi.split_tv.b1": "Mi TV Bar", "xiaomi.split_tv.v1": "Mi TV Bar", - "xiaomi.switch.test2": "小米自动OTA测试2", - "xiaomi.switch.test3": "小米自动OTA测试3", + "xiaomi.switch.a2ota": "Universal Remote", "xiaomi.tv.4kh1": "Mi 4K TV 82", "xiaomi.tv.8kh1": "Mi 8K TV 82", - "xiaomi.tv.b1": "Mi TV", + "xiaomi.tv.b1": "Xiaomi TV", + "xiaomi.tv.dso1h1": "小米电视 大师77吋 OLED", "xiaomi.tv.eaffh1": "小米电视EA系列", "xiaomi.tv.eanfv1": "小米电视EA系列", - "xiaomi.tv.esh1": "小米电视ES 2022", + "xiaomi.tv.eaprh1": "Xiaomi TV EA Pro", + "xiaomi.tv.esh1": "Xiaomi TV ES 2022", "xiaomi.tv.fsi1": "Mi Full Screen TV", "xiaomi.tv.fsprov1": "Mi Full Screen TV Pro", "xiaomi.tv.fsv1": "Mi Full Screen TV", "xiaomi.tv.h1": "Mi Mural TV 65", - "xiaomi.tv.i1": "Mi TV", + "xiaomi.tv.i1": "Xiaomi TV", "xiaomi.tv.mitv5h1": "MiTV 5", "xiaomi.tv.mitv5proh1": "MiTV 5Pro", "xiaomi.tv.mitvsp": "Xiaomi TV", "xiaomi.tv.oledh1": "Mi OLED TV 65 inch", + "xiaomi.tv.rmaxh1": "Redmi Max Smart TV", "xiaomi.tv.rmaxv1": "redmi Max系列", - "xiaomi.tv.rmh1": "RedmiTV X series", + "xiaomi.tv.rmh1": "Redmi TV X series", "xiaomi.tv.rmi1": "Redmi TV", "xiaomi.tv.rmv1": "Redmi TV", "xiaomi.tv.tv6h1": "小米电视6至尊版", - "xiaomi.tv.v1": "Mi TV", + "xiaomi.tv.v1": "Xiaomi TV", + "xiaomi.tv.z2oh1": "小米电视6 oled系列", "xiaomi.tvbox.4prob1": "小米盒子4S Pro", "xiaomi.tvbox.b1": "Mi Box", "xiaomi.tvbox.i1": "Mi Box", @@ -3182,7 +4205,9 @@ "xiaomi.watch.band1S": "小米手环", "xiaomi.watch.band2": "小米手环", "xiaomi.wifispeaker.l04m": "Mi Smart Clock 4inch", + "xiaomi.wifispeaker.l05b": "Mi AI Speaker Play", "xiaomi.wifispeaker.l05c": "Mi AI Speaker Play Plus", + "xiaomi.wifispeaker.l05g": "Xiaomi Smart Speaker (IR Control)", "xiaomi.wifispeaker.l06a": "Mi AI Speaker II", "xiaomi.wifispeaker.l09a": "Mi Smart Speaker Art", "xiaomi.wifispeaker.l09b": "Mi Smart Speaker Art II", @@ -3191,7 +4216,7 @@ "xiaomi.wifispeaker.l7a": "Redmi AI Speaker Play", "xiaomi.wifispeaker.lx01": "Mi AI Speaker Mini", "xiaomi.wifispeaker.lx04": "Mi Smart Clock 4inch", - "xiaomi.wifispeaker.lx05": "Mi AI Speaker Play", + "xiaomi.wifispeaker.lx05": "Mi AI Speaker Play 2019", "xiaomi.wifispeaker.lx06": "Mi AI Speaker Pro", "xiaomi.wifispeaker.lx5a": "Mi AI Speaker Remote", "xiaomi.wifispeaker.s12": "Mi AI Speaker", @@ -3200,12 +4225,17 @@ "xiaomi.wifispeaker.x08a": "Mi Smart Display Pro 8inch", "xiaomi.wifispeaker.x08c": "Redmi Smart Display 8inch", "xiaomi.wifispeaker.x08e": "Redmi Smart Display Pro 8inch", + "xiaomi.wifispeaker.x10a": "Xiaomi Smart Display 10", "xiaovv.camera.c1": "xiaovv Babymonitor", + "xiaovv.camera.c12k": "xiaovv Babymonitor 2K", "xiaovv.camera.lamp": "Flood light camera", "xiaovv.camera.p1": "xiaovv Outdoor PTZ Camera 2K", + "xiaovv.camera.p94mp": "xiaovv Outdoor PTZ Camera Pro 2.5K", "xiaovv.camera.ptz": "Outdoor 360 Webcam", "xiaovv.camera.q1": "xiaovv PTZ Dome Camera", "xiaovv.camera.q12": "xiaovv Kitten Camera", + "xiaovv.camera.q2": "xiaovv Kitten Camera Pro", + "xiaovv.camera.q24mp": "xiaovv Kitten Camera Pro 2.5K", "xiaovv.camera.q8": "xiaovv Smart Wifi PTZ Camera 2K", "xiaovv.camera.xva3": "xiaovv smart panoramic camera", "xiaovv.camera.xvb4": "xiaovv Outdoor Camera", @@ -3226,7 +4256,9 @@ "xiaoxun.watch.sw762": "Mi Kids Smartwatch 4C", "xiaoxun.watch.sw763": "Mi Kids Smartwatch 4X", "xiaoxun.watch.sw766": "Mi Kids Smartwatch 5X", + "xiaoxun.watch.sw767": "Mi Kids Smartwatch 5Pro", "xiaoxun.watch.sw768": "Mi Kids Smartwatch 5C", + "xiaoxun.watch.sw772": "Mi Kids Smartwatch 6C", "xiaoxun.watch.sw900": "Xun AI Smartwatch MAX Pro", "xiaoxun.watch.sw960": "Mi Kids Smartwatch 4 Pro", "xiaoxun.watch.v1": "Mi Bunny Watch", @@ -3249,9 +4281,11 @@ "xjx.toilet.pro": "Uclean Smart Toilet Seat", "xjx.toilet.pure": "Uclean smart toilet pure", "xjx.toilet.relax": "Uclean smart toilet relax", + "xjx.toilet.relaxp": "Uclean smart toilet relax plus", "xjx.toilet.zero": "Whale Spout Smart Toilet Zero", "xkwl.light.dlight": "Domoticz-灯", "xkwl.switch.donoff": "Domoticz-开关", + "xlang.desk.djz": "NOC LOC intelligent E-sports lifting table", "xlang.desk.nlocv1": "NOC LOC Intelligent electric learning desk for children", "xlang.desk.vlocv2": "Adult lift table", "xqh.curtain.a20311": "窗帘电机 Zigbee版", @@ -3276,15 +4310,33 @@ "xzh.tv.tv": "电视", "xzh.vacuum.vacuum": "扫地机器人", "xzx.light.wyxzx1": "Xi Zhi Xi Intelligent Lamp", + "yafeng.aircondition.woa": "WO空调", + "yafeng.airfresh.woaf": "WO新风", + "yafeng.airrtc.wortc": "WO温控器", + "yafeng.curtain.woc": "WO窗帘", + "yafeng.light.wol": "WO灯光", "yaguan.light.bulb": "彩灯", "yaguan.light.bulb02": "RGBCW彩灯", "yaguan.light.dimmer": "调光面板", + "yaguan.light.double": "CW双色灯", + "yaguan.light.fan2": "风扇灯", + "yaguan.light.fan3": "风扇灯", "yaguan.light.light": "灯带", "yaguan.light.strip": "灯带", "yaguan.light.strip2": "RGBCW灯带", + "yaguan.light.strip3": "CW灯带", "yaguan.plug.outlet": "插座", + "yaguan.switch.oneswi": "开关", "yaguan.switch.relay": "通断器", "yaguan.switch.test": "雅观接入测试", + "yakino.curtain.e10": "YaKino smart curtain", + "yankon.airer.l66": "Yankon Intelligent Clothes Rack", + "yankon.light.xdd01": "Sunshine Smart Ceiling Light", + "yanmin.light.light": "照明灯", + "yanmin.plug.socket": "智能插座", + "ybb.magic_touch.ms": "Eye Massager", + "ybb.massage.ms": "Smart Massage Chair", + "ybf.light.wy0a01": "One hundred points smart spotlight", "ydhome.cateye.pr1": "Uodi Smart Doorbell R1", "ydhome.lock.c1p": "Uodi Smart Lock C1P", "ydhome.lock.m2lite": "优点智能锁M2Lite", @@ -3295,14 +4347,21 @@ "ydhome.switch.s3": "Youdian Smart Bluetooth Mesh Switch", "ydzl.waterpuri.t1": "Uodi Cuber", "yeedi.vacuum.750": "yeedi-k750", + "yeedi.vacuum.k10": "yeedi-k10", + "yeedi.vacuum.k20": "yeedi-k20", "yeedi.vacuum.k650": "yeedi-k650", "yeedi.vacuum.k720": "yeedi-k720", + "yeedi.vacuum.k730": "yeedi-k730", "yeedi.vacuum.k781": "yeedi-k781", "yeedi.vacuum.k801": "yeedi-k801", "yeedi.vacuum.k802": "yeedi-k802", "yeedi.vacuum.k803": "yeedi-k803", "yeedi.vacuum.k804": "yeedi-k804", + "yeedi.vacuum.k950": "yeedi-k950", + "yeedi.vacuum.k960": "yeedi-k960", "yeedi.vacuum.yeedi": "一点智能扫地机器人", + "yeelink.airer.fold1": "Yeelight Smart Clothes Rack E2101", + "yeelink.airer.fold2": "Yeelight Smart Clothes Rack E2102", "yeelink.bhf_light.v1": "Yeelight Smart Bath Heater Pro", "yeelink.bhf_light.v2": "Yeelight Smart Bath Heater", "yeelink.bhf_light.v3": "Yeelight Smart Bath Heater Duo", @@ -3313,6 +4372,7 @@ "yeelink.controller.v2": "Yeelight S21 Smart Scene Panel", "yeelink.curtain.ctmt1": "Yeelight Smart Curtain Controller", "yeelink.curtain.procm1": "易来窗帘电机", + "yeelink.curtain.ywc01": "Yeelight Pro Curtain", "yeelink.gateway.pro01": "Yeelight Pro Gateway", "yeelink.gateway.v1": "Yeelight Mesh Hub", "yeelink.gateway.va": "Yeelight Mesh Hub", @@ -3328,10 +4388,13 @@ "yeelink.light.ceil32": "Yeelight Ceiling Light E2001", "yeelink.light.ceil33": "Yeelight Ceiling Light for Children C2002", "yeelink.light.ceil34": "Mi Smart LED Ceiling Light (350mm)", + "yeelink.light.ceil35": "Yeelight Intelligent Led Droplight", + "yeelink.light.ceil36": "Yeelight Intelligent Led Droplight S", "yeelink.light.ceila": "Yeelight LED Ceiling Light Pro", "yeelink.light.ceilb": "Yeelight Arwen Ceiling Light 450S/550S", - "yeelink.light.ceilc": "Yeelight Arwen Ceiling Light 450C/550C", + "yeelink.light.ceilc": "Yeelight Ambience Ceiling Light", "yeelink.light.ceild": "Yeelight Minas Ceiling Light", + "yeelink.light.ceile": "Yeelight LED Ceiling Light Pro", "yeelink.light.ceiling1": "Yeelight Ceiling Light", "yeelink.light.ceiling10": "Yeelight Crystal Pendant Lamp", "yeelink.light.ceiling11": "Yeelight Ceiling Light 320 1S", @@ -3365,8 +4428,10 @@ "yeelink.light.colora": "Yeelight Smart LED Bulb 1SE (color)", "yeelink.light.colorb": "Yeelight LED smart bulb W3(Multicolor)", "yeelink.light.colorc": "Yeelight GU10 smart bulb W1(multicolor)", + "yeelink.light.colore": "Yeelight Smart LED Bulb W4 lite(multicolor)", "yeelink.light.ct2": "Yeelight LED Bulb (Tunable)", "yeelink.light.cta": "Yeelight LED smart bulb W3(tunable white)", + "yeelink.light.ctc": "Yeelight Smart LED Bulb W4 lite(dimmable)", "yeelink.light.dn2grp": "Mi Mesh Downlight", "yeelink.light.dnlight2": "Yeelight Mesh LED Downlight", "yeelink.light.fancl1": "Yeelight Smart Ceiling Fan", @@ -3378,6 +4443,9 @@ "yeelink.light.lamp15": "Yeelight Screen Light Bar", "yeelink.light.lamp17": "Yeelight Wireless Charging Table Lamp", "yeelink.light.lamp2": "Mi Smart LED Desk Lamp Pro", + "yeelink.light.lamp21": "Mi Smart Rechargeable Desk Lamp", + "yeelink.light.lamp22": "Mi Smart Computer Monitor Light Bar 1S", + "yeelink.light.lamp27": "Mijia LED Desk Lamp 1S (Enhanced Edition)", "yeelink.light.lamp3": "Yeelight LED Lamp", "yeelink.light.lamp4": "Mi LED Desk Lamp 1S", "yeelink.light.lamp5": "Yeelight Smart Desk Lamp Prime", @@ -3389,7 +4457,7 @@ "yeelink.light.mbulb3": "Mi Smart Bluetooth Mesh LED Bulb", "yeelink.light.meshbulb1": "Yeelight Mesh LED Bulb", "yeelink.light.meshbulb2": "Yeelight Mesh LED Bulb", - "yeelink.light.ml1": "Yeelight tunable white downlight \u0026 spotlight M2", + "yeelink.light.ml1": "Yeelight downlight \u0026 spotlight", "yeelink.light.ml2": "Yeelight tunable white bulb M2", "yeelink.light.ml3": "Yeelight Surface mounted downlight M1", "yeelink.light.mono1": "Yeelight Bulb", @@ -3399,6 +4467,7 @@ "yeelink.light.monoa": "Yeelight LED smart bulb W3(dimmable)", "yeelink.light.monob": "Yeelight GU10 Smart Bulb W1(dimmable)", "yeelink.light.nl1": "Mi Motion-Activated Night Light 2 (Bluetooth)", + "yeelink.light.nl2": "MI Smart Night Light", "yeelink.light.panel1": "Yeelight Whiteglow Panel Light", "yeelink.light.panel3": "Yeelight Haobai LED Panel light Pro", "yeelink.light.plate2": "Yeelight Magi Light", @@ -3410,16 +4479,35 @@ "yeelink.light.strip2": "Yeelight Lightstrip Plus", "yeelink.light.strip4": "Yeelight Willow LED Lightstrip", "yeelink.light.strip6": "Yeelight LED Lightstrip 1S", + "yeelink.light.strip8": "Yeelight Chameleon Gorgeous Light strip", "yeelink.light.stripa": "Yeelight LED Lightstrip 1S", "yeelink.light.virtual": "Light Group (Mi \u0026 Yeelight)", "yeelink.light.vtl_ble1": "Yeelight Bedside Lamp(Virtual)", "yeelink.light.yct01": "Yeelight Pro Tunable White Light", + "yeelink.light.ydim01": "Yeelight Pro Dimmable Light", + "yeelink.light.yrgb01": "Yeelight Pro Color Light", + "yeelink.magnet.ycs02": "Yeelight Pro Contact Sensor", "yeelink.mirror.bm1": "diiib\u0026Yeelight smart mirror cabinet", + "yeelink.motion.ymt02": "Yeelight Pro Motion Sensor", "yeelink.plug.prosw": "pro_switch", + "yeelink.remote.contrl": "Xiaomi Wireless Switch (2 Gang)", "yeelink.remote.remote": "Yeelight Wireless Switch S1", + "yeelink.remote.yrm02": "Yeelight Pro Control Panel (8-keys)", + "yeelink.remote.yrm03": "Yeelight Pro Control Panel (4-keys)", + "yeelink.remote.yrm04": "Yeelight Pro Control Panel (6-keys)", + "yeelink.remote.yrm05": "Yeelight Pro Remote Knob", "yeelink.switch.prosw1": "易来开关", "yeelink.switch.sw1": "Yeelight Smart Dual Control Module", + "yeelink.switch.szsw1": "Yeelight smart switch C1 (single key)", + "yeelink.switch.szsw2": "Yeelight smart switch C1 (double keys)", + "yeelink.switch.szsw3": "Yeelight smart switch C1 (three keys)", + "yeelink.switch.ysw01": "Yeelight Pro Switch", + "yeelink.switch.ysw02": "Yeelight Pro Switch (2-Channels)", + "yeelink.switch.ysw03": "Yeelight Pro Switch (3-Channels)", + "yeelink.switch.ysw05": "Yeelight Pro Dual Control Module", "yeelink.ven_fan.vf1": "Yeelight Smart Panel Fan", + "yeelink.ven_fan.vf3": "Yeelight Smart Panel Fan E1", + "yeelink.ven_fan.vf5": "Yeelight Smart Ventilation Fan E1", "yeelink.wifispeaker.v1": "Yeelight Smart Speaker", "yh1209.airer.public": "yonghui intelligent clothes hanger", "yh1209.airer.yhxm": "Heichuang intelligent washing machine", @@ -3441,17 +4529,22 @@ "yio.light.lwy": "B型控制板", "yio.switch.sw": "随意贴开关", "yjy.wifispeaker.u9": "8inch music panel", + "ykbn.curtain.k9": "YKBN Intelligent curtain motor", "ykkj.aircondition.ykm04": "空调", "ykkj.fan.ykm103": "风扇", "ykkj.plug.ykm122": "插座", "ykkj.stb.ykm101": "机顶盒", "ykkj.switch.ykm121": "开关", + "ylyd.blanket.sk1000": "Eluadi Water heating blanket", "ylzm.light.wyly01": "Leiyuan Intelligent Living Room Lamp", "ymj.light.wy0a02": "Yi Mei Home Smart Light", "ymj.light.wyymj1": "EMG Top halo ceiling lamp", - "ymt.flowerpot.v1": "Yimitian smart food", + "ymt.flowerpot.v1": "Onemi smart planting partner", + "ymt.flowerpot.v3": "Onemi Smart Planter", + "ymwl.curtain.cura01": "YMWL smart curtain motor M1", "yongqi.aircondition.ac": "空调", "yongqi.airfresh.fw": "新风", + "yongqi.airrtc.dn": "地暖", "yongqi.curtain.curt": "窗帘", "yongqi.switch.yq0001": "开关", "yonsz.aircondition.air": "红外空调", @@ -3465,8 +4558,11 @@ "yonsz.tv.0": "红外电视", "yonsz.vacuum.0": "红外扫地机", "yonsz.vacuum.1": "红外扫地机", + "yooeca.tow_w.mj04": "Smart towel dryer", "yooled.light.light": "YOOLED智能吸顶灯", + "yopins.tow_w.mimj01": "Youpin intelligent towel machine", "youpon.bhf_light.yb0a01": "YOUPON Smart bath heater DZ", + "yszh.wopener.bs817": "Intelligent window pusher", "yszj.aircondition.f0025": "空调遥控器", "yszj.bed.5110": "智能床垫", "yszj.curtain.2a10": "智能窗帘", @@ -3476,9 +4572,13 @@ "yszj.light.2210": "单色灯", "yszj.switch.2410": "智能插座", "yszj.tv.f0022": "电视遥控器", + "yszn01.airer.ys2102": "晾芯M1智能电动晾衣机pro", + "yszn01.airer.ys2103": "M1 intelligent electric clothes dryer", "yuadon.plug.socket": "socket", "yuansh.light.lysp": "智能灯", "yuemee.airmonitor.mhfd1": "Honeywell Smart Formaldehyde Monitor", + "yuemee.sensor_gas.0605": "Residential Flammable Gas Detector", + "yuemee.sensor_gas.56712": "Residential Flammable Gas Detector", "yuerzj.aircondition.air": "16A Intelligent Air Conditioning Socket", "yuerzj.curtain.cm01": "智能窗帘", "yuerzj.plug.zgsocket": "zigbee wall socket", @@ -3486,6 +4586,7 @@ "yuerzj.vacuum.rc01": "扫地机器人", "yugang.plug.dmc605": "WiFi智能墙壁插座", "yugang.switch.switch": "智能双控开关", + "yunlu.door.sd2101": "Yunlu Security Smart Door P1", "yunmai.scales.m1690": "YUNMAI mini2", "yunmi.kettle.r1": "VIOMI Smart Instant Heating Water Dispenser (MINI)", "yunmi.kettle.r2": "VIOMI Smart Instant Heating Water Dispenser (MINI)", @@ -3516,6 +4617,10 @@ "yunmi.waterpuri.lx14": "Mi Water Purifier H1000G", "yunmi.waterpuri.lx2": "Mi Water Purifier", "yunmi.waterpuri.lx20": "Mi Water Purifier 1200G", + "yunmi.waterpuri.lx21": "Mi instant heating water purifier Q800", + "yunmi.waterpuri.lx24": "Mi Dual-core Water Purifier 1000G", + "yunmi.waterpuri.lx27": "Mijia Water Purifier 1000G", + "yunmi.waterpuri.lx28": "Mijia Water Purifier 1200G", "yunmi.waterpuri.lx3": "Mi Water Purifier (Under Counter)", "yunmi.waterpuri.lx4": "Mi Water Purifier", "yunmi.waterpuri.lx5": "Mi Water Purifier 1A/400G Pro", @@ -3526,19 +4631,27 @@ "yunmi.waterpuri.mg5": "Viomi instant heat pipeline machine(MG2-A)", "yunmi.waterpuri.s10": "viomi water purifier super 1200G", "yunmi.waterpuri.s11": "Viomi Smart Water purifier Blues (400G)", - "yunmi.waterpuri.s12": "Viomi Smart Water purifier Blues (600G)", + "yunmi.waterpuri.s12": "Viomi Blues Lite 600G", "yunmi.waterpuri.s14": "云米·泉先互联网净水器 小白龙 600G", "yunmi.waterpuri.s15": "viomi smart water purifier dolphin 400G", "yunmi.waterpuri.s16": "viomi smart water purifier dolphin 600G", "yunmi.waterpuri.s17": "viomi smart water purifier Fast3 800G", "yunmi.waterpuri.s18": "viomi smart water purifier Mee 600G", + "yunmi.waterpuri.s19": "Viomi water purifier super PRO 1000G", "yunmi.waterpuri.s20": "viomi water purifier super PRO 1200G", + "yunmi.waterpuri.s21": "Viomi AI heating water purifier Super Pro", + "yunmi.waterpuri.s23": "Viomi AI water purifier Super Pro 1200G", + "yunmi.waterpuri.s26": "Viomi AI water purifier Blues", + "yunmi.waterpuri.s29": "Viomi AI water purifier Super S", "yunmi.waterpuri.s3": "Viomi smart water purifier SE(400G)", + "yunmi.waterpuri.s30": "Viomi AI water purifier Super Y", "yunmi.waterpuri.s4": "Viomi smart water purifier S2(500G)", "yunmi.waterpuri.s5": "Viomi smart water purifier S2(600G)", "yunmi.waterpuri.s9": "Viomi water purifier super 1000G", "yunmi.waterpuri.x10": "Viomi ai protable heating water purifier X2", "yunmi.waterpuri.x11": "Viomi protable heating water purifier X2 mini", + "yunmi.waterpuri.x12": "Viomi protable heating water purifier X2 face", + "yunmi.waterpuri.x13": "Viomi protable heating water purifier X2 mini Mango", "yunmi.waterpuri.x2": "Viomi 1sec instant Water Heating Purifier X3(Premium Version)", "yunmi.waterpuri.x7": "Viomi 1sec Instant Water Heating Purifier X1", "yunmi.waterpuri.x8": "Viomi protable heating water purifier X2", @@ -3546,12 +4659,21 @@ "yunmi.waterpurifier.v2": "Mi Water Purifier", "yunmi.waterpurifier.v3": "Mi Water Purifier (Under sink)", "yunmi.waterpurifier.vtl_v2": "Mi Water Purifier(Virtual)", + "yunmi.ysj.mg6": "Viomi instant heat pipeline water dispenser", + "yunmi.ysj.v1": "Mijia Smart Hot and Cold Water Dispenser", + "yunmi.ysj.x1": "Viomi AI protable heating water purifier X2 mini Mango", "yunshi.lock.s2": "SherLock M1", + "yuntec.light.lampt1": "灯带控制器", + "yuntec.switch.switch": "智能开关", "yuntec.switch.tkwv1": "三键开关v1", + "yuntec.switch.valvv1": "阀门控制器", "yunyi.camera.v1": "Yi Smart Webcam", "yuxuan.light.wy0a01": "Yuxuan Smart Ceiling Light", + "yxrj.switch.c100": "智能开关", + "yyunyi.wopener.yylt": "Yunyi chain window opener", "yyunyi.wopener.yypy24": "Yun yi translation window opener", "yyzhn.gateway.yn181126": "LangYueSecurity", + "zair.light.zcw02w": "ZAIR Smart floor lamp (Common)", "zdeer.ajh.a8": "ZDEER Moxibustion Box", "zdeer.ajh.a9": "ZDEER Moxibustion Box 2th", "zdeer.ajh.ajb": "Warm Moxibustion Box", @@ -3569,6 +4691,7 @@ "zdzn.light.zun222": "广州准德", "zdzn.stb.66960": "广州准德机顶盒类", "zdzn.switch.66749": "广州准德开关类", + "zemi.curtain.zm25": "Zemismart Smart rolling curtain motor", "zengge.light.bulb": "Bulb", "zengge.light.cct": "CCT Bulb", "zengge.light.cctc": "CCT LED Controller", @@ -3581,6 +4704,14 @@ "zeroy.fan.m93": "风扇", "zeroy.humidifier.m91": "加湿器", "zeroy.light.m930": "灯", + "zgzlj.light.bulb": "Bulb", + "zgzlj.light.cct": "CCT Bulb", + "zgzlj.light.cctc": "CCT LED Controller", + "zgzlj.light.ctrler": "LED Controller", + "zgzlj.light.rgb": "RGB LED Controller", + "zgzlj.light.rgbw": "RGBCW Bulb", + "zgzlj.light.rgbwc": "RGBCW LED Controller", + "zhaomu.safe.bgx45l": "BOFON W series", "zhihw.aircondition.yk7": "遥控大师-wifi转红外-空调", "zhihw.curtain.uiid11": "curtain", "zhihw.fan.yk0006": "遥控大师-wifi转红外-风扇", @@ -3599,7 +4730,7 @@ "zhihw.tv.yk0002": "遥控大师-wifi转红外-电视机", "zhihw.tv.yk0010": "遥控大师-wifi转红外-电视盒子", "zhihw.wifispeaker.yk13": "遥控大师-wifi转红外-音箱", - "zhij.toothbrush.bv1": "Smartknow智能杀菌杯蓝牙版", + "zhij.toothbrush.bv1": "Smart Sterilization Cup", "zhijia.aircondition.ktq": "ZJ Air conditioning controller", "zhijia.curtain.mckhl101": "Intelligent Electric Curtain Motor (Open and Close Curtain)", "zhijia.fan.fan": "ZJ智能风扇", @@ -3624,9 +4755,21 @@ "zhimi.airfresh.va2": "Smartmi Ventilation System", "zhimi.airfresh.va4": "Smartmi Fresh Air System (Heating)", "zhimi.airmonitor.v1": "Mi PM2.5 Air Quality Monitor", + "zhimi.airp.cpa4": "Xiaomi Smart Air Purifier 4 Compact", + "zhimi.airp.ja1": "Jya Fjord Atom Formaldehyde Air Purifier", + "zhimi.airp.ja2": "Jya Fjord Anti Sterilize Air Purifier", + "zhimi.airp.jc1": "Jya Fjord Atom Air Purifier", "zhimi.airp.mb3a": "Mi Air Purifier 3/3H", "zhimi.airp.mb4a": "Mi Air Purifier 3C", + "zhimi.airp.mb5": "Xiaomi Smart Air Purifier 4", + "zhimi.airp.mb5a": "Xiaomi Smart Air Purifier 4", + "zhimi.airp.mp4": "Xiaomi Smart Air Purifier 4", + "zhimi.airp.mp4a": "Xiaomi Smart Air Purifier 4", + "zhimi.airp.rmb1": "Xiaomi Smart Air Purifier 4 Lite", + "zhimi.airp.va2": "Xiaomi Smart Air Purifier 4 Pro", + "zhimi.airp.va2a": "Xiaomi Smart Air Purifier 4 Pro", "zhimi.airp.vb2a": "Mi Air Purifier Pro H", + "zhimi.airp.vb4": "Xiaomi Smart Air Purifier 4 Pro", "zhimi.airpurifier.m1": "Mi Air Purifier 2", "zhimi.airpurifier.m2": "Mi Air Purifier 2", "zhimi.airpurifier.ma1": "Mi Air Purifier 2S", @@ -3638,6 +4781,7 @@ "zhimi.airpurifier.mc1": "Mi Air Purifier 2S", "zhimi.airpurifier.mc2": "Mi Air Purifier 2H", "zhimi.airpurifier.oa1": "Mi Desktop Air Purifier", + "zhimi.airpurifier.rma1": "Xiaomi Smart Air Purifier 4 Lite", "zhimi.airpurifier.sa2": "Mi Air Purifier MAX / MAX Pro", "zhimi.airpurifier.sb1": "Mi Air Purifier MAX", "zhimi.airpurifier.v1": "Mi Air Purifier", @@ -3662,28 +4806,41 @@ "zhimi.fan.za1": "Smartmi Inverter Pedestal Fan", "zhimi.fan.za3": "Smartmi Standing Fan 2", "zhimi.fan.za4": "Smartmi Standing Fan 2S", - "zhimi.fan.za5": "Smartmi Standing Fan 3 ", + "zhimi.fan.za5": "Smartmi Standing Fan 3", "zhimi.heater.ma1": "Mi Smart Space Heater", "zhimi.heater.ma2": "Mi Smart Space Heater S", + "zhimi.heater.ma2a": "Mi Smart Space Heater S", "zhimi.heater.ma3": "Mi Smart Baseboard Heater E", + "zhimi.heater.ma4": "Xiaomi Smart Graphene Space Heater", "zhimi.heater.mc2": "Mi Smart Space Heater S", "zhimi.heater.na1": "Smartmi Smart Fan", "zhimi.heater.nb1": "Smartmi Smart Fan Heater", "zhimi.heater.za1": "Smartmi Radiant Heater Smart Version", "zhimi.heater.za2": "Smartmi Smart Convector Heater 1S", + "zhimi.heater.za3": "Smartmi smart graphene heater", "zhimi.heater.zb1": "Smartmi Smart Convector Heater 1S", + "zhimi.heater.zb1a": "Smartmi Smart Convector Heater 1S", "zhimi.humidifier.ca1": "Smartmi Evaporative Humidifier", "zhimi.humidifier.ca4": "Smartmi Evaporative Humidifer 2", "zhimi.humidifier.cb1": "Smartmi Evaporative Humidifier", + "zhimi.humidifier.cb1a": "Smartmi Evaporative Humidifier", "zhimi.humidifier.cb2": "Smartmi Evaporative Humidifier", "zhimi.humidifier.v1": "Smartmi Humidifier", "zhimi.humidifier.va1": "Smartmi Antibacterial Humidifier 1S", "zhimi.lock.da1": "Xiaomiyoupin Smart Door Lock", "zhimi.lock.da2": "Noc Loc Smart Door Lock - Stainless Steel Version", "zhimi.toilet.pa2": "Smartmi Smart Toilet M1", + "zhimi.toilet.pa3": "Smartmi Smart Toilet C1", "zhimi.toilet.sa1": "Smartmi Smart Bidet Toilet Seat Pro", + "zhimi.toilet.ta1": "Smartmi Smart Bidet Toilet Seat (Fragrance Linkage Version)", "zhimi.toilet.va1": "Smartmi Smart Bidet Toilet Seat S", + "zhimi.vacuum.ga1": "Lydsto Inertial Navigation Sweeping and Mopping Robot G1", + "zhimi.vacuum.hd1": "Lydsto Inertial Navigation Sweeping and Mopping Robot G2", + "zhimi.vacuum.l1": "Lydsto Sweeping and Mopping Robot L1", + "zhimi.vacuum.wa1": "Smartmi VortexWave Robot Vacuum Cleaner", "zhimi.vacuum.xa1": "Lydsto Sweeping and Mopping Robot R1", + "zhimi.vacuum.xa2": "Lydsto Sweeping and Mopping Robot R1D", + "zhimi.vacuum.xa3": "Lydsto Sweeping and Mopping Robot S1", "zhimi.waterpuri.ma1": "Mi Water Purifier H400G", "zhiqin.switch.mij04": "Chloroplast G1 Smart Wall Switch (Zero Fire Wire Single Key)", "zhiqin.switch.mij05": "Chloroplast G1 Smart Wall Switch (Zero Fire Line Double Bond)", @@ -3691,12 +4848,22 @@ "zhiqin.switch.x101": "CHLOROP G1 Smart Wall Switch (No Neutral Single Rocher)", "zhiqin.switch.x102": "CHLOROP G1 Smart Wall Switch (No Neutral Double Rocher)", "zhiqin.switch.x103": "CHLOROP G1 Smart Wall Switch (No Neutral Triple Rocher)", + "zhltkj.aircondition.ac": "LTECH空调", + "zhltkj.curtain.lt": "LTECH窗帘", + "zhltkj.fan.fan": "LTECH风扇", + "zhltkj.light.light": "LTECH灯具", + "zhltkj.tv.tv": "LTECH电视", + "zhtech.curtain.zm25s": "Smart emperor intelligent rolling curtain motor", + "zhtech.curtain.zm83": "Smart emperor open and close curtain motor", "zhu123.curtain.curtain1": "smartplus", "zhu123.curtain.v3": "smart plus curtain", "zhu123.switch.1": "smart light", "zhuyun.curtain.1": "电机控制", "zhuyun.light.22": "智能LED灯", "zhuyun.light.zy22": "智能LED灯3", + "zhzt.switch.nia11": "NVC Mesh Wall Switch(1 key)", + "zhzt.switch.nia21": "NVC Mesh Wall Switch(2 keys)", + "zhzt.switch.nia31": "NVC Mesh Wall Switch(3 keys)", "zichen.light.wy0a01": "Xiao ran all copper Smart Light", "zigma.airp.aerio": "Zigma空气净化器系列", "zigma.vacuum.laser": "Zigma扫地机系列", @@ -3708,23 +4875,43 @@ "zimi.powerstrip.v2": "Mi Smart Power Strip", "zimi.powerstrip.vtl_v2": "Mi Smart Power Strip(Virtual)", "zimi.projector.v1": "Mijia projector TYY01ZM", - "zimi.switch.dhkg01": "Mi Smart Single one way Wall Switch ", - "zimi.switch.dhkg02": "Mi Smart Dual one way Wall Switch", + "zimi.switch.dhkg01": "Mi Smart Single One Way Wall Switch", + "zimi.switch.dhkg02": "Mi Smart Dual One Way Wall Switch", + "zimi.switch.dhkg05": "Xiaomi Smart Wall Switch (3 Gang)", "zinguo.plug.c2m": "Smart Plug (Wi-Fi)", + "zinguo.switch.b5m": "Smart Bath Heater Switch", "zinguo.switch.k2": "WIFI Switch", "ziwooo.s_lamp.uvc": "ZIWOOO Intelligent germicidal lamp Pro", + "zjaupu.airer.l557": "AUPU Airer-L557", + "zjaupu.airer.lxx6": "AUPU hot-drying airer", + "zjaupu.airer.lxx62": "AUPU hot-drying airer", + "zjaupu.airer.lxx7": "AUPU swing and hot-drying airer", "zjcx.switch.s6": "6 Gangs Switch", "zjyg.dishwasher.zzy01": " Smart washing dishwasher", "zkea.aircondition.zkea": "三恒系统", + "zktech.switch.pcsw": "远程开机盒", + "znsn.switch.zm1a": "ZNSN BLE-Mesh Wall Switch M1", + "znsn.switch.zm1d": "ZNSN BLE-Mesh Wall Switch ML1 (No Neutral)", + "znsn.switch.zm2a": "ZNSN BLE-Mesh Wall Switch M2", + "znsn.switch.zm2d": "ZNSN BLE-Mesh Wall Switch ML2 (No Neutral)", + "znsn.switch.zm3a": "ZNSN BLE-Mesh Wall Switch M3", + "znsn.switch.zm3d": "ZNSN BLE-Mesh Wall Switch ML3 (No Neutral)", + "zs1001.mosq.s12pro": "Qualitell Digital Display Mosquito Swatter --Wifi Version", "zszm.light.wy0a02": "ZSZM Intelligent spotlight", "zszm.light.wy0a03": "ZSZM Intelligent ceiling lamp", "ztao.switch.pck154": "电脑开机卡", + "zting.curtain.333": "智汀窗帘", + "zting.light.1234": "灯", + "zting.switch.111": "智汀单键开关", "zunder.aircondition.air": "air", "zunder.light.66744": "声控精灵灯光类2", "zunder.light.zun111": "light", "zunder.light.zun222": "声控精灵灯光类4", "zunder.stb.zunder": "Set top box", "zunder.switch.onoff": "out", + "zunge.light.wy0a01": "Zun Pavilion Crown Smart Lamp", + "zxgs.light.mdcl01": "mesh dual color light", + "zxzm.light.j01": "J.zao ceiling light", "zyzy1.aircondition.0715": "空调", "zyzy1.aircondition.y712": "智能空调", "zyzy1.fan.y07011": "智能米家风扇", From b0d4f5a5fd7468ca45cc6ffd36778a6476dd7438 Mon Sep 17 00:00:00 2001 From: Florian Hotze Date: Sun, 6 Nov 2022 20:15:10 +0100 Subject: [PATCH 33/55] [jsscripting] Update openhab-js version to 2.1.0 (#13664) Signed-off-by: Florian Hotze --- .../README.md | 223 ++++++++---------- .../pom.xml | 2 +- 2 files changed, 94 insertions(+), 131 deletions(-) diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md index 12cee25054a3f..6c29440ec7246 100644 --- a/bundles/org.openhab.automation.jsscripting/README.md +++ b/bundles/org.openhab.automation.jsscripting/README.md @@ -140,10 +140,14 @@ Script debug logging is enabled by default at the `INFO` level, but can be confi log:set DEBUG org.openhab.automation.script ``` -The default logger name prefix is `org.openhab.automation.script`, this can be changed by assigning a new prefix to the `loggerName` property of the console. +The default logger name prefix is `org.openhab.automation.script`, this can be changed by assigning a new string to the `loggerName` property of the console. + +Please be aware that messages might not appear in the logs if the logger name does not start with `org.openhab`. +This behaviour is due to [log4j2](https://logging.apache.org/log4j/2.x/) requiring definition for each logger prefix. + ```javascript -console.loggerName = "custom" +console.loggerName = "org.openhab.custom" ``` Supported logging functions include: @@ -162,58 +166,76 @@ See for more informat ### Timers +JS Scripting provides access to the global `setTimeout`, `setInterval`, `clearTimeout` and `clearInterval` methods specified in the [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API). + +When a script is unloaded, all created timers and intervals are automatically cancelled. + #### SetTimeout -The global `setTimeout()` method sets a timer which executes a function or specified piece of code once the timer expires. +The global [`setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) method sets a timer which executes a function or specified piece of code once the timer expires. +`setTimeout()` returns a `timeoutId` (a positive integer value) which identifies the timer created. ```javascript -var ohTimer = setTimeout(callbackFunction, delay); +var timeoutId = setTimeout(callbackFunction, delay); ``` -The global `clearTimeout()` method cancels a timeout previously established by calling `setTimeout()`. +`delay` is an integer value that represents the amount of milliseconds to wait before the timer expires. -The openHAB implementation of `setTimeout()` differs from the [HTML DOM API's `setTimeout()`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout). -openHAB does not return the integer timeoutID as standard JS does, instead it returns an instance of [openHAB Timer](#openhab-timer). +The global [`clearTimeout(timeoutId)`](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout) method cancels a timeout previously established by calling `setTimeout()`. #### SetInterval -The setInterval() method repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. +The global [`setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) method repeatedly calls a function or executes a code snippet, with a fixed time delay between each call. +`setInterval()` returns an `intervalId` (a positive integer value) which identifies the interval created. ```javascript -var ohIntervalTimer = setInterval(callbackFunction, delay); +var intervalId = setInterval(callbackFunction, delay); ``` -The global `clearInterval()` method cancels a timed, repeating action which was previously established by a call to `setInterval()`. +`delay` is an integer value that represents the amount of milliseconds to wait before the timer expires. -NOTE: Timers will not be canceled if a script is deleted or modified, it is up to the user to manage timers. -See using the [cache](#cache) namespace as well as [ScriptLoaded](#initialization-hook-scriptloaded) and [ScriptUnLoaded](#deinitialization-hook-scriptunloaded) for a convenient way of managing persisted objects, such as timers between reloads or deletions of scripts. +The global [`clearInterval(intervalId)`](https://developer.mozilla.org/en-US/docs/Web/API/clearInterval) method cancels a timed, repeating action which was previously established by a call to `setInterval()`. -The openHAB implementation of `setInterval()` differs from the [HTML DOM API's `setInterval()`](https://developer.mozilla.org/en-US/docs/Web/API/setInterval). -openHAB does not return the integer timeoutID as standard JS does, instead it returns an instance of [openHAB Timer](#openhab-timer). +#### Accessing Variables -#### openHAB Timer +You can access all variables of the current context in the created timers. -A native openHAB Timer instance has the following methods: +Note: Variables can be mutated (changed) after the timer has been created. +Be aware that this can lead to unattended side effects, e.g. when you change the variable after timer creation, which can make debugging quite difficult! -- `cancel()`: Cancels the timer. ⇒ `boolean`: true, if cancellation was successful -- `getExecutionTime()`: The scheduled execution time or null if timer was cancelled. ⇒ `time.ZonedDateTime` or `null` -- `isActive()`: Whether the scheduled execution is yet to happen. ⇒ `boolean` -- `isCancelled()`: Whether the timer has been cancelled. ⇒ `boolean` -- `isRunning()`: Whether the scheduled code is currently executed. ⇒ `boolean` -- `hasTerminated()`: Whether the scheduled execution has already terminated. ⇒ `boolean` -- `reschedule(time.ZonedDateTime)`: Reschedules a timer to a new starting time. This can also be called after a timer has terminated, which will result in another execution of the same code. ⇒ `boolean`: true, if rescheduling was successful +```javascript +var myVar = 'Hello world!'; -Examples: +// Schedule a timer that expires in ten seconds +setTimeout(() => { + console.info(`Timer expired with myVar = "${myVar}"`); +}, 10000); -```javascript -var timer = setTimeout(() => { console.log('Timer expired.'); }, 10000); // Would log 'Timer expired.' in 10s. -if (timer.isActive()) console.log('Timer is waiting to execute.'); -timer.cancel(); -if (timer.isCancelled()) console.log('Timer has been cancelled.'); -timer.reschedule(time.ZonedDateTime.now().plusSeconds(2)); // Logs 'Timer expired.' in 2s. +myVar = 'Hello mutation!'; // When the timer runs, it will log "Hello mutation!" instead of "Hello world!" ``` -See [openHAB JavaDoc - Timer](https://www.openhab.org/javadoc/latest/org/openhab/core/model/script/actions/timer) for full API documentation. +If you need to pass some variables to the timer but avoid that they can get mutated, use a function generator. + +**Pass variables using a function generator** + +This allows you to pass variables to the timer's callback function during timer creation. +The variables can NOT be mutated after the timer function generator was used to create the callback function. + +```javascript +// Function generator for the timer's callback function +function cbFuncGen (myVariable) { + return function () { + console.info(`Timer expired with myVar = "${myVariable}"`); + }; +} + +var myVar = 'Hello world!'; + +// Schedule a timer that expires in ten seconds +setTimeout(cbFuncGen(myVar), 10000); + +myVar = 'Hello mutation!'; // When the timer runs, it will log "Hello world!" +``` ### Paths @@ -231,14 +253,14 @@ The items namespace allows interactions with openHAB items. See [openhab-js : items](https://openhab.github.io/openhab-js/items.html) for full API documentation. -- items : object - - .getItem(name, nullIfMissing) ⇒ Item - - .getItems() ⇒ Array.<Item> - - .getItemsByTag(...tagNames) ⇒ Array.<Item> +- items : `object` + - .getItem(name, nullIfMissing) ⇒ `Item` + - .getItems() ⇒ `Array[Item]` + - .getItemsByTag(...tagNames) ⇒ `Array[Item]` - .addItem([itemConfig](#itemconfig)) - - .removeItem(itemOrItemName) ⇒ Boolean + - .removeItem(itemOrItemName) ⇒ `boolean` - .replaceItem([itemConfig](#itemconfig)) - - .safeItemName(s) ⇒ String + - .safeItemName(s) ⇒ `string` ```javascript const item = items.getItem("KitchenLight"); @@ -249,24 +271,26 @@ console.log("Kitchen Light State", item.state); Calling `getItem(...)` returns an `Item` object with the following properties: -- Item : object - - .type ⇒ String - - .name ⇒ String - - .label ⇒ String +- Item : `object` + - .rawItem ⇒ `HostItem` - .history ⇒ [`ItemHistory`](#itemhistory) - - .state ⇒ String - - .rawState ⇒ HostState - - .members ⇒ Array.<Item> - - .descendents ⇒ Array.<Item> - - .isUninitialized ⇒ Boolean - - .groupNames ⇒ Array.<String> - - .tags ⇒ Array.<String> - - .getMetadataValue(namespace) ⇒ String - - .updateMetadataValue(namespace, value) ⇒ String - - .upsertMetadataValue(namespace, value) ⇒ Boolean + - .semantics ⇒ [`ItemSemantics`](https://openhab.github.io/openhab-js/items.ItemSemantics.html) + - .type ⇒ `string` + - .name ⇒ `string` + - .label ⇒ `string` + - .state ⇒ `string` + - .rawState ⇒ `HostState` + - .members ⇒ `Array[Item]` + - .descendents ⇒ `Array[Item]` + - .isUninitialized ⇒ `boolean` + - .groupNames ⇒ `Array[string]` + - .tags ⇒ `Array[string]` + - .getMetadataValue(namespace) ⇒ `string` + - .updateMetadataValue(namespace, value) ⇒ `string` + - .upsertMetadataValue(namespace, value) ⇒ `boolean` - .updateMetadataValues(namespaceToValues) - .sendCommand(value) - - .sendCommandIfDifferent(value) ⇒ Boolean + - .sendCommandIfDifferent(value) ⇒ `boolean` - .postUpdate(value) - .addGroups(...groupNamesOrItems) - .removeGroups(...groupNamesOrItems) @@ -287,17 +311,17 @@ console.log("KitchenLight state", item.state) Calling `addItem(itemConfig)` or `replaceItem(itemConfig)` requires the `itemConfig` object with the following properties: -- itemConfig : object - - .type ⇒ String - - .name ⇒ String - - .label ⇒ String - - .category (icon) ⇒ String - - .groups ⇒ Array.<String> - - .tags ⇒ Array.<String> - - .channels ⇒ String|Object { channeluid: { config } } - - .metadata ⇒ Object { namespace: value }|Object { namespace: { value: value , config: { config } } } - - .giBaseType ⇒ String - - .groupFunction ⇒ String +- itemConfig : `object` + - .type ⇒ `string` + - .name ⇒ `string` + - .label ⇒ `string` + - .category (icon) ⇒ `string` + - .groups ⇒ `Array[string]` + - .tags ⇒ `Array[string]` + - .channels ⇒ `string | Object { channeluid: { config } }` + - .metadata ⇒ `Object { namespace: value } | Object { namespace: { value: value , config: { config } } }` + - .giBaseType ⇒ `string` + - .groupFunction ⇒ `string` Note: `.type` and `.name` are required. Basic UI and the mobile apps need `metadata.stateDescription.config.pattern` to render the state of an Item. @@ -486,74 +510,13 @@ Replace `` with the request url. #### ScriptExecution Actions -See [openhab-js : actions.ScriptExecution](https://openhab.github.io/openhab-js/actions.html#.ScriptExecution) for complete documentation. - -```javascript -var now = time.ZonedDateTime.now(); - -// Function to run when the timer goes off. -function timerOver () { - console.info('The timer is over.'); -} - -// Create the Timer. -var myTimer = actions.ScriptExecution.createTimer('My Timer', now.plusSeconds(10), timerOver); - -// Cancel the timer. -myTimer.cancel(); +The `ScriptExecution` actions provide the `callScript(string scriptName)` method, which calls a script located at the `$OH_CONF/scripts` folder. -// Check whether the timer is active. Returns true if the timer is active and will be executed as scheduled. -var active = myTimer.isActive(); +Please note that `actions.ScriptExecution` also provides access to methods for creating timers, but it is NOT recommended to create timers using that raw Java API! +Usage of those timer creation methods can lead to failing timers. +Instead of those, use the [native JS methods for timer creation](#timers). -// Reschedule the timer. -myTimer.reschedule(now.plusSeconds(5)); -``` - -If you need to pass some variables to the timer, there are two options to do so: - -**Use a function generator** - -This allows you to pass variables to the timer‘s callback function at timer creation. -The variables can not be mutated after the timer function generator was used to create the function. - -```javascript -var now = time.ZonedDateTime.now(); - -// Function to run when the timer goes off. -function timerOver (myVariable) { - return function () { - console.info('The timer is over. myVariable is: ' + myVariable); - } -} - -// Create the Timer. -var myTimer = actions.ScriptExecution.createTimer('My Timer', now.plusSeconds(10), timerOver('Hello world!')); -``` - -**Pass a single variable to the timer** - -This allows you to pass a single variable to the timer's callback function. -Variables can be mutated (changed) after the timer has been created. -Be aware that this can lead to unattended side effects, e.g. when you change the variable after timer creation, which can make debugging quite difficult! - -```javascript -var now = time.ZonedDateTime.now(); - -// Function to run when the timer goes off. -function timerOver () { - console.info('The timer is over. myVariable is: ' + myVariable); -} - -var myVariable = 'Hello world!'; - -// Create the Timer. -var myTimer = actions.ScriptExecution.createTimerWithArgument('My Timer', now.plusSeconds(10), myVariable, timerOver); - -myVariable = 'Hello mutation!'; -// When the timer runs, it will log "Hello mutation!" instead of "Hello world!" -``` - -You may also pass `this` to the timer to have access to all variables from the current context. +See [openhab-js : actions.ScriptExecution](https://openhab.github.io/openhab-js/actions.html#.ScriptExecution) for complete documentation. #### Semantics Actions @@ -954,7 +917,7 @@ This tables gives an overview over the `event` object: | `newState` | `ItemStateChangeTrigger`, `GroupStateChangeTrigger` | New state of Item or Group that triggered event | N/A | | `receivedState` | `ItemStateUpdateTrigger`, `GroupStateUpdateTrigger` | State of Item that triggered event | `triggeringItem.state` | | `receivedCommand` | `ItemCommandTrigger`, `GroupCommandTrigger` | Command that triggered event | `receivedCommand` | -| `itemName` | `Item****Trigger` | Name of Item that triggered event | `triggeringItem.name` | +| `itemName` | `Item****Trigger`, `Group****Trigger` | Name of Item that triggered event | `triggeringItem.name` | | `receivedEvent` | `ChannelEventTrigger` | Channel event that triggered event | N/A | | `channelUID` | `ChannelEventTrigger` | UID of channel that triggered event | N/A | | `oldStatus` | `ThingStatusChangeTrigger` | Previous state of Thing that triggered event | N/A | diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml index 0f3f46aa70cc3..fd2259ec8baf8 100644 --- a/bundles/org.openhab.automation.jsscripting/pom.xml +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -25,7 +25,7 @@ 22.3.0 6.2.1 ${project.version} - openhab@2.0.4 + openhab@2.1.0 From 0470d3978f2c66fb28f5785e042097201a812cbc Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Sun, 6 Nov 2022 20:22:08 +0100 Subject: [PATCH 34/55] [groheondus] Upgrade dependencies (#13665) * Upgrade commons-text to 1.10.0 (prevents CVE-2022-42889) * Upgrade commons-lang3 to 3.12.0 * Remove commons-text, wrap from feature because it is embedded into the bundle Signed-off-by: Wouter Born --- bundles/org.openhab.binding.groheondus/pom.xml | 4 ++-- .../src/main/feature/feature.xml | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.groheondus/pom.xml b/bundles/org.openhab.binding.groheondus/pom.xml index cbd9e8714ef87..9aa25209cf3a7 100644 --- a/bundles/org.openhab.binding.groheondus/pom.xml +++ b/bundles/org.openhab.binding.groheondus/pom.xml @@ -28,13 +28,13 @@ org.apache.commons commons-text - 1.6 + 1.10.0 compile org.apache.commons commons-lang3 - 3.8.1 + 3.12.0 compile diff --git a/bundles/org.openhab.binding.groheondus/src/main/feature/feature.xml b/bundles/org.openhab.binding.groheondus/src/main/feature/feature.xml index 8b7deb7b59864..1d48af09cb19c 100644 --- a/bundles/org.openhab.binding.groheondus/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.groheondus/src/main/feature/feature.xml @@ -4,9 +4,7 @@ openhab-runtime-base - wrap openhab.tp-jackson - wrap:mvn:org.apache.commons/commons-text/1.6 mvn:org.openhab.addons.bundles/org.openhab.binding.groheondus/${project.version} From c7020bfee88535f87b1c42e8b4e863b0e94b56a2 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 7 Nov 2022 06:49:05 +0100 Subject: [PATCH 35/55] [kodi] Fix bridge initialization when parameter group is not configured (#13669) Fixes #13668 Signed-off-by: Jacob Laursen --- .../src/main/resources/OH-INF/config/config.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/config/config.xml index d77aa238aa714..2d3ce0030551d 100644 --- a/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.kodi/src/main/resources/OH-INF/config/config.xml @@ -64,7 +64,7 @@ - + The PVR channel group name used to identify the channel id. All channels From c46713920865e3396ee8c6c7766aedb9b9ade8bf Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Mon, 7 Nov 2022 09:20:20 +0100 Subject: [PATCH 36/55] Fix bridge initialization when parameter quoteList is not configured (#13667) Fixes #13666 Signed-off-by: Jacob Laursen --- .../src/main/resources/OH-INF/thing/thing-types.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.squeezebox/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.squeezebox/src/main/resources/OH-INF/thing/thing-types.xml index d0e872e68d931..ea22fe691f382 100644 --- a/bundles/org.openhab.binding.squeezebox/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.squeezebox/src/main/resources/OH-INF/thing/thing-types.xml @@ -121,7 +121,7 @@ Comma-separated list of favorites of form favoriteId=favoriteName - + Wrap the right hand side of the favorites in quotes false From 0154427c99e5f55dd201d74d079e98afe26fcaab Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Mon, 7 Nov 2022 13:25:06 +0100 Subject: [PATCH 37/55] New Crowdin updates (#13671) * New translations meater.properties (Danish) --- .../OH-INF/i18n/meater_da.properties | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 bundles/org.openhab.binding.meater/src/main/resources/OH-INF/i18n/meater_da.properties diff --git a/bundles/org.openhab.binding.meater/src/main/resources/OH-INF/i18n/meater_da.properties b/bundles/org.openhab.binding.meater/src/main/resources/OH-INF/i18n/meater_da.properties new file mode 100644 index 0000000000000..befdef22fe1c8 --- /dev/null +++ b/bundles/org.openhab.binding.meater/src/main/resources/OH-INF/i18n/meater_da.properties @@ -0,0 +1,63 @@ +# binding + +binding.meater.name = MEATER Binding +binding.meater.description = Dette er bindingen til MEATER trådløst stegetermometer. + +# thing types + +thing-type.meater.meaterapi.label = MEATER Cloud REST API +thing-type.meater.meaterapi.description = Denne bro repræsenterer MEATER Cloud REST API connector +thing-type.meater.meaterprobe.label = MEATER stegetermometer +thing-type.meater.meaterprobe.description = Denne ting repræsenterer et MEATER stegetermometer + +# thing types config + +thing-type.config.meater.meaterapi.email.label = E-mail +thing-type.config.meater.meaterapi.email.description = E-mailen der bruges til at logge ind på MEATER Cloud-konto +thing-type.config.meater.meaterapi.password.label = Adgangskode +thing-type.config.meater.meaterapi.password.description = Den adgangskode, der bruges til at logge ind på MEATER Cloud-konto +thing-type.config.meater.meaterapi.refresh.label = Genopfriskningsinterval +thing-type.config.meater.meaterapi.refresh.description = Angiver genopfriskningsintervallet i sekunder +thing-type.config.meater.meaterprobe.deviceId.label = Enheds-id +thing-type.config.meater.meaterprobe.deviceId.description = Unikt id til dit MEATER stegetermometer + +# channel types + +channel-type.meater.ambientTemperature.label = Omgivende temperatur +channel-type.meater.ambientTemperature.description = Omgivende temperaturaflæsning for MEATER stegetermometer. Hvis omgivende er mindre end intern, vil det være intern temperatur +channel-type.meater.cookElapsedTime.label = Forløbet tid +channel-type.meater.cookElapsedTime.description = Tid siden starten af tilberedningen i sekunder. +channel-type.meater.cookEstimatedEndTime.label = Estimeret sluttid +channel-type.meater.cookEstimatedEndTime.description = Dato og klokkeslæt for estimeret sluttid af nuværende tilberedning +channel-type.meater.cookEstimatedEndTime.state.pattern = %1$tY-%1$tm-%1$td %1$tH\:%1$tM +channel-type.meater.cookId.label = Nuværende tilberednings-ID +channel-type.meater.cookId.description = Unikt ID for nuværende tilberedning +channel-type.meater.cookName.label = Nuværende tilberedningsnavn +channel-type.meater.cookName.description = Navn på det valgte kød på dit sprog eller brugerdefineret navn +channel-type.meater.cookPeakTemperature.label = Nuværende tilbererednings spidstemperatur +channel-type.meater.cookPeakTemperature.description = Spidstemperatur for nuværende tilberedning +channel-type.meater.cookRemainingTime.label = Resterende tilberedningstid +channel-type.meater.cookRemainingTime.description = Resterende tid i sekunder eller UNDEF når ukendt +channel-type.meater.cookState.label = Nuværende tilberedningstilstand +channel-type.meater.cookState.description = En af Not Started, Configured, Started, Ready For Resting, Resting, Slightly Underdone, Finished, Slightly Overdone, OVERCOOK\! +channel-type.meater.cookTargetTemperature.label = Nuværende tilbererednings måltemperatur +channel-type.meater.cookTargetTemperature.description = Måltemperatur for den nuværende tilberedning +channel-type.meater.internalTemperature.label = Stegetermometer intern temperatur +channel-type.meater.internalTemperature.description = Intern temperaturaflæsning af MEATER stegetermometer +channel-type.meater.lastConnection.label = Seneste forbindelse +channel-type.meater.lastConnection.description = Dato og klokkeslæt for seneste forbindelse til stegetermometer +channel-type.meater.lastConnection.state.pattern = %1$tY-%1$tm-%1$td %1$tH\:%1$tM +channel-type.meater.status.label = Status +channel-type.meater.status.description = Kan bruges til at udløse en øjeblikkelig opdatering + +# thing types config + +config.missing-username-password.description = Konfiguration af brugernavn og adgangskode er obligatorisk + +# thing status descriptions + +offline.communication-error.description = MEATER stegetermometer er offline + +# discovery result + +discovery.probe.label = MEATER stegetermometer From b9c19f5156575eb3a2cd2114384886015280bfff Mon Sep 17 00:00:00 2001 From: lolodomo Date: Mon, 7 Nov 2022 18:44:56 +0100 Subject: [PATCH 38/55] [easee] Add author in CODEOWNERS (#13674) Signed-off-by: Laurent Garnier Signed-off-by: Laurent Garnier --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 181b2e4d01d03..cec68f78a93dc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ /bundles/org.openhab.binding.dsmr/ @Hilbrand /bundles/org.openhab.binding.dwdpollenflug/ @DerOetzi /bundles/org.openhab.binding.dwdunwetter/ @limdul79 +/bundles/org.openhab.binding.easee/ @alexf2015 /bundles/org.openhab.binding.echonetlite/ @mikeb01 /bundles/org.openhab.binding.ecobee/ @mhilbush /bundles/org.openhab.binding.ecotouch/ @sibbi77 From bf804ce42c82729fedcd65890cb411c48e3d0b46 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Mon, 7 Nov 2022 18:45:19 +0100 Subject: [PATCH 39/55] [mercedesme] Add author in CODEOWNERS (#13675) Signed-off-by: Laurent Garnier Signed-off-by: Laurent Garnier --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index cec68f78a93dc..0cb0627f27b41 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -186,6 +186,7 @@ /bundles/org.openhab.binding.meater/ @jannegpriv /bundles/org.openhab.binding.mecmeter/ @kaikreuzer /bundles/org.openhab.binding.melcloud/ @lucacalcaterra @paulianttila @thewiep +/bundles/org.openhab.binding.mercedesme/ @weymann /bundles/org.openhab.binding.meteoalerte/ @clinique /bundles/org.openhab.binding.meteoblue/ @9037568 /bundles/org.openhab.binding.meteostick/ @cdjackson From 51f1c0978bec9f20420bf148fbbca6617875ba41 Mon Sep 17 00:00:00 2001 From: Bernd Weymann Date: Mon, 7 Nov 2022 18:48:48 +0100 Subject: [PATCH 40/55] [mercedesme] New API Migration (#13670) * adjust servers and scope * ensure IPv4 address for callback Signed-off-by: Bernd Weymann --- .../org/openhab/binding/mercedesme/internal/Constants.java | 5 +++-- .../mercedesme/internal/config/AccountConfiguration.java | 2 +- .../openhab/binding/mercedesme/internal/server/Utils.java | 1 + .../org/openhab/binding/mercedesme/ConfigurationTest.java | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java index 55fd210f5e2af..0c616598c59b1 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/Constants.java @@ -48,8 +48,8 @@ public class Constants { public static final String GROUP_LOCATION = "location"; public static final String GROUP_IMAGE = "image"; - public static final String MB_AUTH_URL = "https://id.mercedes-benz.com/as/authorization.oauth2"; - public static final String MB_TOKEN_URL = "https://id.mercedes-benz.com/as/token.oauth2"; + public static final String MB_AUTH_URL = "https://ssoalpha.dvb.corpinter.net/v1/auth"; + public static final String MB_TOKEN_URL = "https://ssoalpha.dvb.corpinter.net/v1/token"; public static final String CALLBACK_ENDPOINT = "/mb-callback"; public static final String OAUTH_CLIENT_NAME = "#byocar"; @@ -64,6 +64,7 @@ public class Constants { // https://developer.mercedes-benz.com/products/vehicle_status/docs public static final String SCOPE_STATUS = "mb:vehicle:mbdata:vehiclestatus"; public static final String SCOPE_OFFLINE = "offline_access"; + public static final String SCOPE_OPENID = "openid"; public static final String BASE_URL = "https://api.mercedes-benz.com/vehicledata/v2"; public static final String ODO_URL = BASE_URL + "/vehicles/%s/containers/payasyoudrive"; diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java index 6c4c43c041cca..8f647b12afcc6 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/config/AccountConfiguration.java @@ -40,7 +40,7 @@ public class AccountConfiguration { // https://developer.mercedes-benz.com/products/electric_vehicle_status/docs#_required_scopes public String getScope() { StringBuffer sb = new StringBuffer(); - sb.append(SCOPE_OFFLINE); + sb.append(SCOPE_OPENID).append(SPACE).append(SCOPE_OFFLINE); if (odoScope) { sb.append(SPACE).append(SCOPE_ODO); } diff --git a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java index c9545dfde17c5..852021a15887f 100644 --- a/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java +++ b/bundles/org.openhab.binding.mercedesme/src/main/java/org/openhab/binding/mercedesme/internal/server/Utils.java @@ -61,6 +61,7 @@ public static synchronized void removePort(int portNr) { } public static String getCallbackIP() throws SocketException { + // https://stackoverflow.com/questions/901755/how-to-get-the-ip-of-the-computer-on-linux-through-java // https://stackoverflow.com/questions/1062041/ip-address-not-obtained-in-java for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces .hasMoreElements();) { diff --git a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java index 72714670684b0..69ed1a0f7a50d 100644 --- a/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java +++ b/bundles/org.openhab.binding.mercedesme/src/test/java/org/openhab/binding/mercedesme/ConfigurationTest.java @@ -36,7 +36,7 @@ class ConfigurationTest { void testScope() { AccountConfiguration ac = new AccountConfiguration(); assertEquals( - "offline_access mb:vehicle:mbdata:payasyoudrive mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:evstatus", + "openid offline_access mb:vehicle:mbdata:payasyoudrive mb:vehicle:mbdata:vehiclestatus mb:vehicle:mbdata:vehiclelock mb:vehicle:mbdata:fuelstatus mb:vehicle:mbdata:evstatus", ac.getScope()); } From a33f08f517e1acff7d8a6f0ba5b5cb42953a47d2 Mon Sep 17 00:00:00 2001 From: lolodomo Date: Mon, 7 Nov 2022 18:48:51 +0100 Subject: [PATCH 41/55] [vesync] Add author in CODEOWNERS (#13676) Signed-off-by: Laurent Garnier Signed-off-by: Laurent Garnier --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 0cb0627f27b41..3f7ad568df44c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -350,6 +350,7 @@ /bundles/org.openhab.binding.venstarthermostat/ @hww3 @digitaldan /bundles/org.openhab.binding.ventaair/ @t2000 /bundles/org.openhab.binding.verisure/ @jannegpriv +/bundles/org.openhab.binding.vesync/ @dag81 /bundles/org.openhab.binding.vigicrues/ @clinique /bundles/org.openhab.binding.vitotronic/ @steand /bundles/org.openhab.binding.volvooncall/ @clinique @Jamstah From cdff5a06e7e9b719b9d3bc484c52012eeed30ba2 Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Tue, 8 Nov 2022 11:07:37 +0100 Subject: [PATCH 42/55] New translations jdbc.properties (German) (#13678) --- .../src/main/resources/OH-INF/i18n/jdbc_de.properties | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties index bdef661f2f69e..3d338c81771a8 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties @@ -9,7 +9,7 @@ persistence.config.jdbc.minimumIdle.description = Überschreibt die minimale Dat persistence.config.jdbc.password.label = Datenbank Passwort persistence.config.jdbc.password.description = Definiert das Datenbank-Passwort. persistence.config.jdbc.rebuildTableNames.label = Tabellenname neu aufbauen -persistence.config.jdbc.rebuildTableNames.description = Bestehende Tabellen mit 'Tablename Realname Generation' und 'Tablename Suffix ID Count', umbenennen. Optional, Voreinstellung\: deaktiviert.
Mit VORSICHT nutzen. Deaktivieren, wenn die Umbenennung abgeschlossen ist\! +persistence.config.jdbc.rebuildTableNames.description = Bestehende Tabellen mit 'Tabellenname für Real-Name Generierung' und 'Suffix-ID Zähler für Tabellenname', umbenennen. Optional, Voreinstellung\: deaktiviert.
Mit VORSICHT nutzen. Deaktivieren, wenn die Umbenennung abgeschlossen ist\! persistence.config.jdbc.rebuildTableNames.option.true = Aktivieren persistence.config.jdbc.rebuildTableNames.option.false = Deaktivieren persistence.config.jdbc.sqltype.CALL.label = SQL-Typ CALL @@ -36,9 +36,13 @@ persistence.config.jdbc.sqltype.STRING.label = SQL-Typ STRING persistence.config.jdbc.sqltype.STRING.description = Überschreibt den JDBC/SQL-Datentyp für PLAYER.
Optional, Voreinstellung\: "VARCHAR(65500)". persistence.config.jdbc.sqltype.SWITCH.label = SQL-Typ SWITCH persistence.config.jdbc.sqltype.SWITCH.description = Überschreibt den JDBC/SQL-Datentyp für SWITCH.
Optional, Voreinstellung\: "VARCHAR(6)". +persistence.config.jdbc.tableCaseSensitiveItemNames.label = Groß-/Kleinschreibung für Tabellenamen +persistence.config.jdbc.tableCaseSensitiveItemNames.description = Aktiviert die Groß-/Kleinschreibung für Tabellenamen, wenn 'Tabellenname für Real-Name Generierung' aktiviert ist.
Optional, Vorsteinstellung\: deaktiviert -> Tabellenamen werden klein geschrieben und der Suffix-ID Zähler wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Prefix ignoriert. +persistence.config.jdbc.tableCaseSensitiveItemNames.option.true = Aktivieren +persistence.config.jdbc.tableCaseSensitiveItemNames.option.false = Deaktivieren persistence.config.jdbc.tableIdDigitCount.label = Suffix-ID Zähler für Tabellenname persistence.config.jdbc.tableIdDigitCount.description = Suffix-ID Zähler für Tabellenname.
Optional, Voreinstellung\: 4 -> 0001-9999.
Auf 0 setzen für die Migration des MySQL Bundles. -persistence.config.jdbc.tableNamePrefix.label = Prefix Tabellenname +persistence.config.jdbc.tableNamePrefix.label = Tabellennamen-Präfix persistence.config.jdbc.tableNamePrefix.description = Prefix für Tabellenname. Optional, Voreinstellung\: "item".
Auf 'item' für die Migration des mySQL-Bundles setzen. persistence.config.jdbc.tableUseRealItemNames.label = Tabellenname für Real-Name Generierung persistence.config.jdbc.tableUseRealItemNames.description = Erlaubt die Prefix-Generierung für Tabellenname pro Item-Realname.
Optional, Vorsteinstellung\: deaktiviert -> Tabellennamen-Prefix wird verwendet.
Wenn aktiviert wird der Tabellennamen-Prefix String ignoriert. @@ -51,4 +55,4 @@ persistence.config.jdbc.user.description = Definiert den Datenbankbenutzer. # service -service.persistence.jdbc.label = JDBC Persistenzdienst +service.persistence.jdbc.label = JDBC Persistence Service From a8f671960dc690831abbba6187be03bd46cc8295 Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Wed, 9 Nov 2022 10:52:30 +0100 Subject: [PATCH 43/55] New translations jdbc.properties (German) (#13686) --- .../main/resources/OH-INF/i18n/jdbc_de.properties | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties index 3d338c81771a8..0f0bdbfa4d06b 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties @@ -1,5 +1,5 @@ -persistence.config.jdbc.enableLogTime.label = Logzeiterfassung aktivieren -persistence.config.jdbc.enableLogTime.description = Zeit- und Performance-Management aktivieren (optional, Voreinstellung\: deaktiviert) +persistence.config.jdbc.enableLogTime.label = Zeitmessung +persistence.config.jdbc.enableLogTime.description = Aktiviert die Zeit- und Performancemessung. Optional, Voreinstellung\: deaktiviert. persistence.config.jdbc.enableLogTime.option.true = Aktivieren persistence.config.jdbc.enableLogTime.option.false = Deaktivieren persistence.config.jdbc.maximumPoolSize.label = Maximale Pool-Größe der Verbindungen @@ -13,7 +13,7 @@ persistence.config.jdbc.rebuildTableNames.description = Bestehende Tabellen mit persistence.config.jdbc.rebuildTableNames.option.true = Aktivieren persistence.config.jdbc.rebuildTableNames.option.false = Deaktivieren persistence.config.jdbc.sqltype.CALL.label = SQL-Typ CALL -persistence.config.jdbc.sqltype.CALL.description = Überschreibt den JDBC/SQL Datentyp für CALL
/Optional, Voreinstellung\: "VARCHAR(200).
Allgemeine Informationen zu JDBC/SQL-Typen\: https\://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html
und http\://www.h2database.com/html/datatypes.html
see\: http\://www.postgresql.org/docs/9.5/static/datatype.html +persistence.config.jdbc.sqltype.CALL.description = Überschreibt den JDBC/SQL Datentyp für CALL
Optional, Voreinstellung\: "VARCHAR(200)".
Allgemeine Informationen zu JDBC/SQL-Typen\: https\://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html
und http\://www.h2database.com/html/datatypes.html
und http\://www.postgresql.org/docs/9.5/static/datatype.html persistence.config.jdbc.sqltype.COLOR.label = SQL-Typ COLOR persistence.config.jdbc.sqltype.COLOR.description = Überschreibt den JDBC/SQL-Datentyp für COLOR.
Optional, Default\: "VARCHAR(70)". persistence.config.jdbc.sqltype.CONTACT.label = SQL-Typ CONTACT @@ -33,19 +33,19 @@ persistence.config.jdbc.sqltype.PLAYER.description = Überschreibt den JDBC/SQL- persistence.config.jdbc.sqltype.ROLLERSHUTTER.label = SQL-Typ ROLLERSHUTTER persistence.config.jdbc.sqltype.ROLLERSHUTTER.description = Überschreibt den JDBC/SQL-Datentyp für ROLLERSHUTTER.
Optional, Voreinstellung\: "TINYINT". persistence.config.jdbc.sqltype.STRING.label = SQL-Typ STRING -persistence.config.jdbc.sqltype.STRING.description = Überschreibt den JDBC/SQL-Datentyp für PLAYER.
Optional, Voreinstellung\: "VARCHAR(65500)". +persistence.config.jdbc.sqltype.STRING.description = Überschreibt den JDBC/SQL-Datentyp für STRING.
Optional, Voreinstellung\: "VARCHAR(65500)". persistence.config.jdbc.sqltype.SWITCH.label = SQL-Typ SWITCH persistence.config.jdbc.sqltype.SWITCH.description = Überschreibt den JDBC/SQL-Datentyp für SWITCH.
Optional, Voreinstellung\: "VARCHAR(6)". persistence.config.jdbc.tableCaseSensitiveItemNames.label = Groß-/Kleinschreibung für Tabellenamen -persistence.config.jdbc.tableCaseSensitiveItemNames.description = Aktiviert die Groß-/Kleinschreibung für Tabellenamen, wenn 'Tabellenname für Real-Name Generierung' aktiviert ist.
Optional, Vorsteinstellung\: deaktiviert -> Tabellenamen werden klein geschrieben und der Suffix-ID Zähler wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Prefix ignoriert. +persistence.config.jdbc.tableCaseSensitiveItemNames.description = Aktiviert die Groß-/Kleinschreibung für Tabellenamen, wenn 'Tabellenname für Real-Name Generierung' aktiviert ist.
Optional, Vorsteinstellung\: deaktiviert -> Tabellenamen werden klein geschrieben und der Suffix-ID Zähler wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. persistence.config.jdbc.tableCaseSensitiveItemNames.option.true = Aktivieren persistence.config.jdbc.tableCaseSensitiveItemNames.option.false = Deaktivieren persistence.config.jdbc.tableIdDigitCount.label = Suffix-ID Zähler für Tabellenname persistence.config.jdbc.tableIdDigitCount.description = Suffix-ID Zähler für Tabellenname.
Optional, Voreinstellung\: 4 -> 0001-9999.
Auf 0 setzen für die Migration des MySQL Bundles. persistence.config.jdbc.tableNamePrefix.label = Tabellennamen-Präfix -persistence.config.jdbc.tableNamePrefix.description = Prefix für Tabellenname. Optional, Voreinstellung\: "item".
Auf 'item' für die Migration des mySQL-Bundles setzen. +persistence.config.jdbc.tableNamePrefix.description = Präfix für Tabellenname. Optional, Voreinstellung\: "item".
Auf "item" für die Migration des mySQL-Bundles setzen. persistence.config.jdbc.tableUseRealItemNames.label = Tabellenname für Real-Name Generierung -persistence.config.jdbc.tableUseRealItemNames.description = Erlaubt die Prefix-Generierung für Tabellenname pro Item-Realname.
Optional, Vorsteinstellung\: deaktiviert -> Tabellennamen-Prefix wird verwendet.
Wenn aktiviert wird der Tabellennamen-Prefix String ignoriert. +persistence.config.jdbc.tableUseRealItemNames.description = Erlaubt die Präfix-Generierung für Tabellenname pro Item-Realname.
Optional, Vorsteinstellung\: deaktiviert -> Tabellennamen-Präfix wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. persistence.config.jdbc.tableUseRealItemNames.option.true = Aktivieren persistence.config.jdbc.tableUseRealItemNames.option.false = Deaktivieren persistence.config.jdbc.url.label = Datenbank-URL From 64791b386c05a0293ec69141a5e20a87a0d1a362 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Wed, 9 Nov 2022 23:01:15 +0100 Subject: [PATCH 44/55] [jdbc] Upgrade MariaDB connector to 3.0.8 (#13659) Signed-off-by: Jacob Laursen --- bundles/org.openhab.persistence.jdbc/README.md | 2 +- bundles/org.openhab.persistence.jdbc/pom.xml | 2 +- .../org.openhab.persistence.jdbc/src/main/feature/feature.xml | 2 +- .../openhab/persistence/jdbc/internal/JdbcConfiguration.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index 1cf0ef8f6903e..bc20c206a6be0 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -12,7 +12,7 @@ The following databases are currently supported and tested: | [Apache Derby](https://db.apache.org/derby/) | [derby-10.14.2.0.jar](https://mvnrepository.com/artifact/org.apache.derby/derby) | | [H2](https://www.h2database.com/) | [h2-1.4.191.jar](https://mvnrepository.com/artifact/com.h2database/h2) | | [HSQLDB](http://hsqldb.org/) | [hsqldb-2.3.3.jar](https://mvnrepository.com/artifact/org.hsqldb/hsqldb) | -| [MariaDB](https://mariadb.org/) | [mariadb-java-client-1.4.6.jar](https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client) | +| [MariaDB](https://mariadb.org/) | [mariadb-java-client-3.0.8.jar](https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client) | | [MySQL](https://www.mysql.com/) | [mysql-connector-java-8.0.30.jar](https://mvnrepository.com/artifact/mysql/mysql-connector-java) | | [PostgreSQL](https://www.postgresql.org/) | [postgresql-42.4.1.jar](https://mvnrepository.com/artifact/org.postgresql/postgresql) | | [SQLite](https://www.sqlite.org/) | [sqlite-jdbc-3.16.1.jar](https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc) | diff --git a/bundles/org.openhab.persistence.jdbc/pom.xml b/bundles/org.openhab.persistence.jdbc/pom.xml index 84e3b6ae996b7..7ad8183c05c0d 100644 --- a/bundles/org.openhab.persistence.jdbc/pom.xml +++ b/bundles/org.openhab.persistence.jdbc/pom.xml @@ -28,7 +28,7 @@ 10.14.2.0 1.4.191 2.3.3 - 1.4.6 + 3.0.8 8.0.30 42.4.1 3.16.1 diff --git a/bundles/org.openhab.persistence.jdbc/src/main/feature/feature.xml b/bundles/org.openhab.persistence.jdbc/src/main/feature/feature.xml index 20816900db05c..934525c6f4981 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/feature/feature.xml +++ b/bundles/org.openhab.persistence.jdbc/src/main/feature/feature.xml @@ -27,7 +27,7 @@ mvn:org.openhab.addons.features.karaf/org.openhab.addons.features.karaf.openhab-addons-external/${project.version}/cfg/jdbc openhab-runtime-base - mvn:org.mariadb.jdbc/mariadb-java-client/1.4.6 + mvn:org.mariadb.jdbc/mariadb-java-client/3.0.8 mvn:org.openhab.addons.bundles/org.openhab.persistence.jdbc/${project.version} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java index 56f76d745f3ef..b67fb9a31a238 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcConfiguration.java @@ -320,7 +320,7 @@ private void testJDBCDriver(String driver) { warn += "\tHSQLDB: version >= 2.3.3 from https://mvnrepository.com/artifact/org.hsqldb/hsqldb\n"; break; case "mariadb": - warn += "\tMariaDB: version >= 1.4.6 from https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client\n"; + warn += "\tMariaDB: version >= 3.0.8 from https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client\n"; break; case "mysql": warn += "\tMySQL: version >= 8.0.30 from https://mvnrepository.com/artifact/mysql/mysql-connector-java\n"; From 49fe49c1a97f8ac0db5c5c0298d84f250fbffa1c Mon Sep 17 00:00:00 2001 From: Stian Kjoglum <47720690+kjoglum@users.noreply.github.com> Date: Thu, 10 Nov 2022 20:57:45 +0100 Subject: [PATCH 45/55] Updates for breaking API changes (#13680) Signed-off-by: kjoglum --- .../internal/TibberBindingConstants.java | 4 +-- .../internal/handler/TibberHandler.java | 36 +++++++++++++++---- .../TibberPriceConsumptionHandler.java | 5 +++ 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/TibberBindingConstants.java b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/TibberBindingConstants.java index d0d479097f979..6bd373b0ff4c6 100644 --- a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/TibberBindingConstants.java +++ b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/TibberBindingConstants.java @@ -34,8 +34,8 @@ public class TibberBindingConstants { // Tibber base URL for queries and mutations public static final String BASE_URL = "https://api.tibber.com/v1-beta/gql"; - // Tibber websocket endpoint for live subscription - public static final String SUBSCRIPTION_URL = "wss://api.tibber.com/v1-beta/gql/subscriptions"; + // Tibber driver version + public static final String TIBBER_DRIVER = "com.tibber/1.8.3"; // List of all Thing Type UIDs public static final ThingTypeUID TIBBER_THING_TYPE = new ThingTypeUID(BINDING_ID, "tibberapi"); diff --git a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java index fb2e64fc4edad..27ca48e64d669 100644 --- a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java +++ b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberHandler.java @@ -26,6 +26,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; @@ -50,6 +52,7 @@ import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.openhab.core.types.RefreshType; +import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,6 +79,8 @@ public class TibberHandler extends BaseThingHandler { private @Nullable ScheduledFuture pollingJob; private @Nullable Future sessionFuture; private String rtEnabled = "false"; + private @Nullable String subscriptionURL; + private @Nullable String versionString; public TibberHandler(Thing thing) { super(thing); @@ -86,6 +91,9 @@ public void initialize() { updateStatus(ThingStatus.UNKNOWN); tibberConfig = getConfigAs(TibberConfiguration.class); + versionString = FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); + logger.debug("Binding version: {}", versionString); + getTibberParameters(); startRefresh(tibberConfig.getRefresh()); } @@ -104,7 +112,9 @@ public void getTibberParameters() { try { httpHeader.put("cache-control", "no-cache"); httpHeader.put("content-type", JSON_CONTENT_TYPE); - httpHeader.put("Authorization", "Bearer " + tibberConfig.getToken()); + httpHeader.put(HttpHeader.USER_AGENT.asString(), + "openHAB/Tibber " + versionString + " Tibber driver " + TIBBER_DRIVER); + httpHeader.put(HttpHeader.AUTHORIZATION.asString(), "Bearer " + tibberConfig.getToken()); TibberPriceConsumptionHandler tibberQuery = new TibberPriceConsumptionHandler(); InputStream connectionStream = tibberQuery.connectionInputStream(tibberConfig.getHomeid()); @@ -124,6 +134,15 @@ public void getTibberParameters() { if ("true".equals(rtEnabled)) { logger.debug("Pulse associated with HomeId: Live stream will be started"); + + InputStream wsURL = tibberQuery.getWebsocketUrl(); + String wsResponse = HttpUtil.executeUrl("POST", BASE_URL, httpHeader, wsURL, null, REQUEST_TIMEOUT); + + JsonObject wsobject = (JsonObject) JsonParser.parseString(wsResponse); + subscriptionURL = wsobject.getAsJsonObject("data").getAsJsonObject("viewer") + .get("websocketSubscriptionUrl").toString().replaceAll("^\"|\"$", ""); + logger.debug("Subscribing to: {}", subscriptionURL); + open(); } else { logger.debug("No Pulse associated with HomeId: No live stream will be started"); @@ -318,7 +337,7 @@ public void open() { sslContextFactory.setTrustAll(true); sslContextFactory.setEndpointIdentificationAlgorithm(null); - client = new WebSocketClient(sslContextFactory); + client = new WebSocketClient(new HttpClient(sslContextFactory)); client.setMaxIdleTimeout(30 * 1000); this.client = client; @@ -330,8 +349,10 @@ public void open() { } ClientUpgradeRequest newRequest = new ClientUpgradeRequest(); - newRequest.setHeader("Authorization", "Bearer " + tibberConfig.getToken()); - newRequest.setSubProtocols("graphql-subscriptions"); + newRequest.setHeader(HttpHeader.USER_AGENT.asString(), + "openHAB/Tibber " + versionString + " Tibber driver " + TIBBER_DRIVER); + newRequest.setHeader(HttpHeader.AUTHORIZATION.asString(), "Bearer " + tibberConfig.getToken()); + newRequest.setSubProtocols("graphql-transport-ws"); try { logger.debug("Starting Websocket connection"); @@ -341,7 +362,7 @@ public void open() { } try { logger.debug("Connecting Websocket connection"); - sessionFuture = client.connect(socket, new URI(SUBSCRIPTION_URL), newRequest); + sessionFuture = client.connect(socket, new URI(subscriptionURL), newRequest); try { Thread.sleep(10 * 1000); } catch (InterruptedException e) { @@ -412,7 +433,8 @@ public class TibberWebSocketListener { public void onConnect(Session wssession) { TibberHandler.this.session = wssession; TibberWebSocketListener socket = TibberHandler.this.socket; - String connection = "{\"type\":\"connection_init\", \"payload\":\"token=" + tibberConfig.getToken() + "\"}"; + String connection = "{\"type\":\"connection_init\", \"payload\":{\"token\":\"" + tibberConfig.getToken() + + "\"}}"; try { if (socket != null) { logger.debug("Sending websocket connect message"); @@ -530,7 +552,7 @@ private void sendMessage(String message) throws IOException { } public void startSubscription() { - String query = "{\"id\":\"1\",\"type\":\"start\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\"" + String query = "{\"id\":\"1\",\"type\":\"subscribe\",\"payload\":{\"variables\":{},\"extensions\":{},\"operationName\":null,\"query\":\"subscription {\\n liveMeasurement(homeId:\\\"" + tibberConfig.getHomeid() + "\\\") {\\n timestamp\\n power\\n lastMeterConsumption\\n accumulatedConsumption\\n accumulatedCost\\n currency\\n minPower\\n averagePower\\n maxPower\\n" + "voltagePhase1\\n voltagePhase2\\n voltagePhase3\\n currentL1\\n currentL2\\n currentL3\\n powerProduction\\n accumulatedProduction\\n minPowerProduction\\n maxPowerProduction\\n }\\n }\\n\"}}"; diff --git a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberPriceConsumptionHandler.java b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberPriceConsumptionHandler.java index a78f1c4e7ec3e..a32db9f5576f9 100644 --- a/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberPriceConsumptionHandler.java +++ b/bundles/org.openhab.binding.tibber/src/main/java/org/openhab/binding/tibber/internal/handler/TibberPriceConsumptionHandler.java @@ -42,4 +42,9 @@ public InputStream getRealtimeInputStream(String homeId) { + "\\\") {features {realTimeConsumptionEnabled }}}}\"}"; return new ByteArrayInputStream(realtimeenabledquery.getBytes(StandardCharsets.UTF_8)); } + + public InputStream getWebsocketUrl() { + String websocketquery = "{\"query\": \"{viewer {websocketSubscriptionUrl }}\"}"; + return new ByteArrayInputStream(websocketquery.getBytes(StandardCharsets.UTF_8)); + } } From 49fa34959045adf0e5d9e21e1d54003ae2b30e73 Mon Sep 17 00:00:00 2001 From: David Pace Date: Fri, 11 Nov 2022 08:23:48 +0100 Subject: [PATCH 46/55] [boschshc] Support obtaining battery states (#13461) (#13631) This change adds support for obtaining the battery state for the following devices: * Motion Detector * Thermostat * Twinguard * Wall Thermostat * Window/Door Contact The following changes were made: * Add system.battery-level and system.low-battery channels to Motion Detector, Thermostat, Twinguard, Wall Thermostat and Window/Door Contact * Add constant for battery-level and low-battery channels in BoschSHCBindingConstants * Implement abstract handler and service for battery-powered devices * Let appropriate devices inherit the abstract implementation * Add missing super calls in initializeServices() methods * Rename existing getServiceURL() to getServiceStateURL() in HTTP client * Add methods to retrieve service states without the suffix "/state" in the URL * Rename DeviceStatusUpdate to DeviceServiceData * Let DeviceServiceData extend BoschSHCServiceState * Extend DeviceServiceData DTO with model for faults * Enhance bridge handler: handle updates without state sub-objects, extract methods to enhance readability * Add unit tests for all affected devices * Minor code enhancements * Update documentation Signed-off-by: David Pace --- .../org.openhab.binding.boschshc/README.md | 12 +- .../AbstractBatteryPoweredDeviceHandler.java | 58 +++++++ .../devices/BoschSHCBindingConstants.java | 2 + .../internal/devices/BoschSHCHandler.java | 2 +- .../devices/bridge/BoschHttpClient.java | 48 ++++-- .../devices/bridge/BridgeHandler.java | 141 +++++++++++++----- ...atusUpdate.java => DeviceServiceData.java} | 26 ++-- .../internal/devices/bridge/dto/Fault.java | 33 ++++ .../internal/devices/bridge/dto/Faults.java | 39 +++++ .../devices/bridge/dto/LongPollResult.java | 2 +- .../motiondetector/MotionDetectorHandler.java | 4 +- .../devices/thermostat/ThermostatHandler.java | 18 +-- .../devices/twinguard/TwinguardHandler.java | 13 +- .../wallthermostat/WallThermostatHandler.java | 15 +- .../windowcontact/WindowContactHandler.java | 6 +- .../internal/services/BoschSHCService.java | 26 ++-- .../services/batterylevel/BatteryLevel.java | 119 +++++++++++++++ .../batterylevel/BatteryLevelService.java | 53 +++++++ .../services/dto/BoschSHCServiceState.java | 4 +- .../resources/OH-INF/thing/thing-types.xml | 12 +- ...stractBatteryPoweredDeviceHandlerTest.java | 113 ++++++++++++++ .../AbstractPowerSwitchHandlerTest.java | 9 -- .../devices/AbstractSHCHandlerTest.java | 8 + .../devices/bridge/BoschHttpClientTest.java | 8 +- .../MotionDetectorHandlerTest.java | 43 ++++++ .../thermostat/ThermostatHandlerTest.java | 43 ++++++ .../twinguard/TwinguardHandlerTest.java | 43 ++++++ .../WallThermostatHandlerTest.java | 43 ++++++ .../WindowContactHandlerTest.java | 43 ++++++ .../batterylevel/BatteryLevelTest.java | 98 ++++++++++++ 30 files changed, 963 insertions(+), 121 deletions(-) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java rename bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/{DeviceStatusUpdate.java => DeviceServiceData.java} (68%) create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java create mode 100644 bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java create mode 100644 bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index 979f54659e496..f5ba3b26141c4 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -49,7 +49,7 @@ A compact smart plug with energy monitoring capabilities. | power-consumption | Number:Power | ☐ | Current power consumption (W) of the device. | | energy-consumption | Number:Energy | ☐ | Cumulated energy consumption (Wh) of the device. | -### Twinguard smoke detector +### Twinguard Smoke Detector The Twinguard smoke detector warns you in case of fire and constantly monitors the air. @@ -65,6 +65,8 @@ The Twinguard smoke detector warns you in case of fire and constantly monitors t | purity-rating | String | ☐ | Rating of current measured purity. | | air-description | String | ☐ | Overall description of the air quality. | | combined-rating | String | ☐ | Combined rating of the air quality. | +| battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | +| low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | ### Door/Window Contact @@ -75,6 +77,8 @@ Detects open windows and doors. | Channel Type ID | Item Type | Writable | Description | | --------------- | --------- | :------: | ---------------------------- | | contact | Contact | ☐ | Contact state of the device. | +| battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | +| low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | ### Motion Detector @@ -85,6 +89,8 @@ Detects every movement through an intelligent combination of passive infra-red t | Channel Type ID | Item Type | Writable | Description | | --------------- | --------- | :------: | ------------------------------ | | latest-motion | DateTime | ☐ | The date of the latest motion. | +| battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | +| low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | ### Shutter Control @@ -107,6 +113,8 @@ Radiator thermostat | temperature | Number:Temperature | ☐ | Current measured temperature. | | valve-tappet-position | Number:Dimensionless | ☐ | Current open ratio of valve tappet (0 to 100). | | child-lock | Switch | ☑ | Indicates if child lock is active. | +| battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | +| low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | ### Climate Control @@ -129,6 +137,8 @@ Display of the current room temperature as well as the relative humidity in the | --------------- | -------------------- | :------: | ------------------------------------- | | temperature | Number:Temperature | ☐ | Current measured temperature. | | humidity | Number:Dimensionless | ☐ | Current measured humidity (0 to 100). | +| battery-level | Number | ☐ | Current battery level percentage as integer number. Bosch-specific battery levels are mapped to numbers as follows: `OK`: 100, `LOW_BATTERY`: 10, `CRITICAL_LOW`: 1, `CRITICALLY_LOW_BATTERY`: 1, `NOT_AVAILABLE`: `UNDEF`. | +| low-battery | Switch | ☐ | Indicates whether the battery is low (`ON`) or OK (`OFF`). | ### Security Camera 360 diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java new file mode 100644 index 0000000000000..4e87b10844f93 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandler.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; + +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevel; +import org.openhab.binding.boschshc.internal.services.batterylevel.BatteryLevelService; +import org.openhab.core.thing.Thing; + +/** + * Abstract implementation for battery-powered devices. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class AbstractBatteryPoweredDeviceHandler extends BoschSHCDeviceHandler { + + /** + * Service to monitor the battery level of the device + */ + private final BatteryLevelService batteryLevelService; + + public AbstractBatteryPoweredDeviceHandler(Thing thing) { + super(thing); + this.batteryLevelService = new BatteryLevelService(); + } + + @Override + protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + + registerService(batteryLevelService, this::updateChannels, List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_LOW_BATTERY), + true); + } + + private void updateChannels(DeviceServiceData deviceServiceData) { + BatteryLevel batteryLevel = BatteryLevel.fromDeviceServiceData(deviceServiceData); + super.updateState(CHANNEL_BATTERY_LEVEL, batteryLevel.toState()); + super.updateState(CHANNEL_LOW_BATTERY, batteryLevel.toLowBatteryState()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java index 7097deba80a60..56f61ce5c108d 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCBindingConstants.java @@ -74,6 +74,8 @@ public class BoschSHCBindingConstants { public static final String CHANNEL_ARM_ACTION = "arm-action"; public static final String CHANNEL_DISARM_ACTION = "disarm-action"; public static final String CHANNEL_MUTE_ACTION = "mute-action"; + public static final String CHANNEL_BATTERY_LEVEL = "battery-level"; + public static final String CHANNEL_LOW_BATTERY = "low-battery"; // static device/service names public static final String SERVICE_INTRUSION_DETECTION = "intrusionDetectionSystem"; diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java index 9e3d798369987..a4c8e596685e5 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/BoschSHCHandler.java @@ -161,7 +161,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { * @param serviceName Name of service the update came from. * @param stateData Current state of device service. Serialized as JSON. */ - public void processUpdate(String serviceName, JsonElement stateData) { + public void processUpdate(String serviceName, @Nullable JsonElement stateData) { // Check services of device to correctly for (DeviceService deviceService : this.services) { BoschSHCService service = deviceService.service; diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java index fd372f5f938d5..7c214a7de285f 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java @@ -77,7 +77,7 @@ public String getPublicInformationUrl() { /** * Returns the pairing URL for the Bosch SHC clients, using port 8443. * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md - * + * * @return URL for pairing */ public String getPairingUrl() { @@ -86,7 +86,7 @@ public String getPairingUrl() { /** * Returns a Bosch SHC URL for the endpoint, using port 8444. - * + * * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html * @return Bosch SHC URL for passed endpoint */ @@ -96,7 +96,7 @@ public String getBoschShcUrl(String endpoint) { /** * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()} - * + * * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html * @return SmartHome URL for passed endpoint */ @@ -105,17 +105,43 @@ public String getBoschSmartHomeUrl(String endpoint) { } /** - * Returns a device & service URL. + * Returns a URL to get or put a service state. + *

+ * Example: + * + *

+     * https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/AirQualityLevel/state
+     * 
+ * * see https://apidocs.bosch-smarthome.com/local/index.html - * + * * @param serviceName the name of the service * @param deviceId the device identifier - * @return SmartHome URL for passed endpoint + * @return a URL to get or put a service state */ - public String getServiceUrl(String serviceName, String deviceId) { + public String getServiceStateUrl(String serviceName, String deviceId) { return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName)); } + /** + * Returns a URL to get general information about a service. + *

+ * Example: + * + *

+     * https://localhost:8444/smarthome/devices/hdm:ZigBee:000d6f0016d1cdae/services/BatteryLevel
+     * 
+ * + * In some cases this URL has to be used to get the service state, for example for battery levels. + * + * @param serviceName the name of the service + * @param deviceId the device identifier + * @return a URL to retrieve general service information + */ + public String getServiceUrl(String serviceName, String deviceId) { + return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s", deviceId, serviceName)); + } + /** * Checks if the Bosch SHC is online. * @@ -177,7 +203,7 @@ public boolean isAccessPossible() throws InterruptedException { /** * Pairs this client with the Bosch SHC. * Press pairing button on the Bosch Smart Home Controller! - * + * * @return true if pairing was successful, otherwise false * @throws InterruptedException in case of an interrupt */ @@ -228,7 +254,7 @@ public boolean doPairing() throws InterruptedException { /** * Creates a HTTP request. - * + * * @param url for the HTTP request * @param method for the HTTP request * @return created HTTP request instance @@ -239,7 +265,7 @@ public Request createRequest(String url, HttpMethod method) { /** * Creates a HTTP request. - * + * * @param url for the HTTP request * @param method for the HTTP request * @param content for the HTTP request @@ -265,7 +291,7 @@ public Request createRequest(String url, HttpMethod method, @Nullable Object con /** * Sends a request and expects a response of the specified type. - * + * * @param request Request to send * @param responseContentClass Type of expected response * @param contentValidator Checks if the parsed response is valid diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java index a7a134ed049eb..a7f3e6bab91e0 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BridgeHandler.java @@ -30,7 +30,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device; -import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult; import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; @@ -51,6 +51,7 @@ import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.reflect.TypeToken; /** @@ -299,53 +300,97 @@ private boolean getDevices() throws InterruptedException { /** * Bridge callback handler for the results of long polls. * - * It will check the result and - * forward the received to the bosch thing handlers. + * It will check the results and + * forward the received states to the Bosch thing handlers. * * @param result Results from Long Polling */ private void handleLongPollResult(LongPollResult result) { - for (DeviceStatusUpdate update : result.result) { - if (update != null && update.state != null) { - logger.debug("Got update for service {} of type {}: {}", update.id, update.type, update.state); + for (DeviceServiceData deviceServiceData : result.result) { + handleDeviceServiceData(deviceServiceData); + } + } - var updateDeviceId = update.deviceId; - if (updateDeviceId == null) { - continue; - } + /** + * Processes a single long poll result. + * + * @param deviceServiceData object representing a single long poll result + */ + private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) { + if (deviceServiceData != null) { + JsonElement state = obtainState(deviceServiceData); - logger.debug("Got update for device {}", updateDeviceId); + logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type, + state); - boolean handled = false; + var updateDeviceId = deviceServiceData.deviceId; + if (updateDeviceId == null || state == null) { + return; + } - Bridge bridge = this.getThing(); - for (Thing childThing : bridge.getThings()) { - // All children of this should implement BoschSHCHandler - @Nullable - ThingHandler baseHandler = childThing.getHandler(); - if (baseHandler != null && baseHandler instanceof BoschSHCHandler) { - BoschSHCHandler handler = (BoschSHCHandler) baseHandler; - @Nullable - String deviceId = handler.getBoschID(); + logger.debug("Got update for device {}", updateDeviceId); - handled = true; - logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId); + forwardStateToHandlers(deviceServiceData, state, updateDeviceId); + } + } - if (deviceId != null && updateDeviceId.equals(deviceId)) { - logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, update.id, - update.state); - handler.processUpdate(update.id, update.state); - } - } else { - logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler); - } - } + /** + * Extracts the actual state object from the given {@link DeviceServiceData} instance. + *

+ * In some special cases like the BatteryLevel service the {@link DeviceServiceData} object itself + * contains the state. + * In all other cases, the state is contained in a sub-object named state. + * + * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained + * @return the state sub-object or the {@link DeviceServiceData} object itself + */ + @Nullable + private JsonElement obtainState(DeviceServiceData deviceServiceData) { + // the battery level service receives no individual state object but rather requires the DeviceServiceData + // structure + if ("BatteryLevel".equals(deviceServiceData.id)) { + return gson.toJsonTree(deviceServiceData); + } + + return deviceServiceData.state; + } - if (!handled) { - logger.debug("Could not find a thing for device ID: {}", updateDeviceId); + /** + * Tries to find handlers for the device with the given ID and forwards the received state to the handlers. + * + * @param deviceServiceData object representing updates received in long poll results + * @param state the received state object as JSON element + * @param updateDeviceId the ID of the device for which the state update was received + */ + private void forwardStateToHandlers(DeviceServiceData deviceServiceData, JsonElement state, String updateDeviceId) { + boolean handled = false; + + Bridge bridge = this.getThing(); + for (Thing childThing : bridge.getThings()) { + // All children of this should implement BoschSHCHandler + @Nullable + ThingHandler baseHandler = childThing.getHandler(); + if (baseHandler != null && baseHandler instanceof BoschSHCHandler) { + BoschSHCHandler handler = (BoschSHCHandler) baseHandler; + @Nullable + String deviceId = handler.getBoschID(); + + handled = true; + logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId); + + if (deviceId != null && updateDeviceId.equals(deviceId)) { + logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, + deviceServiceData.id, state); + handler.processUpdate(deviceServiceData.id, state); } + } else { + logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler); } } + + if (!handled) { + logger.debug("Could not find a thing for device ID: {}", updateDeviceId); + } } /** @@ -447,7 +492,7 @@ public Device getDeviceInfo(String deviceId) * Query the Bosch Smart Home Controller for the state of the given device. *

* The URL used for retrieving the state has the following structure: - * + * *

      * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
      * 
@@ -470,14 +515,14 @@ public Device getDeviceInfo(String deviceId) return null; } - String url = httpClient.getServiceUrl(stateName, deviceId); + String url = httpClient.getServiceStateUrl(stateName, deviceId); logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url); return getState(httpClient, url, stateClass); } /** * Queries the Bosch Smart Home Controller for the state using an explicit endpoint. - * + * * @param Type to which the resulting JSON should be deserialized to * @param endpoint The destination endpoint part of the URL * @param stateClass Class to convert the resulting JSON to @@ -503,7 +548,7 @@ public Device getDeviceInfo(String deviceId) /** * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller. - * + * * @param Type to which the resulting JSON should be deserialized to * @param httpClient HTTP client used for sending the request * @param url URL at which the state should be retrieved @@ -566,7 +611,7 @@ public Device getDeviceInfo(String deviceId) } // Create request - String url = httpClient.getServiceUrl(serviceName, deviceId); + String url = httpClient.getServiceStateUrl(serviceName, deviceId); Request request = httpClient.createRequest(url, PUT, state); // Send request @@ -575,7 +620,7 @@ public Device getDeviceInfo(String deviceId) /** * Sends a HTTP POST request without a request body to the given endpoint. - * + * * @param endpoint The destination endpoint part of the URL * @return the HTTP response * @throws InterruptedException @@ -589,7 +634,7 @@ public Device getDeviceInfo(String deviceId) /** * Sends a HTTP POST request with a request body to the given endpoint. - * + * * @param Type of the request * @param endpoint The destination endpoint part of the URL * @param requestBody object representing the request body to be sent, may be null @@ -611,4 +656,18 @@ public Device getDeviceInfo(String deviceId) Request request = httpClient.createRequest(url, POST, requestBody); return request.send(); } + + public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName) + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + @Nullable + BoschHttpClient httpClient = this.httpClient; + if (httpClient == null) { + logger.warn("HttpClient not initialized"); + return null; + } + + String url = httpClient.getServiceUrl(serviceName, deviceId); + logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url); + return getState(httpClient, url, DeviceServiceData.class); + } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java similarity index 68% rename from bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java rename to bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java index 0da4e24344eae..82f5d495e5d77 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceStatusUpdate.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/DeviceServiceData.java @@ -13,9 +13,9 @@ package org.openhab.binding.boschshc.internal.devices.bridge.dto; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState; import com.google.gson.JsonElement; -import com.google.gson.annotations.SerializedName; /** * Represents a device status update as represented by the Smart Home @@ -24,18 +24,13 @@ * @author Stefan Kästle - Initial contribution * @author Christian Oeing - refactorings of e.g. server registration */ -public class DeviceStatusUpdate { +public class DeviceServiceData extends BoschSHCServiceState { + /** * Url path of the service the update came from. */ public String path; - /** - * The type of message. - */ - @SerializedName("@type") - public String type; - /** * Name of service the update came from. */ @@ -44,15 +39,26 @@ public class DeviceStatusUpdate { /** * Current state of device. Serialized as JSON. */ - public JsonElement state; + public @Nullable JsonElement state; /** * Id of device the update is for. */ public @Nullable String deviceId; + /** + * An optional object containing information about device faults. + *

+ * Example: low battery warnings are stored as faults with category WARNING + */ + public @Nullable Faults faults; + + public DeviceServiceData() { + super("DeviceServiceData"); + } + @Override public String toString() { - return this.deviceId + "state: " + this.type; + return this.deviceId + " state: " + this.type; } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java new file mode 100644 index 0000000000000..070c8f8068a89 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Fault.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +/** + * A fault entry containing a category and a type. + *

+ * Example JSON: + * + *

+ * {
+ *   "type":"LOW_BATTERY",
+ *   "category":"WARNING"
+ * }
+ * 
+ * + * @author David Pace - Initial contribution + * + */ +public class Fault { + public String type; + public String category; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java new file mode 100644 index 0000000000000..3e3df0d0ca0ab --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/Faults.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.bridge.dto; + +import java.util.List; + +/** + * A container object for faults that can be contained in {@link DeviceServiceData}. + *

+ * Example JSON: + * + *

+ * "faults": {
+ *   "entries": [
+ *     {
+ *       "type":"LOW_BATTERY",
+ *       "category":"WARNING"
+ *     }
+ *   ]
+   }
+ * 
+ * + * @author David Pace - Initial contribution + * + */ +public class Faults { + + public List entries; +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java index ec56a13ca5df9..ea830b35fa3ba 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/dto/LongPollResult.java @@ -35,6 +35,6 @@ public class LongPollResult { * ],"jsonrpc":"2.0"} */ - public ArrayList result; + public ArrayList result; public String jsonrpc; } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java index f4a088ceca364..64b16712efe85 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandler.java @@ -17,7 +17,7 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.latestmotion.LatestMotionService; import org.openhab.binding.boschshc.internal.services.latestmotion.dto.LatestMotionServiceState; @@ -32,7 +32,7 @@ * @author Christian Oeing - Use service instead of custom logic */ @NonNullByDefault -public class MotionDetectorHandler extends BoschSHCDeviceHandler { +public class MotionDetectorHandler extends AbstractBatteryPoweredDeviceHandler { public MotionDetectorHandler(Thing thing) { super(thing); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java index ff00373bea3ac..b0f956a6517d2 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandler.java @@ -12,14 +12,12 @@ */ package org.openhab.binding.boschshc.internal.devices.thermostat; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_CHILD_LOCK; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_VALVE_TAPPET_POSITION; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.childlock.ChildLockService; import org.openhab.binding.boschshc.internal.services.childlock.dto.ChildLockServiceState; @@ -33,11 +31,11 @@ /** * Handler for a thermostat device. - * + * * @author Christian Oeing - Initial contribution */ @NonNullByDefault -public final class ThermostatHandler extends BoschSHCDeviceHandler { +public final class ThermostatHandler extends AbstractBatteryPoweredDeviceHandler { private ChildLockService childLockService; @@ -48,6 +46,8 @@ public ThermostatHandler(Thing thing) { @Override protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); this.createService(ValveTappetService::new, this::updateChannels, List.of(CHANNEL_VALVE_TAPPET_POSITION)); this.registerService(this.childLockService, this::updateChannels, List.of(CHANNEL_CHILD_LOCK)); @@ -67,7 +67,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Updates the channels which are linked to the {@link TemperatureLevelService} * of the device. - * + * * @param state Current state of {@link TemperatureLevelService}. */ private void updateChannels(TemperatureLevelServiceState state) { @@ -77,7 +77,7 @@ private void updateChannels(TemperatureLevelServiceState state) { /** * Updates the channels which are linked to the {@link ValveTappetService} of * the device. - * + * * @param state Current state of {@link ValveTappetService}. */ private void updateChannels(ValveTappetServiceState state) { @@ -87,7 +87,7 @@ private void updateChannels(ValveTappetServiceState state) { /** * Updates the channels which are linked to the {@link ChildLockService} of the * device. - * + * * @param state Current state of {@link ChildLockService}. */ private void updateChannels(ChildLockServiceState state) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandler.java index d155c18965fb8..a4b0f3ac8244d 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandler.java @@ -12,14 +12,7 @@ */ package org.openhab.binding.boschshc.internal.devices.twinguard; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_AIR_DESCRIPTION; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_COMBINED_RATING; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY_RATING; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_PURITY_RATING; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE_RATING; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; import java.util.List; @@ -27,7 +20,7 @@ import javax.measure.quantity.Temperature; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.airqualitylevel.AirQualityLevelService; import org.openhab.binding.boschshc.internal.services.airqualitylevel.dto.AirQualityLevelServiceState; @@ -44,7 +37,7 @@ * @author Christian Oeing - Use service instead of custom logic */ @NonNullByDefault -public class TwinguardHandler extends BoschSHCDeviceHandler { +public class TwinguardHandler extends AbstractBatteryPoweredDeviceHandler { public TwinguardHandler(Thing thing) { super(thing); diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandler.java index 411c52ebb7a52..96de7355bdf30 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandler.java @@ -12,13 +12,12 @@ */ package org.openhab.binding.boschshc.internal.devices.wallthermostat; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_HUMIDITY; -import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.CHANNEL_TEMPERATURE; +import static org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants.*; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.humiditylevel.HumidityLevelService; import org.openhab.binding.boschshc.internal.services.humiditylevel.dto.HumidityLevelServiceState; @@ -28,11 +27,11 @@ /** * Handler for a wall thermostat device. - * + * * @author Christian Oeing - Initial contribution */ @NonNullByDefault -public final class WallThermostatHandler extends BoschSHCDeviceHandler { +public final class WallThermostatHandler extends AbstractBatteryPoweredDeviceHandler { public WallThermostatHandler(Thing thing) { super(thing); @@ -40,13 +39,15 @@ public WallThermostatHandler(Thing thing) { @Override protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + this.createService(TemperatureLevelService::new, this::updateChannels, List.of(CHANNEL_TEMPERATURE)); this.createService(HumidityLevelService::new, this::updateChannels, List.of(CHANNEL_HUMIDITY)); } /** * Updates the channels which are linked to the {@link TemperatureLevelService} of the device. - * + * * @param state Current state of {@link TemperatureLevelService}. */ private void updateChannels(TemperatureLevelServiceState state) { @@ -55,7 +56,7 @@ private void updateChannels(TemperatureLevelServiceState state) { /** * Updates the channels which are linked to the {@link HumidityLevelService} of the device. - * + * * @param state Current state of {@link HumidityLevelService}. */ private void updateChannels(HumidityLevelServiceState state) { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java index 08a34099cae78..1e79dea1ee742 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandler.java @@ -17,7 +17,7 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.boschshc.internal.devices.BoschSHCDeviceHandler; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandler; import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactService; import org.openhab.binding.boschshc.internal.services.shuttercontact.ShutterContactState; @@ -32,7 +32,7 @@ * @author Stefan Kästle - Initial contribution */ @NonNullByDefault -public class WindowContactHandler extends BoschSHCDeviceHandler { +public class WindowContactHandler extends AbstractBatteryPoweredDeviceHandler { public WindowContactHandler(Thing thing) { super(thing); @@ -40,6 +40,8 @@ public WindowContactHandler(Thing thing) { @Override protected void initializeServices() throws BoschSHCException { + super.initializeServices(); + this.createService(ShutterContactService::new, this::updateChannels, List.of(CHANNEL_CONTACT)); } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java index d3a0db77dfbf2..f0b7bf76827a8 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/BoschSHCService.java @@ -32,17 +32,17 @@ * same endpoint. *

* The endpoints of this service have the following URL structure: - * + * *

  * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
  * 
- * + * * The HTTP client of the bridge will use GET requests to retrieve the state and PUT requests * to set the state. *

* The services of the devices and their official APIs can be found * here. - * + * * @author Christian Oeing - Initial contribution * @author David Pace - Service abstraction */ @@ -63,7 +63,7 @@ public abstract class BoschSHCService exten /** * Constructor - * + * * @param serviceName Unique name of the service. * @param stateClass State class that this service uses for data transfers * from/to the device. @@ -75,7 +75,7 @@ protected BoschSHCService(String serviceName, Class stateClass) { /** * Initializes the service - * + * * @param bridgeHandler Bridge to use for communication from/to the device * @param deviceId Id of device this service is for * @param stateUpdateListener Function to call when a state update was received @@ -89,7 +89,7 @@ public void initialize(BridgeHandler bridgeHandler, String deviceId, /** * Returns the class of the state this service provides. - * + * * @return Class of the state this service provides. */ public Class getStateClass() { @@ -98,7 +98,7 @@ public Class getStateClass() { /** * Requests the current state of the service and updates it. - * + * * @throws ExecutionException * @throws TimeoutException * @throws InterruptedException @@ -114,7 +114,7 @@ public void refreshState() throws InterruptedException, TimeoutException, Execut /** * Requests the current state of the device with the specified id. - * + * * @return Current state of the device. * @throws ExecutionException * @throws TimeoutException @@ -136,7 +136,7 @@ public void refreshState() throws InterruptedException, TimeoutException, Execut /** * Sets the state of the device with the specified id. - * + * * @param state State to set. * @throws InterruptedException * @throws ExecutionException @@ -156,10 +156,10 @@ public void setState(TState state) throws InterruptedException, TimeoutException /** * A state update was received from the bridge - * + * * @param stateData Current state of service. Serialized as JSON. */ - public void onStateUpdate(JsonElement stateData) { + public void onStateUpdate(@Nullable JsonElement stateData) { @Nullable TState state = BoschSHCServiceState.fromJson(stateData, this.stateClass); if (state == null) { @@ -171,7 +171,7 @@ public void onStateUpdate(JsonElement stateData) { /** * A state update was received from the bridge. - * + * * @param state Current state of service as an instance of the state class. */ private void onStateUpdate(TState state) { @@ -184,7 +184,7 @@ private void onStateUpdate(TState state) { /** * Allows a service to handle a command and create a new state out of it. * The new state still has to be set via setState. - * + * * @param command Command to handle * @throws BoschSHCException If service can not handle command */ diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java new file mode 100644 index 0000000000000..de580326070c3 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevel.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.batterylevel; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Fault; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; + +/** + * Possible battery levels. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public enum BatteryLevel { + OK, + LOW_BATTERY, + CRITICAL_LOW, + CRITICALLY_LOW_BATTERY, + NOT_AVAILABLE; + + /** + * Derives a battery level by analyzing the fault elements in the given device service data. + *

+ * Note that no fault elements are present when the battery level is OK. + * + * @param deviceServiceData a device service data model + * @return the derived battery level + */ + public static BatteryLevel fromDeviceServiceData(DeviceServiceData deviceServiceData) { + Faults faults = deviceServiceData.faults; + if (faults == null || faults.entries == null || faults.entries.isEmpty()) { + return OK; + } + + for (Fault faultEntry : faults.entries) { + if ("warning".equalsIgnoreCase(faultEntry.category)) { + BatteryLevel batteryLevelState = BatteryLevel.get(faultEntry.type); + if (batteryLevelState != null) { + return batteryLevelState; + } + } + } + + return OK; + } + + /** + * Returns the corresponding battery level for the given string or null if no state matches. + * + * @param identifier the battery level identifier + * + * @return the matching battery level or null + */ + public static @Nullable BatteryLevel get(String identifier) { + for (BatteryLevel batteryLevelState : values()) { + if (batteryLevelState.toString().equalsIgnoreCase(identifier)) { + return batteryLevelState; + } + } + + return null; + } + + /** + * Transforms a Bosch-specific battery level to a percentage for the system.battery-level channel. + * + * @return a percentage between 0 and 100 as integer + */ + public State toState() { + switch (this) { + case LOW_BATTERY: + return new DecimalType(10); + case CRITICAL_LOW: + case CRITICALLY_LOW_BATTERY: + return new DecimalType(1); + case NOT_AVAILABLE: + return UnDefType.UNDEF; + default: + return new DecimalType(100); + } + } + + /** + * Transforms a Bosch-specific battery level to an ON/OFF state for the + * system.low-battery channel. + *

+ * If the result is ON, the battery is low; if the result is OFF the battery level is OK. + * + * @return + */ + public OnOffType toLowBatteryState() { + switch (this) { + case LOW_BATTERY: + case CRITICAL_LOW: + case CRITICALLY_LOW_BATTERY: + return OnOffType.ON; + default: + return OnOffType.OFF; + } + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java new file mode 100644 index 0000000000000..a4a6b4dd8079e --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelService.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.batterylevel; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.boschshc.internal.devices.bridge.BridgeHandler; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; +import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException; +import org.openhab.binding.boschshc.internal.services.BoschSHCService; + +/** + * Service to retrieve battery levels. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class BatteryLevelService extends BoschSHCService { + + public BatteryLevelService() { + super("BatteryLevel", DeviceServiceData.class); + } + + @Override + public @Nullable DeviceServiceData getState() + throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + + String deviceId = getDeviceId(); + if (deviceId == null) { + return null; + } + + BridgeHandler bridgeHandler = getBridgeHandler(); + if (bridgeHandler == null) { + return null; + } + return bridgeHandler.getServiceData(deviceId, getServiceName()); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java index 81d37ae924fc0..190b7ceb7e7f2 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/services/dto/BoschSHCServiceState.java @@ -22,7 +22,7 @@ /** * Base Bosch Smart Home Controller service state. - * + * * @author Christian Oeing - Initial contribution */ public class BoschSHCServiceState { @@ -40,7 +40,7 @@ public class BoschSHCServiceState { private @Nullable String stateType = null; @SerializedName("@type") - private final String type; + public final String type; protected BoschSHCServiceState(String type) { this.type = type; diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml index 12c1dda89eacb..5f3095f76da1e 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -53,7 +53,7 @@ - + The Twinguard smoke detector warns you in case of fire and constantly monitors the air. @@ -65,6 +65,8 @@ + + @@ -81,6 +83,8 @@ + + @@ -98,6 +102,8 @@ + + @@ -132,6 +138,8 @@ + + @@ -167,6 +175,8 @@ + + diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java new file mode 100644 index 0000000000000..6f6707e4db720 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractBatteryPoweredDeviceHandlerTest.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices; + +import static org.mockito.Mockito.verify; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.types.UnDefType; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +/** + * Abstract test implementation for battery-powered devices. + * + * @author David Pace - Initial contribution + * + * @param type of the battery-powered device to be tested + */ +@NonNullByDefault +public abstract class AbstractBatteryPoweredDeviceHandlerTest + extends AbstractBoschSHCDeviceHandlerTest { + + @Test + public void testProcessUpdate_BatteryLevel_LowBattery() { + JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n" + + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n" + + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n" + + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n" + + " \"type\":\"LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + " }\n" + + " ]\n" + " }\n" + "}"); + getFixture().processUpdate("BatteryLevel", deviceServiceData); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), + new DecimalType(10)); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON); + } + + @Test + public void testProcessUpdate_BatteryLevel_CriticalLow() { + JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n" + + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n" + + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n" + + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n" + + " \"type\":\"CRITICAL_LOW\",\n" + " \"category\":\"WARNING\"\n" + + " }\n" + " ]\n" + " }\n" + "}"); + getFixture().processUpdate("BatteryLevel", deviceServiceData); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), + new DecimalType(1)); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON); + } + + @Test + public void testProcessUpdate_BatteryLevel_CriticallyLowBattery() { + JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n" + + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n" + + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n" + + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n" + + " \"type\":\"CRITICALLY_LOW_BATTERY\",\n" + " \"category\":\"WARNING\"\n" + + " }\n" + " ]\n" + " }\n" + "}"); + getFixture().processUpdate("BatteryLevel", deviceServiceData); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), + new DecimalType(1)); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.ON); + } + + @Test + public void testProcessUpdate_BatteryLevel_OK() { + JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n" + + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n" + + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\" }"); + getFixture().processUpdate("BatteryLevel", deviceServiceData); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), + new DecimalType(100)); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF); + } + + @Test + public void testProcessUpdate_BatteryLevel_NotAvailable() { + JsonElement deviceServiceData = JsonParser.parseString("{ \n" + " \"@type\":\"DeviceServiceData\",\n" + + " \"path\":\"/devices/hdm:ZigBee:000d6f0004b93361/services/BatteryLevel\",\n" + + " \"id\":\"BatteryLevel\",\n" + " \"deviceId\":\"hdm:ZigBee:000d6f0004b93361\",\n" + + " \"faults\":{ \n" + " \"entries\":[\n" + " {\n" + + " \"type\":\"NOT_AVAILABLE\",\n" + " \"category\":\"WARNING\"\n" + + " }\n" + " ]\n" + " }\n" + "}"); + getFixture().processUpdate("BatteryLevel", deviceServiceData); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_BATTERY_LEVEL), UnDefType.UNDEF); + verify(getCallback()).stateUpdated( + new ChannelUID(getThing().getUID(), BoschSHCBindingConstants.CHANNEL_LOW_BATTERY), OnOffType.OFF); + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java index 38432d552aab4..f3bd2ecda33e0 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/AbstractPowerSwitchHandlerTest.java @@ -31,7 +31,6 @@ import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; import com.google.gson.JsonElement; @@ -60,8 +59,6 @@ public abstract class AbstractPowerSwitchHandlerTest { @BeforeEach public void beforeEach() { fixture = createFixture(); + lenient().when(thing.getUID()).thenReturn(getThingUID()); when(thing.getBridgeUID()).thenReturn(new ThingUID("boschshc", "shc", "myBridgeUID")); when(callback.getBridge(any())).thenReturn(bridge); fixture.setCallback(callback); @@ -72,6 +74,12 @@ protected T getFixture() { return fixture; } + protected ThingUID getThingUID() { + return new ThingUID(getThingTypeUID(), "abcdef"); + } + + protected abstract ThingTypeUID getThingTypeUID(); + protected Configuration getConfiguration() { return new Configuration(); } diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java index 9877caeb65813..48ab25ddaa1b0 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java @@ -71,10 +71,16 @@ void getBoschSmartHomeUrl() { @Test void getServiceUrl() { - assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state", + assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService", httpClient.getServiceUrl("testService", "testDevice")); } + @Test + void getServiceStateUrl() { + assertEquals("https://127.0.0.1:8444/smarthome/devices/testDevice/services/testService/state", + httpClient.getServiceStateUrl("testService", "testDevice")); + } + @Test void isAccessPossible() throws InterruptedException { assertFalse(httpClient.isAccessPossible()); diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java new file mode 100644 index 0000000000000..e35d145a78ded --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/motiondetector/MotionDetectorHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.motiondetector; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit Tests for {@link MotionDetectorHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class MotionDetectorHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { + + @Override + protected MotionDetectorHandler createFixture() { + return new MotionDetectorHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:000d6f0012fd2571"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_MOTION_DETECTOR; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java new file mode 100644 index 0000000000000..73b9b201f1fee --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/thermostat/ThermostatHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.thermostat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit Tests for {@link ThermostatHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class ThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { + + @Override + protected ThermostatHandler createFixture() { + return new ThermostatHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:000d6f0017f1ace2"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_THERMOSTAT; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java new file mode 100644 index 0000000000000..0b22f87c0b1d6 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/twinguard/TwinguardHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.twinguard; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit Tests for {@link TwinguardHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class TwinguardHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { + + @Override + protected TwinguardHandler createFixture() { + return new TwinguardHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:000d6f0016d1a193"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_TWINGUARD; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java new file mode 100644 index 0000000000000..8f1c2a67bca4f --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/wallthermostat/WallThermostatHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.wallthermostat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit Tests for {@link WallThermostatHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class WallThermostatHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { + + @Override + protected WallThermostatHandler createFixture() { + return new WallThermostatHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:ZigBee:000d6f0016d1a193"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_WALL_THERMOSTAT; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java new file mode 100644 index 0000000000000..9acd2cc1fa9d4 --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/windowcontact/WindowContactHandlerTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.devices.windowcontact; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.boschshc.internal.devices.AbstractBatteryPoweredDeviceHandlerTest; +import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants; +import org.openhab.core.thing.ThingTypeUID; + +/** + * Unit Tests for {@link WindowContactHandler}. + * + * @author David Pace - Initial contribution + * + */ +@NonNullByDefault +public class WindowContactHandlerTest extends AbstractBatteryPoweredDeviceHandlerTest { + + @Override + protected WindowContactHandler createFixture() { + return new WindowContactHandler(getThing()); + } + + @Override + protected String getDeviceID() { + return "hdm:HomeMaticIP:3014D711A000009D545DEB39D"; + } + + @Override + protected ThingTypeUID getThingTypeUID() { + return BoschSHCBindingConstants.THING_TYPE_WINDOW_CONTACT; + } +} diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java new file mode 100644 index 0000000000000..b788074d3934c --- /dev/null +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/services/batterylevel/BatteryLevelTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.boschshc.internal.services.batterylevel; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Fault; +import org.openhab.binding.boschshc.internal.devices.bridge.dto.Faults; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.UnDefType; + +/** + * Unit tests for {@link BatteryLevel}. + * + * @author David Pace - Initial contribution + * + */ +class BatteryLevelTest { + + @Test + void testGet() { + assertSame(BatteryLevel.OK, BatteryLevel.get("OK")); + assertSame(BatteryLevel.OK, BatteryLevel.get("ok")); + assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("LOW_BATTERY")); + assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.get("low_battery")); + assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("CRITICAL_LOW")); + assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.get("critical_low")); + assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("CRITICALLY_LOW_BATTERY")); + assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.get("critically_low_battery")); + assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("NOT_AVAILABLE")); + assertSame(BatteryLevel.NOT_AVAILABLE, BatteryLevel.get("not_available")); + assertNull(BatteryLevel.get("foo")); + } + + @Test + void testFromDeviceServiceData() { + DeviceServiceData deviceServiceData = new DeviceServiceData(); + assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + Faults faults = new Faults(); + deviceServiceData.faults = faults; + assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + ArrayList entries = new ArrayList<>(); + faults.entries = entries; + assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + Fault fault = new Fault(); + entries.add(fault); + assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + fault.category = "WARNING"; + fault.type = "LOW_BATTERY"; + assertSame(BatteryLevel.LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + fault.type = "CRITICAL_LOW"; + assertSame(BatteryLevel.CRITICAL_LOW, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + fault.type = "CRITICALLY_LOW_BATTERY"; + assertSame(BatteryLevel.CRITICALLY_LOW_BATTERY, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + + fault.type = "FOO"; + assertSame(BatteryLevel.OK, BatteryLevel.fromDeviceServiceData(deviceServiceData)); + } + + @Test + void testToState() { + assertEquals(new DecimalType(100), BatteryLevel.OK.toState()); + assertEquals(new DecimalType(10), BatteryLevel.LOW_BATTERY.toState()); + assertEquals(new DecimalType(1), BatteryLevel.CRITICAL_LOW.toState()); + assertEquals(new DecimalType(1), BatteryLevel.CRITICALLY_LOW_BATTERY.toState()); + assertEquals(UnDefType.UNDEF, BatteryLevel.NOT_AVAILABLE.toState()); + } + + @Test + void testToLowBatteryState() { + assertEquals(OnOffType.OFF, BatteryLevel.OK.toLowBatteryState()); + assertEquals(OnOffType.ON, BatteryLevel.LOW_BATTERY.toLowBatteryState()); + assertEquals(OnOffType.ON, BatteryLevel.CRITICAL_LOW.toLowBatteryState()); + assertEquals(OnOffType.ON, BatteryLevel.CRITICALLY_LOW_BATTERY.toLowBatteryState()); + assertEquals(OnOffType.OFF, BatteryLevel.NOT_AVAILABLE.toLowBatteryState()); + } +} From 3ed44f1ba5abbe8e0af74c427543e7c4a3d2a73e Mon Sep 17 00:00:00 2001 From: Fabian Wolter Date: Fri, 11 Nov 2022 10:12:32 +0100 Subject: [PATCH 47/55] [lcn] Fix setting of variables (#13690) Signed-off-by: Fabian Wolter --- bundles/org.openhab.binding.lcn/README.md | 14 +++++++------- .../binding/lcn/internal/common/PckGenerator.java | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bundles/org.openhab.binding.lcn/README.md b/bundles/org.openhab.binding.lcn/README.md index 35fa8c774e6d7..03534af0de4fe 100644 --- a/bundles/org.openhab.binding.lcn/README.md +++ b/bundles/org.openhab.binding.lcn/README.md @@ -140,7 +140,7 @@ If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende T | Motor/Shutter on Dimmer Outputs | Motor/Rollladen an Ausgängen | rollershutteroutput | 1-4 | Rollershutter | Control roller shutters on dimmer outputs | | Motor/Shutter on Relays | Motor/Rollladen an Relais | rollershutterrelay | 1-4 | Rollershutter, Dimmer | Control position of roller shutters on relays (Supports UpDown, StopMove, Percent) | | Shutter Slat Angle on Relays | Rollladenlamellen an Relais | rollershutterrelayslat | 1-4 | Rollershutter, Dimmer | Control slat angle of roller shutters on relays (Supports UpDown, StopMove, Percent) | -| Variables | Variable anzeigen | variable | 1-12 | Number | Sets and visualizes the value of a variable. | +| Variables | Variable anzeigen | variable | 1-12 | Number | Sets and visualizes the value of a variable. See note. | | Regulator Set Setpoint | Regler Sollwert ändern | rvarsetpoint | 1-2 | Number | Sets and visualizes the setpoint of a regulator. | | Regulator Set Mode | Reglerverhalten ändern | rvarmode | 1-2 | String | Sets the mode of the regulator: `HEATING` or `COOLING` | | Regulator Lock | Regler sperren | rvarlock | 1-2 | Switch | Locks a regulator and visualizes its locking state. | @@ -194,13 +194,13 @@ If a special command is needed, the [Hit Key](#hit-key) action (German: "Sende T | Set S0 Counters | S0-Zähler setzen | - | - | - | Not implemented | | Status Command | Statuskommandos | - | - | - | Not implemented | -**For some *Channel*s a unit should be configured for visualization.** By default the native LCN value is used. +*Notes:* -S0 counter Channels need to be the pulses per kWh configured. If the value is left blank, a default value of 1000 pulses/kWh is set. - -The Rollershutter Channels provide the boolean parameter `invertUpDown`, which can be set to 'true' if the Up/Down wires are interchanged. - -The binary sensor Channels provide the boolean parameter `invertState`, which can be set to 'true' if the binary sensor connected uses inverted logic for signaling open/closed. +- **For some *Channel*s (e.g. temperature) a unit should be configured in the channel configuration.** By default the native LCN value is used. +- S0 counter Channels need to be the pulses per kWh configured. If the value is left blank, a default value of 1000 pulses/kWh is set. +- When setting a variable via openHAB, the variable must be configured as counter in LCN-PRO. The variable must be set initially by the module after power up. +- The Rollershutter Channels provide the boolean parameter `invertUpDown`, which can be set to 'true' if the Up/Down wires are interchanged. +- The binary sensor Channels provide the boolean parameter `invertState`, which can be set to 'true' if the binary sensor connected uses inverted logic for signaling open/closed. ### Transponder/Fingerprints diff --git a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java index 62bd7b8f2c7b0..988b147d6b526 100644 --- a/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java +++ b/bundles/org.openhab.binding.lcn/src/main/java/org/openhab/binding/lcn/internal/common/PckGenerator.java @@ -513,13 +513,13 @@ public static String requestVarStatus(Variable variable, int firmwareVersion) th case UNKNOWN: throw new LcnException("Variable unknown"); case VARIABLE: - return String.format("MWT%03d", id + 1); + return "MWT" + (id + 1); case REGULATOR: - return String.format("MWS%03d", id + 1); + return "MWS" + (id + 1); case THRESHOLD: - return String.format("SE%03d", id + 1); // Whole register + return "SE" + (id + 1); // Whole register case S0INPUT: - return String.format("MWC%03d", id + 1); + return "MWC" + (id + 1); } throw new LcnException("Unsupported variable type: " + variable); } else { From 69b9f9895f6f42eb7999505a4672cfb5c80fa585 Mon Sep 17 00:00:00 2001 From: mlobstein Date: Fri, 11 Nov 2022 06:07:59 -0600 Subject: [PATCH 48/55] [monopriceaudio] Update README (#13688) * Update README Signed-off-by: Michael Lobstein --- bundles/org.openhab.binding.monopriceaudio/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bundles/org.openhab.binding.monopriceaudio/README.md b/bundles/org.openhab.binding.monopriceaudio/README.md index d7ead8dd027dc..eb30ee1fd35f1 100644 --- a/bundles/org.openhab.binding.monopriceaudio/README.md +++ b/bundles/org.openhab.binding.monopriceaudio/README.md @@ -18,8 +18,8 @@ Or you can connect it for example to a Raspberry Pi and use [ser2net Linux tool] ## Supported Things -There is exactly one supported thing type, which represents the amplifier controller. -It has the `amplifier` id. +Monoprice 10761 & 39261 and Dayton Audio DAX66 Amplifiers use the `amplifier` thing id. Up to 18 zones with 3 linked amps, 6 source inputs. +Note: Compatible clones (including 4 zone versions) from McLELLAND, Factor, Soundavo, etc. should work as well. ## Discovery @@ -130,7 +130,7 @@ sitemap monoprice label="Audio Control" { Setpoint item=all_volume minValue=0 maxValue=100 step=1 Switch item=all_mute } - + Frame label="Zone 1" { Switch item=z1_power Selection item=z1_source visibility=[z1_power==ON] @@ -144,7 +144,7 @@ sitemap monoprice label="Audio Control" { Text item=z1_page label="Page Active: [%s]" visibility=[z1_power==ON] Text item=z1_keypad label="Keypad Connected: [%s]" visibility=[z1_power==ON] } - + // repeat for zones 2-18 (substitute z1) } ``` From 62899e0fa529a3ecbc6b5e7e3b622d90a90693a4 Mon Sep 17 00:00:00 2001 From: openhab-bot Date: Fri, 11 Nov 2022 22:36:39 +0100 Subject: [PATCH 49/55] New translations jdbc.properties (German) (#13697) --- .../main/resources/OH-INF/i18n/jdbc_de.properties | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties index 0f0bdbfa4d06b..bf3c72b0fedec 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties +++ b/bundles/org.openhab.persistence.jdbc/src/main/resources/OH-INF/i18n/jdbc_de.properties @@ -3,9 +3,9 @@ persistence.config.jdbc.enableLogTime.description = Aktiviert die Zeit- und Perf persistence.config.jdbc.enableLogTime.option.true = Aktivieren persistence.config.jdbc.enableLogTime.option.false = Deaktivieren persistence.config.jdbc.maximumPoolSize.label = Maximale Pool-Größe der Verbindungen -persistence.config.jdbc.maximumPoolSize.description = Maximale Poolgröße für die Datenbankverbindung überschreiben.
Optional, Vorteinstellung weicht für jede Datenbank ab
https\://github.com/brettwooldridge/HikariCP/issues/256 +persistence.config.jdbc.maximumPoolSize.description = Maximale Poolgröße für die Datenbankverbindung überschreiben.
Optional, Voreinstellung weicht für jede Datenbank ab
https\://github.com/brettwooldridge/HikariCP/issues/256 persistence.config.jdbc.minimumIdle.label = Verbindung\: Minimale Leerlaufzeit -persistence.config.jdbc.minimumIdle.description = Überschreibt die minimale Datenbankverbindungs-Leerlaufzeot.
Optional, Voreinstellung varriert je nach Datenbank
https\://github.com/brettwooldridge/HikariCP/issues/256 +persistence.config.jdbc.minimumIdle.description = Überschreibt die minimale Datenbankverbindungs-Leerlaufzeit.
Optional, Voreinstellung variiert je nach Datenbank
https\://github.com/brettwooldridge/HikariCP/issues/256 persistence.config.jdbc.password.label = Datenbank Passwort persistence.config.jdbc.password.description = Definiert das Datenbank-Passwort. persistence.config.jdbc.rebuildTableNames.label = Tabellenname neu aufbauen @@ -15,13 +15,13 @@ persistence.config.jdbc.rebuildTableNames.option.false = Deaktivieren persistence.config.jdbc.sqltype.CALL.label = SQL-Typ CALL persistence.config.jdbc.sqltype.CALL.description = Überschreibt den JDBC/SQL Datentyp für CALL
Optional, Voreinstellung\: "VARCHAR(200)".
Allgemeine Informationen zu JDBC/SQL-Typen\: https\://mybatis.github.io/mybatis-3/apidocs/reference/org/apache/ibatis/type/JdbcType.html
und http\://www.h2database.com/html/datatypes.html
und http\://www.postgresql.org/docs/9.5/static/datatype.html persistence.config.jdbc.sqltype.COLOR.label = SQL-Typ COLOR -persistence.config.jdbc.sqltype.COLOR.description = Überschreibt den JDBC/SQL-Datentyp für COLOR.
Optional, Default\: "VARCHAR(70)". +persistence.config.jdbc.sqltype.COLOR.description = Überschreibt den JDBC/SQL-Datentyp für COLOR.
Optional, Voreinstellung\: "VARCHAR(70)". persistence.config.jdbc.sqltype.CONTACT.label = SQL-Typ CONTACT persistence.config.jdbc.sqltype.CONTACT.description = Überschreibt den JDBC/SQL-Datentyp für CONTACT.
Optional, Voreinstellung\: "VARCHAR(6)". persistence.config.jdbc.sqltype.DATETIME.label = SQL-Typ DATETIME persistence.config.jdbc.sqltype.DATETIME.description = Überschreibt den JDBC/SQL-Datentyp für DATETIME.
Optional, Voreinstellung\: "DATETIME". persistence.config.jdbc.sqltype.DIMMER.label = SQL-Typ DIMMER -persistence.config.jdbc.sqltype.DIMMER.description = Überschreibt den JDBC/SQL-Datentyp für DIMMER.
Optional, Default\: "TINYINT". +persistence.config.jdbc.sqltype.DIMMER.description = Überschreibt den JDBC/SQL-Datentyp für DIMMER.
Optional, Voreinstellung\: "TINYINT". persistence.config.jdbc.sqltype.IMAGE.label = SQL-Typ IMAGE persistence.config.jdbc.sqltype.IMAGE.description = Überschreibt den JDBC/SQL-Datentyp für IMAGE.
Optional, Voreinstellung\: "VARCHAR(65500)". persistence.config.jdbc.sqltype.LOCATION.label = SQL-Typ LOCATION @@ -36,8 +36,8 @@ persistence.config.jdbc.sqltype.STRING.label = SQL-Typ STRING persistence.config.jdbc.sqltype.STRING.description = Überschreibt den JDBC/SQL-Datentyp für STRING.
Optional, Voreinstellung\: "VARCHAR(65500)". persistence.config.jdbc.sqltype.SWITCH.label = SQL-Typ SWITCH persistence.config.jdbc.sqltype.SWITCH.description = Überschreibt den JDBC/SQL-Datentyp für SWITCH.
Optional, Voreinstellung\: "VARCHAR(6)". -persistence.config.jdbc.tableCaseSensitiveItemNames.label = Groß-/Kleinschreibung für Tabellenamen -persistence.config.jdbc.tableCaseSensitiveItemNames.description = Aktiviert die Groß-/Kleinschreibung für Tabellenamen, wenn 'Tabellenname für Real-Name Generierung' aktiviert ist.
Optional, Vorsteinstellung\: deaktiviert -> Tabellenamen werden klein geschrieben und der Suffix-ID Zähler wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. +persistence.config.jdbc.tableCaseSensitiveItemNames.label = Groß-/Kleinschreibung für Tabellennamen +persistence.config.jdbc.tableCaseSensitiveItemNames.description = Aktiviert die Groß-/Kleinschreibung für Tabellennamen, wenn 'Tabellenname für Real-Name Generierung' aktiviert ist.
Optional, Voreinstellung\: deaktiviert -> Tabellennamen werden klein geschrieben und der Suffix-ID Zähler wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. persistence.config.jdbc.tableCaseSensitiveItemNames.option.true = Aktivieren persistence.config.jdbc.tableCaseSensitiveItemNames.option.false = Deaktivieren persistence.config.jdbc.tableIdDigitCount.label = Suffix-ID Zähler für Tabellenname @@ -45,7 +45,7 @@ persistence.config.jdbc.tableIdDigitCount.description = Suffix-ID Zähler für T persistence.config.jdbc.tableNamePrefix.label = Tabellennamen-Präfix persistence.config.jdbc.tableNamePrefix.description = Präfix für Tabellenname. Optional, Voreinstellung\: "item".
Auf "item" für die Migration des mySQL-Bundles setzen. persistence.config.jdbc.tableUseRealItemNames.label = Tabellenname für Real-Name Generierung -persistence.config.jdbc.tableUseRealItemNames.description = Erlaubt die Präfix-Generierung für Tabellenname pro Item-Realname.
Optional, Vorsteinstellung\: deaktiviert -> Tabellennamen-Präfix wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. +persistence.config.jdbc.tableUseRealItemNames.description = Erlaubt die Präfix-Generierung für Tabellenname pro Item-Realname.
Optional, Voreinstellung\: deaktiviert -> Tabellennamen-Präfix wird verwendet.
Wenn aktiviert, dann wird der Tabellennamen-Präfix ignoriert. persistence.config.jdbc.tableUseRealItemNames.option.true = Aktivieren persistence.config.jdbc.tableUseRealItemNames.option.false = Deaktivieren persistence.config.jdbc.url.label = Datenbank-URL From 8c7d77407428aebda1ad2d8e9b2615e0bfe1529f Mon Sep 17 00:00:00 2001 From: quidam Date: Fri, 11 Nov 2022 23:13:05 +0100 Subject: [PATCH 50/55] [avmfritz] Decode alarm state for blinds (#13672) * [avmfritz] Decode alarm state for blinds Signed-off-by: Ulrich Mertin --- .../org.openhab.binding.avmfritz/README.md | 2 ++ .../internal/AVMFritzBindingConstants.java | 2 ++ .../avmfritz/internal/dto/AlertModel.java | 12 ++++++++++++ .../handler/AVMFritzBaseThingHandler.java | 19 ++++++++++++++++++- .../resources/OH-INF/i18n/avmfritz.properties | 4 ++++ .../resources/OH-INF/thing/channel-types.xml | 15 ++++++++++++++- .../resources/OH-INF/thing/thing-types.xml | 2 ++ 7 files changed, 54 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.avmfritz/README.md b/bundles/org.openhab.binding.avmfritz/README.md index f3b1c9115b292..d0421234a9fd8 100644 --- a/bundles/org.openhab.binding.avmfritz/README.md +++ b/bundles/org.openhab.binding.avmfritz/README.md @@ -199,6 +199,8 @@ The AIN (actor identification number) can be found in the FRITZ!Box interface -> | contact_state | Contact | Contact state information (OPEN/CLOSED). | HAN-FUN contact (e.g. SmartHome Tür-/Fensterkontakt or SmartHome Bewegungsmelder)- FRITZ!OS 7 | | last_change | DateTime | States the last time the button was pressed. | FRITZ!DECT 400, FRITZ!DECT 440, HAN-FUN switch (e.g. SmartHome Wandtaster) - FRITZ!OS 7 | | rollershutter | Rollershutter | Rollershutter control and status. Accepts UP/DOWN/STOP commands and the opening level in percent. States the opening level in percent. | HAN-FUN blind (e.g. Rolltron DECT 1213) - FRITZ!OS 7 | +| obstruction_alarm | Obstruction Alarm | Rollershutter obstruction alarm (ON/OFF) | HAN-FUN blind (e.g. Rolltron DECT 1213) - FRITZ!OS 7 | +| temperature_alarm | Temperature Alarm | Rollershutter temperature alarm (ON/OFF) | HAN-FUN blind (e.g. Rolltron DECT 1213) - FRITZ!OS 7 | ### Triggers diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java index 10da4a4bf1ebb..51b9a149a2c73 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/AVMFritzBindingConstants.java @@ -136,6 +136,8 @@ public class AVMFritzBindingConstants { public static final String CHANNEL_NEXTTEMP = "next_temp"; public static final String CHANNEL_BATTERY_LOW = "battery_low"; public static final String CHANNEL_BATTERY = "battery_level"; + public static final String CHANNEL_OBSTRUCTION_ALARM = "obstruction_alarm"; + public static final String CHANNEL_TEMPERATURE_ALARM = "temperature_alarm"; public static final String CHANNEL_CONTACT_STATE = "contact_state"; public static final String CHANNEL_PRESS = "press"; public static final String CHANNEL_LAST_CHANGE = "last_change"; diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AlertModel.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AlertModel.java index 3ce765eae2665..7c7df41d80e59 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AlertModel.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/dto/AlertModel.java @@ -41,6 +41,18 @@ public void setState(BigDecimal state) { this.state = state; } + public boolean hasObstructionAlarmOccurred() { + return (state.intValue() & 1) != 0; + } + + public boolean hasTemperaturAlarmOccurred() { + return (state.intValue() & 2) != 0; + } + + public boolean hasUnknownAlarmOccurred() { + return ((state.intValue() & 255) >> 2) != 0; + } + @Override public String toString() { return new StringBuilder().append("[state=").append(state).append("]").toString(); diff --git a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java index 6641749972c00..e901bc4fcacaf 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java +++ b/bundles/org.openhab.binding.avmfritz/src/main/java/org/openhab/binding/avmfritz/internal/handler/AVMFritzBaseThingHandler.java @@ -152,7 +152,11 @@ public void onDeviceUpdated(ThingUID thingUID, AVMFritzBaseModel device) { updateHumiditySensor(deviceModel.getHumidity()); } if (deviceModel.isHANFUNAlarmSensor()) { - updateHANFUNAlarmSensor(deviceModel.getAlert()); + if (deviceModel.isHANFUNBlinds()) { + updateHANFUNBlindsAlarmSensor(deviceModel.getAlert()); + } else { + updateHANFUNAlarmSensor(deviceModel.getAlert()); + } } if (deviceModel.isHANFUNBlinds()) { updateLevelControl(deviceModel.getLevelControlModel()); @@ -174,6 +178,17 @@ private void updateHANFUNAlarmSensor(@Nullable AlertModel alertModel) { } } + private void updateHANFUNBlindsAlarmSensor(@Nullable AlertModel alertModel) { + if (alertModel != null) { + updateThingChannelState(CHANNEL_OBSTRUCTION_ALARM, + OnOffType.from(alertModel.hasObstructionAlarmOccurred())); + updateThingChannelState(CHANNEL_TEMPERATURE_ALARM, OnOffType.from(alertModel.hasTemperaturAlarmOccurred())); + if (alertModel.hasUnknownAlarmOccurred()) { + logger.warn("Unknown blinds alarm {}", alertModel.getState()); + } + } + } + protected void updateTemperatureSensor(@Nullable TemperatureModel temperatureModel) { if (temperatureModel != null) { updateThingChannelState(CHANNEL_TEMPERATURE, @@ -397,6 +412,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { case CHANNEL_BATTERY_LOW: case CHANNEL_CONTACT_STATE: case CHANNEL_LAST_CHANGE: + case CHANNEL_OBSTRUCTION_ALARM: + case CHANNEL_TEMPERATURE_ALARM: logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command); break; case CHANNEL_OUTLET: diff --git a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz.properties b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz.properties index f7198f4ce5ec4..6f9ef4909952e 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz.properties +++ b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/i18n/avmfritz.properties @@ -137,6 +137,10 @@ channel-type.avmfritz.comfort_temp.label = Comfort Temperature channel-type.avmfritz.comfort_temp.description = Thermostat Comfort temperature. channel-type.avmfritz.contact_state.label = Contact State channel-type.avmfritz.contact_state.description = Contact state information (OPEN/CLOSED). +channel-type.avmfritz.obstruction_alarm.label = Obstruction Alarm +channel-type.avmfritz.obstruction_alarm.description = Obstruction alarm of the blinds. The blinds were stopped and moved a bit in the opposite direction. +channel-type.avmfritz.temperature_alarm.label = Temperature Alarm +channel-type.avmfritz.temperature_alarm.description = Temperature alarm of the blinds. Indicates overheating of the motor. channel-type.avmfritz.device_locked.label = Locked (manual) channel-type.avmfritz.device_locked.description = Device is locked for switching by pressing the button on the device. channel-type.avmfritz.eco_temp.label = Eco Temperature diff --git a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/channel-types.xml index 2c80f7ff878ec..69543c6c8d735 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/channel-types.xml +++ b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/channel-types.xml @@ -224,6 +224,20 @@ + + Switch + + Obstruction alarm of the blinds. The blinds were stopped and moved a bit in the opposite direction. + + + + + Switch + + Temperature alarm of the blinds. Indicates overheating of the motor. + + + DateTime @@ -236,7 +250,6 @@ Rollershutter Controls the rollershutter and states its opening level in percent - Blinds diff --git a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml index b6cf1e3dfeed0..00c37e4350d45 100644 --- a/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.avmfritz/src/main/resources/OH-INF/thing/thing-types.xml @@ -334,6 +334,8 @@ + + ain From fe269b127ae83b09fb90eeb9df646d7599c30365 Mon Sep 17 00:00:00 2001 From: Mark Herwege Date: Fri, 11 Nov 2022 23:17:52 +0100 Subject: [PATCH 51/55] [Systeminfo] Add comment on temperature readings on Windows (#13696) * Add comment on temperature readings on Windows Signed-off-by: Mark Herwege mark.herwege@telenet.be --- bundles/org.openhab.binding.systeminfo/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bundles/org.openhab.binding.systeminfo/README.md b/bundles/org.openhab.binding.systeminfo/README.md index 500c631315753..bc3abf59fe129 100644 --- a/bundles/org.openhab.binding.systeminfo/README.md +++ b/bundles/org.openhab.binding.systeminfo/README.md @@ -167,6 +167,10 @@ This makes the channels from this groups very flexible - they can change its PID Parameter PID has a default value 0 - this is the PID of the System Idle process in Windows OS. +## Known issues and workarounds + +- Temperature readings are not well supported on standard Windows systems, run [OpenHardwareMonitor.exe](https://openhardwaremonitor.org) for the binding to get more reliable readings. + ## Reporting issues As already mentioned this binding depends heavily on the [OSHI](https://github.com/oshi/oshi) API to provide the operating system and hardware information. From 248ca1830a2d5a3a0c6b965dcb95c14cd9863dcf Mon Sep 17 00:00:00 2001 From: Boris Krivonog Date: Sat, 12 Nov 2022 10:44:45 +0100 Subject: [PATCH 52/55] If for some reason HP response fails to properly parse, i.e. `java.lang.NumberFormatException: For input string: "##"` exception is not handled and scheduler is not re-triggered (polling stops).Fixed build warnings. (#13685) Signed-off-by: Boris Krivonog --- .../internal/handler/HusdataHandler.java | 28 ++++++-- .../internal/handler/IpHusdataHandler.java | 2 + .../handler/IpRego6xxHeatPumpHandler.java | 2 + .../handler/Rego6xxHeatPumpHandler.java | 66 ++++++++++++------- .../handler/SerialHusdataHandler.java | 13 +++- .../handler/SerialRego6xxHeatPumpHandler.java | 13 +++- .../internal/protocol/IpRegoConnection.java | 24 +++++-- .../internal/protocol/RegoConnection.java | 3 + .../protocol/SerialRegoConnection.java | 31 +++++++-- .../rego6xx/AbstractLongResponseParser.java | 3 + .../rego6xx/AbstractResponseParser.java | 2 + .../internal/rego6xx/Checksum.java | 3 + .../internal/rego6xx/CommandFactory.java | 3 + .../internal/rego6xx/ErrorLine.java | 5 ++ .../rego6xx/ErrorLineResponseParser.java | 5 +- .../rego6xx/Rego6xxProtocolException.java | 3 + .../internal/rego6xx/RegoRegisterMapper.java | 13 ++-- .../internal/rego6xx/ResponseParser.java | 3 + .../rego6xx/ResponseParserFactory.java | 5 +- .../internal/rego6xx/ShortResponseParser.java | 3 + .../rego6xx/StringResponseParser.java | 3 + .../internal/rego6xx/ValueConverter.java | 3 + .../internal/rego6xx/WriteResponse.java | 9 ++- 23 files changed, 191 insertions(+), 54 deletions(-) diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/HusdataHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/HusdataHandler.java index 050e271db07f5..e6362393de17b 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/HusdataHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/HusdataHandler.java @@ -26,6 +26,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; @@ -34,6 +36,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; @@ -45,13 +48,14 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault abstract class HusdataHandler extends BaseThingHandler { private static final Map MAPPINGS; private final Logger logger = LoggerFactory.getLogger(HusdataHandler.class); - private RegoConnection connection; - private ScheduledFuture scheduledRefreshFuture; - private BufferedReader bufferedReader; + private @Nullable RegoConnection connection; + private @Nullable ScheduledFuture scheduledRefreshFuture; + private @Nullable BufferedReader bufferedReader; static { MAPPINGS = mappings(); @@ -61,12 +65,18 @@ protected HusdataHandler(Thing thing) { super(thing); } - protected abstract RegoConnection createConnection(); + protected abstract RegoConnection createConnection() throws IOException; @Override public void initialize() { bufferedReader = null; - connection = createConnection(); + + try { + connection = createConnection(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return; + } updateStatus(ThingStatus.UNKNOWN); @@ -78,14 +88,16 @@ public void initialize() { public void dispose() { super.dispose(); + ScheduledFuture scheduledRefreshFuture = this.scheduledRefreshFuture; + this.scheduledRefreshFuture = null; if (scheduledRefreshFuture != null) { scheduledRefreshFuture.cancel(true); - scheduledRefreshFuture = null; } + RegoConnection connection = this.connection; + this.connection = null; if (connection != null) { connection.close(); - connection = null; } } @@ -112,8 +124,10 @@ private synchronized void handleDataFromHusdataInterface() { outputStream.flush(); } + BufferedReader bufferedReader = this.bufferedReader; if (bufferedReader == null) { bufferedReader = new BufferedReader(new InputStreamReader(connection.inputStream())); + this.bufferedReader = bufferedReader; } final String line = bufferedReader.readLine(); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpHusdataHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpHusdataHandler.java index 53307ccdf1176..24d9dc4fcd975 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpHusdataHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpHusdataHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.regoheatpump.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants; import org.openhab.binding.regoheatpump.internal.protocol.IpRegoConnection; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; @@ -23,6 +24,7 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class IpHusdataHandler extends HusdataHandler { public IpHusdataHandler(Thing thing) { super(thing); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpRego6xxHeatPumpHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpRego6xxHeatPumpHandler.java index 1a3d6da161cee..4d8bcaeaae604 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpRego6xxHeatPumpHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/IpRego6xxHeatPumpHandler.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.regoheatpump.internal.handler; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants; import org.openhab.binding.regoheatpump.internal.protocol.IpRegoConnection; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; @@ -23,6 +24,7 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class IpRego6xxHeatPumpHandler extends Rego6xxHeatPumpHandler { public IpRego6xxHeatPumpHandler(Thing thing) { super(thing); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/Rego6xxHeatPumpHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/Rego6xxHeatPumpHandler.java index 0cc2ba760c03b..bcaf5feeb413e 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/Rego6xxHeatPumpHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/Rego6xxHeatPumpHandler.java @@ -29,6 +29,8 @@ import javax.measure.Unit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; import org.openhab.binding.regoheatpump.internal.rego6xx.CommandFactory; import org.openhab.binding.regoheatpump.internal.rego6xx.ErrorLine; @@ -60,13 +62,15 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault abstract class Rego6xxHeatPumpHandler extends BaseThingHandler { private static final class ChannelDescriptor { - private Date lastUpdate; - private byte[] cachedValue; + private @Nullable Date lastUpdate; + private byte @Nullable [] cachedValue; - public byte[] cachedValueIfNotExpired(int refreshTime) { + public byte @Nullable [] cachedValueIfNotExpired(int refreshTime) { + Date lastUpdate = this.lastUpdate; if (lastUpdate == null || (lastUpdate.getTime() + refreshTime * 900 < new Date().getTime())) { return null; } @@ -82,23 +86,27 @@ public void setValue(byte[] value) { private final Logger logger = LoggerFactory.getLogger(Rego6xxHeatPumpHandler.class); private final Map channelDescriptors = new HashMap<>(); + private final RegoRegisterMapper mapper = RegoRegisterMapper.REGO600; + private @Nullable RegoConnection connection; + private @Nullable ScheduledFuture scheduledRefreshFuture; private int refreshInterval; - private RegoConnection connection; - private RegoRegisterMapper mapper; - private ScheduledFuture scheduledRefreshFuture; protected Rego6xxHeatPumpHandler(Thing thing) { super(thing); } - protected abstract RegoConnection createConnection(); + protected abstract RegoConnection createConnection() throws IOException; @Override public void initialize() { - mapper = RegoRegisterMapper.REGO600; refreshInterval = ((Number) getConfig().get(REFRESH_INTERVAL)).intValue(); - connection = createConnection(); + try { + connection = createConnection(); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + return; + } scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, 2, refreshInterval, TimeUnit.SECONDS); @@ -109,21 +117,21 @@ public void initialize() { public void dispose() { super.dispose(); + RegoConnection connection = this.connection; + this.connection = null; if (connection != null) { connection.close(); } + ScheduledFuture scheduledRefreshFuture = this.scheduledRefreshFuture; + this.scheduledRefreshFuture = null; if (scheduledRefreshFuture != null) { scheduledRefreshFuture.cancel(true); - scheduledRefreshFuture = null; } synchronized (channelDescriptors) { channelDescriptors.clear(); } - - connection = null; - mapper = null; } @Override @@ -220,7 +228,7 @@ private void readAndUpdateLastErrorTimestamp() { private void readAndUpdateLastError(String channelIID, Function converter) { executeCommandAndUpdateState(channelIID, CommandFactory.createReadLastErrorCommand(), ResponseParserFactory.ERROR_LINE, e -> { - return e == null ? UnDefType.NULL : converter.apply(e); + return e == ErrorLine.NO_ERROR ? UnDefType.NULL : converter.apply(e); }); } @@ -252,7 +260,7 @@ private void executeCommandAndUpdateState(String channelIID, byte[] command, }); } - private synchronized void executeCommand(String channelIID, byte[] command, ResponseParser parser, + private synchronized void executeCommand(@Nullable String channelIID, byte[] command, ResponseParser parser, Consumer resultProcessor) { try { T result = executeCommandWithRetry(channelIID, command, parser, 5); @@ -265,17 +273,19 @@ private synchronized void executeCommand(String channelIID, byte[] command, } updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (Rego6xxProtocolException e) { + } catch (Rego6xxProtocolException | RuntimeException e) { logger.warn("Executing command for channel '{}' failed.", channelIID, e); - updateState(channelIID, UnDefType.UNDEF); + if (channelIID != null) { + updateState(channelIID, UnDefType.UNDEF); + } } catch (InterruptedException e) { logger.debug("Execution interrupted when accessing value for channel '{}'.", channelIID, e); Thread.currentThread().interrupt(); } } - private T executeCommandWithRetry(String channelIID, byte[] command, ResponseParser parser, int retry) - throws Rego6xxProtocolException, IOException, InterruptedException { + private T executeCommandWithRetry(@Nullable String channelIID, byte[] command, ResponseParser parser, + int retry) throws Rego6xxProtocolException, IOException, InterruptedException { try { checkRegoDevice(); return executeCommand(channelIID, command, parser); @@ -316,11 +326,12 @@ private ChannelDescriptor channelDescriptorForChannel(String channelIID) { } } - private T executeCommand(String channelIID, byte[] command, ResponseParser parser) + private T executeCommand(@Nullable String channelIID, byte[] command, ResponseParser parser) throws Rego6xxProtocolException, IOException, InterruptedException { try { return executeCommandInternal(channelIID, command, parser); } catch (IOException e) { + RegoConnection connection = this.connection; if (connection != null) { connection.close(); } @@ -329,7 +340,7 @@ private T executeCommand(String channelIID, byte[] command, ResponseParser T executeCommandInternal(String channelIID, byte[] command, ResponseParser parser) + private T executeCommandInternal(@Nullable String channelIID, byte[] command, ResponseParser parser) throws Rego6xxProtocolException, IOException, InterruptedException { // CHANNEL_LAST_ERROR_CODE and CHANNEL_LAST_ERROR_TIMESTAMP are read from same // register. To prevent accessing same register twice when both channels are linked, @@ -338,8 +349,12 @@ private T executeCommandInternal(String channelIID, byte[] command, Response || CHANNEL_LAST_ERROR_TIMESTAMP.equals(channelIID)) ? CHANNEL_LAST_ERROR : channelIID; // Use transient channel descriptor for null (not cached) channels. - ChannelDescriptor descriptor = channelIID == null ? new ChannelDescriptor() - : channelDescriptorForChannel(mappedChannelIID); + ChannelDescriptor descriptor; + if (mappedChannelIID == null) { + descriptor = new ChannelDescriptor(); + } else { + descriptor = channelDescriptorForChannel(mappedChannelIID); + } byte[] cachedValue = descriptor.cachedValueIfNotExpired(refreshInterval); if (cachedValue != null) { @@ -348,6 +363,11 @@ private T executeCommandInternal(String channelIID, byte[] command, Response } // Send command to device and wait for response. + RegoConnection connection = this.connection; + if (connection == null) { + throw new IOException("Unable to execute command - no connection available"); + + } if (!connection.isConnected()) { connection.connect(); } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialHusdataHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialHusdataHandler.java index a37d471fe8ed7..f93bca4dbfffc 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialHusdataHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialHusdataHandler.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.regoheatpump.internal.handler; +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; import org.openhab.binding.regoheatpump.internal.protocol.SerialRegoConnection; @@ -27,9 +31,10 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class SerialHusdataHandler extends HusdataHandler { private final SerialPortManager serialPortManager; - private SerialPortIdentifier serialPortIdentifier; + private @Nullable SerialPortIdentifier serialPortIdentifier; public SerialHusdataHandler(Thing thing, SerialPortManager serialPortManager) { super(thing); @@ -49,7 +54,11 @@ public void initialize() { } @Override - protected RegoConnection createConnection() { + protected RegoConnection createConnection() throws IOException { + SerialPortIdentifier serialPortIdentifier = this.serialPortIdentifier; + if (serialPortIdentifier == null) { + throw new IOException("Serial port does not exist"); + } return new SerialRegoConnection(serialPortIdentifier, 19200); } } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialRego6xxHeatPumpHandler.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialRego6xxHeatPumpHandler.java index 02476464d2544..709326cf2bb02 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialRego6xxHeatPumpHandler.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/handler/SerialRego6xxHeatPumpHandler.java @@ -12,6 +12,10 @@ */ package org.openhab.binding.regoheatpump.internal.handler; +import java.io.IOException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.regoheatpump.internal.RegoHeatPumpBindingConstants; import org.openhab.binding.regoheatpump.internal.protocol.RegoConnection; import org.openhab.binding.regoheatpump.internal.protocol.SerialRegoConnection; @@ -27,9 +31,10 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class SerialRego6xxHeatPumpHandler extends Rego6xxHeatPumpHandler { private final SerialPortManager serialPortManager; - private SerialPortIdentifier serialPortIdentifier; + private @Nullable SerialPortIdentifier serialPortIdentifier; public SerialRego6xxHeatPumpHandler(Thing thing, SerialPortManager serialPortManager) { super(thing); @@ -49,7 +54,11 @@ public void initialize() { } @Override - protected RegoConnection createConnection() { + protected RegoConnection createConnection() throws IOException { + SerialPortIdentifier serialPortIdentifier = this.serialPortIdentifier; + if (serialPortIdentifier == null) { + throw new IOException("Serial port does not exist"); + } return new SerialRegoConnection(serialPortIdentifier, 19200); } } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/IpRegoConnection.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/IpRegoConnection.java index f81295ef27860..365858bb5eca0 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/IpRegoConnection.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/IpRegoConnection.java @@ -18,6 +18,8 @@ import java.net.InetSocketAddress; import java.net.Socket; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +28,7 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class IpRegoConnection implements RegoConnection { /** * Connection timeout in milliseconds @@ -40,7 +43,7 @@ public class IpRegoConnection implements RegoConnection { private final Logger logger = LoggerFactory.getLogger(IpRegoConnection.class); private final String address; private final int port; - private Socket clientSocket; + private @Nullable Socket clientSocket; public IpRegoConnection(String address, int port) { this.address = address; @@ -50,10 +53,12 @@ public IpRegoConnection(String address, int port) { @Override public void connect() throws IOException { logger.debug("Connecting to '{}', port = {}.", address, port); + Socket clientSocket = this.clientSocket; if (clientSocket == null) { clientSocket = new Socket(); clientSocket.setSoTimeout(SOCKET_READ_TIMEOUT); clientSocket.setKeepAlive(true); + this.clientSocket = clientSocket; } clientSocket.connect(new InetSocketAddress(address, port), CONNECTION_TIMEOUT); logger.debug("Connected to '{}', port = {}.", address, port); @@ -61,12 +66,15 @@ public void connect() throws IOException { @Override public boolean isConnected() { + Socket clientSocket = this.clientSocket; return clientSocket != null && clientSocket.isConnected(); } @Override public void close() { try { + Socket clientSocket = this.clientSocket; + this.clientSocket = null; if (clientSocket != null) { clientSocket.close(); } @@ -74,17 +82,23 @@ public void close() { // There is really not much we can do here, ignore the error and continue execution. logger.warn("Closing socket failed", e); } - - clientSocket = null; } @Override public OutputStream outputStream() throws IOException { - return clientSocket.getOutputStream(); + return getClientSocket().getOutputStream(); } @Override public InputStream inputStream() throws IOException { - return clientSocket.getInputStream(); + return getClientSocket().getInputStream(); + } + + private Socket getClientSocket() throws IOException { + Socket clientSocket = this.clientSocket; + if (clientSocket == null) { + throw new IOException("Socket closed"); + } + return clientSocket; } } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/RegoConnection.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/RegoConnection.java index 6dda7a585791e..b233855978f02 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/RegoConnection.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/RegoConnection.java @@ -16,11 +16,14 @@ import java.io.InputStream; import java.io.OutputStream; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link RegoConnection} is responsible for creating connections to clients. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public interface RegoConnection { /** * Connect to the receiver. Return true if the connection has succeeded or if already connected. diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/SerialRegoConnection.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/SerialRegoConnection.java index 2521b681526c0..d337b2dd6af85 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/SerialRegoConnection.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/protocol/SerialRegoConnection.java @@ -16,6 +16,8 @@ import java.io.InputStream; import java.io.OutputStream; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.io.transport.serial.PortInUseException; import org.openhab.core.io.transport.serial.SerialPort; import org.openhab.core.io.transport.serial.SerialPortIdentifier; @@ -26,10 +28,11 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class SerialRegoConnection implements RegoConnection { private final int baudRate; private final String portName; - private SerialPort serialPort; + private @Nullable SerialPort serialPort; private final SerialPortIdentifier serialPortIdentifier; public SerialRegoConnection(SerialPortIdentifier serialPortIdentifier, int baudRate) { @@ -41,10 +44,11 @@ public SerialRegoConnection(SerialPortIdentifier serialPortIdentifier, int baudR @Override public void connect() throws IOException { try { - serialPort = serialPortIdentifier.open(SerialRegoConnection.class.getCanonicalName(), 2000); + SerialPort serialPort = serialPortIdentifier.open(SerialRegoConnection.class.getCanonicalName(), 2000); serialPort.enableReceiveTimeout(100); serialPort.setSerialPortParams(baudRate, SerialPort.DATABITS_8, SerialPort.STOPBITS_1, SerialPort.PARITY_NONE); + this.serialPort = serialPort; } catch (PortInUseException e) { throw new IOException("Serial port already used: " + portName, e); } catch (UnsupportedCommOperationException e) { @@ -59,19 +63,36 @@ public boolean isConnected() { @Override public void close() { + SerialPort serialPort = this.serialPort; + this.serialPort = null; if (serialPort != null) { serialPort.close(); - serialPort = null; } } @Override public OutputStream outputStream() throws IOException { - return serialPort.getOutputStream(); + OutputStream outputStream = getSerialPort().getOutputStream(); + if (outputStream == null) { + throw new IOException("Sending data is not supported"); + } + return outputStream; } @Override public InputStream inputStream() throws IOException { - return serialPort.getInputStream(); + InputStream inputStream = getSerialPort().getInputStream(); + if (inputStream == null) { + throw new IOException("Receiving data is not supported"); + } + return inputStream; + } + + private SerialPort getSerialPort() throws IOException { + SerialPort serialPort = this.serialPort; + if (serialPort == null) { + throw new IOException("Connection closed"); + } + return serialPort; } } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractLongResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractLongResponseParser.java index 75ac37f72fd2d..ed16a7f7caa85 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractLongResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractLongResponseParser.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link AbstractLongResponseParser} is responsible for parsing long form responses. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault abstract class AbstractLongResponseParser extends AbstractResponseParser { @Override public int responseLength() { diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractResponseParser.java index e8340fd29e406..a118281c423f8 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/AbstractResponseParser.java @@ -12,6 +12,7 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.core.util.HexUtils; /** @@ -20,6 +21,7 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault abstract class AbstractResponseParser implements ResponseParser { private static final byte COMPUTER_ADDRESS = (byte) 0x01; diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Checksum.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Checksum.java index b95f05f8f0b50..18c238d64401a 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Checksum.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Checksum.java @@ -14,11 +14,14 @@ import java.util.Arrays; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link Checksum} is responsible for calculating checksum of given data. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault class Checksum { static byte calculate(byte[]... lists) { return Arrays.stream(lists).reduce((byte) 0, Checksum::calculate, (a, b) -> b); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/CommandFactory.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/CommandFactory.java index c3333b58600f2..27c6ce06798df 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/CommandFactory.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/CommandFactory.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link CommandFactory} is responsible for creating different commands that can * be send to a rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class CommandFactory { private static final byte DEVICE_ADDRESS = (byte) 0x81; diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLine.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLine.java index ca7941b35964c..3097515c2599a 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLine.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLine.java @@ -15,15 +15,20 @@ import java.time.ZoneId; import java.time.ZonedDateTime; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ErrorLine} is responsible for holding information about a single error line. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class ErrorLine { private final byte error; private final String timestamp; + public static final ErrorLine NO_ERROR = new ErrorLine((byte) 0, ""); + public ErrorLine(byte error, String timestamp) { this.error = error; this.timestamp = timestamp; diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLineResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLineResponseParser.java index cc693124d77c2..00ecfe7934022 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLineResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ErrorLineResponseParser.java @@ -12,18 +12,21 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ErrorLineResponseParser} is responsible for parsing error information (log) entry. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault class ErrorLineResponseParser extends AbstractLongResponseParser { @Override protected ErrorLine convert(byte[] responseBytes) { // 255 marks no error. if (responseBytes[1] == (byte) 255) { - return null; + return ErrorLine.NO_ERROR; } return new ErrorLine(ValueConverter.arrayToByte(responseBytes, 1), diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Rego6xxProtocolException.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Rego6xxProtocolException.java index b1646b15680f7..5204316da865d 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Rego6xxProtocolException.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/Rego6xxProtocolException.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link Rego6xxProtocolException} is responsible for holding information about an Rego6xx protocol error. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class Rego6xxProtocolException extends Exception { private static final long serialVersionUID = 7556083982084149686L; diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/RegoRegisterMapper.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/RegoRegisterMapper.java index 96af4081d9330..c5276e3789420 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/RegoRegisterMapper.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/RegoRegisterMapper.java @@ -19,6 +19,8 @@ import javax.measure.Unit; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.Units; @@ -27,6 +29,7 @@ * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class RegoRegisterMapper { public static final RegoRegisterMapper REGO600; @@ -35,7 +38,7 @@ public static interface Channel { public double scaleFactor(); - public Unit unit(); + public @Nullable Unit unit(); public int convertValue(short value); } @@ -44,9 +47,9 @@ private static class ChannelFactory { private static class ChannelImpl implements Channel { private final short address; private final double scaleFactor; - private final Unit unit; + private @Nullable final Unit unit; - private ChannelImpl(short address, double scaleFactor, Unit unit) { + private ChannelImpl(short address, double scaleFactor, @Nullable Unit unit) { this.address = address; this.scaleFactor = scaleFactor; this.unit = unit; @@ -63,7 +66,7 @@ public double scaleFactor() { } @Override - public Unit unit() { + public @Nullable Unit unit() { return unit; } @@ -104,7 +107,7 @@ private RegoRegisterMapper(Map mappings) { this.mappings = mappings; } - public Channel map(String channelIID) { + public @Nullable Channel map(String channelIID) { return mappings.get(channelIID); } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParser.java index 42371597c7382..4619354d03ce7 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParser.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ResponseParser} is responsible for parsing arbitrary data coming from a rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public interface ResponseParser { public int responseLength(); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParserFactory.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParserFactory.java index 2b03c84a36f46..31b7016e5914e 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParserFactory.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ResponseParserFactory.java @@ -12,15 +12,18 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ResponseParserFactory} is responsible for providing parsers for all known data * forms coming from the rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault public class ResponseParserFactory { public static final ResponseParser SHORT = new ShortResponseParser(); public static final ResponseParser STRING = new StringResponseParser(); public static final ResponseParser ERROR_LINE = new ErrorLineResponseParser(); - public static final ResponseParser WRITE = new WriteResponse(); + public static final ResponseParser WRITE = new WriteResponse(); } diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ShortResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ShortResponseParser.java index e9216eb811b5b..8ebc0c6ab4689 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ShortResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ShortResponseParser.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ShortResponseParser} is responsible for parsing short form data format * coming from the rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault class ShortResponseParser extends AbstractResponseParser { @Override diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/StringResponseParser.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/StringResponseParser.java index 5f86aa7ad55f7..ab2cee10f0e4a 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/StringResponseParser.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/StringResponseParser.java @@ -12,12 +12,15 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link StringResponseParser} is responsible for parsing long (text) form data format * coming from the rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault class StringResponseParser extends AbstractLongResponseParser { @Override diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ValueConverter.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ValueConverter.java index e9b9658a75757..1670b48884f26 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ValueConverter.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/ValueConverter.java @@ -12,11 +12,14 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link ValueConverter} is responsible for converting various rego 6xx specific data types. * * @author Boris Krivonog - Initial contribution */ +@NonNullByDefault class ValueConverter { public static byte[] shortToSevenBitFormat(short value) { byte b1 = (byte) ((value & 0xC000) >> 14); diff --git a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/WriteResponse.java b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/WriteResponse.java index 8359087baf987..23cfe77819313 100644 --- a/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/WriteResponse.java +++ b/bundles/org.openhab.binding.regoheatpump/src/main/java/org/openhab/binding/regoheatpump/internal/rego6xx/WriteResponse.java @@ -12,20 +12,23 @@ */ package org.openhab.binding.regoheatpump.internal.rego6xx; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * The {@link WriteResponse} is responsible for parsing write responses * coming from the rego 6xx unit. * * @author Boris Krivonog - Initial contribution */ -class WriteResponse extends AbstractResponseParser { +@NonNullByDefault +class WriteResponse extends AbstractResponseParser { @Override public int responseLength() { return 1; } @Override - protected Void convert(byte[] responseBytes) { - return null; + protected String convert(byte[] responseBytes) { + return ""; } } From 159054a99c7aba9af75bb7246eab855b6de9b750 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Sat, 12 Nov 2022 12:30:53 +0100 Subject: [PATCH 53/55] [jdbc] Add console maintenance commands (#13662) * Add console command for listing tables * Query row counts only when needed and while generating output * Add cleanup command * Add documentation Signed-off-by: Jacob Laursen --- .../org.openhab.persistence.jdbc/README.md | 57 ++++++ .../persistence/jdbc/ItemTableCheckEntry.java | 45 +++++ .../jdbc/ItemTableCheckEntryStatus.java | 69 +++++++ .../jdbc/console/JdbcCommandExtension.java | 191 ++++++++++++++++++ .../persistence/jdbc/db/JdbcBaseDAO.java | 32 ++- .../persistence/jdbc/internal/JdbcMapper.java | 21 +- .../jdbc/internal/JdbcPersistenceService.java | 135 ++++++++++++- .../JdbcPersistenceServiceConstants.java | 29 +++ 8 files changed, 568 insertions(+), 11 deletions(-) create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java create mode 100644 bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java diff --git a/bundles/org.openhab.persistence.jdbc/README.md b/bundles/org.openhab.persistence.jdbc/README.md index bc20c206a6be0..bec3af3fd3304 100644 --- a/bundles/org.openhab.persistence.jdbc/README.md +++ b/bundles/org.openhab.persistence.jdbc/README.md @@ -29,6 +29,7 @@ The following databases are currently supported and tested: - [Database Table Schema](#database-table-schema) - [Number Precision](#number-precision) - [Rounding results](#rounding-results) + - [Maintenance](#maintenance) - [For Developers](#for-developers) - [Performance Tests](#performance-tests) @@ -138,6 +139,62 @@ The results of database queries of number items are rounded to three decimal pla With `numberDecimalcount` decimals can be changed. Especially if sql types `DECIMAL` or `NUMERIC` are used for `sqltype.NUMBER`, rounding can be disabled by setting `numberDecimalcount=-1`. +### Maintenance + +Some maintenance tools are provided as console commands. + +#### List Tables + +Tables and corresponding items can be listed with the command `jdbc tables list`. +Per default only tables with some kind of problem are listed. +To list all tables, use the command `jdbc tables list all`. + +The list contains table name, item name, row count and status, which can be one of: + +- **Valid:** Table is consistent. +- **Item missing:** Table has no corresponding item. +- **Table missing:** Referenced table does not exist. +- **Item and table missing:** Referenced table does not exist nor has corresponding item. +- **Orphan table:** Mapping for table does not exist in index. + +#### Clean Inconsistent Items + +Some issues can be fixed automatically using the command `jdbc clean` (all items having issues) or `jdbc clean ` (single item). +This cleanup operation will remove items from the index (table `Items`) if the referenced table does not exist. + +If the item does not exist, the table will be physically deleted, but only if it's empty. +This precaution is taken because items may have existed previously, and the data might still be valuable. +For example, an item for a lost or repurposed sensor could have been deleted from the system while preserving persisted data. +To skip this check for a single item, use `jdbc clean force` with care. + +Prior to performing a `jdbc clean` operation, it's recommended to review the result of `jdbc tables list`. + +Fixing integrity issues can be useful before performing a migration to another naming scheme. +For example, when migrating to `tableCaseSensitiveItemNames`, an index will no longer exist after the migration: + +**Before migration:** + +| Table | Row count | Item | Status | +|-------------------|---------: |--------|---------------| +| ActualItem | 0 | | Orphan table | +| TableNotBelonging | 0 | | Orphan table | +| item0077 | 0 | MyItem | Table missing | + +**After migration:** + +| Table | Row count | Item | Status | +|-------------------|---------: |-------------------|---------------| +| ActualItem | 0 | ActualItem | Valid | +| TableNotBelonging | 0 | TableNotBelonging | Item missing | + +This happened: + +- `ActualItem` was missing in the index and became valid because it was left untouched, not being a part of the migration. After the migration, it happened to match the name of an existing item, thus it became valid. +- `TableNotBelonging` was also not part of the migration, but since now assumed to match an item, status changed since no item with that name exists. +- `item0077`, being the only correct table name according to previous naming scheme, disappeared from the list since it didn't have a corresponding table, and is now no longer part of any index. + +In other words, extracting this information from the index before removing it, can be beneficial in order to understand the issues and possible causes. + ### For Developers * Clearly separated source files for the database-specific part of openHAB logic. diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java new file mode 100644 index 0000000000000..e644138ae2d1a --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntry.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents a checked item/table relation. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class ItemTableCheckEntry { + private String itemName; + private String tableName; + private ItemTableCheckEntryStatus status; + + public ItemTableCheckEntry(String itemName, String tableName, ItemTableCheckEntryStatus status) { + this.itemName = itemName; + this.tableName = tableName; + this.status = status; + } + + public String getItemName() { + return itemName; + } + + public String getTableName() { + return tableName; + } + + public ItemTableCheckEntryStatus getStatus() { + return status; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java new file mode 100644 index 0000000000000..3553aee29d5ee --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/ItemTableCheckEntryStatus.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * This class represents status for an {@link ItemTableCheckEntry}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public enum ItemTableCheckEntryStatus { + /** + * Table is consistent. + */ + VALID { + @Override + public String toString() { + return "Valid"; + } + }, + /** + * Table has no corresponding item. + */ + ITEM_MISSING { + @Override + public String toString() { + return "Item missing"; + } + }, + /** + * Referenced table does not exist. + */ + TABLE_MISSING { + @Override + public String toString() { + return "Table missing"; + } + }, + /** + * Referenced table does not exist nor has corresponding item. + */ + ITEM_AND_TABLE_MISSING { + @Override + public String toString() { + return "Item and table missing"; + } + }, + /** + * Mapping for table does not exist in index. + */ + ORPHAN_TABLE { + @Override + public String toString() { + return "Orphan table"; + } + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java new file mode 100644 index 0000000000000..0bcab36a2ac49 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/console/JdbcCommandExtension.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.console; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.console.Console; +import org.openhab.core.io.console.ConsoleCommandCompleter; +import org.openhab.core.io.console.StringsCompleter; +import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; +import org.openhab.core.io.console.extensions.ConsoleCommandExtension; +import org.openhab.core.persistence.PersistenceService; +import org.openhab.core.persistence.PersistenceServiceRegistry; +import org.openhab.persistence.jdbc.ItemTableCheckEntry; +import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus; +import org.openhab.persistence.jdbc.internal.JdbcPersistenceService; +import org.openhab.persistence.jdbc.internal.JdbcPersistenceServiceConstants; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link JdbcCommandExtension} is responsible for handling console commands + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +@Component(service = ConsoleCommandExtension.class) +public class JdbcCommandExtension extends AbstractConsoleCommandExtension implements ConsoleCommandCompleter { + + private static final String CMD_TABLES = "tables"; + private static final String SUBCMD_TABLES_LIST = "list"; + private static final String SUBCMD_TABLES_CLEAN = "clean"; + private static final String PARAMETER_ALL = "all"; + private static final String PARAMETER_FORCE = "force"; + private static final StringsCompleter CMD_COMPLETER = new StringsCompleter(List.of(CMD_TABLES), false); + private static final StringsCompleter SUBCMD_TABLES_COMPLETER = new StringsCompleter( + List.of(SUBCMD_TABLES_LIST, SUBCMD_TABLES_CLEAN), false); + + private final PersistenceServiceRegistry persistenceServiceRegistry; + + @Activate + public JdbcCommandExtension(final @Reference PersistenceServiceRegistry persistenceServiceRegistry) { + super(JdbcPersistenceServiceConstants.SERVICE_ID, "Interact with the JDBC persistence service."); + this.persistenceServiceRegistry = persistenceServiceRegistry; + } + + @Override + public void execute(String[] args, Console console) { + if (args.length < 2 || args.length > 4 || !CMD_TABLES.equals(args[0])) { + printUsage(console); + return; + } + JdbcPersistenceService persistenceService = getPersistenceService(); + if (persistenceService == null) { + return; + } + if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) { + listTables(persistenceService, console, args.length == 3 && PARAMETER_ALL.equalsIgnoreCase(args[2])); + return; + } else if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) { + if (args.length == 3) { + cleanupItem(persistenceService, console, args[2], false); + return; + } else if (args.length == 4 && PARAMETER_FORCE.equalsIgnoreCase(args[3])) { + cleanupItem(persistenceService, console, args[2], true); + return; + } else { + cleanupTables(persistenceService, console); + return; + } + } + printUsage(console); + } + + private @Nullable JdbcPersistenceService getPersistenceService() { + for (PersistenceService persistenceService : persistenceServiceRegistry.getAll()) { + if (persistenceService instanceof JdbcPersistenceService) { + return (JdbcPersistenceService) persistenceService; + } + } + return null; + } + + private void listTables(JdbcPersistenceService persistenceService, Console console, Boolean all) { + List entries = persistenceService.getCheckedEntries(); + if (!all) { + entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID); + } + entries.sort(Comparator.comparing(ItemTableCheckEntry::getTableName)); + int itemNameMaxLength = Math + .max(entries.stream().map(t -> t.getItemName().length()).max(Integer::compare).get(), 4); + int tableNameMaxLength = Math + .max(entries.stream().map(t -> t.getTableName().length()).max(Integer::compare).get(), 5); + int statusMaxLength = Stream.of(ItemTableCheckEntryStatus.values()).map(t -> t.toString().length()) + .max(Integer::compare).get(); + console.println(String.format( + "%1$-" + (tableNameMaxLength + 2) + "sRow Count %2$-" + (itemNameMaxLength + 2) + "s%3$s", "Table", + "Item", "Status")); + console.println("-".repeat(tableNameMaxLength) + " " + "--------- " + "-".repeat(itemNameMaxLength) + " " + + "-".repeat(statusMaxLength)); + for (ItemTableCheckEntry entry : entries) { + String tableName = entry.getTableName(); + ItemTableCheckEntryStatus status = entry.getStatus(); + long rowCount = status == ItemTableCheckEntryStatus.VALID + || status == ItemTableCheckEntryStatus.ITEM_MISSING ? persistenceService.getRowCount(tableName) : 0; + console.println(String.format( + "%1$-" + (tableNameMaxLength + 2) + "s%2$9d %3$-" + (itemNameMaxLength + 2) + "s%4$s", tableName, + rowCount, entry.getItemName(), status)); + } + } + + private void cleanupTables(JdbcPersistenceService persistenceService, Console console) { + console.println("Cleaning up all inconsistent items..."); + List entries = persistenceService.getCheckedEntries(); + entries.removeIf(t -> t.getStatus() == ItemTableCheckEntryStatus.VALID || t.getItemName().isEmpty()); + for (ItemTableCheckEntry entry : entries) { + console.print(entry.getItemName() + " -> "); + if (persistenceService.cleanupItem(entry)) { + console.println("done."); + } else { + console.println("skipped/failed."); + } + } + } + + private void cleanupItem(JdbcPersistenceService persistenceService, Console console, String itemName, + boolean force) { + console.print("Cleaning up item " + itemName + "... "); + if (persistenceService.cleanupItem(itemName, force)) { + console.println("done."); + } else { + console.println("skipped/failed."); + } + } + + @Override + public List getUsages() { + return Arrays.asList( + buildCommandUsage(CMD_TABLES + " " + SUBCMD_TABLES_LIST + " [" + PARAMETER_ALL + "]", + "list tables (all = include valid)"), + buildCommandUsage( + CMD_TABLES + " " + SUBCMD_TABLES_CLEAN + " []" + " [" + PARAMETER_FORCE + "]", + "clean inconsistent items (remove from index and drop tables)")); + } + + @Override + public @Nullable ConsoleCommandCompleter getCompleter() { + return this; + } + + @Override + public boolean complete(String[] args, int cursorArgumentIndex, int cursorPosition, List candidates) { + if (cursorArgumentIndex <= 0) { + return CMD_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } else if (cursorArgumentIndex == 1) { + if (CMD_TABLES.equalsIgnoreCase(args[0])) { + return SUBCMD_TABLES_COMPLETER.complete(args, cursorArgumentIndex, cursorPosition, candidates); + } + } else if (cursorArgumentIndex == 2) { + if (CMD_TABLES.equalsIgnoreCase(args[0])) { + if (SUBCMD_TABLES_CLEAN.equalsIgnoreCase(args[1])) { + JdbcPersistenceService persistenceService = getPersistenceService(); + if (persistenceService != null) { + return new StringsCompleter(persistenceService.getItemNames(), true).complete(args, + cursorArgumentIndex, cursorPosition, candidates); + } + } else if (SUBCMD_TABLES_LIST.equalsIgnoreCase(args[1])) { + new StringsCompleter(List.of(PARAMETER_ALL), false).complete(args, cursorArgumentIndex, + cursorPosition, candidates); + } + } + } + return false; + } +} diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java index 6bc19dc7ab16e..393635d968c41 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/db/JdbcBaseDAO.java @@ -85,11 +85,13 @@ public class JdbcBaseDAO { protected String sqlCreateNewEntryInItemsTable = "INSERT INTO #itemsManageTable# (ItemName) VALUES ('#itemname#')"; protected String sqlCreateItemsTableIfNot = "CREATE TABLE IF NOT EXISTS #itemsManageTable# (ItemId INT NOT NULL AUTO_INCREMENT,#colname# #coltype# NOT NULL,PRIMARY KEY (ItemId))"; protected String sqlDropItemsTableIfExists = "DROP TABLE IF EXISTS #itemsManageTable#"; - protected String sqlDeleteItemsEntry = "DELETE FROM items WHERE ItemName=#itemname#"; + protected String sqlDropTable = "DROP TABLE #tableName#"; + protected String sqlDeleteItemsEntry = "DELETE FROM #itemsManageTable# WHERE ItemName='#itemname#'"; protected String sqlGetItemIDTableNames = "SELECT ItemId, ItemName FROM #itemsManageTable#"; protected String sqlGetItemTables = "SELECT table_name FROM information_schema.tables WHERE table_type='BASE TABLE' AND table_schema='#jdbcUriDatabaseName#' AND NOT table_name='#itemsManageTable#'"; protected String sqlCreateItemTable = "CREATE TABLE IF NOT EXISTS #tableName# (time #tablePrimaryKey# NOT NULL, value #dbType#, PRIMARY KEY(time))"; protected String sqlInsertItemValue = "INSERT INTO #tableName# (TIME, VALUE) VALUES( #tablePrimaryValue#, ? ) ON DUPLICATE KEY UPDATE VALUE= ?"; + protected String sqlGetRowCount = "SELECT COUNT(*) FROM #tableName#"; /******** * INIT * @@ -264,6 +266,14 @@ public boolean doIfTableExists(ItemsVO vo) { return Objects.nonNull(result); } + public boolean doIfTableExists(String tableName) { + String sql = StringUtilsExt.replaceArrayMerge(sqlIfTableExists, new String[] { "#searchTable#" }, + new String[] { tableName }); + logger.debug("JDBC::doIfTableExists sql={}", sql); + final @Nullable String result = Yank.queryScalar(sql, String.class, null); + return Objects.nonNull(result); + } + public Long doCreateNewEntryInItemsTable(ItemsVO vo) { String sql = StringUtilsExt.replaceArrayMerge(sqlCreateNewEntryInItemsTable, new String[] { "#itemsManageTable#", "#itemname#" }, @@ -289,9 +299,17 @@ public ItemsVO doDropItemsTableIfExists(ItemsVO vo) { return vo; } + public void doDropTable(String tableName) { + String sql = StringUtilsExt.replaceArrayMerge(sqlDropTable, new String[] { "#tableName#" }, + new String[] { tableName }); + logger.debug("JDBC::doDropTable sql={}", sql); + Yank.execute(sql, null); + } + public void doDeleteItemsEntry(ItemsVO vo) { - String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, new String[] { "#itemname#" }, - new String[] { vo.getItemName() }); + String sql = StringUtilsExt.replaceArrayMerge(sqlDeleteItemsEntry, + new String[] { "#itemsManageTable#", "#itemname#" }, + new String[] { vo.getItemsManageTable(), vo.getItemName() }); logger.debug("JDBC::doDeleteItemsEntry sql={}", sql); Yank.execute(sql, null); } @@ -373,6 +391,14 @@ public void doDeleteItemValues(FilterCriteria filter, String table, ZoneId timeZ Yank.execute(sql, null); } + public long doGetRowCount(String tableName) { + final String sql = StringUtilsExt.replaceArrayMerge(sqlGetRowCount, new String[] { "#tableName#" }, + new String[] { tableName }); + logger.debug("JDBC::doGetRowCount sql={}", sql); + final @Nullable Long result = Yank.queryScalar(sql, Long.class, null); + return Objects.requireNonNullElse(result, 0L); + } + /************* * Providers * *************/ diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java index f59e7c8b6bd05..c956e706ecda8 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcMapper.java @@ -106,6 +106,14 @@ public boolean ifItemsTableExists() { return res; } + public boolean ifTableExists(String tableName) { + logger.debug("JDBC::ifTableExists"); + long timerStart = System.currentTimeMillis(); + boolean res = conf.getDBDAO().doIfTableExists(tableName); + logTime("doIfTableExists", timerStart, System.currentTimeMillis()); + return res; + } + public ItemsVO createNewEntryInItemsTable(ItemsVO vo) { logger.debug("JDBC::createNewEntryInItemsTable"); long timerStart = System.currentTimeMillis(); @@ -131,6 +139,13 @@ public boolean dropItemsTableIfExists(ItemsVO vo) { return true; } + public void dropTable(String tableName) { + logger.debug("JDBC::dropTable"); + long timerStart = System.currentTimeMillis(); + conf.getDBDAO().doDropTable(tableName); + logTime("doDropTable", timerStart, System.currentTimeMillis()); + } + public ItemsVO deleteItemsEntry(ItemsVO vo) { logger.debug("JDBC::deleteItemsEntry"); long timerStart = System.currentTimeMillis(); @@ -189,6 +204,10 @@ public Item storeItemValue(Item item, State itemState, @Nullable ZonedDateTime d return item; } + public long getRowCount(String tableName) { + return conf.getDBDAO().doGetRowCount(tableName); + } + public List getHistItemFilterQuery(FilterCriteria filter, int numberDecimalcount, String table, Item item) { logger.debug( @@ -350,7 +369,7 @@ private void formatTableNames() { } List itemIdTableNames = ifItemsTableExists() ? getItemIDTableNames() : new ArrayList(); - List itemTables = getItemTables().stream().map(t -> t.getTableName()).collect(Collectors.toList()); + var itemTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toList()); List oldNewTableNames; if (itemIdTableNames.isEmpty()) { diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java index bac25c3e5a11e..d267ad097045d 100644 --- a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceService.java @@ -13,11 +13,15 @@ package org.openhab.persistence.jdbc.internal; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -36,6 +40,9 @@ import org.openhab.core.persistence.strategy.PersistenceStrategy; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; +import org.openhab.persistence.jdbc.ItemTableCheckEntry; +import org.openhab.persistence.jdbc.ItemTableCheckEntryStatus; +import org.openhab.persistence.jdbc.dto.ItemsVO; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.service.component.annotations.Activate; @@ -55,13 +62,9 @@ @Component(service = { PersistenceService.class, QueryablePersistenceService.class }, configurationPid = "org.openhab.jdbc", // property = Constants.SERVICE_PID + "=org.openhab.jdbc") -@ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceService.CONFIG_URI) +@ConfigurableService(category = "persistence", label = "JDBC Persistence Service", description_uri = JdbcPersistenceServiceConstants.CONFIG_URI) public class JdbcPersistenceService extends JdbcMapper implements ModifiablePersistenceService { - private static final String SERVICE_ID = "jdbc"; - private static final String SERVICE_LABEL = "JDBC"; - protected static final String CONFIG_URI = "persistence:jdbc"; - private final Logger logger = LoggerFactory.getLogger(JdbcPersistenceService.class); private final ItemRegistry itemRegistry; @@ -116,12 +119,12 @@ public void deactivate(final int reason) { @Override public String getId() { logger.debug("JDBC::getName: returning name 'jdbc' for queryable persistence service."); - return SERVICE_ID; + return JdbcPersistenceServiceConstants.SERVICE_ID; } @Override public String getLabel(@Nullable Locale locale) { - return SERVICE_LABEL; + return JdbcPersistenceServiceConstants.SERVICE_LABEL; } @Override @@ -275,4 +278,122 @@ public boolean remove(FilterCriteria filter) throws IllegalArgumentException { return result; } + + /** + * Get a list of names of persisted items. + */ + public Collection getItemNames() { + return itemNameToTableNameMap.keySet(); + } + + /** + * Get a list of all items with corresponding tables and an {@link ItemTableCheckEntryStatus} indicating + * its condition. + * + * @return list of {@link ItemTableCheckEntry} + */ + public List getCheckedEntries() { + List entries = new ArrayList<>(); + + if (!checkDBAccessability()) { + logger.warn("JDBC::getCheckedEntries: database not connected"); + return entries; + } + + var orphanTables = getItemTables().stream().map(ItemsVO::getTableName).collect(Collectors.toSet()); + for (Entry entry : itemNameToTableNameMap.entrySet()) { + String itemName = entry.getKey(); + String tableName = entry.getValue(); + entries.add(getCheckedEntry(itemName, tableName, orphanTables.contains(tableName))); + orphanTables.remove(tableName); + } + for (String orphanTable : orphanTables) { + entries.add(new ItemTableCheckEntry("", orphanTable, ItemTableCheckEntryStatus.ORPHAN_TABLE)); + } + return entries; + } + + private ItemTableCheckEntry getCheckedEntry(String itemName, String tableName, boolean tableExists) { + boolean itemExists; + try { + itemRegistry.getItem(itemName); + itemExists = true; + } catch (ItemNotFoundException e) { + itemExists = false; + } + + ItemTableCheckEntryStatus status; + if (!tableExists) { + if (itemExists) { + status = ItemTableCheckEntryStatus.TABLE_MISSING; + } else { + status = ItemTableCheckEntryStatus.ITEM_AND_TABLE_MISSING; + } + } else if (itemExists) { + status = ItemTableCheckEntryStatus.VALID; + } else { + status = ItemTableCheckEntryStatus.ITEM_MISSING; + } + return new ItemTableCheckEntry(itemName, tableName, status); + } + + /** + * Clean up inconsistent item: Remove from index and drop table. + * Tables with any rows are skipped, unless force is set. + * + * @param itemName Name of item to clean + * @param force If true, non-empty tables will be dropped too + * @return true if item was cleaned up + */ + public boolean cleanupItem(String itemName, boolean force) { + String tableName = itemNameToTableNameMap.get(itemName); + if (tableName == null) { + return false; + } + ItemTableCheckEntry entry = getCheckedEntry(itemName, tableName, ifTableExists(tableName)); + return cleanupItem(entry, force); + } + + /** + * Clean up inconsistent item: Remove from index and drop table. + * Tables with any rows are skipped. + * + * @param entry + * @return true if item was cleaned up + */ + public boolean cleanupItem(ItemTableCheckEntry entry) { + return cleanupItem(entry, false); + } + + private boolean cleanupItem(ItemTableCheckEntry entry, boolean force) { + if (!checkDBAccessability()) { + logger.warn("JDBC::cleanupItem: database not connected"); + return false; + } + + ItemTableCheckEntryStatus status = entry.getStatus(); + String tableName = entry.getTableName(); + switch (status) { + case ITEM_MISSING: + if (!force && getRowCount(tableName) > 0) { + return false; + } + dropTable(tableName); + // Fall through to remove from index. + case TABLE_MISSING: + case ITEM_AND_TABLE_MISSING: + if (!conf.getTableUseRealCaseSensitiveItemNames()) { + ItemsVO itemsVo = new ItemsVO(); + itemsVo.setItemName(entry.getItemName()); + deleteItemsEntry(itemsVo); + } + itemNameToTableNameMap.remove(entry.getItemName()); + return true; + case ORPHAN_TABLE: + case VALID: + default: + // Nothing to clean. + return false; + } + } } diff --git a/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java new file mode 100644 index 0000000000000..53d4ee5cfff09 --- /dev/null +++ b/bundles/org.openhab.persistence.jdbc/src/main/java/org/openhab/persistence/jdbc/internal/JdbcPersistenceServiceConstants.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2022 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.persistence.jdbc.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link JdbcPersistenceServiceConstants} class defines common constants, which are + * used across the whole persistence service. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class JdbcPersistenceServiceConstants { + + public static final String SERVICE_ID = "jdbc"; + public static final String SERVICE_LABEL = "JDBC"; + public static final String CONFIG_URI = "persistence:jdbc"; +} From a1270c78226ef69a5486c0d45f9e8c76eb3ba720 Mon Sep 17 00:00:00 2001 From: Sami Salonen Date: Sat, 12 Nov 2022 13:46:43 +0200 Subject: [PATCH 54/55] [mqtt.homeassistant] Fix binding crash when home assistant discovery topics update with content (#13518) * [mqtt.homeassistant] Fix for discovery topics that update with content Fixes #13517 Possibly resolves #9711 and #12295 as well. * [mqtt.homeassistant] Sort channels before changing thing * [mqtt.homeassistant] logging + removed unnecessary synchronization * Resolve bunch of warnings in homeassistant bundle * [mqtt.homeassistant] Handling null warnings and unnecessary null checks * [mqtt.homeassistant] Removing unnecessary null checks Signed-off-by: Sami Salonen Co-Authored-by: @antroids github handle --- .../internal/component/Climate.java | 7 +- .../internal/component/Vacuum.java | 5 +- ...hannelConfigurationTypeAdapterFactory.java | 2 + .../config/ConnectionDeserializer.java | 5 +- .../discovery/HomeAssistantDiscovery.java | 15 +-- .../exception/ConfigurationException.java | 2 + .../UnsupportedComponentException.java | 2 + .../handler/HomeAssistantThingHandler.java | 48 ++++++-- .../internal/AbstractHomeAssistantTests.java | 17 ++- .../component/AbstractComponentTests.java | 29 ++--- .../component/AlarmControlPanelTests.java | 2 +- .../internal/component/ClimateTests.java | 4 +- .../internal/component/CoverTests.java | 2 +- .../internal/component/FanTests.java | 2 +- .../component/HAConfigurationTests.java | 1 + .../internal/component/LockTests.java | 2 +- .../internal/component/SensorTests.java | 2 +- .../internal/component/SwitchTests.java | 3 +- .../internal/component/VacuumTests.java | 3 +- .../HomeAssistantDiscoveryTests.java | 2 +- .../HomeAssistantThingHandlerTests.java | 108 +++++++++++++++++- 21 files changed, 203 insertions(+), 60 deletions(-) diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java index 41531396a6122..df63de2d2a647 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Climate.java @@ -241,9 +241,10 @@ public Climate(ComponentFactory.ComponentConfiguration componentConfiguration) { updateListener, channelConfiguration.fanModeCommandTemplate, channelConfiguration.fanModeCommandTopic, channelConfiguration.fanModeStateTemplate, channelConfiguration.fanModeStateTopic, commandFilter); - if (channelConfiguration.holdModes != null && !channelConfiguration.holdModes.isEmpty()) { - buildOptionalChannel(HOLD_CH_ID, new TextValue(channelConfiguration.holdModes.toArray(new String[0])), - updateListener, channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic, + List holdModes = channelConfiguration.holdModes; + if (holdModes != null && !holdModes.isEmpty()) { + buildOptionalChannel(HOLD_CH_ID, new TextValue(holdModes.toArray(new String[0])), updateListener, + channelConfiguration.holdCommandTemplate, channelConfiguration.holdCommandTopic, channelConfiguration.holdStateTemplate, channelConfiguration.holdStateTopic, commandFilter); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java index f4b526da894ed..ad9766892c57a 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/component/Vacuum.java @@ -197,9 +197,10 @@ public Vacuum(ComponentFactory.ComponentConfiguration componentConfiguration) { final var allowedSupportedFeatures = channelConfiguration.schema == Schema.LEGACY ? LEGACY_SUPPORTED_FEATURES : STATE_SUPPORTED_FEATURES; - final var configSupportedFeatures = channelConfiguration.supportedFeatures == null + final var supportedFeatures = channelConfiguration.supportedFeatures; + final var configSupportedFeatures = supportedFeatures == null ? channelConfiguration.schema == Schema.LEGACY ? LEGACY_DEFAULT_FEATURES : STATE_DEFAULT_FEATURES - : channelConfiguration.supportedFeatures; + : supportedFeatures; List deviceSupportedFeatures = Collections.emptyList(); if (!configSupportedFeatures.isEmpty()) { diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java index b1418c2d13eb2..139fd2aa5372c 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ChannelConfigurationTypeAdapterFactory.java @@ -15,6 +15,7 @@ import java.io.IOException; import java.lang.reflect.Field; import java.util.Arrays; +import java.util.Objects; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -119,6 +120,7 @@ private void expandTidleInTopics(AbstractChannelConfiguration config) { String parentTopic = config.getParentTopic(); while (type != Object.class) { + Objects.requireNonNull(type, "Bug: type is null"); // Should not happen? Making compiler happy Arrays.stream(type.getDeclaredFields()).filter(this::isMqttTopicField) .forEach(field -> replacePlaceholderByParentTopic(config, field, parentTopic)); type = type.getSuperclass(); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java index a35be7ca4221f..490de07a8a632 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/config/ConnectionDeserializer.java @@ -33,8 +33,9 @@ */ @NonNullByDefault public class ConnectionDeserializer implements JsonDeserializer { - public @Nullable Connection deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { + @Override + public @Nullable Connection deserialize(@Nullable JsonElement json, Type typeOfT, + JsonDeserializationContext context) throws JsonParseException { JsonArray list; if (json == null) { throw new JsonParseException("JSON element is null, but must be connection definition."); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java index 3d90235cb8165..aaab4b549a58c 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscovery.java @@ -66,7 +66,6 @@ @Component(service = DiscoveryService.class, configurationPid = "discovery.mqttha") @NonNullByDefault public class HomeAssistantDiscovery extends AbstractMQTTDiscovery { - @SuppressWarnings("unused") private final Logger logger = LoggerFactory.getLogger(HomeAssistantDiscovery.class); protected final Map> componentsPerThingID = new TreeMap<>(); protected final Map thingIDPerTopic = new TreeMap<>(); @@ -260,14 +259,16 @@ public void topicVanished(ThingUID connectionBridge, MqttBrokerConnection connec } if (thingIDPerTopic.containsKey(topic)) { ThingUID thingUID = thingIDPerTopic.remove(topic); - final String thingID = thingUID.getId(); + if (thingUID != null) { + final String thingID = thingUID.getId(); - HaID haID = new HaID(topic); + HaID haID = new HaID(topic); - Set components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet()); - components.remove(haID); - if (components.isEmpty()) { - thingRemoved(thingUID); + Set components = componentsPerThingID.getOrDefault(thingID, Collections.emptySet()); + components.remove(haID); + if (components.isEmpty()) { + thingRemoved(thingUID); + } } } } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/ConfigurationException.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/ConfigurationException.java index 111ca895c049d..5998e9e3db287 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/ConfigurationException.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/ConfigurationException.java @@ -21,6 +21,8 @@ */ @NonNullByDefault public class ConfigurationException extends RuntimeException { + private static final long serialVersionUID = -4828651603869498942L; + public ConfigurationException(String message) { super(message); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/UnsupportedComponentException.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/UnsupportedComponentException.java index 27341e9e7aa3a..ff4ff4ce536e1 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/UnsupportedComponentException.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/exception/UnsupportedComponentException.java @@ -21,6 +21,8 @@ */ @NonNullByDefault public class UnsupportedComponentException extends ConfigurationException { + private static final long serialVersionUID = 5134690914728956232L; + public UnsupportedComponentException(String message) { super(message); } diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java index 6e147f20a36fb..89f560ab2e65a 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/main/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandler.java @@ -12,7 +12,8 @@ */ package org.openhab.binding.mqtt.homeassistant.internal.handler; -import java.util.Collection; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -49,6 +50,7 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.builder.ThingBuilder; import org.openhab.core.thing.type.ChannelDefinition; import org.openhab.core.thing.type.ChannelGroupDefinition; import org.openhab.core.thing.type.ChannelGroupType; @@ -80,6 +82,8 @@ public class HomeAssistantThingHandler extends AbstractMQTTThingHandler implements ComponentDiscovered, Consumer>> { public static final String AVAILABILITY_CHANNEL = "availability"; + private static final Comparator CHANNEL_COMPARATOR_BY_UID = Comparator + .comparing(channel -> channel.getUID().toString());; private final Logger logger = LoggerFactory.getLogger(HomeAssistantThingHandler.class); @@ -120,13 +124,12 @@ public HomeAssistantThingHandler(Thing thing, MqttChannelTypeProvider channelTyp this.transformationServiceProvider); } - @SuppressWarnings({ "null", "unused" }) @Override public void initialize() { started = false; config = getConfigAs(HandlerConfiguration.class); - if (config.topics == null || config.topics.isEmpty()) { + if (config.topics.isEmpty()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Device topics unknown"); return; } @@ -222,7 +225,6 @@ protected void stop() { super.stop(); } - @SuppressWarnings({ "null", "unused" }) @Override public @Nullable ChannelState getChannelState(ChannelUID channelUID) { String groupID = channelUID.getGroupId(); @@ -255,7 +257,6 @@ public void componentDiscovered(HaID homeAssistantTopicID, AbstractComponent * Callback of {@link DelayedBatchProcessing}. * Add all newly discovered components to the Thing and start the components. */ - @SuppressWarnings("null") @Override public void accept(List> discoveredComponentsList) { MqttBrokerConnection connection = this.connection; @@ -288,13 +289,44 @@ public void accept(List> discoveredComponentsList) { return null; }); - Collection channels = discovered.getChannelMap().values().stream() + List discoveredChannels = discovered.getChannelMap().values().stream() .map(ComponentChannel::getChannel).collect(Collectors.toList()); - ThingHelper.addChannelsToThing(thing, channels); + if (known != null) { + // We had previously known component with different config hash + // We remove all conflicting old channels, they will be re-added below based on the new discovery + logger.debug( + "Received component {} with slightly different config. Making sure we re-create conflicting channels...", + discovered.getGroupUID()); + removeJustRediscoveredChannels(discoveredChannels); + } + + // Add newly discovered channels. We sort the channels + // for (mostly) consistent jsondb serialization + discoveredChannels.sort(CHANNEL_COMPARATOR_BY_UID); + ThingHelper.addChannelsToThing(thing, discoveredChannels); } + updateThingType(); + } + } + + private void removeJustRediscoveredChannels(List discoveredChannels) { + ArrayList mutableChannels = new ArrayList<>(getThing().getChannels()); + Set newChannelUIDs = discoveredChannels.stream().map(Channel::getUID).collect(Collectors.toSet()); + // Take current channels but remove those channels that were just re-discovered + List existingChannelsWithNewlyDiscoveredChannelsRemoved = mutableChannels.stream() + .filter(existingChannel -> !newChannelUIDs.contains(existingChannel.getUID())) + .collect(Collectors.toList()); + if (existingChannelsWithNewlyDiscoveredChannelsRemoved.size() < mutableChannels.size()) { + // We sort the channels for (mostly) consistent jsondb serialization + existingChannelsWithNewlyDiscoveredChannelsRemoved.sort(CHANNEL_COMPARATOR_BY_UID); + updateThingChannels(existingChannelsWithNewlyDiscoveredChannelsRemoved); } + } - updateThingType(); + private void updateThingChannels(List channelList) { + ThingBuilder thingBuilder = editThing(); + thingBuilder.withChannels(channelList); + updateThing(thingBuilder.build()); } @Override diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java index ae9eab0f475a9..1a867b68c8f6f 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/AbstractHomeAssistantTests.java @@ -12,19 +12,15 @@ */ package org.openhab.binding.mqtt.homeassistant.internal; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -64,7 +60,6 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings({ "ConstantConditions" }) @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @NonNullByDefault @@ -87,7 +82,6 @@ public abstract class AbstractHomeAssistantTests extends JavaTest { protected @Mock @NonNullByDefault({}) ThingTypeRegistry thingTypeRegistry; protected @Mock @NonNullByDefault({}) TransformationServiceProvider transformationServiceProvider; - @SuppressWarnings("NotNullFieldNotInitialized") protected @NonNullByDefault({}) MqttChannelTypeProvider channelTypeProvider; protected final Bridge bridgeThing = BridgeBuilder.create(BRIDGE_TYPE_UID, BRIDGE_UID).build(); @@ -126,7 +120,9 @@ protected void setupConnection() { final var subscriber = (MqttMessageSubscriber) invocation.getArgument(1); subscriptions.putIfAbsent(topic, ConcurrentHashMap.newKeySet()); - subscriptions.get(topic).add(subscriber); + Set subscribers = subscriptions.get(topic); + Objects.requireNonNull(subscribers); // Invariant, thanks to putIfAbsent above. To make compiler happy + subscribers.add(subscriber); return CompletableFuture.completedFuture(true); }).when(bridgeConnection).subscribe(any(), any()); @@ -154,6 +150,7 @@ protected void setupConnection() { * @param relativePath path from src/test/java/org/openhab/binding/mqtt/homeassistant/internal * @return path */ + @SuppressWarnings("null") protected Path getResourcePath(String relativePath) { try { return Paths.get(AbstractHomeAssistantTests.class.getResource(relativePath).toURI()); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java index 2336b80afe3f4..5679af518c930 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AbstractComponentTests.java @@ -12,19 +12,11 @@ */ package org.openhab.binding.mqtt.homeassistant.internal.component; -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.nio.charset.StandardCharsets; import java.util.List; @@ -38,6 +30,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.openhab.binding.mqtt.generic.MqttChannelTypeProvider; import org.openhab.binding.mqtt.generic.TransformationServiceProvider; @@ -59,7 +52,6 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings({ "ConstantConditions" }) @NonNullByDefault public abstract class AbstractComponentTests extends AbstractHomeAssistantTests { private static final int SUBSCRIBE_TIMEOUT = 10000; @@ -178,6 +170,7 @@ protected static void assertChannel(ComponentChannel stateChannel, String stateT * @param channelId channel * @param state expected state */ + @SuppressWarnings("null") protected static void assertState(AbstractComponent<@NonNull ? extends AbstractChannelConfiguration> component, String channelId, State state) { assertThat(component.getChannel(channelId).getState().getCache().getChannelState(), is(state)); @@ -190,8 +183,8 @@ protected static void assertState(AbstractComponent<@NonNull ? extends AbstractC * @param payload payload */ protected void assertPublished(String mqttTopic, String payload) { - verify(bridgeConnection).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), - anyBoolean()); + verify(bridgeConnection).publish(eq(mqttTopic), ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), + anyInt(), anyBoolean()); } /** @@ -202,8 +195,8 @@ protected void assertPublished(String mqttTopic, String payload) { * @param t payload must be published N times on given topic */ protected void assertPublished(String mqttTopic, String payload, int t) { - verify(bridgeConnection, times(t)).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), - anyInt(), anyBoolean()); + verify(bridgeConnection, times(t)).publish(eq(mqttTopic), + ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean()); } /** @@ -213,8 +206,8 @@ protected void assertPublished(String mqttTopic, String payload, int t) { * @param payload payload */ protected void assertNotPublished(String mqttTopic, String payload) { - verify(bridgeConnection, never()).publish(eq(mqttTopic), eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), - anyBoolean()); + verify(bridgeConnection, never()).publish(eq(mqttTopic), + ArgumentMatchers.eq(payload.getBytes(StandardCharsets.UTF_8)), anyInt(), anyBoolean()); } /** diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java index 9e1456267f496..8118312ce9037 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/AlarmControlPanelTests.java @@ -27,11 +27,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class AlarmControlPanelTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "alarm_control_panel/0x0000000000000000_alarm_control_panel_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void testAlarmControlPanel() { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java index 1f39a197d8bbe..d13d542b213f8 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/ClimateTests.java @@ -34,11 +34,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class ClimateTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "climate/0x847127fffe11dd6a_climate_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void testTS0601Climate() { var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{" @@ -102,6 +102,7 @@ public void testTS0601Climate() { assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25"); } + @SuppressWarnings("null") @Test public void testTS0601ClimateNotSendIfOff() { var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), "{" @@ -185,6 +186,7 @@ public void testTS0601ClimateNotSendIfOff() { assertPublished("zigbee2mqtt/th1/set/current_heating_setpoint", "25"); } + @SuppressWarnings("null") @Test public void testClimate() { var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java index 0bfeee34a5405..a3230625de88d 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/CoverTests.java @@ -28,11 +28,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class CoverTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "cover/0x0000000000000000_cover_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void test() throws InterruptedException { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java index a0390dad9ba28..bc8cc4da5f27b 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/FanTests.java @@ -27,11 +27,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ALL") @NonNullByDefault public class FanTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "fan/0x0000000000000000_fan_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void test() throws InterruptedException { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java index ef518cff1312c..83c60419457fd 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/HAConfigurationTests.java @@ -148,6 +148,7 @@ public void testDeviceSingleStringConfig() { } } + @SuppressWarnings("null") @Test public void testTS0601ClimateConfig() { String json = readTestJson("configTS0601ClimateThermostat.json"); diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java index 0a8d4de2eff1e..f9920c0b843e8 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/LockTests.java @@ -27,11 +27,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ALL") @NonNullByDefault public class LockTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "lock/0x0000000000000000_lock_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void test() throws InterruptedException { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java index 8b5e412a608c7..2ddab24751ad7 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SensorTests.java @@ -31,11 +31,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class SensorTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "sensor/0x0000000000000000_sensor_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void test() throws InterruptedException { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java index 28738436fdff8..c832ed3d11290 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/SwitchTests.java @@ -27,11 +27,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class SwitchTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "switch/0x847127fffe11dd6a_auto_lock_zigbee2mqtt"; + @SuppressWarnings("null") @Test public void testSwitchWithStateAndCommand() { var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), @@ -92,6 +92,7 @@ public void testSwitchWithState() { assertState(component, Switch.SWITCH_CHANNEL_ID, OnOffType.ON); } + @SuppressWarnings("null") @Test public void testSwitchWithCommand() { var component = discoverComponent(configTopicToMqtt(CONFIG_TOPIC), diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/VacuumTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/VacuumTests.java index 2b0a0f5f20231..df831459c34a3 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/VacuumTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/component/VacuumTests.java @@ -32,11 +32,11 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings("ConstantConditions") @NonNullByDefault public class VacuumTests extends AbstractComponentTests { public static final String CONFIG_TOPIC = "vacuum/rockrobo_vacuum"; + @SuppressWarnings("null") @Test public void testRoborockValetudo() { // @formatter:off @@ -157,6 +157,7 @@ public void testRoborockValetudo() { assertState(component, Vacuum.JSON_ATTRIBUTES_CH_ID, new StringType(jsonValue)); } + @SuppressWarnings("null") @Test public void testLegacySchema() { // @formatter:off diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java index 3ac574196c82e..ea46a42fc55f3 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/discovery/HomeAssistantDiscoveryTests.java @@ -44,7 +44,7 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings({ "ConstantConditions", "unchecked" }) +@SuppressWarnings({ "unchecked" }) @ExtendWith(MockitoExtension.class) @NonNullByDefault public class HomeAssistantDiscoveryTests extends AbstractHomeAssistantTests { diff --git a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java index 4b851692607d8..060540d325072 100644 --- a/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java +++ b/bundles/org.openhab.binding.mqtt.homeassistant/src/test/java/org/openhab/binding/mqtt/homeassistant/internal/handler/HomeAssistantThingHandlerTests.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -32,7 +33,9 @@ import org.openhab.binding.mqtt.homeassistant.internal.HaID; import org.openhab.binding.mqtt.homeassistant.internal.HandlerConfiguration; import org.openhab.binding.mqtt.homeassistant.internal.component.Climate; +import org.openhab.binding.mqtt.homeassistant.internal.component.Sensor; import org.openhab.binding.mqtt.homeassistant.internal.component.Switch; +import org.openhab.core.thing.Channel; import org.openhab.core.thing.binding.ThingHandlerCallback; /** @@ -40,7 +43,6 @@ * * @author Anton Kharuzhy - Initial contribution */ -@SuppressWarnings({ "ConstantConditions" }) @ExtendWith(MockitoExtension.class) @NonNullByDefault public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests { @@ -60,6 +62,7 @@ public class HomeAssistantThingHandlerTests extends AbstractHomeAssistantTests { private @Mock @NonNullByDefault({}) ThingHandlerCallback callbackMock; private @NonNullByDefault({}) HomeAssistantThingHandler thingHandler; + private @NonNullByDefault({}) HomeAssistantThingHandler nonSpyThingHandler; @BeforeEach public void setup() { @@ -74,6 +77,7 @@ public void setup() { SUBSCRIBE_TIMEOUT, ATTRIBUTE_RECEIVE_TIMEOUT); thingHandler.setConnection(bridgeConnection); thingHandler.setCallback(callbackMock); + nonSpyThingHandler = thingHandler; thingHandler = spy(thingHandler); } @@ -117,6 +121,108 @@ public void testInitialize() { verify(channelTypeProvider, times(2)).setChannelGroupType(any(), any()); } + /** + * Test where the same component is published twice to MQTT. The binding should handle this. + * + * @throws InterruptedException + */ + @Test + public void testDuplicateComponentPublish() throws InterruptedException { + thingHandler.initialize(); + + verify(callbackMock).statusUpdated(eq(haThing), any()); + // Expect a call to the bridge status changed, the start, the propertiesChanged method + verify(thingHandler).bridgeStatusChanged(any()); + verify(thingHandler, timeout(SUBSCRIBE_TIMEOUT)).start(any()); + + // Expect subscription on each topic from config + MQTT_TOPICS.forEach(t -> { + verify(bridgeConnection, timeout(SUBSCRIBE_TIMEOUT)).subscribe(eq(t), any()); + }); + + verify(thingHandler, never()).componentDiscovered(any(), any()); + assertThat(haThing.getChannels().size(), CoreMatchers.is(0)); + + // + // + // Publish sensor components with identical payload except for + // change in "name" field. The binding should respect the latest discovery result. + // + // This simulates how multiple OpenMQTTGateway devices would publish + // the same discovery topics for a particular Bluetooth sensor, and thus "competing" with similar but slightly + // different discovery topics. + // + // In fact, only difference is actually "via_device" additional metadata field telling which OpenMQTTGateway + // published the discovery topic. + // + // + + // + // 1. publish corridor temperature sensor + // + var configTopicTempCorridor = "homeassistant/sensor/tempCorridor/config"; + thingHandler.discoverComponents.processMessage(configTopicTempCorridor, new String("{"// + + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","// + + "\"temperature_state_template\": \"{{ value_json.temperature }}\", "// + + "\"name\": \"CorridorTemp\", "// + + "\"unit_of_measurement\": \"°C\" "// + + "}").getBytes(StandardCharsets.UTF_8)); + verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class)); + thingHandler.delayedProcessing.forceProcessNow(); + waitForAssert(() -> { + assertThat("1 channel created", thingHandler.getThing().getChannels().size() == 1); + }); + + // + // 2. publish outside temperature sensor + // + var configTopicTempOutside = "homeassistant/sensor/tempOutside/config"; + thingHandler.discoverComponents.processMessage(configTopicTempOutside, new String("{"// + + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","// + + "\"temperature_state_template\": \"{{ value_json.temperature }}\", " // + + "\"name\": \"OutsideTemp\", "// + + "\"source\": \"gateway2\" "// + + "}").getBytes(StandardCharsets.UTF_8)); + thingHandler.delayedProcessing.forceProcessNow(); + verify(thingHandler, times(1)).componentDiscovered(eq(new HaID(configTopicTempOutside)), any(Sensor.class)); + waitForAssert(() -> { + assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2); + }); + + // + // 3. publish corridor temperature sensor, this time with different name (openHAB channel label) + // + thingHandler.discoverComponents.processMessage(configTopicTempCorridor, new String("{"// + + "\"temperature_state_topic\": \"+/+/BTtoMQTT/mysensor\","// + + "\"temperature_state_template\": \"{{ value_json.temperature }}\", "// + + "\"name\": \"CorridorTemp NEW\", "// + + "\"unit_of_measurement\": \"°C\" "// + + "}").getBytes(StandardCharsets.UTF_8)); + thingHandler.delayedProcessing.forceProcessNow(); + + waitForAssert(() -> { + assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2); + }); + + // + // verify that both channels are there and the label corresponds to newer discovery topic payload + // + Channel corridorTempChannel = nonSpyThingHandler.getThing().getChannel("tempCorridor_5Fsensor#sensor"); + assertThat("Corridor temperature channel is created", corridorTempChannel, CoreMatchers.notNullValue()); + Objects.requireNonNull(corridorTempChannel); // for compiler + assertThat("Corridor temperature channel is having the updated label from 2nd discovery topic publish", + corridorTempChannel.getLabel(), CoreMatchers.is("CorridorTemp NEW")); + + Channel outsideTempChannel = nonSpyThingHandler.getThing().getChannel("tempOutside_5Fsensor#sensor"); + assertThat("Outside temperature channel is created", outsideTempChannel, CoreMatchers.notNullValue()); + + verify(thingHandler, times(2)).componentDiscovered(eq(new HaID(configTopicTempCorridor)), any(Sensor.class)); + + waitForAssert(() -> { + assertThat("2 channel created", thingHandler.getThing().getChannels().size() == 2); + }); + } + @Test public void testDispose() { thingHandler.initialize(); From 160e0c2548f0110799e8f55cc6fc82128b03b8a9 Mon Sep 17 00:00:00 2001 From: mlobstein Date: Sat, 12 Nov 2022 09:57:22 -0600 Subject: [PATCH 55/55] [nuvo] Add zone actions for rules (#13658) * Add zone actions for rules * Don't scan for NuvoNet source messages if openHAB NuvoNet sources are not being used Signed-off-by: Michael Lobstein --- bundles/org.openhab.binding.nuvo/README.md | 105 ++++++++--- .../nuvo/internal/NuvoBindingConstants.java | 15 +- .../internal/communication/NuvoConnector.java | 173 ++++++++++-------- .../communication/NuvoMessageEvent.java | 16 +- .../nuvo/internal/handler/NuvoHandler.java | 148 ++++++++------- .../resources/OH-INF/i18n/nuvo.properties | 2 +- .../main/resources/OH-INF/thing/channels.xml | 3 +- 7 files changed, 275 insertions(+), 187 deletions(-) diff --git a/bundles/org.openhab.binding.nuvo/README.md b/bundles/org.openhab.binding.nuvo/README.md index 618b7b7ead955..e4a407cd821fa 100644 --- a/bundles/org.openhab.binding.nuvo/README.md +++ b/bundles/org.openhab.binding.nuvo/README.md @@ -85,35 +85,36 @@ connection: &conNuvo The following channels are available: -| Channel ID | Item Type | Description | -|--------------------------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------| -| system#alloff | Switch | Turn all zones off simultaneously | -| system#allmute | Switch | Mute or unmute all zones simultaneously | -| system#page | Switch | Turn on or off the Page All Zones feature (while on the amplifier switches to source 6) | -| system#sendcmd | String | Send a command to the amplifier | -| zoneN#power (where N= 1-20) | Switch | Turn the power for a zone on or off | -| zoneN#source (where N= 1-20) | Number | Select the source input for a zone (1-6) | -| zoneN#volume (where N= 1-20) | Dimmer | Control the volume for a zone (0-100%) [translates to 0-79] | -| zoneN#mute (where N= 1-20) | Switch | Mute or unmute a zone | -| zoneN#favorite (where N= 1-20) | Number | Select a preset Favorite for a zone (1-12) | -| zoneN#control (where N= 1-20) | Player | Simulate pressing the transport control buttons on the keypad e.g. play/pause/next/previous | -| zoneN#treble (where N= 1-20) | Number | Adjust the treble control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full | -| zoneN#bass (where N= 1-20) | Number | Adjust the bass control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full | -| zoneN#balance (where N= 1-20) | Number | Adjust the balance control for a zone (-18 to 18 [in increments of 2]) -18=left, 0=center, 18=right | -| zoneN#loudness (where N= 1-20) | Switch | Turn on or off the loudness compensation setting for the zone | -| zoneN#dnd (where N= 1-20) | Switch | Turn on or off the Do Not Disturb for the zone (for when the amplifier's Page All Zones feature is activated) | -| zoneN#lock (where N= 1-20) | Contact | Indicates if this zone is currently locked | -| zoneN#party (where N= 1-20) | Switch | Turn on or off the party mode feature with this zone as the host | -| sourceN#display_line1 (where N= 1-6) | String | 1st line of text being displayed on the keypad. Can be updated for a non NuvoNet source | -| sourceN#display_line2 (where N= 1-6) | String | 2nd line of text being displayed on the keypad. Can be updated for a non NuvoNet source | -| sourceN#display_line3 (where N= 1-6) | String | 3rd line of text being displayed on the keypad. Can be updated for a non NuvoNet source | -| sourceN#display_line4 (where N= 1-6) | String | 4th line of text being displayed on the keypad. Can be updated for a non NuvoNet source | -| sourceN#play_mode (where N= 1-6) | String | The current playback mode of the source, ie: Playing, Paused, etc. (ReadOnly) See rules example for updating | -| sourceN#track_length (where N= 1-6) | Number:Time | The total running time of the current playing track (ReadOnly) See rules example for updating | -| sourceN#track_position (where N= 1-6)| Number:Time | The running time elapsed of the current playing track (ReadOnly) See rules example for updating | -| sourceN#button_press (where N= 1-6) | String | Indicates the last button pressed on the keypad for a non NuvoNet source or openHAB NuvoNet source (ReadOnly) | -| sourceN#art_url (where N= 1-6) | String | MPS4 Only! The URL of the Album Art JPG for this source that is displayed on a CTP-36. See *very advanced* rules (SendOnly) | -| sourceN#album_art (where N= 1-6) | Image | The Album Art loaded from the art_url channel for display in a UI widget (ReadOnly) | +| Channel ID | Item Type | Description | +|--------------------------------------|-------------|--------------------------------------------------------------------------------------------------------------------------------| +| system#alloff | Switch | Turn all zones off simultaneously | +| system#allmute | Switch | Mute or unmute all zones simultaneously | +| system#page | Switch | Turn on or off the Page All Zones feature (while on the amplifier switches to source 6) | +| system#sendcmd | String | Send a command to the amplifier | +| system#buttonpress | String | Indicates the zone number followed by a comma and the last button pressed or NuvoNet menu item selected on a keypad (ReadOnly) | +| zoneN#power (where N= 1-20) | Switch | Turn the power for a zone on or off | +| zoneN#source (where N= 1-20) | Number | Select the source input for a zone (1-6) | +| zoneN#volume (where N= 1-20) | Dimmer | Control the volume for a zone (0-100%) [translates to 0-79] | +| zoneN#mute (where N= 1-20) | Switch | Mute or unmute a zone | +| zoneN#favorite (where N= 1-20) | Number | Select a preset Favorite for a zone (1-12) | +| zoneN#control (where N= 1-20) | Player | Simulate pressing the transport control buttons on the keypad e.g. play/pause/next/previous | +| zoneN#treble (where N= 1-20) | Number | Adjust the treble control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full | +| zoneN#bass (where N= 1-20) | Number | Adjust the bass control for a zone (-18 to 18 [in increments of 2]) -18=none, 0=flat, 18=full | +| zoneN#balance (where N= 1-20) | Number | Adjust the balance control for a zone (-18 to 18 [in increments of 2]) -18=left, 0=center, 18=right | +| zoneN#loudness (where N= 1-20) | Switch | Turn on or off the loudness compensation setting for the zone | +| zoneN#dnd (where N= 1-20) | Switch | Turn on or off the Do Not Disturb for the zone (for when the amplifier's Page All Zones feature is activated) | +| zoneN#lock (where N= 1-20) | Contact | Indicates if this zone is currently locked | +| zoneN#party (where N= 1-20) | Switch | Turn on or off the party mode feature with this zone as the host | +| sourceN#display_line1 (where N= 1-6) | String | 1st line of text being displayed on the keypad. Can be updated for a non NuvoNet source | +| sourceN#display_line2 (where N= 1-6) | String | 2nd line of text being displayed on the keypad. Can be updated for a non NuvoNet source | +| sourceN#display_line3 (where N= 1-6) | String | 3rd line of text being displayed on the keypad. Can be updated for a non NuvoNet source | +| sourceN#display_line4 (where N= 1-6) | String | 4th line of text being displayed on the keypad. Can be updated for a non NuvoNet source | +| sourceN#play_mode (where N= 1-6) | String | The current playback mode of the source, ie: Playing, Paused, etc. (ReadOnly) See rules example for updating | +| sourceN#track_length (where N= 1-6) | Number:Time | The total running time of the current playing track (ReadOnly) See rules example for updating | +| sourceN#track_position (where N= 1-6)| Number:Time | The running time elapsed of the current playing track (ReadOnly) See rules example for updating | +| sourceN#button_press (where N= 1-6) | String | Indicates the last button pressed on the keypad for a non NuvoNet source or openHAB NuvoNet source (ReadOnly) | +| sourceN#art_url (where N= 1-6) | String | MPS4 Only! The URL of the Album Art JPG for this source that is displayed on a CTP-36. See *very advanced* rules (SendOnly) | +| sourceN#album_art (where N= 1-6) | Image | The Album Art loaded from the art_url channel for display in a UI widget (ReadOnly) | ## Full Example @@ -139,6 +140,7 @@ Switch nuvo_system_alloff "All Zones Off" { channel="nuvo:amplifier:myamp:system Switch nuvo_system_allmute "All Zones Mute" { channel="nuvo:amplifier:myamp:system#allmute" } Switch nuvo_system_page "Page All Zones" { channel="nuvo:amplifier:myamp:system#page" } String nuvo_system_sendcmd "Send Command" { channel="nuvo:amplifier:myamp:system#sendcmd" } +String nuvo_system_buttonpress "Zone Button: [%s]" { channel="nuvo:amplifier:myamp:system#buttonpress" } // zones Switch nuvo_z1_power "Power" { channel="nuvo:amplifier:myamp:zone1#power" } @@ -473,12 +475,57 @@ A complete XML string for the desired menu is then stored in the `menuXmlSrcN` c menu3 x menu3 y + ``` When a menu item is selected, the text of the topmenu item and sub menu item (if applicable) will be sent to the button channel in a pipe delimited format. For example, when item `menu1 b` is selected, the text `Top menu 1|menu1 b` will be sent to the button channel. When the item `Top menu 2` is selected the text sent to the button channel will simply be `Top menu 2` since this menu item does not have any sub menu items. +### Rule to trigger an action based on which keypad zone where a button was pressed or menu item selected + +By using the `system#buttonpress` channel it is possible to trigger an action based on which keypad zone was used to send the action. +This channel appends the zone number and a comma before the button action or menu item selection. + +For example if the Play/Pause button is pressed on Zone 7, the channel will display: `7,PLAYPAUSE` +Also if a menu item from a custom menu was selected, ie: `Top menu 1` on Zone 5, the channel will display: `5,Top menu 1` + +The functionality can be used to create very powerful rules as demontrated below. The following rule triggered from a menu item turns off all zones except for the zone that triggered the rule. + +nuvo-turn-off-all-but-caller.rules: + +``` +rule "Turn off all zones except caller zone" +when + Item nuvo_system_buttonpress received update +then + var callerZone = newState.toString().split(",").get(0) + var button = newState.toString().split(",").get(1) + + if (button == "Turn off other zones") { + if (callerZone != "1") { + nuvo_z1_power.sendCommand(OFF) + } + if (callerZone != "2") { + nuvo_z2_power.sendCommand(OFF) + } + if (callerZone != "3") { + nuvo_z3_power.sendCommand(OFF) + } + if (callerZone != "4") { + nuvo_z4_power.sendCommand(OFF) + } + if (callerZone != "5") { + nuvo_z5_power.sendCommand(OFF) + } + if (callerZone != "6") { + nuvo_z6_power.sendCommand(OFF) + } + } +end + +``` + ### MPS4 openHAB NuvoNet source custom integration rules *(very advanced)* The following are a set of example rules necessary to integrate metadata and control of another openHAB connected source (ie: Chromecast) into an openHAB NuvoNet source. diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java index c3cf2497c825f..d65fceda035eb 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/NuvoBindingConstants.java @@ -35,6 +35,7 @@ public class NuvoBindingConstants { public static final String CHANNEL_TYPE_ALLMUTE = "allmute"; public static final String CHANNEL_TYPE_PAGE = "page"; public static final String CHANNEL_TYPE_SENDCMD = "sendcmd"; + public static final String CHANNEL_TYPE_BUTTONPRESS = "buttonpress"; // zone public static final String CHANNEL_TYPE_POWER = "power"; @@ -71,14 +72,14 @@ public class NuvoBindingConstants { public static final String TYPE_PAGE = "page"; public static final String TYPE_SOURCE_UPDATE = "source_update"; public static final String TYPE_ZONE_UPDATE = "zone_update"; - public static final String TYPE_ZONE_BUTTON = "zone_button"; - public static final String TYPE_ZONE_BUTTON2 = "zone_button2"; - public static final String TYPE_ZONE_MENUREQ = "zone_menureq"; - public static final String TYPE_MENU_ITEM_SELECTED = "top_menu_button"; public static final String TYPE_ZONE_CONFIG = "zone_config"; - public static final String TYPE_ALBUM_ART_REQ = "album_art_req"; - public static final String TYPE_ALBUM_ART_FRAG_REQ = "album_art_frag_req"; - public static final String TYPE_FAVORITE_REQ = "favorite_req"; + public static final String TYPE_ZONE_SOURCE_BUTTON = "zone_source_button"; + public static final String TYPE_NN_BUTTON = "nn_button"; + public static final String TYPE_NN_MENUREQ = "nn_menureq"; + public static final String TYPE_NN_MENU_ITEM_SELECTED = "nn_menu_item_selected"; + public static final String TYPE_NN_ALBUM_ART_REQ = "nn_album_art_req"; + public static final String TYPE_NN_ALBUM_ART_FRAG_REQ = "nn_album_art_frag_req"; + public static final String TYPE_NN_FAVORITE_REQ = "nn_favorite_req"; // misc public static final String ON = "ON"; diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java index bb084ffd78240..c3321eb3c650b 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoConnector.java @@ -54,21 +54,21 @@ public abstract class NuvoConnector { private static final Pattern SRC_PATTERN = Pattern.compile("^#S(\\d{1})(.*)$"); private static final Pattern ZONE_PATTERN = Pattern.compile("^#Z(\\d{1,2}),(.*)$"); - private static final Pattern ZONE_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$"); - private static final Pattern ZONE_MENUREQ_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})MENUREQ(.*)$"); - private static final Pattern ZONE_BUTTON2_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTON(.*)$"); - private static final Pattern ZONE_BUTTONTWO_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTONTWO(.*)$"); + private static final Pattern ZONE_SOURCE_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})(.*)$"); + private static final Pattern NN_MENUREQ_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})MENUREQ(.*)$"); + private static final Pattern NN_BUTTON_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTON(.*)$"); + private static final Pattern NN_BUTTONTWO_PATTERN = Pattern.compile("^#Z(\\d{1,2})S(\\d{1})BUTTONTWO(.*)$"); private static final Pattern ZONE_CFG_PATTERN = Pattern.compile("^#ZCFG(\\d{1,2}),(.*)$"); // S2ALBUMARTREQ0x620FD879,80,80,2,0x00C0C0C0,0,0,0,0,1 - private static final Pattern ALBUM_ART_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTREQ(.*)$"); + private static final Pattern NN_ALBUM_ART_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTREQ(.*)$"); // S2ALBUMARTFRAGREQ0x620FD879,0,750 - private static final Pattern ALBUM_ART_FRAG_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTFRAGREQ(.*)$"); + private static final Pattern NN_ALBUM_ART_FRAG_REQ = Pattern.compile("^#S(\\d{1})ALBUMARTFRAGREQ(.*)$"); // S6FAVORITE0x000003E8 - private static final Pattern FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$"); + private static final Pattern NN_FAVORITE_PATTERN = Pattern.compile("^#S(\\d{1})FAVORITE0x(.*)$"); private final Logger logger = LoggerFactory.getLogger(NuvoConnector.class); @@ -88,6 +88,7 @@ public abstract class NuvoConnector { private List listeners = new ArrayList<>(); private boolean isEssentia = true; + private boolean isAnyOhNuvoNet = false; /** * Get whether the connection is established or not @@ -116,6 +117,15 @@ public void setEssentia(boolean isEssentia) { this.isEssentia = isEssentia; } + /** + * Tell the connector to listen for NuvoNet source messages + * + * @param true if any sources are configured as openHAB NuvoNet sources + */ + public void setAnyOhNuvoNet(boolean isAnyOhNuvoNet) { + this.isAnyOhNuvoNet = isAnyOhNuvoNet; + } + /** * Set the thread that handles the feedback messages * @@ -307,7 +317,7 @@ public void removeEventListener(NuvoMessageEventListener listener) { } /** - * Analyze an incoming message and dispatch corresponding (type, key, value) to the event listeners + * Analyze an incoming message and dispatch corresponding (type, zone, src, value) to the event listeners * * @param incomingMessage the received message */ @@ -327,119 +337,117 @@ public void handleIncomingMessage(byte[] incomingMessage) { } catch (NuvoException e) { logger.debug("Error sending response to PING command"); } - dispatchKeyValue(TYPE_PING, BLANK, BLANK); + dispatchKeyValue(TYPE_PING, BLANK); return; } if (RESTART.equals(message)) { - dispatchKeyValue(TYPE_RESTART, BLANK, BLANK); + dispatchKeyValue(TYPE_RESTART, BLANK); return; } if (message.contains(VER_STR_E6) || message.contains(VER_STR_GC)) { // example: #VER"NV-E6G FWv2.66 HWv0" // split on " and return the version number - dispatchKeyValue(TYPE_VERSION, "", message.split("\"")[1]); + dispatchKeyValue(TYPE_VERSION, message.split("\"")[1]); return; } if (message.equals(ALL_OFF)) { - dispatchKeyValue(TYPE_ALLOFF, BLANK, BLANK); + dispatchKeyValue(TYPE_ALLOFF, BLANK); return; } if (message.contains(MUTE)) { - dispatchKeyValue(TYPE_ALLMUTE, BLANK, message.substring(message.length() - 1)); + dispatchKeyValue(TYPE_ALLMUTE, message.substring(message.length() - 1)); return; } if (message.contains(PAGE)) { - dispatchKeyValue(TYPE_PAGE, BLANK, message.substring(message.length() - 1)); + dispatchKeyValue(TYPE_PAGE, message.substring(message.length() - 1)); return; } - // Amp controller sent an album art request - Matcher matcher = ALBUM_ART_REQ.matcher(message); - if (matcher.find()) { - // pull out the source id and the remainder of the message - dispatchKeyValue(TYPE_ALBUM_ART_REQ, matcher.group(1), matcher.group(2)); - return; - } + Matcher matcher; - // Amp controller sent an album art fragment request - matcher = ALBUM_ART_FRAG_REQ.matcher(message); - if (matcher.find()) { - // pull out the source id and the remainder of the message - dispatchKeyValue(TYPE_ALBUM_ART_FRAG_REQ, matcher.group(1), matcher.group(2)); - return; - } + if (isAnyOhNuvoNet) { + // Amp controller sent a NuvoNet album art request + matcher = NN_ALBUM_ART_REQ.matcher(message); + if (matcher.find()) { + dispatchKeyValue(TYPE_NN_ALBUM_ART_REQ, BLANK, matcher.group(1), matcher.group(2)); + return; + } - // Amp controller sent a request to play a favorite - matcher = FAVORITE_PATTERN.matcher(message); - if (matcher.find()) { - // pull out the source id and the remainder of the message - dispatchKeyValue(TYPE_FAVORITE_REQ, matcher.group(1), matcher.group(2)); - return; + // Amp controller sent a NuvoNet album art fragment request + matcher = NN_ALBUM_ART_FRAG_REQ.matcher(message); + if (matcher.find()) { + dispatchKeyValue(TYPE_NN_ALBUM_ART_FRAG_REQ, BLANK, matcher.group(1), matcher.group(2)); + return; + } + + // Amp controller sent a request for a NuvoNet source to play a favorite + matcher = NN_FAVORITE_PATTERN.matcher(message); + if (matcher.find()) { + dispatchKeyValue(TYPE_NN_FAVORITE_REQ, BLANK, matcher.group(1), matcher.group(2)); + return; + } } // Amp controller sent a source update ie: #S2DISPINFO,DUR3380,POS3090,STATUS2 // or #S2DISPLINE1,"1 of 17" matcher = SRC_PATTERN.matcher(message); if (matcher.find()) { - // pull out the source id and the remainder of the message - dispatchKeyValue(TYPE_SOURCE_UPDATE, matcher.group(1), matcher.group(2)); + dispatchKeyValue(TYPE_SOURCE_UPDATE, BLANK, matcher.group(1), matcher.group(2)); return; } // Amp controller sent a zone update ie: #Z11,ON,SRC3,VOL63,DND0,LOCK0 matcher = ZONE_PATTERN.matcher(message); if (matcher.find()) { - // pull out the zone id and the remainder of the message - dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), matcher.group(2)); + dispatchKeyValue(TYPE_ZONE_UPDATE, matcher.group(1), BLANK, matcher.group(2)); return; } - // Amp controller sent a zone BUTTONTWO press event ie: #Z11S3BUTTONTWO4,2,0,0,0 - matcher = ZONE_BUTTONTWO_PATTERN.matcher(message); - if (matcher.find()) { - // redundant - ignore - return; - } + if (isAnyOhNuvoNet) { + // Amp controller sent a NuvoNet zone/source BUTTONTWO press event ie: #Z11S3BUTTONTWO4,2,0,0,0 + matcher = NN_BUTTONTWO_PATTERN.matcher(message); + if (matcher.find()) { + // redundant - ignore + return; + } - // Amp controller sent a zone BUTTON press event ie: #Z4S6BUTTON1,1,0xFFFFFFFF,1,3 - matcher = ZONE_BUTTON2_PATTERN.matcher(message); - if (matcher.find()) { - // pull out the remainder of the message: button #, action, menuid, itemid, itemidx - String[] buttonSplit = matcher.group(3).split(COMMA); - - // second field is button action, only send DOWNUP (0) or DOWN (1), ignore UP (2) - if (ZERO.equals(buttonSplit[1]) || ONE.equals(buttonSplit[1])) { - // a button in a menu was pressed, send SxZy,menuid,itemidx - if (!ZERO.equals(buttonSplit[2])) { - dispatchKeyValue(TYPE_MENU_ITEM_SELECTED, matcher.group(2), SRC_KEY + matcher.group(2) + ZONE_KEY - + matcher.group(1) + COMMA + buttonSplit[2] + COMMA + buttonSplit[3]); - } else { - // send the button # in the event, don't send extra fields menuid, itemid, etc.. - dispatchKeyValue(TYPE_ZONE_BUTTON2, matcher.group(2), buttonSplit[0]); + // Amp controller sent a NuvoNet zone/source BUTTON press event ie: #Z4S6BUTTON1,1,0xFFFFFFFF,1,3 + matcher = NN_BUTTON_PATTERN.matcher(message); + if (matcher.find()) { + // pull out the remainder of the message: button #, action, menuid, itemid, itemidx + String[] buttonSplit = matcher.group(3).split(COMMA); + + // second field is button action, only send DOWNUP (0) or DOWN (1), ignore UP (2) + if (ZERO.equals(buttonSplit[1]) || ONE.equals(buttonSplit[1])) { + // a button in a menu was pressed, send 'menuid,itemidx' + if (!ZERO.equals(buttonSplit[2])) { + dispatchKeyValue(TYPE_NN_MENU_ITEM_SELECTED, matcher.group(1), matcher.group(2), + buttonSplit[2] + COMMA + buttonSplit[3]); + } else { + // send the button # in the event, don't send extra fields menuid, itemid, etc.. + dispatchKeyValue(TYPE_NN_BUTTON, matcher.group(1), matcher.group(2), buttonSplit[0]); + } } + return; } - return; - } - // Amp controller sent a menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0 - matcher = ZONE_MENUREQ_PATTERN.matcher(message); - if (matcher.find()) { - // pull out the source id and send SxZy plus the remainder of the message - dispatchKeyValue(TYPE_ZONE_MENUREQ, matcher.group(2), - SRC_KEY + matcher.group(2) + ZONE_KEY + matcher.group(1) + COMMA + matcher.group(3)); - return; + // Amp controller sent a NuvoNet zone/source menu request event ie: #Z2S6MENUREQ0x0000000B,1,0,0 + matcher = NN_MENUREQ_PATTERN.matcher(message); + if (matcher.find()) { + dispatchKeyValue(TYPE_NN_MENUREQ, matcher.group(1), matcher.group(2), matcher.group(3)); + return; + } } - // Amp controller sent a zone button press event ie: #Z11S3PLAYPAUSE - matcher = ZONE_BUTTON_PATTERN.matcher(message); + // Amp controller sent a zone/source button press event ie: #Z11S3PLAYPAUSE + matcher = ZONE_SOURCE_PATTERN.matcher(message); if (matcher.find()) { - // pull out the source id and the remainder of the message, ignore the zone id - dispatchKeyValue(TYPE_ZONE_BUTTON, matcher.group(2), matcher.group(3)); + dispatchKeyValue(TYPE_ZONE_SOURCE_BUTTON, matcher.group(1), matcher.group(2), matcher.group(3)); return; } @@ -447,7 +455,7 @@ public void handleIncomingMessage(byte[] incomingMessage) { matcher = ZONE_CFG_PATTERN.matcher(message); if (matcher.find()) { // pull out the zone id and the remainder of the message - dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), matcher.group(2)); + dispatchKeyValue(TYPE_ZONE_CONFIG, matcher.group(1), BLANK, matcher.group(2)); return; } @@ -455,14 +463,25 @@ public void handleIncomingMessage(byte[] incomingMessage) { } /** - * Dispatch an event (type, key, value) to the event listeners + * Dispatch a system level event without zone or src to the event listeners + * + * @param type the type + * @param value the value + */ + private void dispatchKeyValue(String type, String value) { + dispatchKeyValue(type, BLANK, BLANK, value); + } + + /** + * Dispatch an event (type, zone, src, value) to the event listeners * * @param type the type - * @param key the key + * @param zone the zone id + * @param src the source id * @param value the value */ - private void dispatchKeyValue(String type, String key, String value) { - NuvoMessageEvent event = new NuvoMessageEvent(this, type, key, value); + private void dispatchKeyValue(String type, String zone, String src, String value) { + NuvoMessageEvent event = new NuvoMessageEvent(this, type, zone, src, value); listeners.forEach(l -> l.onNewMessageEvent(event)); } } diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java index 7539c941bf398..2e6cd59ae0303 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/communication/NuvoMessageEvent.java @@ -25,13 +25,15 @@ public class NuvoMessageEvent extends EventObject { private static final long serialVersionUID = 1L; private final String type; - private final String key; + private final String zone; + private final String src; private final String value; - public NuvoMessageEvent(Object source, String type, String key, String value) { + public NuvoMessageEvent(Object source, String type, String zone, String src, String value) { super(source); this.type = type; - this.key = key; + this.zone = zone; + this.src = src; this.value = value; } @@ -39,8 +41,12 @@ public String getType() { return type; } - public String getKey() { - return key; + public String getZone() { + return zone; + } + + public String getSrc() { + return src; } public String getValue() { diff --git a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java index 56320cb5ced2f..bfa27e0837737 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java +++ b/bundles/org.openhab.binding.nuvo/src/main/java/org/openhab/binding/nuvo/internal/handler/NuvoHandler.java @@ -215,7 +215,7 @@ public void initialize() { if (serialPort != null && !serialPort.isEmpty()) { connector = new NuvoSerialConnector(serialPortManager, serialPort, uid); - } else if (port != null) { + } else if (host != null && port != null) { connector = new NuvoIpConnector(host, port, uid); this.isMps4 = (port.intValue() == MPS4_PORT); } else { @@ -239,6 +239,7 @@ public void initialize() { if (this.isAnyOhNuvoNet) { logger.debug("At least one source is configured as an openHAB NuvoNet source"); + connector.setAnyOhNuvoNet(true); loadMenuConfiguration(config); favoriteMap.put("1", @@ -640,11 +641,12 @@ private synchronized void closeConnection() { */ @Override public void onNewMessageEvent(NuvoMessageEvent evt) { - logger.debug("onNewMessageEvent: key {} = {}", evt.getKey(), evt.getValue()); + logger.debug("onNewMessageEvent: zone {}, source {}, value {}", evt.getZone(), evt.getSrc(), evt.getValue()); lastEventReceived = System.currentTimeMillis(); String type = evt.getType(); - String key = evt.getKey(); + String zoneId = evt.getZone(); + String srcId = evt.getSrc(); String updateData = evt.getValue().trim(); if (this.getThing().getStatus() != ThingStatus.ONLINE) { updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, this.versionString); @@ -687,8 +689,8 @@ public void onNewMessageEvent(NuvoMessageEvent evt) { updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_PAGE, ONE.equals(updateData) ? ON : OFF); break; case TYPE_SOURCE_UPDATE: - logger.debug("Source update: Source: {} - Value: {}", key, updateData); - NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + key); + logger.debug("Source update: Source: {} - Value: {}", srcId, updateData); + NuvoEnum targetSource = NuvoEnum.valueOf(SOURCE + srcId); if (updateData.contains(DISPLINE)) { // example: DISPLINE2,"Play My Song (Featuring Dee Ajayi)" @@ -712,16 +714,16 @@ public void onNewMessageEvent(NuvoMessageEvent evt) { } else if (updateData.contains(NAME_QUOTE)) { // example: NAME"Ipod" String name = updateData.split("\"")[1]; - sourceLabels.put(key, name); + sourceLabels.put(srcId, name); } break; case TYPE_ZONE_UPDATE: - logger.debug("Zone update: Zone: {} - Value: {}", key, updateData); + logger.debug("Zone update: Zone: {} - Value: {}", zoneId, updateData); // example : OFF // or: ON,SRC3,VOL63,DND0,LOCK0 // or: ON,SRC3,MUTE,DND0,LOCK0 - NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + key); + NuvoEnum targetZone = NuvoEnum.valueOf(ZONE + zoneId); if (OFF.equals(updateData)) { updateChannelState(targetZone, CHANNEL_TYPE_POWER, OFF); @@ -746,35 +748,43 @@ public void onNewMessageEvent(NuvoMessageEvent evt) { } } break; - case TYPE_ZONE_BUTTON: - logger.debug("Zone Button pressed: Source: {} - Button: {}", key, updateData); - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData); + case TYPE_ZONE_SOURCE_BUTTON: + logger.debug("Source Button pressed: Source: {} - Button: {}", srcId, updateData); + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, updateData); + updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + updateData); break; - case TYPE_ZONE_BUTTON2: + case TYPE_NN_BUTTON: String buttonAction = NuvoStatusCodes.BUTTON_CODE.get(updateData); if (buttonAction != null) { - logger.debug("Zone NuvoNet Button pressed: Source: {} - Button: {}", key, buttonAction); - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, buttonAction); + logger.debug("NuvoNet Source Button pressed: Source: {} - Button: {}", srcId, buttonAction); + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, buttonAction); + updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + buttonAction); } else { - logger.debug("Zone NuvoNet Button pressed: Source: {} - Unknown button code: {}", key, updateData); - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, updateData); + logger.debug("NuvoNet Source Button pressed: Source: {} - Unknown button code: {}", srcId, + updateData); + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, updateData); + updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, zoneId + COMMA + updateData); } break; - case TYPE_MENU_ITEM_SELECTED: + case TYPE_NN_MENU_ITEM_SELECTED: // ignore this update unless openHAB is handling this source - if (nuvoNetSrcMap.get(key).equals(2)) { + if (nuvoNetSrcMap.get(srcId).equals(2)) { + String sourceZone = SRC_KEY + srcId + ZONE_KEY + zoneId; String[] updateDataSplit = updateData.split(COMMA); - String zoneSource = updateDataSplit[0]; - String menuId = updateDataSplit[1]; - int menuItemIdx = Integer.parseInt(updateDataSplit[2]) - 1; + String menuId = updateDataSplit[0]; + int menuItemIdx = Integer.parseInt(updateDataSplit[1]) - 1; boolean exitMenu = false; if ("0xFFFFFFFF".equals(menuId)) { - TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu() + TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(srcId) - 1).getTopMenu() .get(menuItemIdx); - logger.debug("Top Menu item selected: Source: {} - Menu Item: {}", key, topMenuItem.getText()); - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, topMenuItem.getText()); + logger.debug("Top Menu item selected: Source: {} - Menu Item: {}", srcId, + topMenuItem.getText()); + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, + topMenuItem.getText()); + updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, + zoneId + COMMA + topMenuItem.getText()); List subMenuItems = topMenuItem.getItems(); @@ -784,135 +794,139 @@ public void onNewMessageEvent(NuvoMessageEvent evt) { // send submenu (maximum of 20 items) int subMenuSize = subMenuItems.size() < 20 ? subMenuItems.size() : 20; try { - connector.sendCommand(zoneSource + "MENU" + (menuItemIdx + 11) + ",0,0," + subMenuSize + connector.sendCommand(sourceZone + "MENU" + (menuItemIdx + 11) + ",0,0," + subMenuSize + ",0,0," + subMenuSize + ",\"" + topMenuItem.getText() + "\""); Thread.sleep(SLEEP_BETWEEN_CMD_MS); for (int i = 0; i < subMenuSize; i++) { connector.sendCommand( - zoneSource + "MENUITEM" + (i + 1) + ",0,0,\"" + subMenuItems.get(i) + "\""); + sourceZone + "MENUITEM" + (i + 1) + ",0,0,\"" + subMenuItems.get(i) + "\""); } } catch (NuvoException | InterruptedException e) { - logger.debug("Error sending sub menu for {}", zoneSource); + logger.debug("Error sending sub menu to {}", sourceZone); } } } else { // a sub menu item was selected - TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(key) - 1).getTopMenu() + TopMenu topMenuItem = nuvoMenus.getSource().get(Integer.parseInt(srcId) - 1).getTopMenu() .get(Integer.decode(menuId) - 11); String subMenuItem = topMenuItem.getItems().get(menuItemIdx); - logger.debug("Sub Menu item selected: Source: {} - Menu Item: {}", key, + logger.debug("Sub Menu item selected: Source: {} - Menu Item: {}", srcId, topMenuItem.getText() + "|" + subMenuItem); - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, topMenuItem.getText() + "|" + subMenuItem); + updateChannelState(NuvoEnum.SYSTEM, CHANNEL_TYPE_BUTTONPRESS, + zoneId + COMMA + topMenuItem.getText() + "|" + subMenuItem); exitMenu = true; } if (exitMenu) { try { // tell the zone to exit the menu - connector.sendCommand(zoneSource + "MENU0,0,0,0,0,0,0,\"\""); + connector.sendCommand(sourceZone + "MENU0,0,0,0,0,0,0,\"\""); } catch (NuvoException e) { - logger.debug("Error sending exit menu command for {}", zoneSource); + logger.debug("Error sending exit menu command to {}", sourceZone); } } } break; - case TYPE_ZONE_MENUREQ: + case TYPE_NN_MENUREQ: // ignore this update unless openHAB is handling this source - if (nuvoNetSrcMap.get(key).equals(2)) { - logger.debug("Menu Request: Source: {} - Value: {}", key, updateData); - // For now we only support one level deep menus. If third field is '1', indicates go back to main + if (nuvoNetSrcMap.get(srcId).equals(2)) { + logger.debug("Menu Request: Source: {} - Value: {}", srcId, updateData); + String sourceZone = SRC_KEY + srcId + ZONE_KEY + zoneId; + // For now we only support one level deep menus. If second field is '1', indicates go back to main // menu. - String[] menuDataSplit = updateData.split(","); - if (menuDataSplit.length > 3 && ONE.equals(menuDataSplit[2])) { + String[] menuDataSplit = updateData.split(COMMA); + if (menuDataSplit.length > 2 && ONE.equals(menuDataSplit[1])) { try { - connector.sendCommand(menuDataSplit[0] + "MENU0xFFFFFFFF,0,0,0,0,0,0,\"\""); + connector.sendCommand(sourceZone + "MENU0xFFFFFFFF,0,0,0,0,0,0,\"\""); } catch (NuvoException e) { - logger.debug("Error sending main menu command for {}", menuDataSplit[0]); + logger.debug("Error sending main menu command to {}", sourceZone); } } } break; case TYPE_ZONE_CONFIG: - logger.debug("Zone Configuration: Zone: {} - Value: {}", key, updateData); + logger.debug("Zone Configuration: Zone: {} - Value: {}", zoneId, updateData); // example: BASS1,TREB-2,BALR2,LOUDCMP1 Matcher matcher = ZONE_CFG_PATTERN.matcher(updateData); if (matcher.find()) { - updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BASS, matcher.group(1)); - updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_TREBLE, matcher.group(2)); - updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_BALANCE, + updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_BASS, matcher.group(1)); + updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_TREBLE, matcher.group(2)); + updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_BALANCE, NuvoStatusCodes.getBalanceFromStr(matcher.group(3))); - updateChannelState(NuvoEnum.valueOf(ZONE + key), CHANNEL_TYPE_LOUDNESS, + updateChannelState(NuvoEnum.valueOf(ZONE + zoneId), CHANNEL_TYPE_LOUDNESS, ONE.equals(matcher.group(4)) ? ON : OFF); } else { logger.debug("no match on message: {}", updateData); } break; - case TYPE_ALBUM_ART_REQ: + case TYPE_NN_ALBUM_ART_REQ: // ignore this update unless openHAB is handling this source - if (nuvoNetSrcMap.get(key).equals(2)) { - logger.debug("Album Art Request for Source: {} - Data: {}", key, updateData); + if (nuvoNetSrcMap.get(srcId).equals(2)) { + logger.debug("Album Art Request for Source: {} - Data: {}", srcId, updateData); // 0x620FD879,80,80,2,0x00C0C0C0,0,0,0,0,1 String[] albumArtReq = updateData.split(COMMA); - albumArtIds.put(SRC_KEY + key, Integer.decode(albumArtReq[0])); + albumArtIds.put(SRC_KEY + srcId, Integer.decode(albumArtReq[0])); try { - if (albumArtMap.get(SRC_KEY + key).length > 1) { - connector.sendCommand(SRC_KEY + key + ALBUM_ART_AVAILABLE + albumArtIds.get(SRC_KEY + key) - + COMMA + albumArtMap.get(SRC_KEY + key).length); + if (albumArtMap.get(SRC_KEY + srcId).length > 1) { + connector.sendCommand( + SRC_KEY + srcId + ALBUM_ART_AVAILABLE + albumArtIds.get(SRC_KEY + srcId) + COMMA + + albumArtMap.get(SRC_KEY + srcId).length); } else { - connector.sendCommand(SRC_KEY + key + ALBUM_ART_AVAILABLE + ZERO_COMMA); + connector.sendCommand(SRC_KEY + srcId + ALBUM_ART_AVAILABLE + ZERO_COMMA); } } catch (NuvoException e) { - logger.debug("Error sending ALBUMARTAVAILABLE command for source: {}", key); + logger.debug("Error sending ALBUMARTAVAILABLE command for source: {}", srcId); } } break; - case TYPE_ALBUM_ART_FRAG_REQ: + case TYPE_NN_ALBUM_ART_FRAG_REQ: // ignore this update unless openHAB is handling this source - if (nuvoNetSrcMap.get(key).equals(2)) { - logger.debug("Album Art Fragment Request for Source: {} - Data: {}", key, updateData); + if (nuvoNetSrcMap.get(srcId).equals(2)) { + logger.debug("Album Art Fragment Request for Source: {} - Data: {}", srcId, updateData); // 0x620FD879,0,750 (id, requested offset from start of image, byte length requested) String[] albumArtFragReq = updateData.split(COMMA); int requestedId = Integer.decode(albumArtFragReq[0]); int offset = Integer.parseInt(albumArtFragReq[1]); int length = Integer.parseInt(albumArtFragReq[2]); - if (requestedId == albumArtIds.get(SRC_KEY + key)) { + if (requestedId == albumArtIds.get(SRC_KEY + srcId)) { byte[] chunk = new byte[length]; - byte[] albumArtBytes = albumArtMap.get(SRC_KEY + key); + byte[] albumArtBytes = albumArtMap.get(SRC_KEY + srcId); if (albumArtBytes != null) { System.arraycopy(albumArtBytes, offset, chunk, 0, length); final String frag = Base64.getEncoder().encodeToString(chunk); try { - connector.sendCommand(SRC_KEY + key + ALBUM_ART_FRAG + requestedId + COMMA + offset + connector.sendCommand(SRC_KEY + srcId + ALBUM_ART_FRAG + requestedId + COMMA + offset + COMMA + frag.length() + COMMA + frag); } catch (NuvoException e) { - logger.debug("Error sending ALBUMARTFRAG command for source: {}, artId: {}", key, + logger.debug("Error sending ALBUMARTFRAG command for source: {}, artId: {}", srcId, requestedId); } } } } break; - case TYPE_FAVORITE_REQ: + case TYPE_NN_FAVORITE_REQ: // ignore this update unless openHAB is handling this source - if (nuvoNetSrcMap.get(key).equals(2)) { - logger.debug("Favorite request for source: {} - favoriteId: {}", key, updateData); + if (nuvoNetSrcMap.get(srcId).equals(2)) { + logger.debug("Favorite request for source: {} - favoriteId: {}", srcId, updateData); try { int playlistIdx = Integer.parseInt(updateData, 16) - 1000; - updateChannelState(NuvoEnum.valueOf(SOURCE + key), CHANNEL_BUTTON_PRESS, - "PLAY_MUSIC_PRESET:" + favoriteMap.get(key)[playlistIdx]); + updateChannelState(NuvoEnum.valueOf(SOURCE + srcId), CHANNEL_BUTTON_PRESS, + "PLAY_MUSIC_PRESET:" + favoriteMap.get(srcId)[playlistIdx]); } catch (NumberFormatException nfe) { logger.debug("Unable to parse favoriteId: {}", updateData); } } break; default: - logger.debug("onNewMessageEvent: unhandled key {}", key); + logger.debug("onNewMessageEvent: unhandled event type {}", type); // Return here because receiving an unknown message does not indicate that one can poll return; } diff --git a/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/i18n/nuvo.properties b/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/i18n/nuvo.properties index b882e74e53738..4d69eef37badc 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/i18n/nuvo.properties +++ b/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/i18n/nuvo.properties @@ -165,7 +165,7 @@ channel-type.nuvo.balance.description = Adjust the Balance Setting for the Zone channel-type.nuvo.bass.label = Bass Adjustment channel-type.nuvo.bass.description = Adjust the Bass Setting for the Zone channel-type.nuvo.button_press.label = Button Pressed -channel-type.nuvo.button_press.description = Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source +channel-type.nuvo.button_press.description = Indicates the Last Button Pressed On the Keypad channel-type.nuvo.control.label = Control channel-type.nuvo.control.description = Transport Controls e.g. Play/Pause/Next/Previous for the Current Source channel-type.nuvo.display_line1.label = Display Line 1 diff --git a/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/thing/channels.xml index 023af7f53dd5d..3b35889c4d384 100644 --- a/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.nuvo/src/main/resources/OH-INF/thing/channels.xml @@ -342,6 +342,7 @@ + @@ -518,7 +519,7 @@ String - Indicates the Last Button Pressed On the Keypad for a Non NuvoNet Source + Indicates the Last Button Pressed On the Keypad