From 676d1e52269212caef5a193bac0803898b19fdd8 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 29 Oct 2024 17:02:02 -0400 Subject: [PATCH 01/44] Midea AC after partial PR review Mideaac binding after partial PR review. Main remaining issue is the connection manager which currently needs to be embedded in the MideaACHandler to leverage the OH base thing handler. Signed-off-by: Bob Eckhoff --- bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.mideaac/NOTICE | 13 + bundles/org.openhab.binding.mideaac/README.md | 121 ++ bundles/org.openhab.binding.mideaac/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/MideaACBindingConstants.java | 93 ++ .../internal/MideaACConfiguration.java | 68 + .../internal/MideaACHandlerFactory.java | 72 + .../binding/mideaac/internal/Utils.java | 250 +++ .../internal/discovery/Connection.java | 82 + .../internal/discovery/DiscoveryHandler.java | 31 + .../discovery/MideaACDiscoveryService.java | 353 ++++ .../mideaac/internal/dto/CloudDTO.java | 357 ++++ .../internal/dto/CloudProviderDTO.java | 60 + .../mideaac/internal/dto/CloudsDTO.java | 60 + .../mideaac/internal/handler/CommandBase.java | 314 ++++ .../mideaac/internal/handler/CommandSet.java | 399 +++++ .../internal/handler/MideaACHandler.java | 1466 +++++++++++++++++ .../mideaac/internal/handler/Packet.java | 117 ++ .../mideaac/internal/handler/Response.java | 389 +++++ .../mideaac/internal/handler/Timer.java | 121 ++ .../mideaac/internal/security/Crc8.java | 78 + .../security/Decryption8370Result.java | 59 + .../mideaac/internal/security/Security.java | 627 +++++++ .../mideaac/internal/security/TokenKey.java | 28 + .../src/main/resources/OH-INF/addon/addon.xml | 11 + .../resources/OH-INF/i18n/mideaac.properties | 95 ++ .../resources/OH-INF/thing/thing-types.xml | 266 +++ .../internal/MideaACConfigurationTest.java | 91 + .../MideaACDiscoveryServiceTest.java | 104 ++ .../internal/handler/CommandSetTest.java | 241 +++ .../internal/handler/ResponseTest.java | 197 +++ bundles/pom.xml | 1 + 33 files changed, 6195 insertions(+) create mode 100644 bundles/org.openhab.binding.mideaac/NOTICE create mode 100644 bundles/org.openhab.binding.mideaac/README.md create mode 100644 bundles/org.openhab.binding.mideaac/pom.xml create mode 100644 bundles/org.openhab.binding.mideaac/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml create mode 100644 bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties create mode 100644 bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index e67c844d38a74..2aa407838123e 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1146,6 +1146,11 @@ org.openhab.binding.mffan ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mideaac + ${project.version} + org.openhab.addons.bundles org.openhab.binding.miele diff --git a/bundles/org.openhab.binding.mideaac/NOTICE b/bundles/org.openhab.binding.mideaac/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md new file mode 100644 index 0000000000000..47450dadf0450 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -0,0 +1,121 @@ +# Midea AC Binding + +This binding integrates Air Conditioners that use the Midea protocol. Midea is an OEM for many brands. + +An AC device is likely supported if it uses one of the following Android apps or it's iOS equivalent. + +| Application | Comment | Options | +|--:-------------------------------------------|--:------------------------------------|--------------| +| Midea Air (com.midea.aircondition.obm) | Full Support of key and token updates | Midea Air | +| NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | +| SmartHome/MSmartHome (com.midea.ai.overseas) | Full Support of key and token updates | MSmartHome | + +Note: The Air Conditioner must already be set-up on the WiFi network and have a fixed IP Address with one of the three apps listed above for full discovery and key and token updates. + +## Supported Things + +This binding supports one Thing type `ac`. + +## Discovery + +Once the Air Conditioner is on the network (WiFi active) the other required parameters can be discovered automatically. +An IP broadcast message is sent and every responding unit gets added to the Inbox. +As an alternative use the python application msmart-ng from with the msmart-ng discover ipAddress option. + +## Binding Configuration + +No binding configuration is required. + +## Thing Configuration + +| Parameter | Required ? | Comment | Default | +|--:----------|--:----------|--:----------------------------------------------------------------|---------| +| ipAddress | Yes | IP Address of the device. | | +| ipPort | Yes | IP port of the device | 6444 | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | +| cloud | Yes for V.3 | Cloud Provider name for email and password | | +| email | No | Email for cloud account chosen in Cloud Provider. | | +| password | No | Password for cloud account chosen in Cloud Provider. | | +| token | Yes for V.3 | Secret Token (length 128 HEX) | | +| key | Yes for V.3 | Secret Key (length 64 HEX) | | +| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | +| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | +| promptTone | Yes | "Ding" tone when command is received and executed. | False | +| version | Yes | Version 3 has token, key and cloud requirements. | 3 | + +## Channels + +Following channels are available: + +| Channel | Type | Description | Read only | Advanced | +|--:---------------------------|--:-----------------|--:-----------------------------------------------------------------------------------------------------|--:--------|--:-------| +| power | Switch | Turn the AC on and off. | | | +| target-temperature | Number:Temperature | Target temperature. | | | +| operational-mode | String | Operational mode: OFF (turns off), AUTO, COOL, DRY, HEAT, FAN ONLY | | | +| fan-speed | String | Fan speed: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | +| swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | +| eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | +| turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT mode only. | | | +| sleep-function | Switch | Sleep function ("Moon with a star" icon on IR Remote Controller). | | | +| indoor-temperature | Number:Temperature | Indoor temperature measured in the room, where internal unit is installed. | Yes | | +| outdoor-temperature | Number:Temperature | Outdoor temperature by external unit. Some units do not report reading when off. | Yes | | +| temperature-unit | Switch | Sets the LED display on the evaporator to Fahrenheit (true) or Celsius (false). | | Yes | +| on-timer | String | Sets the future time to turn on the AC. | | Yes | +| off-timer | String | Sets the future time to turn off the AC. | | Yes | +| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | +| humidity | Number | If device supports, the indoor humidity. | Yes | Yes | +| dropped-commands | Number | Quality of WiFi connections - For debugging only. | Yes | Yes | +| appliance-error | Switch | If device supports, appliance error | Yes | Yes | +| auxiliary-heat | Switch | If device supports, auxiliary heat | Yes | Yes | +| alternate-target-temperature | Number:Temperature | Alternate Target Temperature - not currently used | Yes | Yes | + +## Examples + +### `demo.things` Example + +```java +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="6444", deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, timeout=4, promptTone="false", version="3"] +``` + +Option to use the built-in binding discovery of ipPort, deviceId, token and key. + +```java +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="", deviceId="", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="", key ="", pollingTime = 60, timeout=4, promptTone="false", version="3"] +``` + +### `demo.items` Example + +```java +Switch power "Power" { channel="mideaac:ac:mideaac:power" } +Number:Temperature target_temperature "Target Temperature [%.1f °F]" { channel="mideaac:ac:mideaac:target-temperature" } +String operational_mode "Operational Mode" { channel="mideaac:ac:mideaac:operational-mode" } +String fan_speed "Fan Speed" { channel="mideaac:ac:mideaac:fan-speed" } +String swing_mode "Swing Mode" { channel="mideaac:ac:mideaac:swing-mode" } +Number:Temperature indoor_temperature "Indoor Temperature [%.1f °F]" { channel="mideaac:ac:mideaac:indoor-temperature" } +Number:Temperature outdoor_temperature "Current Temperature [%.1f °F]" { channel="mideaac:ac:mideaac:outdoor-temperature" } +Switch eco_mode "Eco Mode" { channel="mideaac:ac:mideaac:eco-mode" } +Switch turbo_mode "Turbo Mode" { channel="mideaac:ac:mideaac:turbo-mode" } +Switch sleep_function "Sleep function" { channel="mideaac:ac:mideaac:sleep-function" } +Switch temperature_unit "Fahrenheit or Celsius" { channel="mideaac:ac:mideaac:temperature-unit" } +``` + +### `demo.sitemap` Example + +```java +sitemap midea label="Split AC MBR"{ + Frame label="AC Unit" { + Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]" + Text item=indoor_temperature label="Indoor Temperature [%.1f °F]" + Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0 + Switch item=power label="Midea AC Power" + Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"] + Selection item=fan_speed label="Midea AC Fan Speed" + Selection item=operational_mode label="Midea AC Mode" + Selection item=swing_mode label="Midea AC Louver Swing Mode" +} +} +``` + +## Debugging and Tracing + +Switch the log level to TRACE or DEBUG on the UI Settings Page (Add-on Settings) diff --git a/bundles/org.openhab.binding.mideaac/pom.xml b/bundles/org.openhab.binding.mideaac/pom.xml new file mode 100644 index 0000000000000..a19f12966b8b3 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.mideaac + + openHAB Add-ons :: Bundles :: MideaAC Binding + + diff --git a/bundles/org.openhab.binding.mideaac/src/main/feature/feature.xml b/bundles/org.openhab.binding.mideaac/src/main/feature/feature.xml new file mode 100644 index 0000000000000..e7f89f4dcb847 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.mideaac/${project.version} + + diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java new file mode 100644 index 0000000000000..35d419e3f272b --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal; + +import java.util.Collections; +import java.util.Set; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link MideaACBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - OH naming conventions + */ +@NonNullByDefault +public class MideaACBindingConstants { + + private static final String BINDING_ID = "mideaac"; + + /** + * Thing Type + */ + public static final ThingTypeUID THING_TYPE_MIDEAAC = new ThingTypeUID(BINDING_ID, "ac"); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MIDEAAC); + + /** + * List of all channel IDS + */ + public static final String CHANNEL_POWER = "power"; + public static final String CHANNEL_APPLIANCE_ERROR = "appliance-error"; + public static final String CHANNEL_TARGET_TEMPERATURE = "target-temperature"; + public static final String CHANNEL_OPERATIONAL_MODE = "operational-mode"; + public static final String CHANNEL_FAN_SPEED = "fan-speed"; + public static final String CHANNEL_ON_TIMER = "on-timer"; + public static final String CHANNEL_OFF_TIMER = "off-timer"; + public static final String CHANNEL_SWING_MODE = "swing-mode"; + public static final String CHANNEL_AUXILIARY_HEAT = "auxiliary-heat"; + public static final String CHANNEL_ECO_MODE = "eco-mode"; + public static final String CHANNEL_TEMPERATURE_UNIT = "temperature-unit"; + public static final String CHANNEL_SLEEP_FUNCTION = "sleep-function"; + public static final String CHANNEL_TURBO_MODE = "turbo-mode"; + public static final String CHANNEL_INDOOR_TEMPERATURE = "indoor-temperature"; + public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature"; + public static final String CHANNEL_HUMIDITY = "humidity"; + public static final String CHANNEL_ALTERNATE_TARGET_TEMPERATURE = "alternate-target-temperature"; + public static final String CHANNEL_SCREEN_DISPLAY = "screen-display"; + public static final String DROPPED_COMMANDS = "dropped-commands"; + + public static final Unit API_TEMPERATURE_UNIT = SIUnits.CELSIUS; + + /** + * Commands sent to/from AC wall unit are ASCII + */ + public static final String CHARSET = "US-ASCII"; + + /** + * List of all AC thing properties + */ + public static final String CONFIG_IP_ADDRESS = "ipAddress"; + public static final String CONFIG_IP_PORT = "ipPort"; + public static final String CONFIG_DEVICEID = "deviceId"; + public static final String CONFIG_CLOUD = "cloud"; + public static final String CONFIG_EMAIL = "email"; + public static final String CONFIG_PASSWORD = "password"; + public static final String CONFIG_TOKEN = "token"; + public static final String CONFIG_KEY = "key"; + public static final String CONFIG_POLLING_TIME = "pollingTime"; + public static final String CONFIG_CONNECTING_TIMEOUT = "timeout"; + public static final String CONFIG_PROMPT_TONE = "promptTone"; + + public static final String PROPERTY_VERSION = "version"; + public static final String PROPERTY_SN = "sn"; + public static final String PROPERTY_SSID = "ssid"; + public static final String PROPERTY_TYPE = "type"; +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java new file mode 100644 index 0000000000000..12667638da4ad --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MideaACConfiguration} class contains fields mapping thing configuration parameters. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - OH addons changes + */ +@NonNullByDefault +public class MideaACConfiguration { + + public String ipAddress = ""; + + public String ipPort = "6444"; + + public String deviceId = ""; + + public String email = ""; + + public String password = ""; + + public String cloud = ""; + + public String token = ""; + + public String key = ""; + + public int pollingTime = 60; + + public int timeout = 4; + + public boolean promptTone; + + public String version = ""; + + /** + * Check during initialization that the params are valid + * + * @return true(valid), false (not valid) + */ + public boolean isValid() { + return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort.isBlank() || ipAddress.isBlank()); + } + + /** + * Check during initialization if discovery is needed + * + * @return true(discovery needed), false (not needed) + */ + public boolean isDiscoveryNeeded() { + return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort.isBlank() || ipAddress.isBlank() + || !Utils.validateIP(ipAddress)); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java new file mode 100644 index 0000000000000..bea42f4ff5617 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal; + +import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.SUPPORTED_THING_TYPES_UIDS; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mideaac.internal.dto.CloudsDTO; +import org.openhab.binding.mideaac.internal.handler.MideaACHandler; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MideaACHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Jacek Dobrowolski - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.mideaac", service = ThingHandlerFactory.class) +public class MideaACHandlerFactory extends BaseThingHandlerFactory { + + private UnitProvider unitProvider; + private final HttpClientFactory httpClientFactory; + private final CloudsDTO clouds; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + /** + * The MideaACHandlerFactory class parameters + * + * @param unitProvider OH unitProvider + * @param httpClientFactory OH httpClientFactory + */ + @Activate + public MideaACHandlerFactory(@Reference UnitProvider unitProvider, @Reference HttpClientFactory httpClientFactory) { + this.unitProvider = unitProvider; + this.httpClientFactory = httpClientFactory; + clouds = new CloudsDTO(); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { + return new MideaACHandler(thing, unitProvider, httpClientFactory.getCommonHttpClient(), clouds); + } + return null; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java new file mode 100644 index 0000000000000..eedad2b658e1c --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.jose4j.base64url.Base64; + +import com.google.gson.JsonObject; + +/** + * The {@link Utils} class defines common byte and String array methods + * which are used across the whole binding. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - JavaDoc + */ +@NonNullByDefault +public class Utils { + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + private static final char[] HEX_ARRAY_LOWERCASE = "0123456789abcdef".toCharArray(); + static byte[] empty = new byte[0]; + + /** + * Converts byte array to upper case hex string + * + * @param bytes bytes to convert + * @return string of hex chars + */ + public static String bytesToHex(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Converts byte array to binary string + * + * @param bytes bytes to convert + * @return string of hex chars + */ + public static String bytesToBinary(byte[] bytes) { + String s1 = ""; + for (int j = 0; j < bytes.length; j++) { + + s1 = s1.concat(Integer.toBinaryString(bytes[j] & 255 | 256).substring(1)); + s1 = s1.concat(" "); + + } + return s1; + } + + /** + * Converts byte array to lower case hex string + * + * @param bytes bytes to convert + * @return string of hex chars + */ + public static String bytesToHexLowercase(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY_LOWERCASE[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY_LOWERCASE[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Validates the IP address format + * + * @param ip string of IP Address + * @return IP pattern OK + */ + public static boolean validateIP(final String ip) { + String pattern = "^((0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)\\.){3}(0|1\\d?\\d?|2[0-4]?\\d?|25[0-5]?|[3-9]\\d?)$"; + + return ip.matches(pattern); + } + + /** + * Converts hex string to a byte array + * + * @param s string to convert to byte array + * @return byte array + */ + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + /** + * Adds two byte arrays together + * + * @param a input byte array 1 + * @param b input byte array 2 + * @return byte array + */ + public static byte[] concatenateArrays(byte[] a, byte[] b) { + byte[] c = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + /** + * Arrange byte order + * + * @param i input + * @return @return byte array + */ + public static byte[] toBytes(short i) { + ByteBuffer b = ByteBuffer.allocate(2); + b.order(ByteOrder.BIG_ENDIAN); // optional, the initial order of a byte buffer is always BIG_ENDIAN. + b.putShort(i); + return b.array(); + } + + /** + * Combine byte arrays + * + * @param array1 input array + * @param array2 input array + * @return byte array + */ + public static byte[] strxor(byte[] array1, byte[] array2) { + byte[] result = new byte[array1.length]; + int i = 0; + for (byte b : array1) { + result[i] = (byte) (b ^ array2[i++]); + } + return result; + } + + /** + * Create String of the v3 Token + * + * @param nbytes number of bytes + * @return String + */ + public static String tokenHex(int nbytes) { + Random r = new Random(); + StringBuffer sb = new StringBuffer(); + for (int n = 0; n < nbytes; n++) { + sb.append(Integer.toHexString(r.nextInt())); + } + return sb.toString().substring(0, nbytes); + } + + /** + * Create URL safe token + * + * @param nbytes number of bytes + * @return encoded string + */ + public static String tokenUrlsafe(int nbytes) { + Random r = new Random(); + byte[] bytes = new byte[nbytes]; + r.nextBytes(bytes); + return Base64.encode(bytes); + } + + /** + * Extracts 6 bits and reorders them based on signed or unsigned + * + * @param i input + * @param order byte order + * @return reordered array + */ + public static byte[] toIntTo6ByteArray(long i, ByteOrder order) { + final ByteBuffer bb = ByteBuffer.allocate(8); + bb.order(order); + + bb.putLong(i); + + if (order == ByteOrder.BIG_ENDIAN) { + return Arrays.copyOfRange(bb.array(), 2, 8); + } + + if (order == ByteOrder.LITTLE_ENDIAN) { + return Arrays.copyOfRange(bb.array(), 0, 6); + } + return empty; + } + + /** + * String Builder + * + * @param json JSON object + * @return string + */ + public static String getQueryString(JsonObject json) { + StringBuilder sb = new StringBuilder(); + Iterator keys = json.keySet().stream().sorted().iterator(); + while (keys.hasNext()) { + @Nullable + String key = keys.next(); + sb.append(key); + sb.append("="); + sb.append(json.get(key).getAsString()); + if (keys.hasNext()) { + sb.append("&"); // To allow for another argument. + } + } + return sb.toString(); + } + + /** + * Used to reverse (or unreverse) the deviceId + * + * @param array input array + * @return reversed array + */ + public static byte[] reverse(byte[] array) { + int left = 0; + int right = array.length - 1; + while (left < right) { + byte temp = array[left]; + array[left] = array[right]; + array[right] = temp; + left++; + right--; + } + return array; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java new file mode 100644 index 0000000000000..44dbc131cc29a --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.discovery; + +import java.io.Closeable; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Connection} Manages the discovery connection to a Midea AC. + * + * @author Jacek Dobrowolski - Initial contribution + */ +@NonNullByDefault +public class Connection implements Closeable { + + /** + * UDP port1 to send command. + */ + public static final int MIDEAAC_SEND_PORT1 = 6445; + /** + * UDP port2 to send command. + */ + public static final int MIDEAAC_SEND_PORT2 = 20086; + /** + * UDP port devices send discover replies back. + */ + public static final int MIDEAAC_RECEIVE_PORT = 6440; + + private final InetAddress iNetAddress; + private final DatagramSocket socket; + + /** + * Initializes a connection to the given IP address. + * + * @param ipAddress IP address of the connection + * @throws UnknownHostException if ipAddress could not be resolved. + * @throws SocketException if no Datagram socket connection could be made. + */ + public Connection(String ipAddress) throws SocketException, UnknownHostException { + iNetAddress = InetAddress.getByName(ipAddress); + socket = new DatagramSocket(); + } + + /** + * Sends the 9 bytes command to the Midea AC device. + * + * @param command the 9 bytes command + * @throws IOException Connection to the LED failed + */ + public void sendCommand(byte[] command) throws IOException { + { + DatagramPacket sendPkt = new DatagramPacket(command, command.length, iNetAddress, MIDEAAC_SEND_PORT1); + socket.send(sendPkt); + } + { + DatagramPacket sendPkt = new DatagramPacket(command, command.length, iNetAddress, MIDEAAC_SEND_PORT2); + socket.send(sendPkt); + } + } + + @Override + public void close() { + socket.close(); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java new file mode 100644 index 0000000000000..a1d25cd41bbee --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.discovery; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.config.discovery.DiscoveryResult; + +/** + * Discovery {@link DiscoveryHandler} + * + * @author Jacek Dobrowolski - Initial contribution + */ +@NonNullByDefault +public interface DiscoveryHandler { + /** + * Discovery result + * + * @param discoveryResult AC device + */ + public void discovered(DiscoveryResult discoveryResult); +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java new file mode 100644 index 0000000000000..cfdbe02de67b8 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -0,0 +1,353 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.discovery; + +import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.openhab.binding.mideaac.internal.handler.CommandBase; +import org.openhab.binding.mideaac.internal.security.Security; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.config.discovery.DiscoveryService; +import org.openhab.core.thing.ThingUID; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MideaACDiscoveryService} service for Midea AC. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - OH naming conventions + */ +@NonNullByDefault +@Component(service = DiscoveryService.class, configurationPid = "discovery.mideaac") +public class MideaACDiscoveryService extends AbstractDiscoveryService { + + private static int discoveryTimeoutSeconds = 5; + private final int receiveJobTimeout = 20000; + private final int udpPacketTimeout = receiveJobTimeout - 50; + private final String mideaacNamePrefix = "MideaAC"; + + private final Logger logger = LoggerFactory.getLogger(MideaACDiscoveryService.class); + + ///// Network + private byte[] buffer = new byte[512]; + @Nullable + private DatagramSocket discoverSocket; + + @Nullable + DiscoveryHandler discoveryHandler; + + private Security security; + + /** + * Discovery Service + */ + public MideaACDiscoveryService() { + super(SUPPORTED_THING_TYPES_UIDS, discoveryTimeoutSeconds, false); + this.security = new Security(CloudProviderDTO.getCloudProvider("")); + } + + @Override + protected void startScan() { + logger.debug("Start scan for Midea AC devices."); + discoverThings(); + } + + @Override + protected void stopScan() { + logger.debug("Stop scan for Midea AC devices."); + closeDiscoverSocket(); + super.stopScan(); + } + + /** + * Performs the actual discovery of Midea AC devices (things). + */ + private void discoverThings() { + try { + final DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length); + // No need to call close first, because the caller of this method already has done it. + startDiscoverSocket(); + // Runs until the socket call gets a time out and throws an exception. When a time out is triggered it means + // no data was present and nothing new to discover. + while (true) { + // Set packet length in case a previous call reduced the size. + receivePacket.setLength(buffer.length); + DatagramSocket discoverSocket = this.discoverSocket; + if (discoverSocket == null) { + break; + } else { + discoverSocket.receive(receivePacket); + } + logger.debug("Midea AC device discovery returned package with length {}", receivePacket.getLength()); + if (receivePacket.getLength() > 0) { + thingDiscovered(receivePacket); + } + } + } catch (SocketTimeoutException e) { + logger.debug("Discovering poller timeout..."); + } catch (IOException e) { + logger.debug("Error during discovery: {}", e.getMessage()); + } finally { + closeDiscoverSocket(); + removeOlderResults(getTimestampOfLastScan()); + } + } + + /** + * Performs the actual discovery of a specific Midea AC device (thing) + * + * @param ipAddress IP Address + * @param discoveryHandler Discovery Handler + */ + public void discoverThing(String ipAddress, DiscoveryHandler discoveryHandler) { + try { + final DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length); + // No need to call close first, because the caller of this method already has done it. + startDiscoverSocket(ipAddress, discoveryHandler); + // Runs until the socket call gets a time out and throws an exception. When a time out is triggered it means + // no data was present and nothing new to discover. + while (true) { + // Set packet length in case a previous call reduced the size. + receivePacket.setLength(buffer.length); + DatagramSocket discoverSocket = this.discoverSocket; + if (discoverSocket == null) { + break; + } else { + discoverSocket.receive(receivePacket); + } + logger.debug("Midea AC device discovery returned package with length {}", receivePacket.getLength()); + if (receivePacket.getLength() > 0) { + thingDiscovered(receivePacket); + } + } + } catch (SocketTimeoutException e) { + logger.debug("Discovering poller timeout..."); + } catch (IOException e) { + logger.debug("Error during discovery: {}", e.getMessage()); + } finally { + closeDiscoverSocket(); + } + } + + /** + * Opens a {@link DatagramSocket} and sends a packet for discovery of Midea AC devices. + * + * @throws SocketException + * @throws IOException + */ + private void startDiscoverSocket() throws SocketException, IOException { + startDiscoverSocket("255.255.255.255", null); + } + + /** + * Start the discovery Socket + * + * @param ipAddress broadcast IP Address + * @param discoveryHandler Discovery handler + * @throws SocketException Socket Exception + * @throws IOException IO Exception + */ + public void startDiscoverSocket(String ipAddress, @Nullable DiscoveryHandler discoveryHandler) + throws SocketException, IOException { + logger.trace("Discovering: {}", ipAddress); + this.discoveryHandler = discoveryHandler; + discoverSocket = new DatagramSocket(new InetSocketAddress(Connection.MIDEAAC_RECEIVE_PORT)); + DatagramSocket discoverSocket = this.discoverSocket; + if (discoverSocket != null) { + discoverSocket.setBroadcast(true); + discoverSocket.setSoTimeout(udpPacketTimeout); + final InetAddress broadcast = InetAddress.getByName(ipAddress); + { + final DatagramPacket discoverPacket = new DatagramPacket(CommandBase.discover(), + CommandBase.discover().length, broadcast, Connection.MIDEAAC_SEND_PORT1); + discoverSocket.send(discoverPacket); + logger.trace("Broadcast discovery package sent to port: {}", Connection.MIDEAAC_SEND_PORT1); + } + { + final DatagramPacket discoverPacket = new DatagramPacket(CommandBase.discover(), + CommandBase.discover().length, broadcast, Connection.MIDEAAC_SEND_PORT2); + discoverSocket.send(discoverPacket); + logger.trace("Broadcast discovery package sent to port: {}", Connection.MIDEAAC_SEND_PORT2); + } + } + } + + /** + * Closes the discovery socket and cleans the value. No need for synchronization as this method is called from a + * synchronized context. + */ + private void closeDiscoverSocket() { + DatagramSocket discoverSocket = this.discoverSocket; + if (discoverSocket != null) { + discoverSocket.close(); + this.discoverSocket = null; + } + } + + /** + * Register a device (thing) with the discovered properties. + * + * @param packet containing data of detected device + */ + private void thingDiscovered(DatagramPacket packet) { + DiscoveryResult dr = discoveryPacketReceived(packet); + if (dr != null) { + DiscoveryHandler discoveryHandler = this.discoveryHandler; + if (discoveryHandler != null) { + discoveryHandler.discovered(dr); + } else { + thingDiscovered(dr); + } + } + } + + /** + * Parses the packet to extract the device properties + * + * @param packet returned paket from device + * @return extracted device properties + */ + @Nullable + public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { + final String ipAddress = packet.getAddress().getHostAddress(); + byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength()); + + logger.debug("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data)); + + if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A") + || Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) { + logger.trace("Device supported"); + String mSmartId, mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", + mSmartType = ""; + if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) { + mSmartVersion = "2"; + } + if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { + mSmartVersion = "3"; + } + if (Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { + data = Arrays.copyOfRange(data, 8, data.length - 16); + } + + logger.trace("Version: {}", mSmartVersion); + + byte[] id = Arrays.copyOfRange(data, 20, 26); + logger.trace("Id Bytes: {}", Utils.bytesToHex(id)); + + byte[] idReverse = Utils.reverse(id); + + BigInteger bigId = new BigInteger(1, idReverse); + mSmartId = bigId.toString(10); + + logger.debug("Id: '{}'", mSmartId); + + byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16); + logger.debug("Encrypt data: '{}'", Utils.bytesToHex(encryptData)); + + byte[] reply = security.aesDecrypt(encryptData); + logger.debug("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply)); + + mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." + + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); + logger.debug("IP: '{}'", mSmartip); + + byte[] portIdBytes = Utils.reverse(Arrays.copyOfRange(reply, 4, 8)); + BigInteger portId = new BigInteger(1, portIdBytes); + mSmartPort = portId.toString(10); + logger.debug("Port: '{}'", mSmartPort); + + mSmartSN = new String(reply, 8, 40 - 8, StandardCharsets.UTF_8); + logger.debug("SN: '{}'", mSmartSN); + + logger.trace("SSID length: '{}'", Byte.toUnsignedInt(reply[40])); + + mSmartSSID = new String(reply, 41, reply[40], StandardCharsets.UTF_8); + logger.debug("SSID: '{}'", mSmartSSID); + + mSmartType = mSmartSSID.split("_")[1]; + logger.debug("Type: '{}'", mSmartType); + + String thingName = createThingName(packet.getAddress().getAddress(), mSmartId); + ThingUID thingUID = new ThingUID(THING_TYPE_MIDEAAC, thingName.toLowerCase()); + + return DiscoveryResultBuilder.create(thingUID).withLabel(thingName) + .withRepresentationProperty(CONFIG_IP_ADDRESS).withThingType(THING_TYPE_MIDEAAC) + .withProperties(collectProperties(ipAddress, mSmartVersion, mSmartId, mSmartPort, mSmartSN, + mSmartSSID, mSmartType)) + .build(); + } else if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) { + logger.debug("Midea AC v1 device was detected, supported, but not implemented yet."); + return null; + } else { + logger.debug( + "Midea AC device was detected, but the retrieved data is incomplete or not supported. Device not registered"); + return null; + } + } + + /** + * Creates a OH name for the Midea AC device. + * + * @return the name for the device + */ + private String createThingName(final byte[] byteIP, String id) { + return mideaacNamePrefix + "-" + Byte.toUnsignedInt(byteIP[3]) + "-" + id; + } + + /** + * Collects properties into a map. + * + * @param ipAddress IP address of the thing + * @param version Version 2 or 3 + * @param id ID of the device + * @param port Port of the device + * @param sn Serial number of the device + * @param ssid Serial id converted with StandardCharsets.UTF_8 + * @param type Type of device (ac) + * @return Map with properties + */ + private Map collectProperties(String ipAddress, String version, String id, String port, String sn, + String ssid, String type) { + Map properties = new TreeMap<>(); + properties.put(CONFIG_IP_ADDRESS, ipAddress); + properties.put(CONFIG_IP_PORT, port); + properties.put(CONFIG_DEVICEID, id); + properties.put(PROPERTY_VERSION, version); + properties.put(PROPERTY_SN, sn); + properties.put(PROPERTY_SSID, ssid); + properties.put(PROPERTY_TYPE, type); + + return properties; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java new file mode 100644 index 0000000000000..22714ebff8432 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java @@ -0,0 +1,357 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.dto; + +import java.nio.ByteOrder; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.security.Security; +import org.openhab.binding.mideaac.internal.security.TokenKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +/** + * The {@link CloudDTO} class connects to the Cloud Provider + * with user supplied information to retrieve the Security + * Token and Key. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - JavaDoc + */ +public class CloudDTO { + private final Logger logger = LoggerFactory.getLogger(CloudDTO.class); + + private static final int CLIENT_TYPE = 1; // Android + private static final int FORMAT = 2; // JSON + private static final String LANGUAGE = "en_US"; + + private Date tokenRequestedAt = new Date(); + + private void setTokenRequested() { + tokenRequestedAt = new Date(); + } + + /** + * Token rquested date + * + * @return tokenRequestedAt + */ + public Date getTokenRequested() { + return tokenRequestedAt; + } + + private HttpClient httpClient; + + /** + * Client for Http requests + * + * @return httpClient + */ + public HttpClient getHttpClient() { + return httpClient; + } + + /** + * Sets Http Client + * + * @param httpClient Http Client + */ + public void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } + + private String errMsg; + + /** + * Gets error message + * + * @return errMsg + */ + public String getErrMsg() { + return errMsg; + } + + private @Nullable String accessToken = ""; + + private String loginAccount; + private String password; + private CloudProviderDTO cloudProvider; + private Security security; + + private @Nullable String loginId; + private String sessionId; + + /** + * Parameters for Cloud Provider + * + * @param email email + * @param password password + * @param cloudProvider Cloud Provider + */ + public CloudDTO(String email, String password, CloudProviderDTO cloudProvider) { + this.loginAccount = email; + this.password = password; + this.cloudProvider = cloudProvider; + this.security = new Security(cloudProvider); + logger.debug("Cloud provider: {}", cloudProvider.name()); + } + + /** + * Set up the initial data payload with the global variable set + */ + private JsonObject apiRequest(String endpoint, JsonObject args, JsonObject data) { + if (data == null) { + data = new JsonObject(); + data.addProperty("appId", cloudProvider.appid()); + data.addProperty("format", FORMAT); + data.addProperty("clientType", CLIENT_TYPE); + data.addProperty("language", LANGUAGE); + data.addProperty("src", cloudProvider.appid()); + data.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); + } + + // Add the method parameters for the endpoint + if (args != null) { + for (Map.Entry entry : args.entrySet()) { + data.add(entry.getKey(), entry.getValue().getAsJsonPrimitive()); + } + } + + // Add the login information to the payload + if (!data.has("reqId") && !Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + data.addProperty("reqId", Utils.tokenHex(16)); + } + + String url = cloudProvider.apiurl() + endpoint; + + int time = (int) (new Date().getTime() / 1000); + + String random = String.valueOf(time); + + // Add the sign to the header + String json = data.toString(); + logger.debug("Request json: {}", json); + + Request request = httpClient.newRequest(url).method(HttpMethod.POST).timeout(15, TimeUnit.SECONDS); + + // .version(HttpVersion.HTTP_1_1) + request.agent("Dalvik/2.1.0 (Linux; U; Android 7.0; SM-G935F Build/NRD90M)"); + + if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + request.header("Content-Type", "application/json"); + } else { + request.header("Content-Type", "application/x-www-form-urlencoded"); + } + request.header("secretVersion", "1"); + if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + String sign = security.newSign(json, random); + request.header("sign", sign); + } else { + if (!Objects.isNull(sessionId) && !sessionId.isBlank()) { + data.addProperty("sessionId", sessionId); + } + String sign = security.sign(url, data); + data.addProperty("sign", sign); + request.header("sign", sign); + } + + request.header("random", random); + request.header("accessToken", accessToken); + + logger.debug("Request headers: {}", request.getHeaders().toString()); + + if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + request.content(new StringContentProvider(json)); + } else { + String body = Utils.getQueryString(data); + logger.debug("Request body: {}", body); + request.content(new StringContentProvider(body)); + } + + // POST the endpoint with the payload + ContentResponse cr = null; + try { + cr = request.send(); + } catch (InterruptedException e) { + logger.warn("an interupted error has occurred{}", e.getMessage()); + } catch (TimeoutException e) { + logger.warn("a timeout error has occurred{}", e.getMessage()); + } catch (ExecutionException e) { + logger.warn("an execution error has occurred{}", e.getMessage()); + } + + if (cr != null) { + logger.debug("Response json: {}", cr.getContentAsString()); + JsonObject result = Objects.requireNonNull(new Gson().fromJson(cr.getContentAsString(), JsonObject.class)); + + int code = -1; + + if (result.get("errorCode") != null) { + code = result.get("errorCode").getAsInt(); + } else if (result.get("code") != null) { + code = result.get("code").getAsInt(); + } else { + errMsg = "No code in cloud response"; + logger.warn("Error logging to Cloud: {}", errMsg); + return null; + } + + String msg = result.get("msg").getAsString(); + if (code != 0) { + errMsg = msg; + handleApiError(code, msg); + logger.warn("Error logging to Cloud: {}", msg); + return null; + } else { + logger.debug("Api response ok: {} ({})", code, msg); + if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + return result.get("data").getAsJsonObject(); + } else { + return result.get("result").getAsJsonObject(); + } + } + } else { + logger.warn("No response from cloud!"); + } + + return null; + } + + /** + * Performs a user login with the credentials supplied to the constructor + * + * @return true or false + */ + public boolean login() { + if (loginId == null) { + if (!getLoginId()) { + return false; + } + } + // Don't try logging in again, someone beat this thread to it + if (!Objects.isNull(sessionId) && !sessionId.isBlank()) { + return true; + } + + logger.trace("Using loginId: {}", loginId); + logger.trace("Using password: {}", password); + + if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + JsonObject newData = new JsonObject(); + + JsonObject data = new JsonObject(); + data.addProperty("platform", FORMAT); + newData.add("data", data); + + JsonObject iotData = new JsonObject(); + iotData.addProperty("appId", cloudProvider.appid()); + iotData.addProperty("clientType", CLIENT_TYPE); + iotData.addProperty("iampwd", security.encryptIamPassword(loginId, password)); + iotData.addProperty("loginAccount", loginAccount); + iotData.addProperty("password", security.encryptPassword(loginId, password)); + iotData.addProperty("pushToken", Utils.tokenUrlsafe(120)); + iotData.addProperty("reqId", Utils.tokenHex(16)); + iotData.addProperty("src", cloudProvider.appid()); + iotData.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); + newData.add("iotData", iotData); + + JsonObject response = apiRequest("/mj/user/login", null, newData); + + if (response == null) { + return false; + } + + accessToken = response.getAsJsonObject("mdata").get("accessToken").getAsString(); + } else { + String passwordEncrypted = security.encryptPassword(loginId, password); + + JsonObject data = new JsonObject(); + data.addProperty("loginAccount", loginAccount); + data.addProperty("password", passwordEncrypted); + + JsonObject response = apiRequest("/v1/user/login", data, null); + + if (response == null) { + return false; + } + + accessToken = response.get("accessToken").getAsString(); + sessionId = response.get("sessionId").getAsString(); + } + + return true; + } + + /** + * Get tokenlist with udpid + * + * @param udpid udp id + * @return token and key + */ + public TokenKey getToken(String udpid) { + long i = Long.valueOf(udpid); + + JsonObject args = new JsonObject(); + args.addProperty("udpid", security.getUdpId(Utils.toIntTo6ByteArray(i, ByteOrder.BIG_ENDIAN))); + JsonObject response = apiRequest("/v1/iot/secure/getToken", args, null); + + if (response == null) { + return null; + } + + JsonArray tokenlist = response.getAsJsonArray("tokenlist"); + JsonObject el = tokenlist.get(0).getAsJsonObject(); + String token = el.getAsJsonPrimitive("token").getAsString(); + String key = el.getAsJsonPrimitive("key").getAsString(); + + setTokenRequested(); + + return new TokenKey(token, key); + } + + /** + * Get the login ID from the email address + */ + private boolean getLoginId() { + JsonObject args = new JsonObject(); + args.addProperty("loginAccount", loginAccount); + JsonObject response = apiRequest("/v1/user/login/id/get", args, null); + if (response == null) { + return false; + } + loginId = response.get("loginId").getAsString(); + return true; + } + + private void handleApiError(int asInt, String asString) { + logger.debug("Api error in Cloud class"); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java new file mode 100644 index 0000000000000..ac92bfd00647f --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CloudProviderDTO} class contains the information + * to allow encryption and decryption for the supported Cloud Providers + * + * @param name Cloud provider + * @param appkey application key + * @param appid application id + * @param apiurl application url + * @param signkey sign key for AES + * @param proxied proxy - MSmarthome only + * @param iotkey iot key - MSmarthome only + * @param hmackey hmac key - MSmarthome only + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - JavaDoc and conversion to record + */ +@NonNullByDefault +public record CloudProviderDTO(String name, String appkey, String appid, String apiurl, String signkey, String proxied, + String iotkey, String hmackey) { + + /** + * Cloud provider information for record + * All providers use the same signkey for AES encryption and Decryption. + * V2 Devices do not require a Cloud Provider entry as they only use AES + * + * @param name Cloud provider + * @return Cloud provider information (appkey, appid, apiurl,signkey, proxied, iotkey, hmackey) + */ + public static CloudProviderDTO getCloudProvider(String name) { + switch (name) { + case "NetHome Plus": + return new CloudProviderDTO("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", + "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + case "Midea Air": + return new CloudProviderDTO("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", + "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + case "MSmartHome": + return new CloudProviderDTO("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", + "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", + "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); + } + return new CloudProviderDTO("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java new file mode 100644 index 0000000000000..3b4552e335751 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.dto; + +import java.util.HashMap; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link Clouds} class securely stores email and password + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - JavaDoc + */ +@NonNullByDefault +public class CloudsDTO { + + private final HashMap clouds; + + /** + * Cloud Provider data + */ + public CloudsDTO() { + clouds = new HashMap(); + } + + private CloudDTO add(String email, String password, CloudProviderDTO cloudProvider) { + int hash = (email + password + cloudProvider.name()).hashCode(); + CloudDTO cloud = new CloudDTO(email, password, cloudProvider); + clouds.put(hash, cloud); + return cloud; + } + + /** + * Gets user provided cloud provider data + * + * @param email your email + * @param password your password + * @param cloudProvider your Cloud Provider + * @return parameters for cloud provider + */ + public @Nullable CloudDTO get(String email, String password, CloudProviderDTO cloudProvider) { + int hash = (email + password + cloudProvider.name()).hashCode(); + if (clouds.containsKey(hash)) { + return clouds.get(hash); + } + return add(email, password, cloudProvider); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java new file mode 100644 index 0000000000000..e81a4cdcb3acd --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -0,0 +1,314 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import java.time.LocalDateTime; +import java.util.Arrays; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.security.Crc8; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link CommandBase} has the discover command and the routine poll command + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - Add Java Docs, minor fixes + */ +@NonNullByDefault +public class CommandBase { + private final Logger logger = LoggerFactory.getLogger(CommandBase.class); + + private static final byte[] DISCOVER_COMMAND = new byte[] { (byte) 0x5a, (byte) 0x5a, (byte) 0x01, (byte) 0x11, + (byte) 0x48, (byte) 0x00, (byte) 0x92, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, + (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x7f, (byte) 0x75, (byte) 0xbd, (byte) 0x6b, + (byte) 0x3e, (byte) 0x4f, (byte) 0x8b, (byte) 0x76, (byte) 0x2e, (byte) 0x84, (byte) 0x9c, (byte) 0x6e, + (byte) 0x57, (byte) 0x8d, (byte) 0x65, (byte) 0x90, (byte) 0x03, (byte) 0x6e, (byte) 0x9d, (byte) 0x43, + (byte) 0x42, (byte) 0xa5, (byte) 0x0f, (byte) 0x1f, (byte) 0x56, (byte) 0x9e, (byte) 0xb8, (byte) 0xec, + (byte) 0x91, (byte) 0x8e, (byte) 0x92, (byte) 0xe5 }; + + protected byte[] data; + + /** + * Operational Modes + */ + public enum OperationalMode { + AUTO(1), + COOL(2), + DRY(3), + HEAT(4), + FAN_ONLY(5), + UNKNOWN(0); + + private final int value; + + private OperationalMode(int value) { + this.value = value; + } + + /** + * Gets Operational Mode value + * + * @return value + */ + public int getId() { + return value; + } + + /** + * Provides Operational Mode Common name + * + * @param id integer from byte response + * @return type + */ + public static OperationalMode fromId(int id) { + for (OperationalMode type : values()) { + if (type.getId() == id) { + return type; + } + } + return UNKNOWN; + } + } + + /** + * Converts byte value to the Swing Mode label by version + * Two versions of V3, Supported Swing or Non-Supported (4) + * V2 set without leading 3, but reports with it (1) + */ + public enum SwingMode { + OFF3(0x30, 3), + OFF4(0x00, 3), + VERTICAL3(0x3C, 3), + VERTICAL4(0xC, 3), + HORIZONTAL3(0x33, 3), + HORIZONTAL4(0x3, 3), + BOTH3(0x3F, 3), + BOTH4(0xF, 3), + OFF2(0, 2), + VERTICAL2(0xC, 2), + VERTICAL1(0x3C, 2), + HORIZONTAL2(0x3, 2), + HORIZONTAL1(0x33, 2), + BOTH2(0xF, 2), + BOTH1(0x3F, 2), + + UNKNOWN(0xFF, 0); + + private final int value; + private final int version; + + private SwingMode(int value, int version) { + this.value = value; + this.version = version; + } + + /** + * Gets Swing Mode value + * + * @return value + */ + public int getId() { + return value; + } + + /** + * Gets device version for swing mode + * + * @return version + */ + public int getVersion() { + return version; + } + + /** + * Gets Swing mode in common language horiontal, vertical, off, etc. + * + * @param id integer from byte response + * @param version device version + * @return type + */ + public static SwingMode fromId(int id, int version) { + for (SwingMode type : values()) { + if (type.getId() == id && type.getVersion() == version) { + return type; + } + } + return UNKNOWN; + } + + @Override + public String toString() { + // Drops the trailing 1 (V2 report) 2, 3 or 4 (nonsupported V3) from the swing mode + return super.toString().replace("1", "").replace("2", "").replace("3", "").replace("4", ""); + } + } + + /** + * Converts byte value to the Fan Speed label by version. + * Some devices do not support all speeds + */ + public enum FanSpeed { + AUTO2(102, 2), + FULL2(100, 2), + HIGH2(80, 2), + MEDIUM2(50, 2), + LOW2(30, 2), + SILENT2(20, 2), + UNKNOWN2(0, 2), + + AUTO3(102, 3), + FULL3(0, 3), + HIGH3(80, 3), + MEDIUM3(60, 3), + LOW3(40, 3), + SILENT3(30, 3), + UNKNOWN3(0, 3), + + UNKNOWN(0, 0); + + private final int value; + + private final int version; + + private FanSpeed(int value, int version) { + this.value = value; + this.version = version; + } + + /** + * Gets Fan Speed value + * + * @return value + */ + public int getId() { + return value; + } + + /** + * Gets device version for Fan Speed + * + * @return version + */ + public int getVersion() { + return version; + } + + /** + * Returns Fan Speed high, medium, low, etc + * + * @param id integer from byte response + * @param version version + * @return type + */ + public static FanSpeed fromId(int id, int version) { + for (FanSpeed type : values()) { + if (type.getId() == id && type.getVersion() == version) { + return type; + } + } + return UNKNOWN; + } + + @Override + public String toString() { + // Drops the trailing 2 or 3 from the fan speed + return super.toString().replace("2", "").replace("3", ""); + } + } + + /** + * Returns the command to discover devices. + * Command is defined above + * + * @return discover command + */ + public static byte[] discover() { + return DISCOVER_COMMAND; + } + + /** + * Byte Array structure for Base commands + */ + public CommandBase() { + data = new byte[] { (byte) 0xaa, + // request is 0x20; setting is 0x23 + (byte) 0x20, + // device type + (byte) 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // request is 0x03; setting is 0x02 + (byte) 0x03, + // Byte0 - Data request/response type: 0x41 - check status; 0x40 - Set up + (byte) 0x41, + // Byte1 + (byte) 0x81, + // Byte2 - operational_mode + 0x00, + // Byte3 + (byte) 0xff, + // Byte4 + 0x03, + // Byte5 + (byte) 0xff, + // Byte6 + 0x00, + // Byte7 - Room Temperature Request: 0x02 - indoor_temperature, 0x03 - outdoor_temperature + // when set, this is swing_mode + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // Message ID + 0x00 }; + LocalDateTime now = LocalDateTime.now(); + data[data.length - 1] = (byte) now.getSecond(); + data[0x02] = (byte) 0xAC; + } + + /** + * Pulls the elements of the Base command together + */ + public void compose() { + logger.trace("Base Bytes before crypt {}", Utils.bytesToHex(data)); + byte crc8 = (byte) Crc8.calculate(Arrays.copyOfRange(data, 10, data.length)); + byte[] newData1 = new byte[data.length + 1]; + System.arraycopy(data, 0, newData1, 0, data.length); + newData1[data.length] = crc8; + data = newData1; + byte chksum = checksum(Arrays.copyOfRange(data, 1, data.length)); + byte[] newData2 = new byte[data.length + 1]; + System.arraycopy(data, 0, newData2, 0, data.length); + newData2[data.length] = chksum; + data = newData2; + } + + /** + * Gets byte array + * + * @return data array + */ + public byte[] getBytes() { + return data; + } + + private static byte checksum(byte[] bytes) { + int sum = 0; + for (byte value : bytes) { + sum = (byte) (sum + value); + } + sum = (byte) ((255 - (sum % 256)) + 1); + return (byte) sum; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java new file mode 100644 index 0000000000000..6be330c8095af --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -0,0 +1,399 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.handler.Timer.TimerData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This {@link CommandSet} class handles the allowed changes originating from + * the items linked to the Midea AC channels. Not all devices + * support all commands. The general process is to clear the + * bit(s) the set them to the commanded value. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - Add Java Docs, minor fixes + */ +@NonNullByDefault +public class CommandSet extends CommandBase { + private final Logger logger = LoggerFactory.getLogger(CommandSet.class); + + /** + * Byte array structure for Command set + */ + public CommandSet() { + data[0x01] = (byte) 0x23; + data[0x09] = (byte) 0x02; + // Set up Mode + data[0x0a] = (byte) 0x40; + + byte[] extra = { 0x00, 0x00, 0x00 }; + byte[] newData = new byte[data.length + 3]; + System.arraycopy(data, 0, newData, 0, data.length); + newData[data.length] = extra[0]; + newData[data.length + 1] = extra[1]; + newData[data.length + 2] = extra[2]; + data = newData; + } + + /** + * These provide continuity so a new command on another channel + * doesn't delete the current states of the other channels + * + * @param response response from last poll or set command + * @return commandSet + */ + public static CommandSet fromResponse(Response response) { + CommandSet commandSet = new CommandSet(); + + commandSet.setPowerState(response.getPowerState()); + commandSet.setTargetTemperature(response.getTargetTemperature()); + commandSet.setOperationalMode(response.getOperationalMode()); + commandSet.setFanSpeed(response.getFanSpeed()); + commandSet.setFahrenheit(response.getFahrenheit()); + commandSet.setTurboMode(response.getTurboMode()); + commandSet.setSwingMode(response.getSwingMode()); + commandSet.setEcoMode(response.getEcoMode()); + commandSet.setSleepMode(response.getSleepFunction()); + commandSet.setOnTimer(response.getOnTimerData()); + commandSet.setOffTimer(response.getOffTimerData()); + return commandSet; + } + + /** + * Causes indoor evaporator to beep when Set command received + * + * @param feedbackEnabled will indoor unit beep + */ + public void setPromptTone(boolean feedbackEnabled) { + if (!feedbackEnabled) { + data[0x0b] &= ~(byte) 0x40; // Clear + } else { + data[0x0b] |= (byte) 0x40; // Set + } + } + + /** + * Turns device On or Off + * + * @param state on or off + */ + public void setPowerState(boolean state) { + if (!state) { + data[0x0b] &= ~0x01; + } else { + data[0x0b] |= 0x01; + } + } + + /** + * For Testing assertion get result + * + * @return true or false + */ + public boolean getPowerState() { + return (data[0x0b] & 0x1) > 0; + } + + /** + * Cool, Heat, Fan Only, etc. See Command Base class + * + * @param mode cool, heat, etc. + */ + public void setOperationalMode(OperationalMode mode) { + data[0x0c] &= ~(byte) 0xe0; + data[0x0c] |= ((byte) mode.getId() << 5) & (byte) 0xe0; + } + + /** + * For Testing assertion get result + * + * @return operational mode + */ + public int getOperationalMode() { + return data[0x0c] &= (byte) 0xe0; + } + + /** + * Clear, then set the temperature bits, including the 0.5 bit + * This is all degrees C + * + * @param temperature target temperature + */ + public void setTargetTemperature(float temperature) { + data[0x0c] &= ~0x0f; + data[0x0c] |= (int) (Math.round(temperature * 2) / 2) & 0xf; + setTemperatureDot5((Math.round(temperature * 2)) % 2 != 0); + } + + /** + * For Testing assertion get Setpoint results + * + * @return target temperature as a number + */ + public float getTargetTemperature() { + return (data[0x0c] & 0xf) + 16.0f + (((data[0x0c] & 0x10) > 0) ? 0.5f : 0.0f); + } + + /** + * Low, Medium, High, Auto etc. See Command Base class + * + * @param speed Set fan speed + */ + public void setFanSpeed(FanSpeed speed) { + data[0x0d] = (byte) (speed.getId()); + } + + /** + * For Testing assertion get Fan Speed results + * + * @return fan speed as a number + */ + public int getFanSpeed() { + return data[0x0d]; + } + + /** + * In cool mode sets Fan to Auto and temp to 24 C + * + * @param ecoModeEnabled true or false + */ + public void setEcoMode(boolean ecoModeEnabled) { + if (!ecoModeEnabled) { + data[0x13] &= ~0x80; + } else { + data[0x13] |= 0x80; + } + } + + /** + * If unit supports, set the vertical and/or horzontal louver + * + * @param mode sets swing mode + */ + public void setSwingMode(SwingMode mode) { + data[0x11] &= ~0x3f; // Clear the mode bits + data[0x11] |= mode.getId() & 0x3f; + } + + /** + * For Testing assertion get Swing result + * + * @return swing mode + */ + public int getSwingMode() { + return data[0x11]; + } + + /** + * Activates the sleep function. Setpoint Temp increases in first + * two hours of sleep by 1 degree in Cool mode + * + * @param sleepModeEnabled true or false + */ + public void setSleepMode(boolean sleepModeEnabled) { + if (sleepModeEnabled) { + data[0x14] |= 0x01; + } else { + data[0x14] &= (~0x01); + } + } + + /** + * Sets the Turbo mode for maximum cooling or heat + * + * @param turboModeEnabled true or false + */ + public void setTurboMode(boolean turboModeEnabled) { + if (turboModeEnabled) { + data[0x14] |= 0x02; + } else { + data[0x14] &= (~0x02); + } + } + + /** + * Set the Indoor Unit display to Fahrenheit from Celsius + * + * @param fahrenheitEnabled true or false + */ + public void setFahrenheit(boolean fahrenheitEnabled) { + if (fahrenheitEnabled) { + data[0x14] |= 0x04; + } else { + data[0x14] &= (~0x04); + } + } + + /** + * Toggles the LED display. + * This uses the request format, so needed modification, but need to keep + * current beep and operating state. + * + * @param screenDisplayToggle true (On) or false (off) + */ + public void setScreenDisplay(boolean screenDisplayToggle) { + modifyBytesForDisplayOff(); + removeExtraBytes(); + logger.trace(" Set Bytes before crypt {}", Utils.bytesToHex(data)); + } + + private void modifyBytesForDisplayOff() { + data[0x01] = (byte) 0x20; + data[0x09] = (byte) 0x03; + data[0x0a] = (byte) 0x41; + data[0x0b] |= 0x02; // Set + data[0x0b] &= ~(byte) 0x80; // Clear + data[0x0c] = (byte) 0x00; + data[0x0d] = (byte) 0xff; + data[0x0e] = (byte) 0x02; + data[0x0f] = (byte) 0x00; + data[0x10] = (byte) 0x02; + data[0x11] = (byte) 0x00; + data[0x12] = (byte) 0x00; + data[0x13] = (byte) 0x00; + data[0x14] = (byte) 0x00; + } + + private void removeExtraBytes() { + byte[] newData = new byte[data.length - 3]; + System.arraycopy(data, 0, newData, 0, newData.length); + data = newData; + } + + /** + * Add 0.5C to the temperature value. If needed + * Target_temperature setter calls this method + */ + private void setTemperatureDot5(boolean temperatureDot5Enabled) { + if (temperatureDot5Enabled) { + data[0x0c] |= 0x10; + } else { + data[0x0c] &= (~0x10); + } + } + + /** + * Set the ON timer for AC device start. + * + * @param timerData status (On or Off), hours, minutes + */ + public void setOnTimer(TimerData timerData) { + setOnTimer(timerData.status, timerData.hours, timerData.minutes); + } + + /** + * Calculates remaining time until On + * + * @param on is timer on + * @param hours hours remaining + * @param minutes minutes remaining + */ + public void setOnTimer(boolean on, int hours, int minutes) { + // Process minutes (1 bit = 15 minutes) + int bits = (int) Math.floor(minutes / 15); + int subtract = 0; + if (bits != 0) { + subtract = (15 - (int) (minutes - bits * 15)); + } + if (bits == 0 && minutes != 0) { + subtract = (15 - minutes); + } + data[0x0e] &= ~(byte) 0xff; // Clear + data[0x10] &= ~(byte) 0xf0; + if (on) { + data[0x0e] |= 0x80; + data[0x0e] |= (hours << 2) & 0x7c; + data[0x0e] |= bits & 0x03; + data[0x10] |= (subtract << 4) & 0xf0; + } else { + data[0x0e] = 0x7f; + } + } + + /** + * For Testing assertion get On Timer result + * + * @return timer data base + */ + public int getOnTimer() { + return (data[0x0e] & 0xff); + } + + /** + * For Testing assertion get On Timer result (subtraction amount) + * + * @return timer data subtraction + */ + public int getOnTimer2() { + return ((data[0x10] & (byte) 0xf0) >> 4) & 0x0f; + } + + /** + * Set the timer for AC device stop. + * + * @param timerData status (On or Off), hours, minutes + */ + public void setOffTimer(TimerData timerData) { + setOffTimer(timerData.status, timerData.hours, timerData.minutes); + } + + /** + * Calculates remaining time until Off + * + * @param on is timer on + * @param hours hours remaining + * @param minutes minutes remaining + */ + public void setOffTimer(boolean on, int hours, int minutes) { + int bits = (int) Math.floor(minutes / 15); + int subtract = 0; + if (bits != 0) { + subtract = (15 - (int) (minutes - bits * 15)); + } + if (bits == 0 && minutes != 0) { + subtract = (15 - minutes); + } + data[0x0f] &= ~(byte) 0xff; // Clear + data[0x10] &= ~(byte) 0x0f; + if (on) { + data[0x0f] |= 0x80; + data[0x0f] |= (hours << 2) & 0x7c; + data[0x0f] |= bits & 0x03; + data[0x10] |= subtract & 0x0f; + } else { + data[0x0f] = 0x7f; + } + } + + /** + * For Testing assertion get Off Timer result + * + * @return hours and minutes + */ + public int getOffTimer() { + return (data[0x0f] & 0xff); + } + + /** + * For Testing assertion get Off Timer result (subtraction) + * + * @return minutes to subtract + */ + public int getOffTimer2() { + return ((data[0x10] & (byte) 0x0f)) & 0x0f; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java new file mode 100644 index 0000000000000..444d13ee74bd6 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -0,0 +1,1466 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; + +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; +import javax.measure.quantity.Temperature; +import javax.measure.spi.SystemOfUnits; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.binding.mideaac.internal.MideaACConfiguration; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.discovery.DiscoveryHandler; +import org.openhab.binding.mideaac.internal.discovery.MideaACDiscoveryService; +import org.openhab.binding.mideaac.internal.dto.CloudDTO; +import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.openhab.binding.mideaac.internal.dto.CloudsDTO; +import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; +import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; +import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; +import org.openhab.binding.mideaac.internal.handler.Timer.TimeParser; +import org.openhab.binding.mideaac.internal.security.Decryption8370Result; +import org.openhab.binding.mideaac.internal.security.Security; +import org.openhab.binding.mideaac.internal.security.Security.MsgType; +import org.openhab.binding.mideaac.internal.security.TokenKey; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.i18n.UnitProvider; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.thing.Channel; +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.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link MideaACHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Justan Oldman - Last Response added + * @author Bob Eckhoff - Longer Polls and OH developer guidelines + * + */ +@NonNullByDefault +public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler { + + private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); + + private MideaACConfiguration config = new MideaACConfiguration(); + private Map properties = new HashMap<>(); + + // Initialize variables to allow the @NonNullByDefault check + private String ipAddress = ""; + private String ipPort = ""; + private String deviceId = ""; + private int version = 3; + + /** + * Create new nonnull cloud provider to start + */ + public CloudProviderDTO cloudProvider = new CloudProviderDTO("", "", "", "", "", "", "", ""); + private Security security = new Security(cloudProvider); + + /** + * Gets the users Cloud provider + * + * @return cloud Provider + */ + public CloudProviderDTO getCloudProvider() { + return cloudProvider; + } + + /** + * Gets the Security class + * + * @return security + */ + public Security getSecurity() { + return security; + } + + /** + * Gets the Device Version (2 or 3) + * + * @return version + */ + public int getVersion() { + return version; + } + + /** + * Set the device version + * + * @param version device version + */ + public void setVersion(int version) { + this.version = version; + } + + private static final StringType OPERATIONAL_MODE_OFF = new StringType("OFF"); + private static final StringType OPERATIONAL_MODE_AUTO = new StringType("AUTO"); + private static final StringType OPERATIONAL_MODE_COOL = new StringType("COOL"); + private static final StringType OPERATIONAL_MODE_DRY = new StringType("DRY"); + private static final StringType OPERATIONAL_MODE_HEAT = new StringType("HEAT"); + private static final StringType OPERATIONAL_MODE_FAN_ONLY = new StringType("FAN_ONLY"); + + private static final StringType FAN_SPEED_OFF = new StringType("OFF"); + private static final StringType FAN_SPEED_SILENT = new StringType("SILENT"); + private static final StringType FAN_SPEED_LOW = new StringType("LOW"); + private static final StringType FAN_SPEED_MEDIUM = new StringType("MEDIUM"); + private static final StringType FAN_SPEED_HIGH = new StringType("HIGH"); + private static final StringType FAN_SPEED_FULL = new StringType("FULL"); + private static final StringType FAN_SPEED_AUTO = new StringType("AUTO"); + + private static final StringType SWING_MODE_OFF = new StringType("OFF"); + private static final StringType SWING_MODE_VERTICAL = new StringType("VERTICAL"); + private static final StringType SWING_MODE_HORIZONTAL = new StringType("HORIZONTAL"); + private static final StringType SWING_MODE_BOTH = new StringType("BOTH"); + private CloudsDTO clouds; + + private ConnectionManager connectionManager; + + private final SystemOfUnits systemOfUnits; + + private final HttpClient httpClient; + + /** + * Set to false when Set Command recieved to speed response + */ + public boolean doPoll = true; + + /** + * True allows one short retry after connection problem + */ + public boolean retry = true; + + /** + * Suppresses the connection message if was online before + */ + public boolean connectionMessage = true; + + private ConnectionManager getConnectionManager() { + return connectionManager; + } + + private Response getLastResponse() { + return getConnectionManager().getLastResponse(); + } + + /** + * Initial creation of the Midea AC Handler + * + * @param thing thing name + * @param unitProvider OH core unit provider + * @param httpClient http Client + * @param clouds cloud + */ + public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient, CloudsDTO clouds) { + super(thing); + this.thing = thing; + this.systemOfUnits = unitProvider.getMeasurementSystem(); + this.httpClient = httpClient; + this.clouds = clouds; + connectionManager = new ConnectionManager(this); + } + + /** + * Returns Cloud Provider + * + * @return clouds + */ + public CloudsDTO getClouds() { + return clouds; + } + + protected boolean isImperial() { + return systemOfUnits instanceof ImperialUnits ? true : false; + } + + /** + * This method handles the Channels that can be set (non-read only) + * First the Routine polling is stopped so there is no conflict + * Then connects and authorizes (if necessary) and returns here to + * create the command set which is then sent to the device. + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString()); + connectionManager.disconnect(); + getConnectionManager().cancelConnectionMonitorJob(); + + /** + * Alternate to routine polling; Use rule to refresh at the desired interval + */ + if (command instanceof RefreshType) { + connectionManager.connect(); + return; + } + + /** + * @param doPoll is set to skip poll after authorization and go directly + * to command set execution + */ + doPoll = false; + connectionManager.connect(); + + if (channelUID.getId().equals(CHANNEL_POWER)) { + handlePower(command); + } else if (channelUID.getId().equals(CHANNEL_OPERATIONAL_MODE)) { + handleOperationalMode(command); + } else if (channelUID.getId().equals(CHANNEL_TARGET_TEMPERATURE)) { + handleTargetTemperature(command); + } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) { + handleFanSpeed(command); + } else if (channelUID.getId().equals(CHANNEL_ECO_MODE)) { + handleEcoMode(command); + } else if (channelUID.getId().equals(CHANNEL_TURBO_MODE)) { + handleTurboMode(command); + } else if (channelUID.getId().equals(CHANNEL_SWING_MODE)) { + handleSwingMode(command); + } else if (channelUID.getId().equals(CHANNEL_SCREEN_DISPLAY)) { + handleScreenDisplay(command); + } else if (channelUID.getId().equals(CHANNEL_TEMPERATURE_UNIT)) { + handleTempUnit(command); + } else if (channelUID.getId().equals(CHANNEL_SLEEP_FUNCTION)) { + handleSleepFunction(command); + } else if (channelUID.getId().equals(CHANNEL_ON_TIMER)) { + handleOnTimer(command); + } else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) { + handleOffTimer(command); + } + } + + /** + * Device Power ON OFF + * + * @param command On or Off + */ + public void handlePower(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command.equals(OnOffType.OFF)) { + commandSet.setPowerState(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setPowerState(true); + } else { + logger.debug("Unknown power state command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Supported AC - Heat Pump modes + * + * @param command Operational Mode Cool, Heat, etc. + */ + public void handleOperationalMode(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command instanceof StringType) { + if (command.equals(OPERATIONAL_MODE_OFF)) { + commandSet.setPowerState(false); + return; + } else if (command.equals(OPERATIONAL_MODE_AUTO)) { + commandSet.setOperationalMode(OperationalMode.AUTO); + } else if (command.equals(OPERATIONAL_MODE_COOL)) { + commandSet.setOperationalMode(OperationalMode.COOL); + } else if (command.equals(OPERATIONAL_MODE_DRY)) { + commandSet.setOperationalMode(OperationalMode.DRY); + } else if (command.equals(OPERATIONAL_MODE_HEAT)) { + commandSet.setOperationalMode(OperationalMode.HEAT); + } else if (command.equals(OPERATIONAL_MODE_FAN_ONLY)) { + commandSet.setOperationalMode(OperationalMode.FAN_ONLY); + } else { + logger.debug("Unknown operational mode command: {}", command); + return; + } + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + private static float convertTargetCelsiusTemperatureToInRange(float temperature) { + if (temperature < 17.0f) { + return 17.0f; + } + if (temperature > 30.0f) { + return 30.0f; + } + + return temperature; + } + + /** + * Device only uses Celsius in 0.5 degree increments + * Fahrenheit is rounded to fit (example + * setting to 64 F is 18 C but will result in 64.4 F display in OH) + * The evaporator only displays 2 digits, so will show 64. + * + * @param command Target Temperature + */ + public void handleTargetTemperature(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command instanceof DecimalType) { + logger.debug("Handle Target Temperature as DecimalType in degrees C"); + commandSet.setTargetTemperature( + convertTargetCelsiusTemperatureToInRange(((DecimalType) command).floatValue())); + getConnectionManager().sendCommandAndMonitor(commandSet); + } else if (command instanceof QuantityType) { + QuantityType quantity = (QuantityType) command; + Unit unit = quantity.getUnit(); + + if (unit.equals(ImperialUnits.FAHRENHEIT) || unit.equals(SIUnits.CELSIUS)) { + logger.debug("Handle Target Temperature with unit {} to degrees C", unit); + if (unit.equals(SIUnits.CELSIUS)) { + commandSet.setTargetTemperature(convertTargetCelsiusTemperatureToInRange(quantity.floatValue())); + } else { + QuantityType celsiusQuantity = quantity.toUnit(SIUnits.CELSIUS); + if (celsiusQuantity != null) { + commandSet.setTargetTemperature( + convertTargetCelsiusTemperatureToInRange(celsiusQuantity.floatValue())); + } else { + logger.warn("Failed to convert quantity to Celsius unit."); + } + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + } else { + logger.debug("Handle Target Temperature unsupported commandType:{}", command.getClass().getTypeName()); + } + } + + /** + * Fan Speeds vary by V2 or V3 and device. This command also turns the power ON + * + * @param command Fan Speed Auto, Low, High, etc. + */ + public void handleFanSpeed(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command instanceof StringType) { + commandSet.setPowerState(true); + if (command.equals(FAN_SPEED_OFF)) { + commandSet.setPowerState(false); + } else if (command.equals(FAN_SPEED_SILENT)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.SILENT2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.SILENT3); + } + } else if (command.equals(FAN_SPEED_LOW)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.LOW2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.LOW3); + } + } else if (command.equals(FAN_SPEED_MEDIUM)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.MEDIUM2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.MEDIUM3); + } + } else if (command.equals(FAN_SPEED_HIGH)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.HIGH2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.HIGH3); + } + } else if (command.equals(FAN_SPEED_FULL)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.FULL2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.FULL3); + } + } else if (command.equals(FAN_SPEED_AUTO)) { + if (getVersion() == 2) { + commandSet.setFanSpeed(FanSpeed.AUTO2); + } else if (getVersion() == 3) { + commandSet.setFanSpeed(FanSpeed.AUTO3); + } + } else { + logger.debug("Unknown fan speed command: {}", command); + return; + } + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Must be set in Cool mode. Fan will switch to Auto + * and temp will be 24 C or 75 F on unit (75.2 F in OH) + * + * @param command Eco Mode + */ + public void handleEcoMode(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command.equals(OnOffType.OFF)) { + commandSet.setEcoMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setEcoMode(true); + } else { + logger.debug("Unknown eco mode command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Modes supported depends on the device + * Power is turned on when swing mode is changed + * + * @param command Swing Mode + */ + public void handleSwingMode(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + commandSet.setPowerState(true); + + if (command instanceof StringType) { + if (command.equals(SWING_MODE_OFF)) { + if (getVersion() == 2) { + commandSet.setSwingMode(SwingMode.OFF2); + } else if (getVersion() == 3) { + commandSet.setSwingMode(SwingMode.OFF3); + } + } else if (command.equals(SWING_MODE_VERTICAL)) { + if (getVersion() == 2) { + commandSet.setSwingMode(SwingMode.VERTICAL2); + } else if (getVersion() == 3) { + commandSet.setSwingMode(SwingMode.VERTICAL3); + } + } else if (command.equals(SWING_MODE_HORIZONTAL)) { + if (getVersion() == 2) { + commandSet.setSwingMode(SwingMode.HORIZONTAL2); + } else if (getVersion() == 3) { + commandSet.setSwingMode(SwingMode.HORIZONTAL3); + } + } else if (command.equals(SWING_MODE_BOTH)) { + if (getVersion() == 2) { + commandSet.setSwingMode(SwingMode.BOTH2); + } else if (getVersion() == 3) { + commandSet.setSwingMode(SwingMode.BOTH3); + } + } else { + logger.debug("Unknown swing mode command: {}", command); + return; + } + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Turbo mode is only with Heat or Cool to quickly change + * Room temperature. Power is turned on. + * + * @param command Turbo mode - Fast cooling or Heating + */ + public void handleTurboMode(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + commandSet.setPowerState(true); + + if (command.equals(OnOffType.OFF)) { + commandSet.setTurboMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setTurboMode(true); + } else { + logger.debug("Unknown turbo mode command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * May not be supported via LAN in all models - IR only + * + * @param command Screen Display Toggle to ON or Off - One command + */ + public void handleScreenDisplay(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command.equals(OnOffType.OFF)) { + commandSet.setScreenDisplay(true); + } else if (command.equals(OnOffType.ON)) { + commandSet.setScreenDisplay(true); + } else { + logger.debug("Unknown screen display command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * This is only for the AC LED device display units, calcs always in Celsius + * + * @param command Temp unit on the indoor evaporator + */ + public void handleTempUnit(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + if (command.equals(OnOffType.OFF)) { + commandSet.setFahrenheit(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setFahrenheit(true); + } else { + logger.debug("Unknown temperature unit/farenheit command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Power turned on with Sleep Mode Change + * Sleep mode increases temp slightly in first 2 hours of sleep + * + * @param command Sleep function + */ + public void handleSleepFunction(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + + commandSet.setPowerState(true); + + if (command.equals(OnOffType.OFF)) { + commandSet.setSleepMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setSleepMode(true); + } else { + logger.debug("Unknown sleep Mode command: {}", command); + return; + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Sets the time (from now) that the device will turn on at it's current settings + * + * @param command Sets On Timer + */ + public void handleOnTimer(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + TimeParser timeParser = timer.new TimeParser(); + if (command instanceof StringType) { + String timeString = ((StringType) command).toString(); + if (!timeString.matches("\\d{2}:\\d{2}")) { + logger.debug("Invalid time format. Expected HH:MM."); + commandSet.setOnTimer(false, hours, minutes); + } else { + int[] timeParts = timeParser.parseTime(timeString); + boolean on = true; + hours = timeParts[0]; + minutes = timeParts[1]; + // Validate minutes and hours + if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { + logger.debug("Invalid hours (24 max) and or minutes (59 max)"); + hours = 0; + minutes = 0; + } + if (hours == 0 && minutes == 0) { + commandSet.setOnTimer(false, hours, minutes); + } else { + commandSet.setOnTimer(on, hours, minutes); + } + } + } else { + logger.debug("Command must be of type StringType: {}", command); + commandSet.setOnTimer(false, hours, minutes); + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Sets the time (from now) that the device will turn off + * + * @param command Sets Off Timer + */ + public void handleOffTimer(Command command) { + CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + TimeParser timeParser = timer.new TimeParser(); + if (command instanceof StringType) { + String timeString = ((StringType) command).toString(); + if (!timeString.matches("\\d{2}:\\d{2}")) { + logger.debug("Invalid time format. Expected HH:MM."); + commandSet.setOffTimer(false, hours, minutes); + } else { + int[] timeParts = timeParser.parseTime(timeString); + boolean on = true; + hours = timeParts[0]; + minutes = timeParts[1]; + // Validate minutes and hours + if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { + logger.debug("Invalid hours (24 max) and or minutes (59 max)"); + hours = 0; + minutes = 0; + } + if (hours == 0 && minutes == 0) { + commandSet.setOffTimer(false, hours, minutes); + } else { + commandSet.setOffTimer(on, hours, minutes); + } + } + } else { + logger.debug("Command must be of type StringType: {}", command); + commandSet.setOffTimer(false, hours, minutes); + } + + getConnectionManager().sendCommandAndMonitor(commandSet); + } + + /** + * Initialize is called on first pass or when a device parameter is changed + * The basic check is if the information from Discovery (or the user update) + * is valid. Because V2 devices do not require a cloud provider (or token/key) + * The check is for the IP, port and deviceID. This method also resets the dropped + * commands, disconnects the socket and stops the connection monitor (if these were + * running) + */ + @Override + public void initialize() { + connectionManager.disconnect(); + getConnectionManager().cancelConnectionMonitorJob(); + connectionManager.resetDroppedCommands(); + connectionManager.updateChannel(DROPPED_COMMANDS, new DecimalType(connectionManager.getDroppedCommands())); + + config = getConfigAs(MideaACConfiguration.class); + + setCloudProvider(CloudProviderDTO.getCloudProvider(config.cloud)); + setSecurity(new Security(cloudProvider)); + + logger.debug("MideaACHandler config for {} is {}", thing.getUID(), config); + + if (!config.isValid()) { + logger.warn("Configuration invalid for {}", thing.getUID()); + if (config.isDiscoveryNeeded()) { + logger.warn("Discovery needed, discovering....{}", thing.getUID()); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, + "Configuration missing, discovery needed. Discovering..."); + MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); + + try { + discoveryService.discoverThing(config.ipAddress, this); + } catch (Exception e) { + logger.error("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); + } + return; + } else { + logger.debug("MideaACHandler config of {} is invalid. Check configuration", thing.getUID()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid MideaAC config. Check configuration."); + return; + } + } else { + logger.debug("Configuration valid for {}", thing.getUID()); + } + + ipAddress = config.ipAddress; + ipPort = config.ipPort; + deviceId = config.deviceId; + version = Integer.parseInt(config.version); + + logger.debug("IPAddress: {}", ipAddress); + logger.debug("IPPort: {}", ipPort); + logger.debug("ID: {}", deviceId); + logger.debug("Version: {}", version); + + updateStatus(ThingStatus.UNKNOWN); + + connectionManager.connect(); + } + + @Override + public void discovered(DiscoveryResult discoveryResult) { + logger.debug("Discovered {}", thing.getUID()); + Map discoveryProps = discoveryResult.getProperties(); + Configuration configuration = editConfiguration(); + + Object propertyDeviceId = Objects.requireNonNull(discoveryProps.get(CONFIG_DEVICEID)); + configuration.put(CONFIG_DEVICEID, propertyDeviceId.toString()); + + Object propertyIpPort = Objects.requireNonNull(discoveryProps.get(CONFIG_IP_PORT)); + configuration.put(CONFIG_IP_PORT, propertyIpPort.toString()); + + updateConfiguration(configuration); + + properties = editProperties(); + + Object propertyVersion = Objects.requireNonNull(discoveryProps.get(PROPERTY_VERSION)); + properties.put(PROPERTY_VERSION, propertyVersion.toString()); + + Object propertySN = Objects.requireNonNull(discoveryProps.get(PROPERTY_SN)); + properties.put(PROPERTY_SN, propertySN.toString()); + + Object propertySSID = Objects.requireNonNull(discoveryProps.get(PROPERTY_SSID)); + properties.put(PROPERTY_SSID, propertySSID.toString()); + + Object propertyType = Objects.requireNonNull(discoveryProps.get(PROPERTY_TYPE)); + properties.put(PROPERTY_TYPE, propertyType.toString()); + + updateProperties(properties); + + initialize(); + } + + /** + * Manage the ONLINE/OFFLINE statuses of the thing with problems (or lack thereof) + */ + private void markOnline() { + if (!isOnline()) { + updateStatus(ThingStatus.ONLINE); + } + } + + private void markOffline() { + if (isOnline()) { + updateStatus(ThingStatus.OFFLINE); + } + } + + private void markOfflineWithMessage(ThingStatusDetail statusDetail, String statusMessage) { + if (!isOffline()) { + updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage); + } + + /** + * This is to space out the looping with a short (5 second) then long (30 second) pause(s). + * Generally a WiFi issue triggers the offline. Could be a blip or something longer term + * Only info log (Connection issue ..) prior to first long pause. + */ + if (retry) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + logger.debug("An interupted error (pause) has occured {}", e.getMessage()); + } + getConnectionManager().cancelConnectionMonitorJob(); + getConnectionManager().disconnect(); + retry = false; + getConnectionManager().connect(); + } else { + if (connectionMessage) { + logger.info("Connection issue, resetting, please wait ..."); + } + connectionMessage = false; + getConnectionManager().cancelConnectionMonitorJob(); + getConnectionManager().disconnect(); + getConnectionManager().scheduleConnectionMonitorJob(); + } + } + + private boolean isOnline() { + return thing.getStatus().equals(ThingStatus.ONLINE); + } + + private boolean isOffline() { + return thing.getStatus().equals(ThingStatus.OFFLINE); + } + + /** + * Cancel the connection manager job which will keep going + * even with the binding removed and cause warnings about + * trying to update Thing Channels with the Handler disposed + */ + @Override + public void dispose() { + connectionManager.cancelConnectionMonitorJob(); + markOffline(); + } + + /** + * DoPoll is set to false in the MideaAC Handler + * if a Command is being sent and picked up by + * the Connection Manager. Then is reset to true + * after the Set command is complete + * + * @return doPoll Sets if the binding will poll after authorization + */ + public boolean getDoPoll() { + return doPoll; + } + + /** + * Resets the doPoll switch + */ + public void resetDoPoll() { + doPoll = true; + } + + /** + * Reset Retry controls the short 5 second delay + * Before starting 30 second delays. (More severe Wifi issue) + * It is reset after a successful connection + */ + public void resetRetry() { + retry = true; + } + + /** + * Limit logging of INFO connection messages to + * only when the device was Offline in its prior + * state + */ + public void resetConnectionMessage() { + connectionMessage = true; + } + + private ThingStatusDetail getDetail() { + return thing.getStatusInfo().getStatusDetail(); + } + + /** + * Sets Cloud Provider + * + * @param cloudProvider Cloud Provider + */ + public void setCloudProvider(CloudProviderDTO cloudProvider) { + this.cloudProvider = cloudProvider; + } + + /** + * Security methods + * + * @param security security class + */ + public void setSecurity(Security security) { + this.security = security; + } + + /** + * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the + * indoor AC unit evaporator. + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command + * + * This gets around the issue that any command needs to be within 30 seconds of the authorization + * in testing this only adds 50 ms, but allows polls at longer intervals + */ + private class ConnectionManager { + private Logger logger = LoggerFactory.getLogger(ConnectionManager.class); + + private boolean deviceIsConnected; + private int droppedCommands = 0; + + private Socket socket = new Socket(); + private InputStream inputStream = new ByteArrayInputStream(new byte[0]); + private DataOutputStream writer = new DataOutputStream(System.out); + + private @Nullable ScheduledFuture connectionMonitorJob = null; + + private byte[] data = HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"); + + private String responseType = "query"; + + private byte bodyType = (byte) 0xc0; + + private Response lastResponse = new Response(data, getVersion(), responseType, bodyType); + private MideaACHandler mideaACHandler; + + /** + * Gets last response + * + * @return byte array of last response + */ + public Response getLastResponse() { + return this.lastResponse; + } + + Runnable connectionMonitorRunnable = () -> { + logger.debug("Connecting to {} at IP {} for Poll", thing.getUID(), ipAddress); + disconnect(); + connect(); + }; + + /** + * Set the parameters for the connection manager + * + * @param mideaACHandler mideaACHandler class + */ + public ConnectionManager(MideaACHandler mideaACHandler) { + deviceIsConnected = false; + this.mideaACHandler = mideaACHandler; + } + + /** + * Validate if String is blank + * + * @param str string to be evaluated + * @return boolean true or false + */ + public static boolean isBlank(String str) { + return str.trim().isEmpty(); + } + + /** + * Reset dropped commands from initialization in MideaACHandler + * Channel created for easy observation + * Dropped commands when no bytes to read after two tries or other + * byte reading problem. Device not responding. + */ + public void resetDroppedCommands() { + droppedCommands = 0; + } + + /** + * Resets Dropped command + * + * @return dropped commands + */ + public int getDroppedCommands() { + return droppedCommands = 0; + } + + /** + * After checking if the key and token need to be updated (Default = 0 Never) + * The socket is established with the writer and inputStream (for reading responses) + * The device is considered connected. V2 devices will proceed to send the poll or the + * set command. V3 devices will proceed to authenticate + */ + protected synchronized void connect() { + logger.trace("Connecting to {} at {}:{}", thing.getUID(), ipAddress, ipPort); + + // Open socket + try { + socket = new Socket(); + socket.setSoTimeout(config.timeout * 1000); + int port = Integer.parseInt(ipPort); + socket.connect(new InetSocketAddress(ipAddress, port), config.timeout * 1000); + } catch (IOException e) { + logger.debug("IOException connecting to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); + String message = e.getMessage(); + if (message != null) { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + } else { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); + } + } + + // Create streams + try { + writer = new DataOutputStream(socket.getOutputStream()); + inputStream = socket.getInputStream(); + } catch (IOException e) { + logger.debug("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(), + e); + String message = e.getMessage(); + if (message != null) { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + } else { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); + } + } + if (!deviceIsConnected || !connectionMessage) { + logger.info("Connected to {} at {}", thing.getUID(), ipAddress); + mideaACHandler.resetRetry(); + mideaACHandler.resetConnectionMessage(); + } + logger.debug("Connected to {} at {}", thing.getUID(), ipAddress); + deviceIsConnected = true; + markOnline(); + if (getVersion() != 3) { + logger.debug("Device {}@{} does not require authentication, updating status", thing.getUID(), + ipAddress); + requestStatus(mideaACHandler.getDoPoll()); + } else { + logger.debug("Device {}@{} require authentication, going to authenticate", thing.getUID(), ipAddress); + authenticate(); + } + } + + /** + * For V3 devices only. This method checks for the Cloud Provider + * key and token (and goes offline if any are missing). It will retrieve the + * missing key and/or token if the account email and password are provided. + */ + public void authenticate() { + logger.trace("Version: {}", getVersion()); + logger.trace("Key: {}", config.key); + logger.trace("Token: {}", config.token); + + if (!isBlank(config.token) && !isBlank(config.key) && !config.cloud.equals("")) { + logger.debug("Device {}@{} authenticating", thing.getUID(), ipAddress); + doAuthentication(); + } else { + if (!isBlank(config.email) && !isBlank(config.password) && !config.cloud.equals("")) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, + "Retrieving Token and/or Key from cloud."); + logger.info("Retrieving Token and/or Key from cloud"); + CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); + getTokenKeyCloud(cloudProvider); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Token and/or Key missing, missing cloud provider information to fetch it"); + logger.warn("Token, Key and or Cloud provider data missing, V3 device {}@{} cannot authenticate", + thing.getUID(), ipAddress); + } + } + } + + private void getTokenKeyCloud(CloudProviderDTO cloudProvider) { + CloudDTO cloud = mideaACHandler.getClouds().get(config.email, config.password, cloudProvider); + if (cloud != null) { + cloud.setHttpClient(httpClient); + if (cloud.login()) { + TokenKey tk = cloud.getToken(config.deviceId); + Configuration configuration = editConfiguration(); + + configuration.put(CONFIG_TOKEN, tk.token()); + configuration.put(CONFIG_KEY, tk.key()); + updateConfiguration(configuration); + + logger.trace("Token: {}", tk.token()); + logger.trace("Key: {}", tk.key()); + logger.info("Token and Key obtained from cloud, saving, initializing"); + initialize(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( + "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); + logger.warn( + "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"); + } + } + } + + /** + * Sends the Handshake Request to the V3 device. Generally quick response + * Without the 1000 ms sleep delay there are problems in sending the Poll/Command + * Suspect that the socket write and read streams need a moment to clear + * as they will be reused in the SendCommand method + */ + private void doAuthentication() { + byte[] request = mideaACHandler.getSecurity().encode8370(Utils.hexStringToByteArray(config.token), + MsgType.MSGTYPE_HANDSHAKE_REQUEST); + try { + logger.trace("Device {}@{} writing handshake_request: {}", thing.getUID(), ipAddress, + Utils.bytesToHex(request)); + + write(request); + byte[] response = read(); + + if (response != null && response.length > 0) { + logger.trace("Device {}@{} response for handshake_request length: {}", thing.getUID(), ipAddress, + response.length); + if (response.length == 72) { + boolean success = mideaACHandler.getSecurity().tcpKey(Arrays.copyOfRange(response, 8, 72), + Utils.hexStringToByteArray(config.key)); + if (success) { + logger.debug("Authentication successful"); + // Altering the sleep caused or can cause write errors problems. Use caution. + // At 500 ms the first write usually fails. Works, but no backup + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.debug("An interupted error (success) has occured {}", e.getMessage()); + } + requestStatus(mideaACHandler.getDoPoll()); + } else { + logger.debug("Invalid Key. Correct Key in configuration"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid Key. Correct Key in configuration."); + } + } else if (Arrays.equals(new String("ERROR").getBytes(), response)) { + logger.warn("Authentication failed!"); + } else { + logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", + response.length); + logger.debug("Invalid Token. Correct Token in configuration"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid Token. Correct Token in configuration."); + } + } + } catch (IOException e) { + logger.warn("An IO error in doAuthentication has occured {}", e.getMessage()); + String message = e.getMessage(); + if (message != null) { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + } else { + markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); + } + } + } + + /** + * After authentication, this switch to either send a + * Poll or the Set command + * + * @param polling polling true or false + */ + public void requestStatus(boolean polling) { + if (polling) { + CommandBase requestStatusCommand = new CommandBase(); + sendCommandAndMonitor(requestStatusCommand); + } + } + + /** + * Calls the sendCommand method, resets the doPoll to true + * Disconnects the socket and schedules the connection manager + * job, if was stopped (to avoid collision) due to a Set command + * + * @param command either the set or polling command + */ + public void sendCommandAndMonitor(CommandBase command) { + sendCommand(command); + mideaACHandler.resetDoPoll(); + if (connectionMonitorJob == null) { + scheduleConnectionMonitorJob(); + } + } + + /** + * Pulls the packet byte array together. There is a check to + * make sure to make sure the input stream is empty before sending + * the new command and another check if input stream is empty after 1.5 seconds. + * Normal device response in 0.75 - 1 second range + * If still empty, send the bytes again. If there are bytes, the read method is called. + * If the socket times out with no response the command is dropped. There will be another poll + * in the time set by the user (30 seconds min) or the set command can be retried + * + * @param command either the set or polling command + */ + public void sendCommand(CommandBase command) { + if (command instanceof CommandSet) { + ((CommandSet) command).setPromptTone(config.promptTone); + } + Packet packet = new Packet(command, deviceId, mideaACHandler); + packet.compose(); + + try { + byte[] bytes = packet.getBytes(); + logger.debug("Writing to {} at {} bytes.length: {}", thing.getUID(), ipAddress, bytes.length); + + if (getVersion() == 3) { + bytes = mideaACHandler.getSecurity().encode8370(bytes, MsgType.MSGTYPE_ENCRYPTED_REQUEST); + } + + // Ensure input stream is empty before writing packet + if (inputStream.available() == 0) { + logger.debug("Input stream empty sending write {}", command); + write(bytes); + } + + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); + } + + if (inputStream.available() == 0) { + logger.debug("Input stream empty sending second write {}", command); + write(bytes); + } + + // Socket timeout (UI parameter) 2 seconds minimum up to 10 seconds. + byte[] responseBytes = read(); + + if (responseBytes != null) { + if (getVersion() == 3) { + Decryption8370Result result = mideaACHandler.getSecurity().decode8370(responseBytes); + for (byte[] response : result.getResponses()) { + logger.debug("Response length:{} thing:{} ", response.length, thing.getUID()); + if (response.length > 40 + 16) { + byte[] data = mideaACHandler.getSecurity() + .aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); + + logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + byte bodyType2 = data[0xa]; + + // data[3]: Device Type - 0xAC = AC + // https://github.com/georgezhao2010/midea_ac_lan/blob/06fc4b582a012bbbfd6bd5942c92034270eca0eb/custom_components/midea_ac_lan/midea_devices.py#L96 + + // data[9]: MessageType - set, query, notify1, notify2, exception, querySN, exception2, + // querySubtype + // https://github.com/georgezhao2010/midea_ac_lan/blob/30d0ff5ff14f150da10b883e97b2f280767aa89a/custom_components/midea_ac_lan/midea/core/message.py#L22-L29 + String responseType = ""; + switch (data[0x9]) { + case 0x02: + responseType = "set"; + break; + case 0x03: + responseType = "query"; + break; + case 0x04: + responseType = "notify1"; + break; + case 0x05: + responseType = "notify2"; + break; + case 0x06: + responseType = "exception"; + break; + case 0x07: + responseType = "querySN"; + break; + case 0x0A: + responseType = "exception2"; + break; + case 0x09: // Helyesen: 0xA0 + responseType = "querySubtype"; + break; + default: + logger.debug("Invalid response type: {}", data[0x9]); + } + logger.trace("Response Type: {} and bodyType:{}", responseType, bodyType2); + + // The response data from the appliance includes a packet header which we don't want + data = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = data[0x0]; + logger.trace("Response Type expected: {} and bodyType:{}", responseType, bodyType); + logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToHex(data)); + logger.debug( + "Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToBinary(data)); + + if (data.length > 0) { + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + if (bodyType != -64) { + if (bodyType == 30) { + logger.warn("Error response 0x1E received {} from:{}", bodyType, + thing.getUID()); + return; + } + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, getVersion(), responseType, bodyType); + try { + processMessage(lastResponse); + logger.trace("data length is {} version is {} thing is {}", data.length, + version, thing.getUID()); + } catch (Exception ex) { + logger.warn("Processing response exception: {}", ex.getMessage()); + } + } + } + } + } else { + byte[] data = mideaACHandler.getSecurity() + .aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); + // The response data from the appliance includes a packet header which we don't want + logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + if (data.length > 0) { + data = Arrays.copyOfRange(data, 10, data.length); + logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToHex(data)); + + lastResponse = new Response(data, getVersion(), "", (byte) 0x00); + processMessage(lastResponse); + logger.debug("V2 data length is {} version is {} thing is {}", data.length, version, + thing.getUID()); + } else { + logger.debug("Problem with reading V2 response, skipping command {}", command); + droppedCommands = droppedCommands + 1; + updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); + } + } + return; + } else { + logger.debug("Problem with reading response, skipping command {}", command); + droppedCommands = droppedCommands + 1; + updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); + return; + } + } catch (SocketException e) { + logger.debug("SocketException writing to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); + String message = e.getMessage(); + droppedCommands = droppedCommands + 1; + updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); + updateStatus(ThingStatus.OFFLINE, getDetail(), message); + return; + } catch (IOException e) { + logger.debug(" Send IOException writing to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); + String message = e.getMessage(); + droppedCommands = droppedCommands + 1; + updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); + updateStatus(ThingStatus.OFFLINE, getDetail(), message); + return; + } + } + + /** + * Closes all elements of the connection before starting a new one + */ + protected synchronized void disconnect() { + // Make sure writer, inputStream and socket are closed before each command is started + logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress); + + InputStream inputStream = this.inputStream; + DataOutputStream writer = this.writer; + Socket socket = this.socket; + try { + writer.close(); + inputStream.close(); + socket.close(); + + } catch (IOException e) { + logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(), + e); + } + socket = null; + inputStream = null; + writer = null; + } + + private void updateChannel(String channelName, State state) { + if (isOffline()) { + return; + } + Channel channel = thing.getChannel(channelName); + if (channel != null) { + updateState(channel.getUID(), state); + } + } + + private void processMessage(Response response) { + updateChannel(CHANNEL_POWER, response.getPowerState() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_APPLIANCE_ERROR, response.getApplianceError() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_TARGET_TEMPERATURE, + new QuantityType(response.getTargetTemperature(), SIUnits.CELSIUS)); + updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString())); + updateChannel(CHANNEL_FAN_SPEED, new StringType(response.getFanSpeed().toString())); + updateChannel(CHANNEL_ON_TIMER, new StringType(response.getOnTimer().toChannel())); + updateChannel(CHANNEL_OFF_TIMER, new StringType(response.getOffTimer().toChannel())); + updateChannel(CHANNEL_SWING_MODE, new StringType(response.getSwingMode().toString())); + updateChannel(CHANNEL_AUXILIARY_HEAT, response.getAuxHeat() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_ECO_MODE, response.getEcoMode() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_TEMPERATURE_UNIT, response.getFahrenheit() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_SLEEP_FUNCTION, response.getSleepFunction() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_TURBO_MODE, response.getTurboMode() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_SCREEN_DISPLAY, response.getDisplayOn() ? OnOffType.ON : OnOffType.OFF); + updateChannel(CHANNEL_ALTERNATE_TARGET_TEMPERATURE, + new QuantityType(response.getAlternateTargetTemperature(), SIUnits.CELSIUS)); + updateChannel(CHANNEL_INDOOR_TEMPERATURE, + new QuantityType(response.getIndoorTemperature(), SIUnits.CELSIUS)); + updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, + new QuantityType(response.getOutdoorTemperature(), SIUnits.CELSIUS)); + updateChannel(CHANNEL_HUMIDITY, new DecimalType(response.getHumidity())); + } + + /** + * Reads the inputStream byte array + * + * @return byte array + */ + public synchronized byte @Nullable [] read() { + byte[] bytes = new byte[512]; + InputStream inputStream = this.inputStream; + + try { + int len = inputStream.read(bytes); + if (len > 0) { + logger.debug("Response received length: {} Thing:{}", len, thing.getUID()); + bytes = Arrays.copyOfRange(bytes, 0, len); + return bytes; + } + } catch (IOException e) { + String message = e.getMessage(); + logger.debug(" Byte read exception {}", message); + } + return null; + } + + /** + * Writes the packet that will be sent to the device + * + * @param buffer socket writer + * @throws IOException writer could be null + */ + public synchronized void write(byte[] buffer) throws IOException { + DataOutputStream writer = this.writer; + + try { + writer.write(buffer, 0, buffer.length); + } catch (IOException e) { + String message = e.getMessage(); + logger.debug("Write error {}", message); + } + } + + /** + * Periodical polling. Thirty seconds minimum + */ + private void scheduleConnectionMonitorJob() { + if (connectionMonitorJob == null) { + logger.debug("Starting connection monitor job in {} seconds for {} at {} after 30 second delay", + config.pollingTime, thing.getUID(), ipAddress); + long frequency = config.pollingTime; + long delay = 30L; + connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable, delay, frequency, + TimeUnit.SECONDS); + } + } + + private void cancelConnectionMonitorJob() { + ScheduledFuture connectionMonitorJob = this.connectionMonitorJob; + if (connectionMonitorJob != null) { + connectionMonitorJob.cancel(true); + logger.debug("Cancelling connection monitor job for {} at {}", thing.getUID(), ipAddress); + this.connectionMonitorJob = null; + } + } + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java new file mode 100644 index 0000000000000..022ae685f285e --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import java.math.BigInteger; +import java.time.LocalDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.Utils; + +/** + * The {@link Packet} class for Midea AC creates the + * byte array that is sent to the device + * + * @author Jacek Dobrowolski - Initial contribution + */ +@NonNullByDefault +public class Packet { + private CommandBase command; + private byte[] packet; + private MideaACHandler mideaACHandler; + + /** + * The Packet class parameters + * + * @param command command from Command Base + * @param deviceId the device ID + * @param mideaACHandler the MideaACHandler class + */ + public Packet(CommandBase command, String deviceId, MideaACHandler mideaACHandler) { + this.command = command; + this.mideaACHandler = mideaACHandler; + + packet = new byte[] { + // 2 bytes - StaticHeader + (byte) 0x5a, (byte) 0x5a, + // 2 bytes - mMessageType + (byte) 0x01, (byte) 0x11, + // 2 bytes - PacketLength + (byte) 0x00, (byte) 0x00, + // 2 bytes + (byte) 0x20, (byte) 0x00, + // 4 bytes - MessageId + 0x00, 0x00, 0x00, 0x00, + // 8 bytes - Date&Time + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 6 bytes - mDeviceID + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // 14 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + LocalDateTime now = LocalDateTime.now(); + byte[] datetimeBytes = { (byte) (now.getYear() / 100), (byte) (now.getYear() % 100), (byte) now.getMonthValue(), + (byte) now.getDayOfMonth(), (byte) now.getHour(), (byte) now.getMinute(), (byte) now.getSecond(), + (byte) System.currentTimeMillis() }; + + System.arraycopy(datetimeBytes, 0, packet, 12, 8); + + byte[] idBytes = new BigInteger(deviceId).toByteArray(); + byte[] idBytesRev = Utils.reverse(idBytes); + System.arraycopy(idBytesRev, 0, packet, 20, 6); + } + + /** + * Final composure of the byte array with the encrypted command + */ + public void compose() { + command.compose(); + + // Append the command data(48 bytes) to the packet + byte[] cmdEncrypted = mideaACHandler.getSecurity().aesEncrypt(command.getBytes()); + + // Ensure 48 bytes + if (cmdEncrypted.length < 48) { + byte[] paddedCmdEncrypted = new byte[48]; + System.arraycopy(cmdEncrypted, 0, paddedCmdEncrypted, 0, cmdEncrypted.length); + cmdEncrypted = paddedCmdEncrypted; + } + + byte[] newPacket = new byte[packet.length + cmdEncrypted.length]; + System.arraycopy(packet, 0, newPacket, 0, packet.length); + System.arraycopy(cmdEncrypted, 0, newPacket, packet.length, cmdEncrypted.length); + packet = newPacket; + + // Override packet length bytes with actual values + byte[] lenBytes = { (byte) (packet.length + 16), 0 }; + System.arraycopy(lenBytes, 0, packet, 4, 2); + + // calculate checksum data + byte[] checksumData = mideaACHandler.getSecurity().encode32Data(packet); + + // Append a basic checksum data(16 bytes) to the packet + byte[] newPacketTwo = new byte[packet.length + checksumData.length]; + System.arraycopy(packet, 0, newPacketTwo, 0, packet.length); + System.arraycopy(checksumData, 0, newPacketTwo, packet.length, checksumData.length); + packet = newPacketTwo; + } + + /** + * Returns the packet for sending + * + * @return packet for socket writer + */ + public byte[] getBytes() { + return packet; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java new file mode 100644 index 0000000000000..b521f5aa9c0a1 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -0,0 +1,389 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; +import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; +import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; +import org.openhab.binding.mideaac.internal.handler.Timer.TimerData; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link Response} performs the byte data stream decoding + * The original reference is + * https://github.com/georgezhao2010/midea_ac_lan/blob/06fc4b582a012bbbfd6bd5942c92034270eca0eb/custom_components/midea_ac_lan/midea/devices/ac/message.py#L418 + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - Add Java Docs, minor fixes + */ +@NonNullByDefault +public class Response { + byte[] data; + + // set empty to match the return from an empty byte avoid null + float empty = (float) -19.0; + private Logger logger = LoggerFactory.getLogger(Response.class); + + private final int version; + String responseType; + byte bodyType; + + private int getVersion() { + return version; + } + + /** + * Response class Parameters + * + * @param data byte array from device + * @param version version of the device + * @param responseType response type + * @param bodyType Body type + */ + public Response(byte[] data, int version, String responseType, byte bodyType) { + this.data = data; + this.version = version; + this.bodyType = bodyType; + this.responseType = responseType; + + if (logger.isDebugEnabled()) { + logger.debug("Power State: {}", getPowerState()); + logger.debug("Target Temperature: {}", getTargetTemperature()); + logger.debug("Operational Mode: {}", getOperationalMode()); + logger.debug("Fan Speed: {}", getFanSpeed()); + logger.debug("On Timer: {}", getOnTimer()); + logger.debug("Off Timer: {}", getOffTimer()); + logger.debug("Swing Mode: {}", getSwingMode()); + logger.debug("Sleep Function: {}", getSleepFunction()); + logger.debug("Turbo Mode: {}", getTurboMode()); + logger.debug("Indoor Temperature: {}", getIndoorTemperature()); + logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); + logger.debug("LED Display: {}", getDisplayOn()); + } + + if (logger.isTraceEnabled()) { + logger.trace("Prompt Tone: {}", getPromptTone()); + logger.trace("Appliance Error: {}", getApplianceError()); + logger.trace("Auxiliary Heat: {}", getAuxHeat()); + logger.trace("Eco Mode: {}", getEcoMode()); + logger.trace("Fahrenheit: {}", getFahrenheit()); + logger.trace("Humidity: {}", getHumidity()); + logger.trace("Alternate Target Temperature {}", getAlternateTargetTemperature()); + } + + /** + * Trace Log Response and Body Type for V3. V2 set at "" and 0x00 + * This was for future development since only 0xC0 is currently used + */ + if (version == 3) { + logger.trace("Response and Body Type: {}, {}", responseType, bodyType); + if ("notify2".equals(responseType) && bodyType == -95) { // 0xA0 = -95 + logger.trace("Response Handler: XA0Message"); + } else if ("notify1".equals(responseType) && bodyType == -91) { // 0xA1 = -91 + logger.trace("Response Handler: XA1Message"); + } else if (("notify2".equals(responseType) || "set".equals(responseType) || "query".equals(responseType)) + && (bodyType == 0xB0 || bodyType == 0xB1 || bodyType == 0xB5)) { + logger.trace("Response Handler: XBXMessage"); + } else if (("set".equals(responseType) || "query".equals(responseType)) && bodyType == -64) { // 0xC0 = -64 + logger.trace("Response Handler: XCOMessage"); + } else if ("query".equals(responseType) && bodyType == 0xC1) { + logger.trace("Response Handler: XC1Message"); + } else { + logger.trace("Response Handler: _general_"); + } + } + } + + /** + * Device On or Off + * + * @return power state true or false + */ + public boolean getPowerState() { + return (data[0x01] & 0x1) > 0; + } + + /** + * Read only + * + * @return prompt tone true or false + */ + public boolean getPromptTone() { + return (data[0x01] & 0x40) > 0; + } + + /** + * Read only + * + * @return appliance error true or false + */ + public boolean getApplianceError() { + return (data[0x01] & 0x80) > 0; + } + + /** + * Setpoint for Heat Pump + * + * @return current setpoint in degrees C + */ + public float getTargetTemperature() { + return (data[0x02] & 0xf) + 16.0f + (((data[0x02] & 0x10) > 0) ? 0.5f : 0.0f); + } + + /** + * Cool, Heat, Fan Only, etc. See Command Base class + * + * @return Cool, Heat, Fan Only, etc. + */ + public OperationalMode getOperationalMode() { + return OperationalMode.fromId((data[0x02] & 0xe0) >> 5); + } + + /** + * Low, Medium, High, Auto etc. See Command Base class + * + * @return Low, Medium, High, Auto etc. + */ + public FanSpeed getFanSpeed() { + return FanSpeed.fromId(data[0x03] & 0x7f, getVersion()); + } + + /** + * Creates String representation of the On timer to the channel + * + * @return String of HH:MM + */ + public Timer getOnTimer() { + return new Timer((data[0x04] & 0x80) > 0, ((data[0x04] & (byte) 0x7c) >> 2), + ((data[0x04] & 0x3) * 15 + 15 - (((data[0x06] & (byte) 0xf0) >> 4) & 0x0f))); + } + + /** + * This is used to carry the current On Timer (last response) through + * subsequent Set commands, so it is not overwritten. + * + * @return status plus String of HH:MM + */ + public TimerData getOnTimerData() { + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + boolean status = (data[0x04] & 0x80) > 0; + hours = ((data[0x04] & (byte) 0x7c) >> 2); + minutes = ((data[0x04] & 0x3) * 15 + 15 - (((data[0x06] & (byte) 0xf0) >> 4) & 0x0f)); + return timer.new TimerData(status, hours, minutes); + } + + /** + * Creates String representation of the Off timer to the channel + * + * @return String of HH:MM + */ + public Timer getOffTimer() { + return new Timer((data[0x05] & 0x80) > 0, ((data[0x05] & (byte) 0x7c) >> 2), + ((data[0x05] & 0x3) * 15 + 15 - (data[0x06] & (byte) 0xf))); + } + + /** + * This is used to carry the Off timer (last response) through + * subsequent Set commands, so it is not overwritten. + * + * @return status plus String of HH:MM + */ + public TimerData getOffTimerData() { + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + boolean status = (data[0x05] & 0x80) > 0; + hours = ((data[0x05] & (byte) 0x7c) >> 2); + minutes = (data[0x05] & 0x3) * 15 + 15 - (((data[0x06] & (byte) 0xf) & 0x0f)); + return timer.new TimerData(status, hours, minutes); + } + + /** + * Status of the vertical and/or horzontal louver + * + * @return Vertical, Horizontal, Off, Both + */ + public SwingMode getSwingMode() { + return SwingMode.fromId(data[0x07] & 0x3f, getVersion()); + } + + /** + * Read only - heat mode only + * + * @return auxiliary heat active + */ + public boolean getAuxHeat() { + return (data[0x09] & (byte) 0x08) != 0; + } + + /** + * Ecomode status - Fan to Auto and temp to 24 C + * + * @return Eco mode on (true) or (false) + */ + public boolean getEcoMode() { + return (data[0x09] & (byte) 0x10) != 0; + } + + /** + * Sleep function status. Setpoint Temp increases in first + * two hours of sleep by 1 degree in Cool mode + * + * @return Sleep mode on (true) or (false) + */ + public boolean getSleepFunction() { + return (data[0x0a] & (byte) 0x01) != 0; + } + + /** + * Turbo mode status for maximum cooling or heat + * + * @return Turbo mode on (true) or (false) + */ + public boolean getTurboMode() { + return (data[0x0a] & (byte) 0x02) != 0; + } + + /** + * If true display on indoor unit is degrees F, else C + * + * @return Fahrenheit on (true) or Celsius + */ + public boolean getFahrenheit() { + return (data[0x0a] & (byte) 0x04) != 0; + } + + /** + * There is some variation in how this is handled by different + * AC models. This covers at least 2 versions found. + * + * @return Indoor temperature + */ + public Float getIndoorTemperature() { + double indoorTempInteger; + double indoorTempDecimal; + + if (data[0] == (byte) 0xc0) { + if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) < -19) { + return (float) -19; + } + if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) > 50) { + return (float) 50; + } else { + indoorTempInteger = (float) ((Byte.toUnsignedInt(data[11]) - 50f) / 2.0f); + } + + indoorTempDecimal = (float) ((data[15] & 0x0F) * 0.1f); + + if (Byte.toUnsignedInt(data[11]) > 49) { + return (float) (indoorTempInteger + indoorTempDecimal); + } else { + return (float) (indoorTempInteger - indoorTempDecimal); + } + } + + /** + * Not observed or tested, but left in from original author + * This was for future development since only 0xC0 is currently used + */ + if (data[0] == (byte) 0xa0 || data[0] == (byte) 0xa1) { + if (data[0] == (byte) 0xa0) { + if ((data[1] >> 2) - 4 == 0) { + indoorTempInteger = -1; + } else { + indoorTempInteger = (data[1] >> 2) + 12; + } + + if (((data[1] >> 1) & 0x01) == 1) { + indoorTempDecimal = 0.5f; + } else { + indoorTempDecimal = 0; + } + } + if (data[0] == (byte) 0xa1) { + if (((Byte.toUnsignedInt(data[13]) - 50) / 2.0f) < -19) { + return (float) -19; + } + if (((Byte.toUnsignedInt(data[13]) - 50) / 2.0f) > 50) { + return (float) 50; + } else { + indoorTempInteger = (float) (Byte.toUnsignedInt(data[13]) - 50f) / 2.0f; + } + indoorTempDecimal = (data[18] & 0x0f) * 0.1f; + + if (Byte.toUnsignedInt(data[13]) > 49) { + return (float) (indoorTempInteger + indoorTempDecimal); + } else { + return (float) (indoorTempInteger - indoorTempDecimal); + } + } + } + return empty; + } + + /** + * There is some variation in how this is handled by different + * AC models. This covers at least 2 versions. Some models + * do not report outside temp when the AC is off. Returns 0.0 in that case. + * + * @return Outdoor temperature + */ + public Float getOutdoorTemperature() { + if (data[12] != (byte) 0xff) { + double tempInteger = (float) (Byte.toUnsignedInt(data[12]) - 50f) / 2.0f; + double tempDecimal = ((data[15] & 0xf0) >> 4) * 0.1f; + if (Byte.toUnsignedInt(data[12]) > 49) { + return (float) (tempInteger + tempDecimal); + } else { + return (float) (tempInteger - tempDecimal); + } + } + return 0.0f; + } + + /** + * Returns the Alternative Target Temperature (not used) + * + * @return Alternate target Temperature + */ + public Float getAlternateTargetTemperature() { + if ((data[13] & 0x1f) != 0) { + return (data[13] & 0x1f) + 12.0f + (((data[0x02] & 0x10) > 0) ? 0.5f : 0.0f); + } else { + return 0.0f; + } + } + + /** + * Returns status of Device LEDs + * + * @return LEDs on (true) or (false) + */ + public boolean getDisplayOn() { + return (data[14] & (byte) 0x70) != (byte) 0x70; + } + + /** + * Not observed with units being tested + * From reference Document + * + * @return humidity + */ + public int getHumidity() { + return (data[19] & (byte) 0x7f); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java new file mode 100644 index 0000000000000..c213ee489dca5 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Timer} class returns the On and Off AC Timer values + * to the channels. + * + * @author Jacek Dobrowolski - Initial contribution + * @author Bob Eckhoff - Add TimeParser and TimeData classes + */ +@NonNullByDefault +public class Timer { + + private boolean status; + private int hours; + private int minutes; + + /** + * Timer class parameters + * + * @param status on or off + * @param hours hours + * @param minutes minutes + */ + public Timer(boolean status, int hours, int minutes) { + this.status = status; + this.hours = hours; + this.minutes = minutes; + } + + /** + * Timer format for the trace log + */ + public String toString() { + if (status) { + return String.format("enabled: %s, hours: %d, minutes: %d", status, hours, minutes); + } else { + return String.format("enabled: %s", status); + } + } + + /** + * Timer format of the OH channel + * + * @return conforming String + */ + public String toChannel() { + if (status) { + return String.format("%02d:%02d", hours, minutes); + } else { + return ""; + } + } + + /** + * This splits the On or off timer channels command back to hours and minutes + * so the AC start and stop timers can be set + */ + public class TimeParser { + /** + * Parse Time string into components + * + * @param time conforming string + * @return hours and minutes + */ + public int[] parseTime(String time) { + String[] parts = time.split(":"); + int hours = Integer.parseInt(parts[0]); + int minutes = Integer.parseInt(parts[1]); + + return new int[] { hours, minutes }; + } + } + + /** + * This allows the continuity of the current timer settings + * when new commands on other channels are set. + */ + public class TimerData { + /** + * Status if timer is on + */ + public boolean status; + + /** + * Current hours + */ + public int hours; + + /** + * Current minutes + */ + public int minutes; + + /** + * Sets the TimerData from the response + * + * @param status true if timer is on + * @param hours hours left + * @param minutes minutes left + */ + public TimerData(boolean status, int hours, int minutes) { + this.status = status; + this.hours = hours; + this.minutes = minutes; + } + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java new file mode 100644 index 0000000000000..601d6bb5c7088 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.security; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Crc8} calculation. + * + * @author Jacek Dobrowolski - Initial Contribution + */ +@NonNullByDefault +public class Crc8 { + private static final byte[] CRC8_854_TABLE = { (byte) 0x00, (byte) 0x5E, (byte) 0xBC, (byte) 0xE2, (byte) 0x61, + (byte) 0x3F, (byte) 0xDD, (byte) 0x83, (byte) 0xC2, (byte) 0x9C, (byte) 0x7E, (byte) 0x20, (byte) 0xA3, + (byte) 0xFD, (byte) 0x1F, (byte) 0x41, (byte) 0x9D, (byte) 0xC3, (byte) 0x21, (byte) 0x7F, (byte) 0xFC, + (byte) 0xA2, (byte) 0x40, (byte) 0x1E, (byte) 0x5F, (byte) 0x01, (byte) 0xE3, (byte) 0xBD, (byte) 0x3E, + (byte) 0x60, (byte) 0x82, (byte) 0xDC, (byte) 0x23, (byte) 0x7D, (byte) 0x9F, (byte) 0xC1, (byte) 0x42, + (byte) 0x1C, (byte) 0xFE, (byte) 0xA0, (byte) 0xE1, (byte) 0xBF, (byte) 0x5D, (byte) 0x03, (byte) 0x80, + (byte) 0xDE, (byte) 0x3C, (byte) 0x62, (byte) 0xBE, (byte) 0xE0, (byte) 0x02, (byte) 0x5C, (byte) 0xDF, + (byte) 0x81, (byte) 0x63, (byte) 0x3D, (byte) 0x7C, (byte) 0x22, (byte) 0xC0, (byte) 0x9E, (byte) 0x1D, + (byte) 0x43, (byte) 0xA1, (byte) 0xFF, (byte) 0x46, (byte) 0x18, (byte) 0xFA, (byte) 0xA4, (byte) 0x27, + (byte) 0x79, (byte) 0x9B, (byte) 0xC5, (byte) 0x84, (byte) 0xDA, (byte) 0x38, (byte) 0x66, (byte) 0xE5, + (byte) 0xBB, (byte) 0x59, (byte) 0x07, (byte) 0xDB, (byte) 0x85, (byte) 0x67, (byte) 0x39, (byte) 0xBA, + (byte) 0xE4, (byte) 0x06, (byte) 0x58, (byte) 0x19, (byte) 0x47, (byte) 0xA5, (byte) 0xFB, (byte) 0x78, + (byte) 0x26, (byte) 0xC4, (byte) 0x9A, (byte) 0x65, (byte) 0x3B, (byte) 0xD9, (byte) 0x87, (byte) 0x04, + (byte) 0x5A, (byte) 0xB8, (byte) 0xE6, (byte) 0xA7, (byte) 0xF9, (byte) 0x1B, (byte) 0x45, (byte) 0xC6, + (byte) 0x98, (byte) 0x7A, (byte) 0x24, (byte) 0xF8, (byte) 0xA6, (byte) 0x44, (byte) 0x1A, (byte) 0x99, + (byte) 0xC7, (byte) 0x25, (byte) 0x7B, (byte) 0x3A, (byte) 0x64, (byte) 0x86, (byte) 0xD8, (byte) 0x5B, + (byte) 0x05, (byte) 0xE7, (byte) 0xB9, (byte) 0x8C, (byte) 0xD2, (byte) 0x30, (byte) 0x6E, (byte) 0xED, + (byte) 0xB3, (byte) 0x51, (byte) 0x0F, (byte) 0x4E, (byte) 0x10, (byte) 0xF2, (byte) 0xAC, (byte) 0x2F, + (byte) 0x71, (byte) 0x93, (byte) 0xCD, (byte) 0x11, (byte) 0x4F, (byte) 0xAD, (byte) 0xF3, (byte) 0x70, + (byte) 0x2E, (byte) 0xCC, (byte) 0x92, (byte) 0xD3, (byte) 0x8D, (byte) 0x6F, (byte) 0x31, (byte) 0xB2, + (byte) 0xEC, (byte) 0x0E, (byte) 0x50, (byte) 0xAF, (byte) 0xF1, (byte) 0x13, (byte) 0x4D, (byte) 0xCE, + (byte) 0x90, (byte) 0x72, (byte) 0x2C, (byte) 0x6D, (byte) 0x33, (byte) 0xD1, (byte) 0x8F, (byte) 0x0C, + (byte) 0x52, (byte) 0xB0, (byte) 0xEE, (byte) 0x32, (byte) 0x6C, (byte) 0x8E, (byte) 0xD0, (byte) 0x53, + (byte) 0x0D, (byte) 0xEF, (byte) 0xB1, (byte) 0xF0, (byte) 0xAE, (byte) 0x4C, (byte) 0x12, (byte) 0x91, + (byte) 0xCF, (byte) 0x2D, (byte) 0x73, (byte) 0xCA, (byte) 0x94, (byte) 0x76, (byte) 0x28, (byte) 0xAB, + (byte) 0xF5, (byte) 0x17, (byte) 0x49, (byte) 0x08, (byte) 0x56, (byte) 0xB4, (byte) 0xEA, (byte) 0x69, + (byte) 0x37, (byte) 0xD5, (byte) 0x8B, (byte) 0x57, (byte) 0x09, (byte) 0xEB, (byte) 0xB5, (byte) 0x36, + (byte) 0x68, (byte) 0x8A, (byte) 0xD4, (byte) 0x95, (byte) 0xCB, (byte) 0x29, (byte) 0x77, (byte) 0xF4, + (byte) 0xAA, (byte) 0x48, (byte) 0x16, (byte) 0xE9, (byte) 0xB7, (byte) 0x55, (byte) 0x0B, (byte) 0x88, + (byte) 0xD6, (byte) 0x34, (byte) 0x6A, (byte) 0x2B, (byte) 0x75, (byte) 0x97, (byte) 0xC9, (byte) 0x4A, + (byte) 0x14, (byte) 0xF6, (byte) 0xA8, (byte) 0x74, (byte) 0x2A, (byte) 0xC8, (byte) 0x96, (byte) 0x15, + (byte) 0x4B, (byte) 0xA9, (byte) 0xF7, (byte) 0xB6, (byte) 0xE8, (byte) 0x0A, (byte) 0x54, (byte) 0xD7, + (byte) 0x89, (byte) 0x6B, (byte) 0x35 }; + + /** + * Calculate crc value + * + * @param bytes input bytes + * @return crcValue + */ + public static int calculate(byte[] bytes) { + int crcValue = 0; + for (byte m : bytes) { + int k = (byte) (crcValue ^ m); + if (k > 256) { + k -= 256; + } + if (k < 0) { + k += 256; + } + crcValue = CRC8_854_TABLE[k]; + } + return crcValue; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java new file mode 100644 index 0000000000000..617dc367aaaca --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.security; + +import java.util.ArrayList; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Decryption8370Result} Protocol. V3 Only + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - JavaDoc + */ +@NonNullByDefault +public class Decryption8370Result { + /** + * Set up for decryption + * + * @return responses + */ + public ArrayList getResponses() { + return responses; + } + + /** + * Buffer + * + * @return buffer + */ + public byte[] getBuffer() { + return buffer; + } + + ArrayList responses; + byte[] buffer; + + /** + * Decryption result + * + * @param responses responses + * @param buffer buffer + */ + public Decryption8370Result(ArrayList responses, byte[] buffer) { + super(); + this.responses = responses; + this.buffer = buffer; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java new file mode 100644 index 0000000000000..8eec22960c516 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -0,0 +1,627 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.security; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Random; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonObject; + +/** + * The {@link Security} class provides Security coding and decoding. + * The basic aes Protocol is used by both V2 and V3 devices. + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - JavaDoc + */ +@NonNullByDefault +public class Security { + + private @Nullable SecretKeySpec encKey = null; + private Logger logger = LoggerFactory.getLogger(Security.class); + private IvParameterSpec iv = new IvParameterSpec(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); + + CloudProviderDTO cloudProvider; + + /** + * Set Cloud Provider + * + * @param cloudProvider Name of Cloud provider + */ + public Security(CloudProviderDTO cloudProvider) { + this.cloudProvider = cloudProvider; + } + + /** + * Basic Decryption for all devices using common signkey + * + * @param encryptData encrypted array + * @return decypted array + */ + public byte[] aesDecrypt(byte[] encryptData) { + byte[] plainText = {}; + + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + SecretKeySpec key = getEncKey(); + + try { + cipher.init(Cipher.DECRYPT_MODE, key); + } catch (InvalidKeyException e) { + logger.warn("AES decryption error: InvalidKeyException: {}", e.getMessage()); + return new byte[0]; + } + + try { + plainText = cipher.doFinal(encryptData); + } catch (IllegalBlockSizeException e) { + logger.warn("AES decryption error: IllegalBlockSizeException: {}", e.getMessage()); + return new byte[0]; + } catch (BadPaddingException e) { + logger.warn("AES decryption error: BadPaddingException: {}", e.getMessage()); + return new byte[0]; + } + + } catch (NoSuchAlgorithmException e) { + logger.warn("AES decryption error: NoSuchAlgorithmException: {}", e.getMessage()); + return new byte[0]; + } catch (NoSuchPaddingException e) { + logger.warn("AES decryption error: NoSuchPaddingException: {}", e.getMessage()); + return new byte[0]; + } + return plainText; + } + + /** + * Basic Encryption for all devices using common signkey + * + * @param plainText Plain Text + * @return encrpted byte[] array + */ + public byte[] aesEncrypt(byte[] plainText) { + byte[] encryptData = {}; + + try { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + + SecretKeySpec key = getEncKey(); + + try { + cipher.init(Cipher.ENCRYPT_MODE, key); + } catch (InvalidKeyException e) { + logger.warn("AES encryption error: InvalidKeyException: {}", e.getMessage()); + } + + try { + encryptData = cipher.doFinal(plainText); + } catch (IllegalBlockSizeException e) { + logger.warn("AES encryption error: IllegalBlockSizeException: {}", e.getMessage()); + return new byte[0]; + } catch (BadPaddingException e) { + logger.warn("AES encryption error: BadPaddingException: {}", e.getMessage()); + return new byte[0]; + } + } catch (NoSuchAlgorithmException e) { + logger.warn("AES encryption error: NoSuchAlgorithmException: {}", e.getMessage()); + return new byte[0]; + } catch (NoSuchPaddingException e) { + logger.warn("AES encryption error: NoSuchPaddingException: {}", e.getMessage()); + return new byte[0]; + } + + return encryptData; + } + + /** + * Secret key using MD5 + * + * @return encKey + * @throws NoSuchAlgorithmException missing algorithm + */ + public @Nullable SecretKeySpec getEncKey() throws NoSuchAlgorithmException { + if (encKey == null) { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(cloudProvider.signkey().getBytes(StandardCharsets.US_ASCII)); + byte[] key = md.digest(); + SecretKeySpec skeySpec = new SecretKeySpec(key, "AES"); + + encKey = skeySpec; + } + + return encKey; + } + + /** + * Encode32 Data + * + * @param raw byte array + * @return byte[] + */ + public byte[] encode32Data(byte[] raw) { + byte[] combine = ByteBuffer + .allocate(raw.length + cloudProvider.signkey().getBytes(StandardCharsets.US_ASCII).length).put(raw) + .put(cloudProvider.signkey().getBytes(StandardCharsets.US_ASCII)).array(); + MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + md.update(combine); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + } + return new byte[0]; + } + + /** + * Message types + */ + public enum MsgType { + MSGTYPE_HANDSHAKE_REQUEST(0x0), + MSGTYPE_HANDSHAKE_RESPONSE(0x1), + MSGTYPE_ENCRYPTED_RESPONSE(0x3), + MSGTYPE_ENCRYPTED_REQUEST(0x6), + MSGTYPE_TRANSPARENT(0xf); + + private final int value; + + private MsgType(int value) { + this.value = value; + } + + /** + * Message type Id + * + * @return message type + */ + public int getId() { + return value; + } + + /** + * Plain language message + * + * @param id id + * @return message type + */ + public static MsgType fromId(int id) { + for (MsgType type : values()) { + if (type.getId() == id) { + return type; + } + } + return MSGTYPE_TRANSPARENT; + } + } + + private int requestCount = 0; + private int responseCount = 0; + private byte[] tcpKey = new byte[0]; + + /** + * Advanced Encryption for V3 devices + * + * @param data input data array + * @param msgtype message type + * @return encoded byte array + */ + public byte[] encode8370(byte[] data, MsgType msgtype) { + ByteBuffer headerBuffer = ByteBuffer.allocate(256); + ByteBuffer dataBuffer = ByteBuffer.allocate(256); + + headerBuffer.put(new byte[] { (byte) 0x83, (byte) 0x70 }); + + int size = data.length; + int padding = 0; + + logger.trace("Size: {}", size); + byte[] paddingData = null; + if (msgtype == MsgType.MSGTYPE_ENCRYPTED_RESPONSE || msgtype == MsgType.MSGTYPE_ENCRYPTED_REQUEST) { + if ((size + 2) % 16 != 0) { + padding = 16 - (size + 2 & 0xf); + size += padding + 32; + logger.trace("Padding size: {}, size: {}", padding, size); + paddingData = getRandomBytes(padding); + } + } + headerBuffer.put(Utils.toBytes((short) size)); + + headerBuffer.put(new byte[] { 0x20, (byte) (padding << 4 | msgtype.value) }); + + if (requestCount > 0xfff) { + logger.trace("requestCount is too big to convert: {}, changing requestCount to 0", requestCount); + requestCount = 0; + } + + dataBuffer.put(Utils.toBytes((short) requestCount)); + requestCount += 1; + + dataBuffer.put(data); + if (paddingData != null) { + dataBuffer.put(paddingData); + } + + headerBuffer.flip(); + byte[] finalHeader = new byte[headerBuffer.remaining()]; + headerBuffer.get(finalHeader); + + dataBuffer.flip(); + byte[] finalData = new byte[dataBuffer.remaining()]; + dataBuffer.get(finalData); + + logger.trace("Header: {}", Utils.bytesToHex(finalHeader)); + + if (msgtype == MsgType.MSGTYPE_ENCRYPTED_RESPONSE || msgtype == MsgType.MSGTYPE_ENCRYPTED_REQUEST) { + byte[] sign = sha256(Utils.concatenateArrays(finalHeader, finalData)); + logger.trace("Sign: {}", Utils.bytesToHex(sign)); + logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); + + finalData = Utils.concatenateArrays(aesCbcEncrypt(finalData, tcpKey), sign); + } + + byte[] result = Utils.concatenateArrays(finalHeader, finalData); + return result; + } + + /** + * Advanced Decryption for V3 devices + * + * @param data input data array + * @return decrypted byte array + * @throws IOException IO exception + */ + public Decryption8370Result decode8370(byte[] data) throws IOException { + if (data.length < 6) { + return new Decryption8370Result(new ArrayList(), data); + } + byte[] header = Arrays.copyOfRange(data, 0, 6); + logger.trace("Header: {}", Utils.bytesToHex(header)); + if (header[0] != (byte) 0x83 || header[1] != (byte) 0x70) { + logger.warn("Not an 8370 message"); + return new Decryption8370Result(new ArrayList(), data); + } + ByteBuffer dataBuffer = ByteBuffer.wrap(data); + int size = dataBuffer.getShort(2) + 8; + logger.trace("Size: {}", size); + byte[] leftover = null; + if (data.length < size) { + return new Decryption8370Result(new ArrayList(), data); + } else if (data.length > size) { + leftover = Arrays.copyOfRange(data, size, data.length); + data = Arrays.copyOfRange(data, 0, size); + } + int padding = header[5] >> 4; + logger.trace("Padding: {}", padding); + MsgType msgtype = MsgType.fromId(header[5] & 0xf); + logger.trace("MsgType: {}", msgtype.toString()); + data = Arrays.copyOfRange(data, 6, data.length); + + if (msgtype == MsgType.MSGTYPE_ENCRYPTED_RESPONSE || msgtype == MsgType.MSGTYPE_ENCRYPTED_REQUEST) { + byte[] sign = Arrays.copyOfRange(data, data.length - 32, data.length); + data = Arrays.copyOfRange(data, 0, data.length - 32); + data = aesCbcDecrypt(data, tcpKey); + byte[] signLocal = sha256(Utils.concatenateArrays(header, data)); + + logger.trace("Sign: {}", Utils.bytesToHex(sign)); + logger.trace("SignLocal: {}", Utils.bytesToHex(signLocal)); + logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); + logger.trace("Data: {}", Utils.bytesToHex(data)); + + if (!Arrays.equals(sign, signLocal)) { + logger.warn("Sign does not match"); + return new Decryption8370Result(new ArrayList(), data); + } + + if (padding > 0) { + data = Arrays.copyOfRange(data, 0, data.length - padding); + } + } else { + logger.warn("MsgType: {}", msgtype.toString()); + throw new IOException(msgtype.toString() + " response was received"); + } + + dataBuffer = ByteBuffer.wrap(data); + responseCount = dataBuffer.getShort(0); + logger.trace("responseCount: {}", responseCount); + logger.trace("requestCount: {}", requestCount); + data = Arrays.copyOfRange(data, 2, data.length); + if (leftover != null) { + Decryption8370Result r = decode8370(leftover); + ArrayList responses = r.getResponses(); + responses.add(0, data); + return new Decryption8370Result(responses, r.buffer); + } + + ArrayList responses = new ArrayList(); + responses.add(data); + return new Decryption8370Result(responses, new byte[] {}); + } + + /** + * Retrieve TCP key + * + * @param response message + * @param key key + * @return tcp key + */ + public boolean tcpKey(byte[] response, byte key[]) { + byte[] payload = Arrays.copyOfRange(response, 0, 32); + byte[] sign = Arrays.copyOfRange(response, 32, 64); + byte[] plain = aesCbcDecrypt(payload, key); + byte[] signLocal = sha256(plain); + + logger.trace("Payload: {}", Utils.bytesToHex(payload)); + logger.trace("Sign: {}", Utils.bytesToHex(sign)); + logger.trace("SignLocal: {}", Utils.bytesToHex(signLocal)); + logger.trace("Plain: {}", Utils.bytesToHex(plain)); + + if (!Arrays.equals(sign, signLocal)) { + logger.warn("Sign does not match"); + return false; + } + tcpKey = Utils.strxor(plain, key); + logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); + return true; + } + + private byte[] aesCbcDecrypt(byte[] encryptData, byte[] decrypt_key) { + byte[] plainText = {}; + + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + SecretKeySpec key = new SecretKeySpec(decrypt_key, "AES"); + + try { + cipher.init(Cipher.DECRYPT_MODE, key, iv); + } catch (InvalidKeyException e) { + logger.warn("AES decryption error: InvalidKeyException: {}", e.getMessage()); + return new byte[0]; + } catch (InvalidAlgorithmParameterException e) { + logger.warn("AES decryption error: InvalidAlgorithmParameterException: {}", e.getMessage()); + return new byte[0]; + } + + try { + plainText = cipher.doFinal(encryptData); + } catch (IllegalBlockSizeException e) { + logger.warn("AES decryption error: IllegalBlockSizeException: {}", e.getMessage()); + return new byte[0]; + } catch (BadPaddingException e) { + logger.warn("AES decryption error: BadPaddingException: {}", e.getMessage()); + return new byte[0]; + } + + } catch (NoSuchAlgorithmException e) { + logger.warn("AES decryption error: NoSuchAlgorithmException: {}", e.getMessage()); + return new byte[0]; + } catch (NoSuchPaddingException e) { + logger.warn("AES decryption error: NoSuchPaddingException: {}", e.getMessage()); + return new byte[0]; + } + + return plainText; + } + + private byte[] aesCbcEncrypt(byte[] plainText, byte[] encrypt_key) { + byte[] encryptData = {}; + + try { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + + SecretKeySpec key = new SecretKeySpec(encrypt_key, "AES"); + + try { + cipher.init(Cipher.ENCRYPT_MODE, key, iv); + } catch (InvalidKeyException e) { + logger.warn("AES encryption error: InvalidKeyException: {}", e.getMessage()); + } catch (InvalidAlgorithmParameterException e) { + logger.warn("AES encryption error: InvalidAlgorithmParameterException: {}", e.getMessage()); + } + + try { + encryptData = cipher.doFinal(plainText); + } catch (IllegalBlockSizeException e) { + logger.warn("AES encryption error: IllegalBlockSizeException: {}", e.getMessage()); + return new byte[0]; + } catch (BadPaddingException e) { + logger.warn("AES encryption error: BadPaddingException: {}", e.getMessage()); + return new byte[0]; + } + } catch (NoSuchAlgorithmException e) { + logger.warn("AES encryption error: NoSuchAlgorithmException: {}", e.getMessage()); + return new byte[0]; + } catch (NoSuchPaddingException e) { + logger.warn("AES encryption error: NoSuchPaddingException: {}", e.getMessage()); + return new byte[0]; + } + + return encryptData; + } + + private byte[] sha256(byte[] bytes) { + try { + return MessageDigest.getInstance("SHA-256").digest(bytes); + } catch (NoSuchAlgorithmException e) { + logger.warn("SHA256 digest error: NoSuchAlgorithmException: {}", e.getMessage()); + return new byte[0]; + } + } + + private byte[] getRandomBytes(int size) { + byte[] random = new byte[size]; + new Random().nextBytes(random); + return random; + } + + /** + * Path to cloud provider + * + * @param url url of cloud provider + * @param payload message + * @return lower case hex string + */ + public @Nullable String sign(String url, JsonObject payload) { + logger.trace("url: {}", url); + String path; + try { + path = new URI(url).getPath(); + + String query = Utils.getQueryString(payload); + + String sign = path + query + cloudProvider.appkey(); + logger.trace("sign: {}", sign); + return Utils.bytesToHexLowercase(sha256((sign).getBytes(StandardCharsets.US_ASCII))); + } catch (URISyntaxException e) { + logger.warn("Syntax error{}", e.getMessage()); + } + + return null; + } + + /** + * Provides a randown iotKey for Cloud Providers that do not have one + * + * @param data input data array + * @param random random values + * @return sign + */ + public @Nullable String newSign(String data, String random) { + String msg = cloudProvider.iotkey(); + if (!data.isEmpty()) { + msg += data; + } + msg += random; + String sign; + + try { + sign = hmac(msg, cloudProvider.hmackey(), "HmacSHA256"); + } catch (InvalidKeyException e) { + logger.warn("HMAC digest error: InvalidKeyException: {}", e.getMessage()); + return null; + } catch (NoSuchAlgorithmException e) { + logger.warn("HMAC digest error: NoSuchAlgorithmException: {}", e.getMessage()); + return null; + } + + return sign; // .hexdigest(); + } + + /** + * Converts parameters to lower case string for communication with cloud + * + * @param data data array + * @param key key + * @param algorithm method + * @throws NoSuchAlgorithmException no Algorithm + * @throws InvalidKeyException bad key + * @return lower case string + */ + public String hmac(String data, String key, String algorithm) throws NoSuchAlgorithmException, InvalidKeyException { + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm); + Mac mac = Mac.getInstance(algorithm); + mac.init(secretKeySpec); + return Utils.bytesToHexLowercase(mac.doFinal(data.getBytes())); + } + + /** + * Encrypts password for cloud API using SHA-256 + * + * @param loginId Login ID + * @param password Login password + * @return string + */ + public @Nullable String encryptPassword(@Nullable String loginId, String password) { + try { + // Hash the password + MessageDigest m = MessageDigest.getInstance("SHA-256"); + m.update(password.getBytes(StandardCharsets.US_ASCII)); + + // Create the login hash with the loginID + password hash + appKey, then hash it all AGAIN + String loginHash = loginId + Utils.bytesToHexLowercase(m.digest()) + cloudProvider.appkey(); + m = MessageDigest.getInstance("SHA-256"); + m.update(loginHash.getBytes(StandardCharsets.US_ASCII)); + return Utils.bytesToHexLowercase(m.digest()); + } catch (NoSuchAlgorithmException e) { + logger.warn("encryptPassword error: NoSuchAlgorithmException: {}", e.getMessage()); + } + return null; + } + + /** + * Encrypts password for cloud API using MD5 + * + * @param loginId Login ID + * @param password Login password + * @return string + */ + public @Nullable String encryptIamPassword(@Nullable String loginId, String password) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(password.getBytes(StandardCharsets.US_ASCII)); + + MessageDigest mdSecond = MessageDigest.getInstance("MD5"); + mdSecond.update(Utils.bytesToHexLowercase(md.digest()).getBytes(StandardCharsets.US_ASCII)); + + // if self._use_china_server: + // return mdSecond.hexdigest() + + String loginHash = loginId + Utils.bytesToHexLowercase(mdSecond.digest()) + cloudProvider.appkey(); + return Utils.bytesToHexLowercase(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))); + } catch (NoSuchAlgorithmException e) { + logger.warn("encryptIamPasswordt error: NoSuchAlgorithmException: {}", e.getMessage()); + } + return null; + } + + /** + * Gets UDPID from byte data + * + * @param data data array + * @return string of lower case bytes + */ + public String getUdpId(byte[] data) { + byte[] b = sha256(data); + byte[] b1 = Arrays.copyOfRange(b, 0, 16); + byte[] b2 = Arrays.copyOfRange(b, 16, b.length); + byte[] b3 = new byte[16]; + int i = 0; + while (i < b1.length) { + b3[i] = (byte) (b1[i] ^ b2[i]); + i++; + } + return Utils.bytesToHexLowercase(b3); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java new file mode 100644 index 0000000000000..8087638cb9b43 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.security; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link TokenKey} returns the active Token and Key. + * + * @param token For coding/decoding messages + * @param key For coding/decoding messages + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - JavaDoc and OH addons review + */ +@NonNullByDefault +public record TokenKey(String token, String key) { +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..4767a09ed3127 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,11 @@ + + + + binding + MideaAC Binding + This is the binding for MideaAC. + local + + diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties new file mode 100644 index 0000000000000..98f30a9f964c5 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -0,0 +1,95 @@ +# add-on + +addon.mideaac.name = MideaAC Binding +addon.mideaac.description = This is the binding for MideaAC. + +# thing types + +thing-type.mideaac.ac.label = Midea Air Conditioner +thing-type.mideaac.ac.description = Midea Air Conditioner with USB WIFI stick. There are 2 versions: v2 - without encryption, v3 - with encryption - Token and Key must be provided, it can be automatically obtained from Cloud. + +# thing types config + +thing-type.config.mideaac.ac.cloud.label = Cloud Provider +thing-type.config.mideaac.ac.cloud.description = Cloud Provider name for email and password. +thing-type.config.mideaac.ac.cloud.option.MSmartHome = MSmartHome +thing-type.config.mideaac.ac.cloud.option.Midea\ Air = Midea Air +thing-type.config.mideaac.ac.cloud.option.NetHome\ Plus = NetHome Plus +thing-type.config.mideaac.ac.deviceId.label = Device ID +thing-type.config.mideaac.ac.deviceId.description = ID of the device. Leave 0 to do ID discovery. +thing-type.config.mideaac.ac.email.label = Email +thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. +thing-type.config.mideaac.ac.ipAddress.label = IP Address +thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. +thing-type.config.mideaac.ac.ipPort.label = IP Port +thing-type.config.mideaac.ac.ipPort.description = IP port of the device (for V2: 6444). +thing-type.config.mideaac.ac.key.label = Key +thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). +thing-type.config.mideaac.ac.password.label = Password +thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. +thing-type.config.mideaac.ac.pollingTime.label = Polling time +thing-type.config.mideaac.ac.pollingTime.description = Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. +thing-type.config.mideaac.ac.promptTone.label = Prompt tone +thing-type.config.mideaac.ac.promptTone.description = After sending a command device will play "ding" tone when command is received and executed. +thing-type.config.mideaac.ac.timeout.label = Timeout +thing-type.config.mideaac.ac.timeout.description = Connecting timeout. Minimum time is 2 second, maximum 10 seconds (4 seconds default). +thing-type.config.mideaac.ac.token.label = Token +thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). +thing-type.config.mideaac.ac.version.label = AC Version +thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover + +# channel types + +channel-type.mideaac.alternate-target-temperature.label = Alternate Target Temperature +channel-type.mideaac.alternate-target-temperature.description = Alternate Target Temperature (Read Only). +channel-type.mideaac.appliance-error.label = Appliance error +channel-type.mideaac.appliance-error.description = Appliance error (Read Only). +channel-type.mideaac.auxiliary-heat.label = Auxiliary heat +channel-type.mideaac.auxiliary-heat.description = Auxiliary heat (Read Only). +channel-type.mideaac.dropped-commands.label = Dropped Command Monitor +channel-type.mideaac.dropped-commands.description = Commands dropped due to TCP read() issues. +channel-type.mideaac.eco-mode.label = Eco mode +channel-type.mideaac.eco-mode.description = Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. +channel-type.mideaac.fan-speed.label = Fan speed +channel-type.mideaac.fan-speed.description = Fan speed: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. +channel-type.mideaac.fan-speed.state.option.SILENT = SILENT +channel-type.mideaac.fan-speed.state.option.LOW = LOW +channel-type.mideaac.fan-speed.state.option.MEDIUM = MEDIUM +channel-type.mideaac.fan-speed.state.option.HIGH = HIGH +channel-type.mideaac.fan-speed.state.option.FULL = FULL +channel-type.mideaac.fan-speed.state.option.AUTO = AUTO +channel-type.mideaac.humidity.label = Humidity +channel-type.mideaac.humidity.description = Humidity measured in the room by the indoor unit. +channel-type.mideaac.indoor-temperature.label = Indoor temperature +channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off +channel-type.mideaac.off-timer.label = OFF Timer +channel-type.mideaac.off-timer.description = OFF Timer (HH:MM) to set. +channel-type.mideaac.on-timer.label = ON Timer +channel-type.mideaac.on-timer.description = ON Timer (HH:MM) to set. +channel-type.mideaac.operational-mode.label = Operational mode +channel-type.mideaac.operational-mode.description = Operational mode: AUTO, COOL, DRY, HEAT. +channel-type.mideaac.operational-mode.state.option.AUTO = AUTO +channel-type.mideaac.operational-mode.state.option.COOL = COOL +channel-type.mideaac.operational-mode.state.option.DRY = DRY +channel-type.mideaac.operational-mode.state.option.HEAT = HEAT +channel-type.mideaac.operational-mode.state.option.FAN_ONLY = FAN ONLY +channel-type.mideaac.outdoor-temperature.label = Outdoor temperature +channel-type.mideaac.outdoor-temperature.description = Outdoor temperature from the external unit. Not frequent when unit is off +channel-type.mideaac.power.label = Power +channel-type.mideaac.power.description = Turn the AC on and off. +channel-type.mideaac.screen-display.label = Screen display +channel-type.mideaac.screen-display.description = Status of LEDs on the device. Not all models work on LAN (only IR). No confirmation possible either. +channel-type.mideaac.sleep-function.label = Sleep function +channel-type.mideaac.sleep-function.description = Sleep function ("Moon with a star" icon on IR Remote Controller). +channel-type.mideaac.swing-mode.label = Swing mode +channel-type.mideaac.swing-mode.description = Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support +channel-type.mideaac.swing-mode.state.option.OFF = OFF +channel-type.mideaac.swing-mode.state.option.VERTICAL = VERTICAL +channel-type.mideaac.swing-mode.state.option.HORIZONTAL = HORIZONTAL +channel-type.mideaac.swing-mode.state.option.BOTH = BOTH +channel-type.mideaac.target-temperature.label = Target temperature +channel-type.mideaac.target-temperature.description = Target temperature. +channel-type.mideaac.temperature-unit.label = Temperature unit on LED Display +channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. +channel-type.mideaac.turbo-mode.label = Turbo mode +channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..a803250bcb91d --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,266 @@ + + + + + + + Midea Air Conditioner with USB WIFI stick. There are 2 versions: v2 - without encryption, v3 - with + encryption - Token and Key must be provided, it can be automatically obtained from Cloud. + + + + + + + + + + + + + + + + + + + + + + + + + + ipAddress + + + + ipAddress + + IP Address of the device. + + + ipPort + + IP port of the device (for V2: 6444). + 6444 + + + deviceId + + ID of the device. Leave 0 to do ID discovery. + 0 + + + cloud + + Cloud Provider name for email and password. + + + + + + + true + + + + email + + Email for cloud account chosen in Cloud Provider. + + + password + + Password for cloud account chosen in Cloud Provider. + + + token + + Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not + known, enter email and password for Cloud to retrieve it). + + + key + + Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not + known, enter email and password for Cloud to retrieve it). + + + pollingTime + + Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. + 60 + + + timeout + + Connecting timeout. Minimum time is 2 second, maximum 10 seconds (4 seconds default). + 4 + + + promptTone + + After sending a command device will play "ding" tone when command is received and executed. + false + + + version + + Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover + 3 + + + + + + + Switch + + Turn the AC on and off. + Switch + + + Number:Temperature + + Target temperature. + Temperature + + + + String + + Operational mode: AUTO, COOL, DRY, HEAT. + + + + + + + + + + + + String + + Fan speed: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. + + + + + + + + + + + + + String + + Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support + + + + + + + + + + + Switch + + Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. + Switch + + + Switch + + Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT + mode. + Switch + + + Number:Temperature + + Indoor temperature measured by the internal unit. Not frequent when unit is off + Temperature + + + + Number:Temperature + + Outdoor temperature from the external unit. Not frequent when unit is off + Temperature + + + + Switch + + Sleep function ("Moon with a star" icon on IR Remote Controller). + Switch + + + Switch + + On = Farenheit on Indoor AC unit LED display, Off = Celsius. + Switch + + + Switch + + Status of LEDs on the device. Not all models work on LAN (only IR). No confirmation + possible either. + Switch + + + Switch + + Appliance error (Read Only). + Switch + + + + String + + ON Timer (HH:MM) to set. + + + String + + OFF Timer (HH:MM) to set. + + + Switch + + Auxiliary heat (Read Only). + Switch + + + + Number + + Humidity measured in the room by the indoor unit. + Humidity + + + + Number:Temperature + + Alternate Target Temperature (Read Only). + Temperature + + + + Number + + Commands dropped due to TCP read() issues. + Number + + + diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java new file mode 100644 index 0000000000000..a728420af666c --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mideaac.internal.security.TokenKey; + +/** + * Testing of the {@link MideaACConfigurationTest} Configuration + * + * @author Robert Eckhoff - Initial contribution + */ +@NonNullByDefault +public class MideaACConfigurationTest { + + MideaACConfiguration config = new MideaACConfiguration(); + + /** + * Test for valid Configs + */ + @Test + public void testValidConfigs() { + config.ipAddress = "192.168.0.1"; + config.ipPort = "6444"; + config.deviceId = "1234567890"; + assertTrue(config.isValid()); + assertFalse(config.isDiscoveryNeeded()); + } + + /** + * Test for non-valid configs + */ + @Test + public void testnonValidConfigs() { + config.ipAddress = "192.168.0.1"; + config.ipPort = ""; + config.deviceId = "1234567890"; + assertFalse(config.isValid()); + assertTrue(config.isDiscoveryNeeded()); + } + + /** + * Test for bad IP configs + */ + @Test + public void testBadIpConfigs() { + config.ipAddress = "192.1680.1"; + config.ipPort = "6444"; + config.deviceId = "1234567890"; + assertTrue(config.isValid()); + assertTrue(config.isDiscoveryNeeded()); + } + + /** + * Test to return cloud provider + */ + @Test + public void testCloudProvider() { + config.cloud = "NetHome Plus"; + assertEquals(config.cloud, "NetHome Plus"); + } + + /** + * Test to return token and key pair + */ + @Test + public void testTokenKey() { + config.token = "D24046B597DB9C8A7CA029660BC606F3FD7EBF12693E73B2EF1FFE4C3B7CA00C824E408C9F3CE972CC0D3F8250AD79D0E67B101B47AC2DD84B396E52EA05193F"; + config.key = "97c65a4eed4f49fda06a1a51d5cbd61d2c9b81d103ca4ca689f352a07a16fae6"; + TokenKey tokenKey = new TokenKey(config.token, config.key); + String tokenTest = tokenKey.token(); + String keyTest = tokenKey.key(); + assertEquals(config.token, tokenTest); + assertEquals(config.key, keyTest); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java new file mode 100644 index 0000000000000..76e9808a6e6fd --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.discovery; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mideaac.internal.Utils; + +/** + * The {@link MideaACDiscoveryServiceTest} tests the discovery byte arrays + * (reply string already decrypted - See SecurityTest) + * to extract the correct device information + * + * @author Robert Eckhoff - Initial contribution + */ +@NonNullByDefault +public class MideaACDiscoveryServiceTest { + + byte[] data = HexFormat.of().parseHex( + "837000C8200F00005A5A0111B8007A80000000006B0925121D071814C0110800008A0000000000000000018000000000AF55C8897BEA338348DA7FC0B3EF1F1C889CD57C06462D83069558B66AF14A2D66353F52BAECA68AEB4C3948517F276F72D8A3AD4652EFA55466D58975AEB8D948842E20FBDCA6339558C848ECE09211F62B1D8BB9E5C25DBA7BF8E0CC4C77944BDFB3E16E33D88768CC4C3D0658937D0BB19369BF0317B24D3A4DE9E6A13106AFFBBE80328AEA7426CD6BA2AD8439F72B4EE2436CC634040CB976A92A53BCD5"); + byte[] reply = HexFormat.of().parseHex( + "F600A8C02C19000030303030303050303030303030305131423838433239353634334243303030300B6E65745F61635F343342430000870002000000000000000000AC00ACAC00000000B88C295643BC150023082122000300000000000000000000000000000000000000000000000000000000000000000000"); + String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", + mSmartType = ""; + + /** + * Test Id + */ + @Test + public void testId() { + if (Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { + data = Arrays.copyOfRange(data, 8, data.length - 16); + } + byte[] id = Utils.reverse(Arrays.copyOfRange(data, 20, 26)); + BigInteger bigId = new BigInteger(1, id); + mSmartId = bigId.toString(10); + assertEquals("151732605161920", mSmartId); + } + + /** + * Test IP address of device + */ + @Test + public void testIPAddress() { + mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." + + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); + assertEquals("192.168.0.246", mSmartip); + } + + /** + * Test Device Port + */ + @Test + public void testPort() { + BigInteger portId = new BigInteger(Utils.reverse(Arrays.copyOfRange(reply, 4, 8))); + mSmartPort = portId.toString(); + assertEquals("6444", mSmartPort); + } + + /** + * Test serial Number + */ + @Test + public void testSN() { + mSmartSN = new String(reply, 8, 40 - 8, StandardCharsets.UTF_8); + assertEquals("000000P0000000Q1B88C295643BC0000", mSmartSN); + } + + /** + * Test SSID - SN converted + */ + @Test + public void testSSID() { + mSmartSSID = new String(reply, 41, reply[40], StandardCharsets.UTF_8); + assertEquals("net_ac_43BC", mSmartSSID); + } + + /** + * Test Type + */ + @Test + public void testType() { + mSmartSSID = new String(reply, 41, reply[40], StandardCharsets.UTF_8); + mSmartType = mSmartSSID.split("_")[1]; + assertEquals("ac", mSmartType); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java new file mode 100644 index 0000000000000..3f75ff40869e5 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; +import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; +import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; + +/** + * The {@link CommandSetTest} compares example SET commands with the + * expected results. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CommandSetTest { + + /** + * Power State Test + */ + @Test + public void setPowerStateTest() { + boolean status = true; + boolean status1 = true; + CommandSet commandSet = new CommandSet(); + commandSet.setPowerState(status); + assertEquals(status1, commandSet.getPowerState()); + } + + /** + * Target temperature tests + */ + @Test + public void testsetTargetTemperature() { + CommandSet commandSet = new CommandSet(); + // Device is limited to 0.5 degree C increments. Check rounding too + + // Test case 1 + float targetTemperature1 = 25.4f; + commandSet.setTargetTemperature(targetTemperature1); + assertEquals(25.5f, commandSet.getTargetTemperature()); + + // Test case 2 + float targetTemperature2 = 17.8f; + commandSet.setTargetTemperature(targetTemperature2); + assertEquals(18.0f, commandSet.getTargetTemperature()); + + // Test case 3 + float targetTemperature3 = 21.26f; + commandSet.setTargetTemperature(targetTemperature3); + assertEquals(21.5f, commandSet.getTargetTemperature()); + + // Test case 4 + float degreefahr = 72.0f; + float targetTemperature4 = ((degreefahr + 40.0f) * (5.0f / 9.0f)) - 40.0f; + commandSet.setTargetTemperature(targetTemperature4); + assertEquals(22.0f, commandSet.getTargetTemperature()); + + // Test case 5 + float degreefahr2 = 66.0f; + float targetTemperature5 = ((degreefahr2 + 40.0f) * (5.0f / 9.0f)) - 40.0f; + commandSet.setTargetTemperature(targetTemperature5); + assertEquals(19.0f, commandSet.getTargetTemperature()); + } + + /** + * Swing Mode test + */ + @Test + public void testHandleSwingMode() { + SwingMode mode = SwingMode.VERTICAL3; + int mode1 = 60; + CommandSet commandSet = new CommandSet(); + commandSet.setSwingMode(mode); + assertEquals(mode1, commandSet.getSwingMode()); + } + + /** + * Fan Speed test + */ + @Test + public void testHandleFanSpeedCommand() { + FanSpeed speed = FanSpeed.AUTO3; + int speed1 = 102; + CommandSet commandSet = new CommandSet(); + commandSet.setFanSpeed(speed); + assertEquals(speed1, commandSet.getFanSpeed()); + } + + /** + * Operational mode test + */ + @Test + public void testHandleOperationalMode() { + OperationalMode mode = OperationalMode.COOL; + int mode1 = 64; + CommandSet commandSet = new CommandSet(); + commandSet.setOperationalMode(mode); + assertEquals(mode1, commandSet.getOperationalMode()); + } + + /** + * On timer test + */ + @Test + public void testHandleOnTimer() { + CommandSet commandSet = new CommandSet(); + boolean on = true; + int hours = 3; + int minutes = 59; + int bits = (int) Math.floor(minutes / 15); + int time = 143; + int remainder = (15 - (int) (minutes - bits * 15)); + commandSet.setOnTimer(on, hours, minutes); + assertEquals(time, commandSet.getOnTimer()); + assertEquals(remainder, commandSet.getOnTimer2()); + } + + /** + * On timer test3 + */ + @Test + public void testHandleOnTimer2() { + CommandSet commandSet = new CommandSet(); + boolean on = false; + int hours = 3; + int minutes = 60; + int time = 127; + int remainder = 0; + commandSet.setOnTimer(on, hours, minutes); + assertEquals(time, commandSet.getOnTimer()); + assertEquals(remainder, commandSet.getOnTimer2()); + } + + /** + * On timer test3 + */ + @Test + public void testHandleOnTimer3() { + CommandSet commandSet = new CommandSet(); + boolean on = true; + int hours = 0; + int minutes = 14; + int time = 128; + int remainder = (15 - minutes); + commandSet.setOnTimer(on, hours, minutes); + assertEquals(time, commandSet.getOnTimer()); + assertEquals(remainder, commandSet.getOnTimer2()); + } + + /** + * Off timer test + */ + @Test + public void testHandleOffTimer() { + CommandSet commandSet = new CommandSet(); + boolean on = true; + int hours = 3; + int minutes = 59; + int bits = (int) Math.floor(minutes / 15); + int time = 143; + int remainder = (15 - (int) (minutes - bits * 15)); + commandSet.setOffTimer(on, hours, minutes); + assertEquals(time, commandSet.getOffTimer()); + assertEquals(remainder, commandSet.getOffTimer2()); + } + + /** + * Off timer test2 + */ + @Test + public void testHandleOffTimer2() { + CommandSet commandSet = new CommandSet(); + boolean on = false; + int hours = 3; + int minutes = 60; + int time = 127; + int remainder = 0; + commandSet.setOffTimer(on, hours, minutes); + assertEquals(time, commandSet.getOffTimer()); + assertEquals(remainder, commandSet.getOffTimer2()); + } + + /** + * Off timer test3 + */ + @Test + public void testHandleOffTimer3() { + CommandSet commandSet = new CommandSet(); + boolean on = true; + int hours = 0; + int minutes = 14; + int time = 128; + int remainder = (15 - minutes); + commandSet.setOffTimer(on, hours, minutes); + assertEquals(time, commandSet.getOffTimer()); + assertEquals(remainder, commandSet.getOffTimer2()); + } + + /** + * Test screen display change command + */ + @Test + public void testSetScreenDisplayOff() { + CommandSet commandSet = new CommandSet(); + commandSet.setScreenDisplay(true); + + // Check the modified bytes + assertEquals((byte) 0x20, commandSet.data[0x01]); + assertEquals((byte) 0x03, commandSet.data[0x09]); + assertEquals((byte) 0x41, commandSet.data[0x0a]); + assertEquals((byte) 0x02, commandSet.data[0x0b] & 0x02); // Check if bit 1 is set + assertEquals((byte) 0x00, commandSet.data[0x0b] & 0x80); // Check if bit 7 is cleared + assertEquals((byte) 0x00, commandSet.data[0x0c]); + assertEquals((byte) 0xff, commandSet.data[0x0d]); + assertEquals((byte) 0x02, commandSet.data[0x0e]); + assertEquals((byte) 0x00, commandSet.data[0x0f]); + assertEquals((byte) 0x02, commandSet.data[0x10]); + assertEquals((byte) 0x00, commandSet.data[0x11]); + assertEquals((byte) 0x00, commandSet.data[0x12]); + assertEquals((byte) 0x00, commandSet.data[0x13]); + assertEquals((byte) 0x00, commandSet.data[0x14]); + + // Check the length of the data array + assertEquals(31, commandSet.data.length); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java new file mode 100644 index 0000000000000..1f113499bb9e3 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -0,0 +1,197 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * The {@link ResponseTest} extracts the AC device response and + * compares them to the expected result. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class ResponseTest { + @org.jupnp.registry.event.Before + + byte[] data = HexFormat.of().parseHex("C00042668387123C00000460FF0C7000000000320000F9ECDB"); + private int version = 3; + String responseType = "query"; + byte bodyType = (byte) 0xC0; + Response response = new Response(data, version, responseType, bodyType); + + /** + * Power State Test + */ + @Test + public void testGetPowerState() { + boolean actualPowerState = response.getPowerState(); + assertEquals(false, actualPowerState); + } + + /** + * Prompt Tone Test + */ + @Test + public void testGetPromptTone() { + assertEquals(false, response.getPromptTone()); + } + + /** + * Appliance Error Test + */ + @Test + public void testGetApplianceError() { + assertEquals(false, response.getApplianceError()); + } + + /** + * Target Temperature Test + */ + @Test + public void testGetTargetTemperature() { + assertEquals(18, response.getTargetTemperature()); + } + + /** + * Operational Mode Test + */ + @Test + public void testGetOperationalMode() { + CommandBase.OperationalMode mode = response.getOperationalMode(); + assertEquals(CommandBase.OperationalMode.COOL, mode); + } + + /** + * Fan Speed Test + */ + @Test + public void testGetFanSpeed() { + CommandBase.FanSpeed fanSpeed = response.getFanSpeed(); + assertEquals(CommandBase.FanSpeed.AUTO3, fanSpeed); + } + + /** + * On timer Test + */ + @Test + public void testGetOnTimer() { + Timer status = response.getOnTimer(); + String expectedString = "enabled: true, hours: 0, minutes: 59"; + assertEquals(expectedString, status.toString()); + } + + /** + * Off timer Test + */ + @Test + public void testGetOffTimer() { + Timer status = response.getOffTimer(); + String expectedString = "enabled: true, hours: 1, minutes: 58"; + assertEquals(expectedString, status.toString()); + } + + /** + * Swing mode Test + */ + @Test + public void testGetSwingMode() { + CommandBase.SwingMode swing = response.getSwingMode(); + assertEquals(CommandBase.SwingMode.VERTICAL3, swing); + } + + /** + * Auxiliary Heat Status Test + */ + @Test + public void testGetAuxHeat() { + assertEquals(false, response.getAuxHeat()); + } + + /** + * Eco Mode Test + */ + @Test + public void testGetEcoMode() { + assertEquals(false, response.getEcoMode()); + } + + /** + * Sleep Function Test + */ + @Test + public void testGetSleepFunction() { + assertEquals(false, response.getSleepFunction()); + } + + /** + * Turbo Mode Test + */ + @Test + public void testGetTurboMode() { + assertEquals(false, response.getTurboMode()); + } + + /** + * Fahrenheit Display Test + */ + @Test + public void testGetFahrenheit() { + assertEquals(true, response.getFahrenheit()); + } + + /** + * Indoor Temperature Test + */ + @Test + public void testGetIndoorTemperature() { + assertEquals(23, response.getIndoorTemperature()); + } + + /** + * Outdoor Temperature Test + */ + @Test + public void testGetOutdoorTemperature() { + assertEquals(0, response.getOutdoorTemperature()); + } + + /** + * LED Display Test + */ + @Test + public void testDisplayOn() { + assertEquals(false, response.getDisplayOn()); + } + + /** + * Humidity Test + */ + @Test + public void testGetHumidity() { + assertEquals(50, response.getHumidity()); + } + + /** + * Alternate Target temperature Test + */ + @Test + public void testAlternateTargetTemperature() { + assertEquals(24, response.getAlternateTargetTemperature()); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 22b817ff3ca64..fd25a11558232 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -265,6 +265,7 @@ org.openhab.binding.meteostick org.openhab.binding.metofficedatahub org.openhab.binding.mffan + org.openhab.binding.mideaac org.openhab.binding.miele org.openhab.binding.mielecloud org.openhab.binding.mihome From 2abb2111f5799084e1375064fa6e3207fa694cf7 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 6 Nov 2024 11:08:59 -0500 Subject: [PATCH 02/44] Working version of split connection manager Working (sort of) version of split connection manager. Problem with the connection manager being null when the binding is stop/start. Need reset to clear with clean-cache too. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 1 - .../internal/MideaACConfiguration.java | 27 +- .../internal/MideaACHandlerFactory.java | 4 +- .../internal/connection/CommandHelper.java | 438 +++++ .../connection/ConnectionManager.java | 555 +++++++ .../MideaAuthenticationException.java | 39 + .../exception/MideaConnectionException.java | 39 + .../connection/exception/MideaException.java | 39 + .../mideaac/internal/handler/Callback.java | 26 + .../internal/handler/MideaACHandler.java | 1451 +++-------------- .../mideaac/internal/handler/Packet.java | 13 +- .../resources/OH-INF/thing/thing-types.xml | 8 - .../internal/MideaACConfigurationTest.java | 6 +- 13 files changed, 1354 insertions(+), 1292 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 47450dadf0450..206126153a12e 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -64,7 +64,6 @@ Following channels are available: | off-timer | String | Sets the future time to turn off the AC. | | Yes | | screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | | humidity | Number | If device supports, the indoor humidity. | Yes | Yes | -| dropped-commands | Number | Quality of WiFi connections - For debugging only. | Yes | Yes | | appliance-error | Switch | If device supports, appliance error | Yes | Yes | | auxiliary-heat | Switch | If device supports, auxiliary heat | Yes | Yes | | alternate-target-temperature | Number:Temperature | Alternate Target Temperature - not currently used | Yes | Yes | diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 12667638da4ad..e19e43335028f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -25,7 +25,7 @@ public class MideaACConfiguration { public String ipAddress = ""; - public String ipPort = "6444"; + public int ipPort = 6444; public String deviceId = ""; @@ -45,7 +45,7 @@ public class MideaACConfiguration { public boolean promptTone; - public String version = ""; + public int version = 0; /** * Check during initialization that the params are valid @@ -53,7 +53,7 @@ public class MideaACConfiguration { * @return true(valid), false (not valid) */ public boolean isValid() { - return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort.isBlank() || ipAddress.isBlank()); + return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank()); } /** @@ -62,7 +62,26 @@ public boolean isValid() { * @return true(discovery needed), false (not needed) */ public boolean isDiscoveryNeeded() { - return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort.isBlank() || ipAddress.isBlank() + return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() || !Utils.validateIP(ipAddress)); } + + /** + * Check during initialization if key and token can be obtained + * from the cloud. + * + * @return true (yes they can), false (they cannot) + */ + public boolean isTokenKeyObtainable() { + return (!email.isBlank() && !password.isBlank() && !"".equals(cloud)); + } + + /** + * Check during initialization if cloud, key and token are true for v3 + * + * @return true (Valid, all items are present) false (key, token and/or provider missing) + */ + public boolean isV3ConfigValid() { + return (!key.isBlank() && !token.isBlank() && !"".equals(cloud)); + } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index bea42f4ff5617..759418a7c29cf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -39,9 +39,9 @@ @Component(configurationPid = "binding.mideaac", service = ThingHandlerFactory.class) public class MideaACHandlerFactory extends BaseThingHandlerFactory { - private UnitProvider unitProvider; private final HttpClientFactory httpClientFactory; private final CloudsDTO clouds; + private final UnitProvider unitProvider; @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { @@ -56,8 +56,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { */ @Activate public MideaACHandlerFactory(@Reference UnitProvider unitProvider, @Reference HttpClientFactory httpClientFactory) { - this.unitProvider = unitProvider; this.httpClientFactory = httpClientFactory; + this.unitProvider = unitProvider; clouds = new CloudsDTO(); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java new file mode 100644 index 0000000000000..61d00d9f02cec --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -0,0 +1,438 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.connection; + +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; +import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; +import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; +import org.openhab.binding.mideaac.internal.handler.CommandSet; +import org.openhab.binding.mideaac.internal.handler.Response; +import org.openhab.binding.mideaac.internal.handler.Timer; +import org.openhab.binding.mideaac.internal.handler.Timer.TimeParser; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.ImperialUnits; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link CommandHelper} is a static class that is able to translate {@link Command} to {@link CommandSet} + * + * @author Leo Siepel - Initial contribution + */ + +@NonNullByDefault +public class CommandHelper { + private static Logger logger = LoggerFactory.getLogger(CommandHelper.class); + + private static final StringType OPERATIONAL_MODE_OFF = new StringType("OFF"); + private static final StringType OPERATIONAL_MODE_AUTO = new StringType("AUTO"); + private static final StringType OPERATIONAL_MODE_COOL = new StringType("COOL"); + private static final StringType OPERATIONAL_MODE_DRY = new StringType("DRY"); + private static final StringType OPERATIONAL_MODE_HEAT = new StringType("HEAT"); + private static final StringType OPERATIONAL_MODE_FAN_ONLY = new StringType("FAN_ONLY"); + + private static final StringType FAN_SPEED_OFF = new StringType("OFF"); + private static final StringType FAN_SPEED_SILENT = new StringType("SILENT"); + private static final StringType FAN_SPEED_LOW = new StringType("LOW"); + private static final StringType FAN_SPEED_MEDIUM = new StringType("MEDIUM"); + private static final StringType FAN_SPEED_HIGH = new StringType("HIGH"); + private static final StringType FAN_SPEED_FULL = new StringType("FULL"); + private static final StringType FAN_SPEED_AUTO = new StringType("AUTO"); + + private static final StringType SWING_MODE_OFF = new StringType("OFF"); + private static final StringType SWING_MODE_VERTICAL = new StringType("VERTICAL"); + private static final StringType SWING_MODE_HORIZONTAL = new StringType("HORIZONTAL"); + private static final StringType SWING_MODE_BOTH = new StringType("BOTH"); + + /** + * Device Power ON OFF + * + * @param command On or Off + */ + public static CommandSet handlePower(Command command, Response lastResponse) throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command.equals(OnOffType.OFF)) { + commandSet.setPowerState(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setPowerState(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown power command: {}", command)); + } + return commandSet; + } + + /** + * Supported AC - Heat Pump modes + * + * @param command Operational Mode Cool, Heat, etc. + */ + public static CommandSet handleOperationalMode(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command instanceof StringType) { + if (command.equals(OPERATIONAL_MODE_OFF)) { + commandSet.setPowerState(false); + } else if (command.equals(OPERATIONAL_MODE_AUTO)) { + commandSet.setOperationalMode(OperationalMode.AUTO); + } else if (command.equals(OPERATIONAL_MODE_COOL)) { + commandSet.setOperationalMode(OperationalMode.COOL); + } else if (command.equals(OPERATIONAL_MODE_DRY)) { + commandSet.setOperationalMode(OperationalMode.DRY); + } else if (command.equals(OPERATIONAL_MODE_HEAT)) { + commandSet.setOperationalMode(OperationalMode.HEAT); + } else if (command.equals(OPERATIONAL_MODE_FAN_ONLY)) { + commandSet.setOperationalMode(OperationalMode.FAN_ONLY); + } else { + throw new UnsupportedOperationException(String.format("Unknown operational mode command: {}", command)); + } + } + return commandSet; + } + + private static float limitTargetTemperatureToRange(float temperatureInCelsius) { + if (temperatureInCelsius < 17.0f) { + return 17.0f; + } + if (temperatureInCelsius > 30.0f) { + return 30.0f; + } + + return temperatureInCelsius; + } + + /** + * Device only uses Celsius in 0.5 degree increments + * Fahrenheit is rounded to fit (example + * setting to 64 F is 18 C but will result in 64.4 F display in OH) + * The evaporator only displays 2 digits, so will show 64. + * + * @param command Target Temperature + */ + public static CommandSet handleTargetTemperature(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command instanceof DecimalType decimalCommand) { + logger.debug("Handle Target Temperature as DecimalType in degrees C"); + commandSet.setTargetTemperature(limitTargetTemperatureToRange(decimalCommand.floatValue())); + } else if (command instanceof QuantityType quantityCommand) { + if (quantityCommand.getUnit().equals(ImperialUnits.FAHRENHEIT)) { + quantityCommand = Objects.requireNonNull(quantityCommand.toUnit(SIUnits.CELSIUS)); + } + commandSet.setTargetTemperature(limitTargetTemperatureToRange(quantityCommand.floatValue())); + } else { + throw new UnsupportedOperationException(String.format("Unknown target temperature command: {}", command)); + } + return commandSet; + } + + /** + * Fan Speeds vary by V2 or V3 and device. This command also turns the power ON + * + * @param command Fan Speed Auto, Low, High, etc. + */ + public static CommandSet handleFanSpeed(Command command, Response lastResponse, int version) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command instanceof StringType) { + commandSet.setPowerState(true); + if (command.equals(FAN_SPEED_OFF)) { + commandSet.setPowerState(false); + } else if (command.equals(FAN_SPEED_SILENT)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.SILENT2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.SILENT3); + } + } else if (command.equals(FAN_SPEED_LOW)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.LOW2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.LOW3); + } + } else if (command.equals(FAN_SPEED_MEDIUM)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.MEDIUM2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.MEDIUM3); + } + } else if (command.equals(FAN_SPEED_HIGH)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.HIGH2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.HIGH3); + } + } else if (command.equals(FAN_SPEED_FULL)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.FULL2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.FULL3); + } + } else if (command.equals(FAN_SPEED_AUTO)) { + if (version == 2) { + commandSet.setFanSpeed(FanSpeed.AUTO2); + } else if (version == 3) { + commandSet.setFanSpeed(FanSpeed.AUTO3); + } + } else { + throw new UnsupportedOperationException(String.format("Unknown fan speed command: {}", command)); + } + } + return commandSet; + } + + /** + * Must be set in Cool mode. Fan will switch to Auto + * and temp will be 24 C or 75 F on unit (75.2 F in OH) + * + * @param command Eco Mode + */ + public static CommandSet handleEcoMode(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command.equals(OnOffType.OFF)) { + commandSet.setEcoMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setEcoMode(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown eco mode command: {}", command)); + } + + return commandSet; + } + + /** + * Modes supported depends on the device + * Power is turned on when swing mode is changed + * + * @param command Swing Mode + */ + public static CommandSet handleSwingMode(Command command, Response lastResponse, int version) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + commandSet.setPowerState(true); + + if (command instanceof StringType) { + if (command.equals(SWING_MODE_OFF)) { + if (version == 2) { + commandSet.setSwingMode(SwingMode.OFF2); + } else if (version == 3) { + commandSet.setSwingMode(SwingMode.OFF3); + } + } else if (command.equals(SWING_MODE_VERTICAL)) { + if (version == 2) { + commandSet.setSwingMode(SwingMode.VERTICAL2); + } else if (version == 3) { + commandSet.setSwingMode(SwingMode.VERTICAL3); + } + } else if (command.equals(SWING_MODE_HORIZONTAL)) { + if (version == 2) { + commandSet.setSwingMode(SwingMode.HORIZONTAL2); + } else if (version == 3) { + commandSet.setSwingMode(SwingMode.HORIZONTAL3); + } + } else if (command.equals(SWING_MODE_BOTH)) { + if (version == 2) { + commandSet.setSwingMode(SwingMode.BOTH2); + } else if (version == 3) { + commandSet.setSwingMode(SwingMode.BOTH3); + } + } else { + throw new UnsupportedOperationException(String.format("Unknown swing mode command: {}", command)); + } + } + + return commandSet; + } + + /** + * Turbo mode is only with Heat or Cool to quickly change + * Room temperature. Power is turned on. + * + * @param command Turbo mode - Fast cooling or Heating + */ + public static CommandSet handleTurboMode(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + commandSet.setPowerState(true); + + if (command.equals(OnOffType.OFF)) { + commandSet.setTurboMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setTurboMode(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown turbo mode command: {}", command)); + } + + return commandSet; + } + + /** + * May not be supported via LAN in all models - IR only + * + * @param command Screen Display Toggle to ON or Off - One command + */ + public static CommandSet handleScreenDisplay(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command.equals(OnOffType.OFF)) { + commandSet.setScreenDisplay(true); + } else if (command.equals(OnOffType.ON)) { + commandSet.setScreenDisplay(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown screen display command: {}", command)); + } + + return commandSet; + } + + /** + * This is only for the AC LED device display units, calcs always in Celsius + * + * @param command Temp unit on the indoor evaporator + */ + public static CommandSet handleTempUnit(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command.equals(OnOffType.OFF)) { + commandSet.setFahrenheit(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setFahrenheit(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown temperature unit command: {}", command)); + } + + return commandSet; + } + + /** + * Power turned on with Sleep Mode Change + * Sleep mode increases temp slightly in first 2 hours of sleep + * + * @param command Sleep function + */ + public static CommandSet handleSleepFunction(Command command, Response lastResponse) + throws UnsupportedOperationException { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + commandSet.setPowerState(true); + + if (command.equals(OnOffType.OFF)) { + commandSet.setSleepMode(false); + } else if (command.equals(OnOffType.ON)) { + commandSet.setSleepMode(true); + } else { + throw new UnsupportedOperationException(String.format("Unknown sleep mode command: {}", command)); + } + + return commandSet; + } + + /** + * Sets the time (from now) that the device will turn on at it's current settings + * + * @param command Sets On Timer + */ + public static CommandSet handleOnTimer(Command command, Response lastResponse) { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + TimeParser timeParser = timer.new TimeParser(); + if (command instanceof StringType) { + String timeString = ((StringType) command).toString(); + if (!timeString.matches("\\d{2}:\\d{2}")) { + logger.debug("Invalid time format. Expected HH:MM."); + commandSet.setOnTimer(false, hours, minutes); + } else { + int[] timeParts = timeParser.parseTime(timeString); + boolean on = true; + hours = timeParts[0]; + minutes = timeParts[1]; + // Validate minutes and hours + if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { + logger.debug("Invalid hours (24 max) and or minutes (59 max)"); + hours = 0; + minutes = 0; + } + if (hours == 0 && minutes == 0) { + commandSet.setOnTimer(false, hours, minutes); + } else { + commandSet.setOnTimer(on, hours, minutes); + } + } + } else { + logger.debug("Command must be of type StringType: {}", command); + commandSet.setOnTimer(false, hours, minutes); + } + + return commandSet; + } + + /** + * Sets the time (from now) that the device will turn off + * + * @param command Sets Off Timer + */ + public static CommandSet handleOffTimer(Command command, Response lastResponse) { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + int hours = 0; + int minutes = 0; + Timer timer = new Timer(true, hours, minutes); + TimeParser timeParser = timer.new TimeParser(); + if (command instanceof StringType) { + String timeString = ((StringType) command).toString(); + if (!timeString.matches("\\d{2}:\\d{2}")) { + logger.debug("Invalid time format. Expected HH:MM."); + commandSet.setOffTimer(false, hours, minutes); + } else { + int[] timeParts = timeParser.parseTime(timeString); + boolean on = true; + hours = timeParts[0]; + minutes = timeParts[1]; + // Validate minutes and hours + if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { + logger.debug("Invalid hours (24 max) and or minutes (59 max)"); + hours = 0; + minutes = 0; + } + if (hours == 0 && minutes == 0) { + commandSet.setOffTimer(false, hours, minutes); + } else { + commandSet.setOffTimer(on, hours, minutes); + } + } + } else { + logger.debug("Command must be of type StringType: {}", command); + commandSet.setOffTimer(false, hours, minutes); + } + + return commandSet; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java new file mode 100644 index 0000000000000..accbb93b82015 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -0,0 +1,555 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.connection; + +import java.io.ByteArrayInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException; +import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException; +import org.openhab.binding.mideaac.internal.connection.exception.MideaException; +import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.openhab.binding.mideaac.internal.handler.Callback; +import org.openhab.binding.mideaac.internal.handler.CommandBase; +import org.openhab.binding.mideaac.internal.handler.CommandSet; +import org.openhab.binding.mideaac.internal.handler.Packet; +import org.openhab.binding.mideaac.internal.handler.Response; +import org.openhab.binding.mideaac.internal.security.Decryption8370Result; +import org.openhab.binding.mideaac.internal.security.Security; +import org.openhab.binding.mideaac.internal.security.Security.MsgType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the + * indoor AC unit evaporator. + * + * @author Jacek Dobrowolski - Initial Contribution + * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command + * + * This gets around the issue that any command needs to be within 30 seconds of the authorization + * in testing this only adds 50 ms, but allows polls at longer intervals + */ +@NonNullByDefault +public class ConnectionManager { + private Logger logger = LoggerFactory.getLogger(ConnectionManager.class); + + private final String ipAddress; + private final int ipPort; + private final int timeout; + private String key; + private String token; + private final String cloud; + private final String deviceId; + private Response lastResponse; + private CloudProviderDTO cloudProvider; + private Security security; + private final int version; + private final boolean promptTone; + + /** + * True allows one short retry after connection problem + */ + private boolean retry = true; + + /** + * Suppresses the connection message if was online before + */ + private boolean connectionMessage = true; + + public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud, + String email, String password, String deviceId, int version, boolean promptTone) { + this.deviceIsConnected = false; + this.ipAddress = ipAddress; + this.ipPort = ipPort; + this.timeout = timeout; + this.key = key; + this.token = token; + this.cloud = cloud; + this.deviceId = deviceId; + this.version = version; + this.promptTone = promptTone; + this.lastResponse = new Response(HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"), + version, "query", (byte) 0xc0); + this.cloudProvider = CloudProviderDTO.getCloudProvider(cloud); + this.security = new Security(cloudProvider); + } + + private boolean deviceIsConnected; + private int droppedCommands = 0; + + private Socket socket = new Socket(); + private InputStream inputStream = new ByteArrayInputStream(new byte[0]); + private DataOutputStream writer = new DataOutputStream(System.out); + + /** + * Gets last response + * + * @return byte array of last response + */ + public Response getLastResponse() { + return this.lastResponse; + } + + /** + * Validate if String is blank + * + * @param str string to be evaluated + * @return boolean true or false + */ + public static boolean isBlank(String str) { + return str.trim().isEmpty(); + } + + /** + * Reset dropped commands from initialization in MideaACHandler + * Channel created for easy observation + * Dropped commands when no bytes to read after two tries or other + * byte reading problem. Device not responding. + */ + public void resetDroppedCommands() { + droppedCommands = 0; + } + + /** + * Resets Dropped command + * + * @return dropped commands + */ + public int getDroppedCommands() { + return droppedCommands = 0; + } + + /** + * After checking if the key and token need to be updated (Default = 0 Never) + * The socket is established with the writer and inputStream (for reading responses) + * The device is considered connected. V2 devices will proceed to send the poll or the + * set command. V3 devices will proceed to authenticate + */ + public synchronized void connect() throws MideaConnectionException, MideaAuthenticationException { + logger.trace("Connecting to {}:{}", ipAddress, ipPort); + + // Open socket + try { + socket = new Socket(); + socket.setSoTimeout(timeout * 1000); + socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); + } catch (IOException e) { + logger.debug("IOException connecting to {}: {}", ipAddress, e.getMessage()); + deviceIsConnected = false; + if (retry) { + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + logger.debug("An interupted error (pause) has occured {}", ex.getMessage()); + } + connect(); + } + throw new MideaConnectionException(e); + } + + // Create streams + try { + writer = new DataOutputStream(socket.getOutputStream()); + inputStream = socket.getInputStream(); + } catch (IOException e) { + logger.debug("IOException getting streams for {}: {}", ipAddress, e.getMessage(), e); + deviceIsConnected = false; + throw new MideaConnectionException(e); + } + if (!deviceIsConnected || !connectionMessage) { + logger.info("Connected to IP {}", ipAddress); + resetConnectionMessage(); + } + logger.debug("Connected to IP {}", ipAddress); + deviceIsConnected = true; + resetRetry(); + + if (version == 3) { + logger.debug("Device {} require authentication, going to authenticate", ipAddress); + try { + authenticate(); + } catch (MideaAuthenticationException | MideaConnectionException e) { + deviceIsConnected = false; + throw e; + } + } + // requestStatus(getDoPoll()); + deviceIsConnected = true; + } + + /** + * For V3 devices only. This method checks for the Cloud Provider + * key and token (and goes offline if any are missing). It will retrieve the + * missing key and/or token if the account email and password are provided. + * + * @throws MideaAuthenticationException + * @throws MideaConnectionException + */ + public void authenticate() throws MideaConnectionException, MideaAuthenticationException { + logger.trace("Key: {}", key); + logger.trace("Token: {}", token); + logger.trace("Cloud {}", cloud); + + if (!isBlank(token) && !isBlank(key) && !"".equals(cloud)) { + logger.debug("Device {} authenticating", ipAddress); + doV3Handshake(); + } else { + throw new MideaAuthenticationException("Token, Key and / or cloud provider missing"); + } + } + + /** + * Sends the Handshake Request to the V3 device. Generally quick response + * Without the 1000 ms sleep delay there are problems in sending the Poll/Command + * Suspect that the socket write and read streams need a moment to clear + * as they will be reused in the SendCommand method + */ + private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException { + byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST); + try { + logger.trace("Device {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request)); + + write(request); + byte[] response = read(); + + if (response != null && response.length > 0) { + logger.trace("Device {} response for handshake_request length: {}", ipAddress, response.length); + if (response.length == 72) { + boolean success = security.tcpKey(Arrays.copyOfRange(response, 8, 72), + Utils.hexStringToByteArray(key)); + if (success) { + logger.debug("Authentication successful"); + // Altering the sleep caused or can cause write errors problems. Use caution. + // At 500 ms the first write usually fails. Works, but no backup + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + logger.debug("An interupted error (success) has occured {}", e.getMessage()); + } + // requestStatus(getDoPoll()); need to handle + } else { + throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); + } + } else if (Arrays.equals(new String("ERROR").getBytes(), response)) { + throw new MideaAuthenticationException("Authentication failed!"); + } else { + logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", response.length); + throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); + } + } + } catch (IOException e) { + throw new MideaConnectionException(e); + } + } + + /** + * Sends the routine polling command from the DoPoll + * in the MideaACHandler + * + * @param callback + * @throws MideaConnectionException + * @throws MideaAuthenticationException + * @throws MideaException + */ + public void getStatus(Callback callback) + throws MideaConnectionException, MideaAuthenticationException, MideaException { + CommandBase requestStatusCommand = new CommandBase(); + sendCommand(requestStatusCommand, callback); + } + + private void ensureConnected() throws MideaConnectionException, MideaAuthenticationException { + disconnect(); + connect(); + } + + /** + * Pulls the packet byte array together. There is a check to + * make sure to make sure the input stream is empty before sending + * the new command and another check if input stream is empty after 1.5 seconds. + * Normal device response in 0.75 - 1 second range + * If still empty, send the bytes again. If there are bytes, the read method is called. + * If the socket times out with no response the command is dropped. There will be another poll + * in the time set by the user (30 seconds min) or the set command can be retried + * + * @param command either the set or polling command + * @throws MideaAuthenticationException + * @throws MideaConnectionException + */ + public synchronized void sendCommand(CommandBase command, @Nullable Callback callback) + throws MideaConnectionException, MideaAuthenticationException { + ensureConnected(); + + if (command instanceof CommandSet) { + ((CommandSet) command).setPromptTone(promptTone); + } + Packet packet = new Packet(command, deviceId, security); + packet.compose(); + + try { + byte[] bytes = packet.getBytes(); + logger.debug("Writing to {} bytes.length: {}", ipAddress, bytes.length); + + if (version == 3) { + bytes = security.encode8370(bytes, MsgType.MSGTYPE_ENCRYPTED_REQUEST); + } + + // Ensure input stream is empty before writing packet + if (inputStream.available() == 0) { + logger.debug("Input stream empty sending write {}", command); + write(bytes); + } + + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); + Thread.currentThread().interrupt(); + // Note, but continue anyway. Command will be dropped + } + + if (inputStream.available() == 0) { + logger.debug("Input stream empty sending second write {}", command); + write(bytes); + } + + // Socket timeout (UI parameter) 2 seconds minimum up to 10 seconds. + byte[] responseBytes = read(); + + if (responseBytes != null) { + if (version == 3) { + Decryption8370Result result = security.decode8370(responseBytes); + for (byte[] response : result.getResponses()) { + logger.debug("Response length:{} IP address:{} ", response.length, ipAddress); + if (response.length > 40 + 16) { + byte[] data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); + + logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + byte bodyType2 = data[0xa]; + + // data[3]: Device Type - 0xAC = AC + // https://github.com/georgezhao2010/midea_ac_lan/blob/06fc4b582a012bbbfd6bd5942c92034270eca0eb/custom_components/midea_ac_lan/midea_devices.py#L96 + + // data[9]: MessageType - set, query, notify1, notify2, exception, querySN, exception2, + // querySubtype + // https://github.com/georgezhao2010/midea_ac_lan/blob/30d0ff5ff14f150da10b883e97b2f280767aa89a/custom_components/midea_ac_lan/midea/core/message.py#L22-L29 + String responseType = ""; + switch (data[0x9]) { + case 0x02: + responseType = "set"; + break; + case 0x03: + responseType = "query"; + break; + case 0x04: + responseType = "notify1"; + break; + case 0x05: + responseType = "notify2"; + break; + case 0x06: + responseType = "exception"; + break; + case 0x07: + responseType = "querySN"; + break; + case 0x0A: + responseType = "exception2"; + break; + case 0x09: // Helyesen: 0xA0 + responseType = "querySubtype"; + break; + default: + logger.debug("Invalid response type: {}", data[0x9]); + } + logger.trace("Response Type: {} and bodyType:{}", responseType, bodyType2); + + // The response data from the appliance includes a packet header which we don't want + data = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = data[0x0]; + logger.trace("Response Type expected: {} and bodyType:{}", responseType, bodyType); + logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToHex(data)); + logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToBinary(data)); + + if (data.length > 0) { + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + if (bodyType != -64) { + if (bodyType == 30) { + logger.warn("Error response 0x1E received {} from IP Address {}", bodyType, + ipAddress); + return; + } + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, version, responseType, bodyType); + try { + logger.trace("data length is {} version is {} IP address is {}", data.length, + version, ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); + } + } catch (Exception ex) { + logger.warn("Processing response exception: {}", ex.getMessage()); + } + } + } + } + } else { + byte[] data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); + // The response data from the appliance includes a packet header which we don't want + logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + if (data.length > 0) { + data = Arrays.copyOfRange(data, 10, data.length); + logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + + lastResponse = new Response(data, version, "", (byte) 0x00); + logger.debug("V2 data length is {} version is {} Ip Address is {}", data.length, version, + ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); + } + } else { + droppedCommands = droppedCommands + 1; + logger.debug("Problem with reading V2 response, skipping command {} dropped count{}", command, + droppedCommands); + } + } + return; + } else { + droppedCommands = droppedCommands + 1; + logger.debug("Problem with reading response, skipping command {} dropped count{}", command, + droppedCommands); + return; + } + } catch (SocketException e) { + logger.debug("SocketException writing to {}: {}", ipAddress, e.getMessage()); + droppedCommands = droppedCommands + 1; + logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); + throw new MideaConnectionException(e); + } catch (IOException e) { + logger.debug(" Send IOException writing to {}: {}", ipAddress, e.getMessage()); + droppedCommands = droppedCommands + 1; + logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); + throw new MideaConnectionException(e); + } + } + + /** + * Closes all elements of the connection before starting a new one + */ + public synchronized void disconnect() { + // Make sure writer, inputStream and socket are closed before each command is started + logger.debug("Disconnecting from device at {}", ipAddress); + + InputStream inputStream = this.inputStream; + DataOutputStream writer = this.writer; + Socket socket = this.socket; + try { + writer.close(); + inputStream.close(); + socket.close(); + + } catch (IOException e) { + logger.warn("IOException closing connection to device at {}: {}", ipAddress, e.getMessage(), e); + } + socket = null; + inputStream = null; + writer = null; + } + + /** + * Reads the inputStream byte array + * + * @return byte array + */ + public synchronized byte @Nullable [] read() { + byte[] bytes = new byte[512]; + InputStream inputStream = this.inputStream; + + try { + int len = inputStream.read(bytes); + if (len > 0) { + logger.debug("Response received length: {} Device IP {}", len, ipAddress); + bytes = Arrays.copyOfRange(bytes, 0, len); + return bytes; + } + } catch (IOException e) { + String message = e.getMessage(); + logger.debug(" Byte read exception {}", message); + } + return null; + } + + /** + * Writes the packet that will be sent to the device + * + * @param buffer socket writer + * @throws IOException writer could be null + */ + public synchronized void write(byte[] buffer) throws IOException { + DataOutputStream writer = this.writer; + + try { + writer.write(buffer, 0, buffer.length); + } catch (IOException e) { + String message = e.getMessage(); + logger.debug("Write error {}", message); + } + } + + /** + * Reset Retry controls the short 5 second delay + * Before starting 30 second delays. (More severe Wifi issue) + * It is reset after a successful connection + */ + private void resetRetry() { + retry = true; + } + + /** + * Limit logging of INFO connection messages to + * only when the device was Offline in its prior + * state + */ + private void resetConnectionMessage() { + connectionMessage = true; + } + + /** + * Disconnects from the device + * + * @param force + */ + public void dispose(boolean force) { + disconnect(); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java new file mode 100644 index 0000000000000..db8b0ce1d9791 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.connection.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MideaAuthenticationException} represents a binding specific {@link Exception}. + * + * @author Leo Siepel - Initial contribution + */ + +@NonNullByDefault +public class MideaAuthenticationException extends Exception { + + private static final long serialVersionUID = 1L; + + public MideaAuthenticationException(String message) { + super(message); + } + + public MideaAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public MideaAuthenticationException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java new file mode 100644 index 0000000000000..efbf5129f8313 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.connection.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MideaConnectionException} represents a binding specific {@link Exception}. + * + * @author Leo Siepel - Initial contribution + */ + +@NonNullByDefault +public class MideaConnectionException extends Exception { + + private static final long serialVersionUID = 1L; + + public MideaConnectionException(String message) { + super(message); + } + + public MideaConnectionException(String message, Throwable cause) { + super(message, cause); + } + + public MideaConnectionException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java new file mode 100644 index 0000000000000..44510e573a8a7 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.connection.exception; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MideaException} represents a binding specific {@link Exception}. + * + * @author Leo Siepel - Initial contribution + */ + +@NonNullByDefault +public class MideaException extends Exception { + + private static final long serialVersionUID = 1L; + + public MideaException(String message) { + super(message); + } + + public MideaException(String message, Throwable cause) { + super(message, cause); + } + + public MideaException(Throwable cause) { + super(cause); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java new file mode 100644 index 0000000000000..98f8464b3e075 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link Response} performs the byte data stream decoding + * + * @author Leo Siepel - Initial contribution + */ +@NonNullByDefault +public interface Callback { + + void updateChannels(Response response); +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 444d13ee74bd6..aad6aeb15dce6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -14,42 +14,30 @@ import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; -import java.io.ByteArrayInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketException; -import java.util.Arrays; import java.util.HashMap; -import java.util.HexFormat; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import javax.measure.Unit; import javax.measure.quantity.Temperature; -import javax.measure.spi.SystemOfUnits; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.mideaac.internal.MideaACConfiguration; -import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.connection.CommandHelper; +import org.openhab.binding.mideaac.internal.connection.ConnectionManager; +import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException; +import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException; +import org.openhab.binding.mideaac.internal.connection.exception.MideaException; import org.openhab.binding.mideaac.internal.discovery.DiscoveryHandler; import org.openhab.binding.mideaac.internal.discovery.MideaACDiscoveryService; import org.openhab.binding.mideaac.internal.dto.CloudDTO; import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; import org.openhab.binding.mideaac.internal.dto.CloudsDTO; -import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; -import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; -import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; -import org.openhab.binding.mideaac.internal.handler.Timer.TimeParser; -import org.openhab.binding.mideaac.internal.security.Decryption8370Result; -import org.openhab.binding.mideaac.internal.security.Security; -import org.openhab.binding.mideaac.internal.security.Security.MsgType; import org.openhab.binding.mideaac.internal.security.TokenKey; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; @@ -79,129 +67,40 @@ * @author Jacek Dobrowolski - Initial contribution * @author Justan Oldman - Last Response added * @author Bob Eckhoff - Longer Polls and OH developer guidelines - * + * @author Leo Siepel - Refactored class, improved seperation of concerns */ @NonNullByDefault public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler { private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); + private final CloudsDTO clouds; + private final boolean imperialUnits; + private final HttpClient httpClient; private MideaACConfiguration config = new MideaACConfiguration(); private Map properties = new HashMap<>(); + private @Nullable ConnectionManager connectionManager; + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private @Nullable ScheduledFuture scheduledTask = null; - // Initialize variables to allow the @NonNullByDefault check - private String ipAddress = ""; - private String ipPort = ""; - private String deviceId = ""; - private int version = 3; - - /** - * Create new nonnull cloud provider to start - */ - public CloudProviderDTO cloudProvider = new CloudProviderDTO("", "", "", "", "", "", "", ""); - private Security security = new Security(cloudProvider); - - /** - * Gets the users Cloud provider - * - * @return cloud Provider - */ - public CloudProviderDTO getCloudProvider() { - return cloudProvider; - } - - /** - * Gets the Security class - * - * @return security - */ - public Security getSecurity() { - return security; - } - - /** - * Gets the Device Version (2 or 3) - * - * @return version - */ - public int getVersion() { - return version; - } - - /** - * Set the device version - * - * @param version device version - */ - public void setVersion(int version) { - this.version = version; - } - - private static final StringType OPERATIONAL_MODE_OFF = new StringType("OFF"); - private static final StringType OPERATIONAL_MODE_AUTO = new StringType("AUTO"); - private static final StringType OPERATIONAL_MODE_COOL = new StringType("COOL"); - private static final StringType OPERATIONAL_MODE_DRY = new StringType("DRY"); - private static final StringType OPERATIONAL_MODE_HEAT = new StringType("HEAT"); - private static final StringType OPERATIONAL_MODE_FAN_ONLY = new StringType("FAN_ONLY"); - - private static final StringType FAN_SPEED_OFF = new StringType("OFF"); - private static final StringType FAN_SPEED_SILENT = new StringType("SILENT"); - private static final StringType FAN_SPEED_LOW = new StringType("LOW"); - private static final StringType FAN_SPEED_MEDIUM = new StringType("MEDIUM"); - private static final StringType FAN_SPEED_HIGH = new StringType("HIGH"); - private static final StringType FAN_SPEED_FULL = new StringType("FULL"); - private static final StringType FAN_SPEED_AUTO = new StringType("AUTO"); - - private static final StringType SWING_MODE_OFF = new StringType("OFF"); - private static final StringType SWING_MODE_VERTICAL = new StringType("VERTICAL"); - private static final StringType SWING_MODE_HORIZONTAL = new StringType("HORIZONTAL"); - private static final StringType SWING_MODE_BOTH = new StringType("BOTH"); - private CloudsDTO clouds; - - private ConnectionManager connectionManager; - - private final SystemOfUnits systemOfUnits; - - private final HttpClient httpClient; - - /** - * Set to false when Set Command recieved to speed response - */ - public boolean doPoll = true; - - /** - * True allows one short retry after connection problem - */ - public boolean retry = true; - - /** - * Suppresses the connection message if was online before - */ - public boolean connectionMessage = true; - - private ConnectionManager getConnectionManager() { - return connectionManager; - } - - private Response getLastResponse() { - return getConnectionManager().getLastResponse(); - } + private Callback callbackLambda = (response) -> { + this.updateChannels(response); + }; /** * Initial creation of the Midea AC Handler * - * @param thing thing name + * @param thing Thing * @param unitProvider OH core unit provider * @param httpClient http Client - * @param clouds cloud + * @param clouds CloudsDTO */ public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient, CloudsDTO clouds) { super(thing); this.thing = thing; - this.systemOfUnits = unitProvider.getMeasurementSystem(); + this.imperialUnits = unitProvider.getMeasurementSystem() instanceof ImperialUnits; this.httpClient = httpClient; this.clouds = clouds; - connectionManager = new ConnectionManager(this); } /** @@ -213,10 +112,6 @@ public CloudsDTO getClouds() { return clouds; } - protected boolean isImperial() { - return systemOfUnits instanceof ImperialUnits ? true : false; - } - /** * This method handles the Channels that can be set (non-read only) * First the Routine polling is stopped so there is no conflict @@ -226,443 +121,57 @@ protected boolean isImperial() { @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString()); - connectionManager.disconnect(); - getConnectionManager().cancelConnectionMonitorJob(); - - /** - * Alternate to routine polling; Use rule to refresh at the desired interval - */ - if (command instanceof RefreshType) { - connectionManager.connect(); + ConnectionManager connectionManager = this.connectionManager; + if (connectionManager == null) { + logger.warn("The connection manager was unexpectedly null, please report a bug"); return; } - - /** - * @param doPoll is set to skip poll after authorization and go directly - * to command set execution - */ - doPoll = false; - connectionManager.connect(); - - if (channelUID.getId().equals(CHANNEL_POWER)) { - handlePower(command); - } else if (channelUID.getId().equals(CHANNEL_OPERATIONAL_MODE)) { - handleOperationalMode(command); - } else if (channelUID.getId().equals(CHANNEL_TARGET_TEMPERATURE)) { - handleTargetTemperature(command); - } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) { - handleFanSpeed(command); - } else if (channelUID.getId().equals(CHANNEL_ECO_MODE)) { - handleEcoMode(command); - } else if (channelUID.getId().equals(CHANNEL_TURBO_MODE)) { - handleTurboMode(command); - } else if (channelUID.getId().equals(CHANNEL_SWING_MODE)) { - handleSwingMode(command); - } else if (channelUID.getId().equals(CHANNEL_SCREEN_DISPLAY)) { - handleScreenDisplay(command); - } else if (channelUID.getId().equals(CHANNEL_TEMPERATURE_UNIT)) { - handleTempUnit(command); - } else if (channelUID.getId().equals(CHANNEL_SLEEP_FUNCTION)) { - handleSleepFunction(command); - } else if (channelUID.getId().equals(CHANNEL_ON_TIMER)) { - handleOnTimer(command); - } else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) { - handleOffTimer(command); - } - } - - /** - * Device Power ON OFF - * - * @param command On or Off - */ - public void handlePower(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command.equals(OnOffType.OFF)) { - commandSet.setPowerState(false); - } else if (command.equals(OnOffType.ON)) { - commandSet.setPowerState(true); - } else { - logger.debug("Unknown power state command: {}", command); - return; - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Supported AC - Heat Pump modes - * - * @param command Operational Mode Cool, Heat, etc. - */ - public void handleOperationalMode(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command instanceof StringType) { - if (command.equals(OPERATIONAL_MODE_OFF)) { - commandSet.setPowerState(false); - return; - } else if (command.equals(OPERATIONAL_MODE_AUTO)) { - commandSet.setOperationalMode(OperationalMode.AUTO); - } else if (command.equals(OPERATIONAL_MODE_COOL)) { - commandSet.setOperationalMode(OperationalMode.COOL); - } else if (command.equals(OPERATIONAL_MODE_DRY)) { - commandSet.setOperationalMode(OperationalMode.DRY); - } else if (command.equals(OPERATIONAL_MODE_HEAT)) { - commandSet.setOperationalMode(OperationalMode.HEAT); - } else if (command.equals(OPERATIONAL_MODE_FAN_ONLY)) { - commandSet.setOperationalMode(OperationalMode.FAN_ONLY); - } else { - logger.debug("Unknown operational mode command: {}", command); - return; - } - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - private static float convertTargetCelsiusTemperatureToInRange(float temperature) { - if (temperature < 17.0f) { - return 17.0f; - } - if (temperature > 30.0f) { - return 30.0f; - } - - return temperature; - } - - /** - * Device only uses Celsius in 0.5 degree increments - * Fahrenheit is rounded to fit (example - * setting to 64 F is 18 C but will result in 64.4 F display in OH) - * The evaporator only displays 2 digits, so will show 64. - * - * @param command Target Temperature - */ - public void handleTargetTemperature(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command instanceof DecimalType) { - logger.debug("Handle Target Temperature as DecimalType in degrees C"); - commandSet.setTargetTemperature( - convertTargetCelsiusTemperatureToInRange(((DecimalType) command).floatValue())); - getConnectionManager().sendCommandAndMonitor(commandSet); - } else if (command instanceof QuantityType) { - QuantityType quantity = (QuantityType) command; - Unit unit = quantity.getUnit(); - - if (unit.equals(ImperialUnits.FAHRENHEIT) || unit.equals(SIUnits.CELSIUS)) { - logger.debug("Handle Target Temperature with unit {} to degrees C", unit); - if (unit.equals(SIUnits.CELSIUS)) { - commandSet.setTargetTemperature(convertTargetCelsiusTemperatureToInRange(quantity.floatValue())); - } else { - QuantityType celsiusQuantity = quantity.toUnit(SIUnits.CELSIUS); - if (celsiusQuantity != null) { - commandSet.setTargetTemperature( - convertTargetCelsiusTemperatureToInRange(celsiusQuantity.floatValue())); - } else { - logger.warn("Failed to convert quantity to Celsius unit."); - } - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - } else { - logger.debug("Handle Target Temperature unsupported commandType:{}", command.getClass().getTypeName()); - } - } - - /** - * Fan Speeds vary by V2 or V3 and device. This command also turns the power ON - * - * @param command Fan Speed Auto, Low, High, etc. - */ - public void handleFanSpeed(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command instanceof StringType) { - commandSet.setPowerState(true); - if (command.equals(FAN_SPEED_OFF)) { - commandSet.setPowerState(false); - } else if (command.equals(FAN_SPEED_SILENT)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.SILENT2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.SILENT3); - } - } else if (command.equals(FAN_SPEED_LOW)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.LOW2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.LOW3); - } - } else if (command.equals(FAN_SPEED_MEDIUM)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.MEDIUM2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.MEDIUM3); - } - } else if (command.equals(FAN_SPEED_HIGH)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.HIGH2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.HIGH3); - } - } else if (command.equals(FAN_SPEED_FULL)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.FULL2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.FULL3); - } - } else if (command.equals(FAN_SPEED_AUTO)) { - if (getVersion() == 2) { - commandSet.setFanSpeed(FanSpeed.AUTO2); - } else if (getVersion() == 3) { - commandSet.setFanSpeed(FanSpeed.AUTO3); - } - } else { - logger.debug("Unknown fan speed command: {}", command); - return; - } - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Must be set in Cool mode. Fan will switch to Auto - * and temp will be 24 C or 75 F on unit (75.2 F in OH) - * - * @param command Eco Mode - */ - public void handleEcoMode(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command.equals(OnOffType.OFF)) { - commandSet.setEcoMode(false); - } else if (command.equals(OnOffType.ON)) { - commandSet.setEcoMode(true); - } else { - logger.debug("Unknown eco mode command: {}", command); - return; - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Modes supported depends on the device - * Power is turned on when swing mode is changed - * - * @param command Swing Mode - */ - public void handleSwingMode(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - commandSet.setPowerState(true); - - if (command instanceof StringType) { - if (command.equals(SWING_MODE_OFF)) { - if (getVersion() == 2) { - commandSet.setSwingMode(SwingMode.OFF2); - } else if (getVersion() == 3) { - commandSet.setSwingMode(SwingMode.OFF3); - } - } else if (command.equals(SWING_MODE_VERTICAL)) { - if (getVersion() == 2) { - commandSet.setSwingMode(SwingMode.VERTICAL2); - } else if (getVersion() == 3) { - commandSet.setSwingMode(SwingMode.VERTICAL3); - } - } else if (command.equals(SWING_MODE_HORIZONTAL)) { - if (getVersion() == 2) { - commandSet.setSwingMode(SwingMode.HORIZONTAL2); - } else if (getVersion() == 3) { - commandSet.setSwingMode(SwingMode.HORIZONTAL3); - } - } else if (command.equals(SWING_MODE_BOTH)) { - if (getVersion() == 2) { - commandSet.setSwingMode(SwingMode.BOTH2); - } else if (getVersion() == 3) { - commandSet.setSwingMode(SwingMode.BOTH3); - } - } else { - logger.debug("Unknown swing mode command: {}", command); - return; + if (command instanceof RefreshType) { + try { + connectionManager.getStatus(callbackLambda); + } catch (MideaAuthenticationException e) { + logger.warn("Unable to proces command: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (MideaConnectionException | MideaException e) { + logger.warn("Unable to proces command: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Turbo mode is only with Heat or Cool to quickly change - * Room temperature. Power is turned on. - * - * @param command Turbo mode - Fast cooling or Heating - */ - public void handleTurboMode(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - commandSet.setPowerState(true); - - if (command.equals(OnOffType.OFF)) { - commandSet.setTurboMode(false); - } else if (command.equals(OnOffType.ON)) { - commandSet.setTurboMode(true); - } else { - logger.debug("Unknown turbo mode command: {}", command); return; } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * May not be supported via LAN in all models - IR only - * - * @param command Screen Display Toggle to ON or Off - One command - */ - public void handleScreenDisplay(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command.equals(OnOffType.OFF)) { - commandSet.setScreenDisplay(true); - } else if (command.equals(OnOffType.ON)) { - commandSet.setScreenDisplay(true); - } else { - logger.debug("Unknown screen display command: {}", command); - return; - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * This is only for the AC LED device display units, calcs always in Celsius - * - * @param command Temp unit on the indoor evaporator - */ - public void handleTempUnit(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - if (command.equals(OnOffType.OFF)) { - commandSet.setFahrenheit(false); - } else if (command.equals(OnOffType.ON)) { - commandSet.setFahrenheit(true); - } else { - logger.debug("Unknown temperature unit/farenheit command: {}", command); - return; - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Power turned on with Sleep Mode Change - * Sleep mode increases temp slightly in first 2 hours of sleep - * - * @param command Sleep function - */ - public void handleSleepFunction(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - - commandSet.setPowerState(true); - - if (command.equals(OnOffType.OFF)) { - commandSet.setSleepMode(false); - } else if (command.equals(OnOffType.ON)) { - commandSet.setSleepMode(true); - } else { - logger.debug("Unknown sleep Mode command: {}", command); - return; - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Sets the time (from now) that the device will turn on at it's current settings - * - * @param command Sets On Timer - */ - public void handleOnTimer(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - int hours = 0; - int minutes = 0; - Timer timer = new Timer(true, hours, minutes); - TimeParser timeParser = timer.new TimeParser(); - if (command instanceof StringType) { - String timeString = ((StringType) command).toString(); - if (!timeString.matches("\\d{2}:\\d{2}")) { - logger.debug("Invalid time format. Expected HH:MM."); - commandSet.setOnTimer(false, hours, minutes); - } else { - int[] timeParts = timeParser.parseTime(timeString); - boolean on = true; - hours = timeParts[0]; - minutes = timeParts[1]; - // Validate minutes and hours - if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { - logger.debug("Invalid hours (24 max) and or minutes (59 max)"); - hours = 0; - minutes = 0; - } - if (hours == 0 && minutes == 0) { - commandSet.setOnTimer(false, hours, minutes); - } else { - commandSet.setOnTimer(on, hours, minutes); - } - } - } else { - logger.debug("Command must be of type StringType: {}", command); - commandSet.setOnTimer(false, hours, minutes); - } - - getConnectionManager().sendCommandAndMonitor(commandSet); - } - - /** - * Sets the time (from now) that the device will turn off - * - * @param command Sets Off Timer - */ - public void handleOffTimer(Command command) { - CommandSet commandSet = CommandSet.fromResponse(getLastResponse()); - int hours = 0; - int minutes = 0; - Timer timer = new Timer(true, hours, minutes); - TimeParser timeParser = timer.new TimeParser(); - if (command instanceof StringType) { - String timeString = ((StringType) command).toString(); - if (!timeString.matches("\\d{2}:\\d{2}")) { - logger.debug("Invalid time format. Expected HH:MM."); - commandSet.setOffTimer(false, hours, minutes); - } else { - int[] timeParts = timeParser.parseTime(timeString); - boolean on = true; - hours = timeParts[0]; - minutes = timeParts[1]; - // Validate minutes and hours - if (minutes < 0 || minutes > 59 || hours > 24 || hours < 0) { - logger.debug("Invalid hours (24 max) and or minutes (59 max)"); - hours = 0; - minutes = 0; - } - if (hours == 0 && minutes == 0) { - commandSet.setOffTimer(false, hours, minutes); - } else { - commandSet.setOffTimer(on, hours, minutes); - } + try { + Response lastresponse = connectionManager.getLastResponse(); + if (channelUID.getId().equals(CHANNEL_POWER)) { + connectionManager.sendCommand(CommandHelper.handlePower(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_OPERATIONAL_MODE)) { + connectionManager.sendCommand(CommandHelper.handleOperationalMode(command, lastresponse), + callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_TARGET_TEMPERATURE)) { + connectionManager.sendCommand(CommandHelper.handleTargetTemperature(command, lastresponse), + callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) { + connectionManager.sendCommand(CommandHelper.handleFanSpeed(command, lastresponse, config.version), + callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_ECO_MODE)) { + connectionManager.sendCommand(CommandHelper.handleEcoMode(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_TURBO_MODE)) { + connectionManager.sendCommand(CommandHelper.handleTurboMode(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_SWING_MODE)) { + connectionManager.sendCommand(CommandHelper.handleSwingMode(command, lastresponse, config.version), + callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_SCREEN_DISPLAY)) { + connectionManager.sendCommand(CommandHelper.handleScreenDisplay(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_TEMPERATURE_UNIT)) { + connectionManager.sendCommand(CommandHelper.handleTempUnit(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_SLEEP_FUNCTION)) { + connectionManager.sendCommand(CommandHelper.handleSleepFunction(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_ON_TIMER)) { + connectionManager.sendCommand(CommandHelper.handleOnTimer(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) { + connectionManager.sendCommand(CommandHelper.handleOffTimer(command, lastresponse), callbackLambda); } - } else { - logger.debug("Command must be of type StringType: {}", command); - commandSet.setOffTimer(false, hours, minutes); + } catch (MideaConnectionException | MideaAuthenticationException e) { + logger.warn("Unable to proces command: {}", e.getMessage()); } - - getConnectionManager().sendCommandAndMonitor(commandSet); } /** @@ -675,18 +184,8 @@ public void handleOffTimer(Command command) { */ @Override public void initialize() { - connectionManager.disconnect(); - getConnectionManager().cancelConnectionMonitorJob(); - connectionManager.resetDroppedCommands(); - connectionManager.updateChannel(DROPPED_COMMANDS, new DecimalType(connectionManager.getDroppedCommands())); - config = getConfigAs(MideaACConfiguration.class); - setCloudProvider(CloudProviderDTO.getCloudProvider(config.cloud)); - setSecurity(new Security(cloudProvider)); - - logger.debug("MideaACHandler config for {} is {}", thing.getUID(), config); - if (!config.isValid()) { logger.warn("Configuration invalid for {}", thing.getUID()); if (config.isDiscoveryNeeded()) { @@ -708,22 +207,100 @@ public void initialize() { return; } } else { - logger.debug("Configuration valid for {}", thing.getUID()); + logger.debug("Non-security Configuration valid for {}", thing.getUID()); } - ipAddress = config.ipAddress; - ipPort = config.ipPort; - deviceId = config.deviceId; - version = Integer.parseInt(config.version); - - logger.debug("IPAddress: {}", ipAddress); - logger.debug("IPPort: {}", ipPort); - logger.debug("ID: {}", deviceId); - logger.debug("Version: {}", version); + if (config.version == 3 && !config.isV3ConfigValid()) { + if (config.isTokenKeyObtainable()) { + logger.info("Retrieving Token and/or Key from cloud"); + CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); + getTokenKeyCloud(cloudProvider); + } else { + logger.warn("Configuration invalid for {}", thing.getUID()); + } + } else { + logger.debug("Security Configuration (V3 Device) valid for {}", thing.getUID()); + } updateStatus(ThingStatus.UNKNOWN); - connectionManager.connect(); + connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, + config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, + config.deviceId, config.version, config.promptTone); + + // startScheduler(2, config.pollingTime, TimeUnit.SECONDS); + scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); + } + + public void startScheduler(long initialDelay, long delay, TimeUnit unit) { + scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit); + logger.debug("Scheduled task started"); + } + + private void pollJob() { + ConnectionManager connectionManager = this.connectionManager; + if (connectionManager == null) { + logger.warn("The connection manager was unexpectedly null, please report a bug"); + return; + } + try { + connectionManager.getStatus(callbackLambda); + updateStatus(ThingStatus.ONLINE); + } catch (MideaAuthenticationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (MideaConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (MideaException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void updateChannel(String channelName, State state) { + if (ThingStatus.OFFLINE.equals(getThing().getStatus())) { + return; + } + Channel channel = thing.getChannel(channelName); + if (channel != null && isLinked(channel.getUID())) { + updateState(channel.getUID(), state); + } + } + + private void updateChannels(Response response) { + updateChannel(CHANNEL_POWER, OnOffType.from(response.getPowerState())); + updateChannel(CHANNEL_APPLIANCE_ERROR, OnOffType.from(response.getApplianceError())); + updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString())); + updateChannel(CHANNEL_FAN_SPEED, new StringType(response.getFanSpeed().toString())); + updateChannel(CHANNEL_ON_TIMER, new StringType(response.getOnTimer().toChannel())); + updateChannel(CHANNEL_OFF_TIMER, new StringType(response.getOffTimer().toChannel())); + updateChannel(CHANNEL_SWING_MODE, new StringType(response.getSwingMode().toString())); + updateChannel(CHANNEL_AUXILIARY_HEAT, OnOffType.from(response.getAuxHeat())); + updateChannel(CHANNEL_ECO_MODE, OnOffType.from(response.getEcoMode())); + updateChannel(CHANNEL_TEMPERATURE_UNIT, OnOffType.from(response.getFahrenheit())); + updateChannel(CHANNEL_SLEEP_FUNCTION, OnOffType.from(response.getSleepFunction())); + updateChannel(CHANNEL_TURBO_MODE, OnOffType.from(response.getTurboMode())); + updateChannel(CHANNEL_SCREEN_DISPLAY, OnOffType.from(response.getDisplayOn())); + updateChannel(CHANNEL_HUMIDITY, new DecimalType(response.getHumidity())); + + QuantityType targetTemperature = new QuantityType(response.getTargetTemperature(), + SIUnits.CELSIUS); + QuantityType alternateTemperature = new QuantityType( + response.getAlternateTargetTemperature(), SIUnits.CELSIUS); + QuantityType outdoorTemperature = new QuantityType(response.getOutdoorTemperature(), + SIUnits.CELSIUS); + QuantityType indoorTemperature = new QuantityType(response.getIndoorTemperature(), + SIUnits.CELSIUS); + + if (imperialUnits) { + targetTemperature = Objects.requireNonNull(targetTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + alternateTemperature = Objects.requireNonNull(alternateTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + outdoorTemperature = Objects.requireNonNull(outdoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + } + + updateChannel(CHANNEL_TARGET_TEMPERATURE, targetTemperature); + updateChannel(CHANNEL_ALTERNATE_TARGET_TEMPERATURE, alternateTemperature); + updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature); + updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); } @Override @@ -759,708 +336,46 @@ public void discovered(DiscoveryResult discoveryResult) { initialize(); } - /** - * Manage the ONLINE/OFFLINE statuses of the thing with problems (or lack thereof) - */ - private void markOnline() { - if (!isOnline()) { - updateStatus(ThingStatus.ONLINE); - } - } - - private void markOffline() { - if (isOnline()) { - updateStatus(ThingStatus.OFFLINE); - } - } + public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { + CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider); + if (cloud != null) { + cloud.setHttpClient(httpClient); + if (cloud.login()) { + TokenKey tk = cloud.getToken(config.deviceId); + Configuration configuration = editConfiguration(); - private void markOfflineWithMessage(ThingStatusDetail statusDetail, String statusMessage) { - if (!isOffline()) { - updateStatus(ThingStatus.OFFLINE, statusDetail, statusMessage); - } + configuration.put(CONFIG_TOKEN, tk.token()); + configuration.put(CONFIG_KEY, tk.key()); + updateConfiguration(configuration); - /** - * This is to space out the looping with a short (5 second) then long (30 second) pause(s). - * Generally a WiFi issue triggers the offline. Could be a blip or something longer term - * Only info log (Connection issue ..) prior to first long pause. - */ - if (retry) { - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - logger.debug("An interupted error (pause) has occured {}", e.getMessage()); - } - getConnectionManager().cancelConnectionMonitorJob(); - getConnectionManager().disconnect(); - retry = false; - getConnectionManager().connect(); - } else { - if (connectionMessage) { - logger.info("Connection issue, resetting, please wait ..."); + logger.trace("Token: {}", tk.token()); + logger.trace("Key: {}", tk.key()); + logger.info("Token and Key obtained from cloud, saving, initializing"); + initialize(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( + "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); + logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"); } - connectionMessage = false; - getConnectionManager().cancelConnectionMonitorJob(); - getConnectionManager().disconnect(); - getConnectionManager().scheduleConnectionMonitorJob(); } } - private boolean isOnline() { - return thing.getStatus().equals(ThingStatus.ONLINE); - } + public void stopScheduler() { + ScheduledFuture localScheduledTask = this.scheduledTask; - private boolean isOffline() { - return thing.getStatus().equals(ThingStatus.OFFLINE); + if (localScheduledTask != null && !localScheduledTask.isCancelled()) { + localScheduledTask.cancel(true); + logger.debug("Scheduled task cancelled."); + scheduledTask = null; + } + if (scheduler != null && !scheduler.isShutdown()) { + scheduler.shutdownNow(); + logger.debug("Scheduler service shut down."); + } } - /** - * Cancel the connection manager job which will keep going - * even with the binding removed and cause warnings about - * trying to update Thing Channels with the Handler disposed - */ @Override public void dispose() { - connectionManager.cancelConnectionMonitorJob(); - markOffline(); - } - - /** - * DoPoll is set to false in the MideaAC Handler - * if a Command is being sent and picked up by - * the Connection Manager. Then is reset to true - * after the Set command is complete - * - * @return doPoll Sets if the binding will poll after authorization - */ - public boolean getDoPoll() { - return doPoll; - } - - /** - * Resets the doPoll switch - */ - public void resetDoPoll() { - doPoll = true; - } - - /** - * Reset Retry controls the short 5 second delay - * Before starting 30 second delays. (More severe Wifi issue) - * It is reset after a successful connection - */ - public void resetRetry() { - retry = true; - } - - /** - * Limit logging of INFO connection messages to - * only when the device was Offline in its prior - * state - */ - public void resetConnectionMessage() { - connectionMessage = true; - } - - private ThingStatusDetail getDetail() { - return thing.getStatusInfo().getStatusDetail(); - } - - /** - * Sets Cloud Provider - * - * @param cloudProvider Cloud Provider - */ - public void setCloudProvider(CloudProviderDTO cloudProvider) { - this.cloudProvider = cloudProvider; - } - - /** - * Security methods - * - * @param security security class - */ - public void setSecurity(Security security) { - this.security = security; - } - - /** - * The {@link ConnectionManager} class is responsible for managing the state of the TCP connection to the - * indoor AC unit evaporator. - * - * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command - * - * This gets around the issue that any command needs to be within 30 seconds of the authorization - * in testing this only adds 50 ms, but allows polls at longer intervals - */ - private class ConnectionManager { - private Logger logger = LoggerFactory.getLogger(ConnectionManager.class); - - private boolean deviceIsConnected; - private int droppedCommands = 0; - - private Socket socket = new Socket(); - private InputStream inputStream = new ByteArrayInputStream(new byte[0]); - private DataOutputStream writer = new DataOutputStream(System.out); - - private @Nullable ScheduledFuture connectionMonitorJob = null; - - private byte[] data = HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"); - - private String responseType = "query"; - - private byte bodyType = (byte) 0xc0; - - private Response lastResponse = new Response(data, getVersion(), responseType, bodyType); - private MideaACHandler mideaACHandler; - - /** - * Gets last response - * - * @return byte array of last response - */ - public Response getLastResponse() { - return this.lastResponse; - } - - Runnable connectionMonitorRunnable = () -> { - logger.debug("Connecting to {} at IP {} for Poll", thing.getUID(), ipAddress); - disconnect(); - connect(); - }; - - /** - * Set the parameters for the connection manager - * - * @param mideaACHandler mideaACHandler class - */ - public ConnectionManager(MideaACHandler mideaACHandler) { - deviceIsConnected = false; - this.mideaACHandler = mideaACHandler; - } - - /** - * Validate if String is blank - * - * @param str string to be evaluated - * @return boolean true or false - */ - public static boolean isBlank(String str) { - return str.trim().isEmpty(); - } - - /** - * Reset dropped commands from initialization in MideaACHandler - * Channel created for easy observation - * Dropped commands when no bytes to read after two tries or other - * byte reading problem. Device not responding. - */ - public void resetDroppedCommands() { - droppedCommands = 0; - } - - /** - * Resets Dropped command - * - * @return dropped commands - */ - public int getDroppedCommands() { - return droppedCommands = 0; - } - - /** - * After checking if the key and token need to be updated (Default = 0 Never) - * The socket is established with the writer and inputStream (for reading responses) - * The device is considered connected. V2 devices will proceed to send the poll or the - * set command. V3 devices will proceed to authenticate - */ - protected synchronized void connect() { - logger.trace("Connecting to {} at {}:{}", thing.getUID(), ipAddress, ipPort); - - // Open socket - try { - socket = new Socket(); - socket.setSoTimeout(config.timeout * 1000); - int port = Integer.parseInt(ipPort); - socket.connect(new InetSocketAddress(ipAddress, port), config.timeout * 1000); - } catch (IOException e) { - logger.debug("IOException connecting to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); - String message = e.getMessage(); - if (message != null) { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } else { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); - } - } - - // Create streams - try { - writer = new DataOutputStream(socket.getOutputStream()); - inputStream = socket.getInputStream(); - } catch (IOException e) { - logger.debug("IOException getting streams for {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(), - e); - String message = e.getMessage(); - if (message != null) { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } else { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); - } - } - if (!deviceIsConnected || !connectionMessage) { - logger.info("Connected to {} at {}", thing.getUID(), ipAddress); - mideaACHandler.resetRetry(); - mideaACHandler.resetConnectionMessage(); - } - logger.debug("Connected to {} at {}", thing.getUID(), ipAddress); - deviceIsConnected = true; - markOnline(); - if (getVersion() != 3) { - logger.debug("Device {}@{} does not require authentication, updating status", thing.getUID(), - ipAddress); - requestStatus(mideaACHandler.getDoPoll()); - } else { - logger.debug("Device {}@{} require authentication, going to authenticate", thing.getUID(), ipAddress); - authenticate(); - } - } - - /** - * For V3 devices only. This method checks for the Cloud Provider - * key and token (and goes offline if any are missing). It will retrieve the - * missing key and/or token if the account email and password are provided. - */ - public void authenticate() { - logger.trace("Version: {}", getVersion()); - logger.trace("Key: {}", config.key); - logger.trace("Token: {}", config.token); - - if (!isBlank(config.token) && !isBlank(config.key) && !config.cloud.equals("")) { - logger.debug("Device {}@{} authenticating", thing.getUID(), ipAddress); - doAuthentication(); - } else { - if (!isBlank(config.email) && !isBlank(config.password) && !config.cloud.equals("")) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, - "Retrieving Token and/or Key from cloud."); - logger.info("Retrieving Token and/or Key from cloud"); - CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); - getTokenKeyCloud(cloudProvider); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Token and/or Key missing, missing cloud provider information to fetch it"); - logger.warn("Token, Key and or Cloud provider data missing, V3 device {}@{} cannot authenticate", - thing.getUID(), ipAddress); - } - } - } - - private void getTokenKeyCloud(CloudProviderDTO cloudProvider) { - CloudDTO cloud = mideaACHandler.getClouds().get(config.email, config.password, cloudProvider); - if (cloud != null) { - cloud.setHttpClient(httpClient); - if (cloud.login()) { - TokenKey tk = cloud.getToken(config.deviceId); - Configuration configuration = editConfiguration(); - - configuration.put(CONFIG_TOKEN, tk.token()); - configuration.put(CONFIG_KEY, tk.key()); - updateConfiguration(configuration); - - logger.trace("Token: {}", tk.token()); - logger.trace("Key: {}", tk.key()); - logger.info("Token and Key obtained from cloud, saving, initializing"); - initialize(); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( - "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); - logger.warn( - "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"); - } - } - } - - /** - * Sends the Handshake Request to the V3 device. Generally quick response - * Without the 1000 ms sleep delay there are problems in sending the Poll/Command - * Suspect that the socket write and read streams need a moment to clear - * as they will be reused in the SendCommand method - */ - private void doAuthentication() { - byte[] request = mideaACHandler.getSecurity().encode8370(Utils.hexStringToByteArray(config.token), - MsgType.MSGTYPE_HANDSHAKE_REQUEST); - try { - logger.trace("Device {}@{} writing handshake_request: {}", thing.getUID(), ipAddress, - Utils.bytesToHex(request)); - - write(request); - byte[] response = read(); - - if (response != null && response.length > 0) { - logger.trace("Device {}@{} response for handshake_request length: {}", thing.getUID(), ipAddress, - response.length); - if (response.length == 72) { - boolean success = mideaACHandler.getSecurity().tcpKey(Arrays.copyOfRange(response, 8, 72), - Utils.hexStringToByteArray(config.key)); - if (success) { - logger.debug("Authentication successful"); - // Altering the sleep caused or can cause write errors problems. Use caution. - // At 500 ms the first write usually fails. Works, but no backup - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - logger.debug("An interupted error (success) has occured {}", e.getMessage()); - } - requestStatus(mideaACHandler.getDoPoll()); - } else { - logger.debug("Invalid Key. Correct Key in configuration"); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid Key. Correct Key in configuration."); - } - } else if (Arrays.equals(new String("ERROR").getBytes(), response)) { - logger.warn("Authentication failed!"); - } else { - logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", - response.length); - logger.debug("Invalid Token. Correct Token in configuration"); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid Token. Correct Token in configuration."); - } - } - } catch (IOException e) { - logger.warn("An IO error in doAuthentication has occured {}", e.getMessage()); - String message = e.getMessage(); - if (message != null) { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } else { - markOfflineWithMessage(ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ""); - } - } - } - - /** - * After authentication, this switch to either send a - * Poll or the Set command - * - * @param polling polling true or false - */ - public void requestStatus(boolean polling) { - if (polling) { - CommandBase requestStatusCommand = new CommandBase(); - sendCommandAndMonitor(requestStatusCommand); - } - } - - /** - * Calls the sendCommand method, resets the doPoll to true - * Disconnects the socket and schedules the connection manager - * job, if was stopped (to avoid collision) due to a Set command - * - * @param command either the set or polling command - */ - public void sendCommandAndMonitor(CommandBase command) { - sendCommand(command); - mideaACHandler.resetDoPoll(); - if (connectionMonitorJob == null) { - scheduleConnectionMonitorJob(); - } - } - - /** - * Pulls the packet byte array together. There is a check to - * make sure to make sure the input stream is empty before sending - * the new command and another check if input stream is empty after 1.5 seconds. - * Normal device response in 0.75 - 1 second range - * If still empty, send the bytes again. If there are bytes, the read method is called. - * If the socket times out with no response the command is dropped. There will be another poll - * in the time set by the user (30 seconds min) or the set command can be retried - * - * @param command either the set or polling command - */ - public void sendCommand(CommandBase command) { - if (command instanceof CommandSet) { - ((CommandSet) command).setPromptTone(config.promptTone); - } - Packet packet = new Packet(command, deviceId, mideaACHandler); - packet.compose(); - - try { - byte[] bytes = packet.getBytes(); - logger.debug("Writing to {} at {} bytes.length: {}", thing.getUID(), ipAddress, bytes.length); - - if (getVersion() == 3) { - bytes = mideaACHandler.getSecurity().encode8370(bytes, MsgType.MSGTYPE_ENCRYPTED_REQUEST); - } - - // Ensure input stream is empty before writing packet - if (inputStream.available() == 0) { - logger.debug("Input stream empty sending write {}", command); - write(bytes); - } - - try { - Thread.sleep(1500); - } catch (InterruptedException e) { - logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); - } - - if (inputStream.available() == 0) { - logger.debug("Input stream empty sending second write {}", command); - write(bytes); - } - - // Socket timeout (UI parameter) 2 seconds minimum up to 10 seconds. - byte[] responseBytes = read(); - - if (responseBytes != null) { - if (getVersion() == 3) { - Decryption8370Result result = mideaACHandler.getSecurity().decode8370(responseBytes); - for (byte[] response : result.getResponses()) { - logger.debug("Response length:{} thing:{} ", response.length, thing.getUID()); - if (response.length > 40 + 16) { - byte[] data = mideaACHandler.getSecurity() - .aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); - - logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); - byte bodyType2 = data[0xa]; - - // data[3]: Device Type - 0xAC = AC - // https://github.com/georgezhao2010/midea_ac_lan/blob/06fc4b582a012bbbfd6bd5942c92034270eca0eb/custom_components/midea_ac_lan/midea_devices.py#L96 - - // data[9]: MessageType - set, query, notify1, notify2, exception, querySN, exception2, - // querySubtype - // https://github.com/georgezhao2010/midea_ac_lan/blob/30d0ff5ff14f150da10b883e97b2f280767aa89a/custom_components/midea_ac_lan/midea/core/message.py#L22-L29 - String responseType = ""; - switch (data[0x9]) { - case 0x02: - responseType = "set"; - break; - case 0x03: - responseType = "query"; - break; - case 0x04: - responseType = "notify1"; - break; - case 0x05: - responseType = "notify2"; - break; - case 0x06: - responseType = "exception"; - break; - case 0x07: - responseType = "querySN"; - break; - case 0x0A: - responseType = "exception2"; - break; - case 0x09: // Helyesen: 0xA0 - responseType = "querySubtype"; - break; - default: - logger.debug("Invalid response type: {}", data[0x9]); - } - logger.trace("Response Type: {} and bodyType:{}", responseType, bodyType2); - - // The response data from the appliance includes a packet header which we don't want - data = Arrays.copyOfRange(data, 10, data.length); - byte bodyType = data[0x0]; - logger.trace("Response Type expected: {} and bodyType:{}", responseType, bodyType); - logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToHex(data)); - logger.debug( - "Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToBinary(data)); - - if (data.length > 0) { - if (data.length < 21) { - logger.warn("Response data is {} long, minimum is 21!", data.length); - return; - } - if (bodyType != -64) { - if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from:{}", bodyType, - thing.getUID()); - return; - } - logger.warn("Unexpected response bodyType {}", bodyType); - return; - } - lastResponse = new Response(data, getVersion(), responseType, bodyType); - try { - processMessage(lastResponse); - logger.trace("data length is {} version is {} thing is {}", data.length, - version, thing.getUID()); - } catch (Exception ex) { - logger.warn("Processing response exception: {}", ex.getMessage()); - } - } - } - } - } else { - byte[] data = mideaACHandler.getSecurity() - .aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); - // The response data from the appliance includes a packet header which we don't want - logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); - if (data.length > 0) { - data = Arrays.copyOfRange(data, 10, data.length); - logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToHex(data)); - - lastResponse = new Response(data, getVersion(), "", (byte) 0x00); - processMessage(lastResponse); - logger.debug("V2 data length is {} version is {} thing is {}", data.length, version, - thing.getUID()); - } else { - logger.debug("Problem with reading V2 response, skipping command {}", command); - droppedCommands = droppedCommands + 1; - updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); - } - } - return; - } else { - logger.debug("Problem with reading response, skipping command {}", command); - droppedCommands = droppedCommands + 1; - updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); - return; - } - } catch (SocketException e) { - logger.debug("SocketException writing to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); - String message = e.getMessage(); - droppedCommands = droppedCommands + 1; - updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); - updateStatus(ThingStatus.OFFLINE, getDetail(), message); - return; - } catch (IOException e) { - logger.debug(" Send IOException writing to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage()); - String message = e.getMessage(); - droppedCommands = droppedCommands + 1; - updateChannel(DROPPED_COMMANDS, new DecimalType(droppedCommands)); - updateStatus(ThingStatus.OFFLINE, getDetail(), message); - return; - } - } - - /** - * Closes all elements of the connection before starting a new one - */ - protected synchronized void disconnect() { - // Make sure writer, inputStream and socket are closed before each command is started - logger.debug("Disconnecting from {} at {}", thing.getUID(), ipAddress); - - InputStream inputStream = this.inputStream; - DataOutputStream writer = this.writer; - Socket socket = this.socket; - try { - writer.close(); - inputStream.close(); - socket.close(); - - } catch (IOException e) { - logger.warn("IOException closing connection to {} at {}: {}", thing.getUID(), ipAddress, e.getMessage(), - e); - } - socket = null; - inputStream = null; - writer = null; - } - - private void updateChannel(String channelName, State state) { - if (isOffline()) { - return; - } - Channel channel = thing.getChannel(channelName); - if (channel != null) { - updateState(channel.getUID(), state); - } - } - - private void processMessage(Response response) { - updateChannel(CHANNEL_POWER, response.getPowerState() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_APPLIANCE_ERROR, response.getApplianceError() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_TARGET_TEMPERATURE, - new QuantityType(response.getTargetTemperature(), SIUnits.CELSIUS)); - updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString())); - updateChannel(CHANNEL_FAN_SPEED, new StringType(response.getFanSpeed().toString())); - updateChannel(CHANNEL_ON_TIMER, new StringType(response.getOnTimer().toChannel())); - updateChannel(CHANNEL_OFF_TIMER, new StringType(response.getOffTimer().toChannel())); - updateChannel(CHANNEL_SWING_MODE, new StringType(response.getSwingMode().toString())); - updateChannel(CHANNEL_AUXILIARY_HEAT, response.getAuxHeat() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_ECO_MODE, response.getEcoMode() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_TEMPERATURE_UNIT, response.getFahrenheit() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_SLEEP_FUNCTION, response.getSleepFunction() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_TURBO_MODE, response.getTurboMode() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_SCREEN_DISPLAY, response.getDisplayOn() ? OnOffType.ON : OnOffType.OFF); - updateChannel(CHANNEL_ALTERNATE_TARGET_TEMPERATURE, - new QuantityType(response.getAlternateTargetTemperature(), SIUnits.CELSIUS)); - updateChannel(CHANNEL_INDOOR_TEMPERATURE, - new QuantityType(response.getIndoorTemperature(), SIUnits.CELSIUS)); - updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, - new QuantityType(response.getOutdoorTemperature(), SIUnits.CELSIUS)); - updateChannel(CHANNEL_HUMIDITY, new DecimalType(response.getHumidity())); - } - - /** - * Reads the inputStream byte array - * - * @return byte array - */ - public synchronized byte @Nullable [] read() { - byte[] bytes = new byte[512]; - InputStream inputStream = this.inputStream; - - try { - int len = inputStream.read(bytes); - if (len > 0) { - logger.debug("Response received length: {} Thing:{}", len, thing.getUID()); - bytes = Arrays.copyOfRange(bytes, 0, len); - return bytes; - } - } catch (IOException e) { - String message = e.getMessage(); - logger.debug(" Byte read exception {}", message); - } - return null; - } - - /** - * Writes the packet that will be sent to the device - * - * @param buffer socket writer - * @throws IOException writer could be null - */ - public synchronized void write(byte[] buffer) throws IOException { - DataOutputStream writer = this.writer; - - try { - writer.write(buffer, 0, buffer.length); - } catch (IOException e) { - String message = e.getMessage(); - logger.debug("Write error {}", message); - } - } - - /** - * Periodical polling. Thirty seconds minimum - */ - private void scheduleConnectionMonitorJob() { - if (connectionMonitorJob == null) { - logger.debug("Starting connection monitor job in {} seconds for {} at {} after 30 second delay", - config.pollingTime, thing.getUID(), ipAddress); - long frequency = config.pollingTime; - long delay = 30L; - connectionMonitorJob = scheduler.scheduleWithFixedDelay(connectionMonitorRunnable, delay, frequency, - TimeUnit.SECONDS); - } - } - - private void cancelConnectionMonitorJob() { - ScheduledFuture connectionMonitorJob = this.connectionMonitorJob; - if (connectionMonitorJob != null) { - connectionMonitorJob.cancel(true); - logger.debug("Cancelling connection monitor job for {} at {}", thing.getUID(), ipAddress); - this.connectionMonitorJob = null; - } - } + // stopScheduler(); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java index 022ae685f285e..7e9424916830d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.security.Security; /** * The {@link Packet} class for Midea AC creates the @@ -28,18 +29,18 @@ public class Packet { private CommandBase command; private byte[] packet; - private MideaACHandler mideaACHandler; + private Security security; /** * The Packet class parameters * * @param command command from Command Base * @param deviceId the device ID - * @param mideaACHandler the MideaACHandler class + * @param security the Security class */ - public Packet(CommandBase command, String deviceId, MideaACHandler mideaACHandler) { + public Packet(CommandBase command, String deviceId, Security security) { this.command = command; - this.mideaACHandler = mideaACHandler; + this.security = security; packet = new byte[] { // 2 bytes - StaticHeader @@ -78,7 +79,7 @@ public void compose() { command.compose(); // Append the command data(48 bytes) to the packet - byte[] cmdEncrypted = mideaACHandler.getSecurity().aesEncrypt(command.getBytes()); + byte[] cmdEncrypted = security.aesEncrypt(command.getBytes()); // Ensure 48 bytes if (cmdEncrypted.length < 48) { @@ -97,7 +98,7 @@ public void compose() { System.arraycopy(lenBytes, 0, packet, 4, 2); // calculate checksum data - byte[] checksumData = mideaACHandler.getSecurity().encode32Data(packet); + byte[] checksumData = security.encode32Data(packet); // Append a basic checksum data(16 bytes) to the packet byte[] newPacketTwo = new byte[packet.length + checksumData.length]; diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index a803250bcb91d..46a1f9a48bbaa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -31,7 +31,6 @@ - ipAddress @@ -256,11 +255,4 @@ Temperature - - Number - - Commands dropped due to TCP read() issues. - Number - - diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index a728420af666c..4c330036448d7 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -36,7 +36,7 @@ public class MideaACConfigurationTest { @Test public void testValidConfigs() { config.ipAddress = "192.168.0.1"; - config.ipPort = "6444"; + config.ipPort = 6444; config.deviceId = "1234567890"; assertTrue(config.isValid()); assertFalse(config.isDiscoveryNeeded()); @@ -48,7 +48,7 @@ public void testValidConfigs() { @Test public void testnonValidConfigs() { config.ipAddress = "192.168.0.1"; - config.ipPort = ""; + config.ipPort = 0; config.deviceId = "1234567890"; assertFalse(config.isValid()); assertTrue(config.isDiscoveryNeeded()); @@ -60,7 +60,7 @@ public void testnonValidConfigs() { @Test public void testBadIpConfigs() { config.ipAddress = "192.1680.1"; - config.ipPort = "6444"; + config.ipPort = 6444; config.deviceId = "1234567890"; assertTrue(config.isValid()); assertTrue(config.isDiscoveryNeeded()); From 51c3cd5433505975dd57fe7c7eca065b080052d6 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 10 Nov 2024 11:15:10 -0500 Subject: [PATCH 03/44] Changes to get separate Connection Manager working After initial changes, tested various scenarios and made changes so they are working like before. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 2 +- .../internal/MideaACBindingConstants.java | 2 +- .../internal/MideaACConfiguration.java | 9 +- .../internal/connection/CommandHelper.java | 2 +- .../connection/ConnectionManager.java | 127 +++++++----------- .../discovery/MideaACDiscoveryService.java | 19 +-- .../internal/handler/MideaACHandler.java | 89 +++++++----- .../mideaac/internal/handler/Response.java | 2 +- .../resources/OH-INF/i18n/mideaac.properties | 4 +- .../resources/OH-INF/thing/thing-types.xml | 10 +- .../internal/MideaACConfigurationTest.java | 53 +++++++- .../MideaACDiscoveryServiceTest.java | 13 ++ 12 files changed, 188 insertions(+), 144 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 206126153a12e..cb41f09359950 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -41,7 +41,7 @@ No binding configuration is required. | pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | | timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | | promptTone | Yes | "Ding" tone when command is received and executed. | False | -| version | Yes | Version 3 has token, key and cloud requirements. | 3 | +| version | Yes | Version 3 has token, key and cloud requirements. | 0 | ## Channels diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index 35d419e3f272b..c9559a571852b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -85,8 +85,8 @@ public class MideaACBindingConstants { public static final String CONFIG_POLLING_TIME = "pollingTime"; public static final String CONFIG_CONNECTING_TIMEOUT = "timeout"; public static final String CONFIG_PROMPT_TONE = "promptTone"; + public static final String CONFIG_VERSION = "version"; - public static final String PROPERTY_VERSION = "version"; public static final String PROPERTY_SN = "sn"; public static final String PROPERTY_SSID = "ssid"; public static final String PROPERTY_TYPE = "type"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index e19e43335028f..b31abacacd020 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -27,7 +27,7 @@ public class MideaACConfiguration { public int ipPort = 6444; - public String deviceId = ""; + public String deviceId = "0"; public String email = ""; @@ -43,7 +43,7 @@ public class MideaACConfiguration { public int timeout = 4; - public boolean promptTone; + public boolean promptTone = false; public int version = 0; @@ -53,7 +53,8 @@ public class MideaACConfiguration { * @return true(valid), false (not valid) */ public boolean isValid() { - return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank()); + return !("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() + || version <= 1); } /** @@ -63,7 +64,7 @@ public boolean isValid() { */ public boolean isDiscoveryNeeded() { return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() - || !Utils.validateIP(ipAddress)); + || !Utils.validateIP(ipAddress) || version <= 1); } /** diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 61d00d9f02cec..1b4e58a168990 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -135,7 +135,7 @@ public static CommandSet handleTargetTemperature(Command command, Response lastR if (command instanceof DecimalType decimalCommand) { logger.debug("Handle Target Temperature as DecimalType in degrees C"); commandSet.setTargetTemperature(limitTargetTemperatureToRange(decimalCommand.floatValue())); - } else if (command instanceof QuantityType quantityCommand) { + } else if (command instanceof QuantityType quantityCommand) { if (quantityCommand.getUnit().equals(ImperialUnits.FAHRENHEIT)) { quantityCommand = Objects.requireNonNull(quantityCommand.toUnit(SIUnits.CELSIUS)); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index accbb93b82015..cd837711aa688 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -66,17 +66,14 @@ public class ConnectionManager { private Security security; private final int version; private final boolean promptTone; + private boolean deviceIsConnected; + private int droppedCommands = 0; /** * True allows one short retry after connection problem */ private boolean retry = true; - /** - * Suppresses the connection message if was online before - */ - private boolean connectionMessage = true; - public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud, String email, String password, String deviceId, int version, boolean promptTone) { this.deviceIsConnected = false; @@ -95,9 +92,6 @@ public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, this.security = new Security(cloudProvider); } - private boolean deviceIsConnected; - private int droppedCommands = 0; - private Socket socket = new Socket(); private InputStream inputStream = new ByteArrayInputStream(new byte[0]); private DataOutputStream writer = new DataOutputStream(System.out); @@ -121,25 +115,6 @@ public static boolean isBlank(String str) { return str.trim().isEmpty(); } - /** - * Reset dropped commands from initialization in MideaACHandler - * Channel created for easy observation - * Dropped commands when no bytes to read after two tries or other - * byte reading problem. Device not responding. - */ - public void resetDroppedCommands() { - droppedCommands = 0; - } - - /** - * Resets Dropped command - * - * @return dropped commands - */ - public int getDroppedCommands() { - return droppedCommands = 0; - } - /** * After checking if the key and token need to be updated (Default = 0 Never) * The socket is established with the writer and inputStream (for reading responses) @@ -155,17 +130,28 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent socket.setSoTimeout(timeout * 1000); socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); } catch (IOException e) { - logger.debug("IOException connecting to {}: {}", ipAddress, e.getMessage()); - deviceIsConnected = false; + // Retry addresses most common wifi connection problems- wait 5 seconds and try again if (retry) { + logger.debug("Retrying Socket, IOException connecting to {}: {}", ipAddress, e.getMessage()); try { Thread.sleep(5000); } catch (InterruptedException ex) { - logger.debug("An interupted error (pause) has occured {}", ex.getMessage()); + logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); } - connect(); + retry = false; + try { + socket = new Socket(); + socket.setSoTimeout(timeout * 1000); + socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); + } catch (IOException e2) { + deviceIsConnected = false; + logger.debug("Second try IOException connecting to {}: {}", ipAddress, e2.getMessage()); + throw new MideaConnectionException(e2); + } + } else { + deviceIsConnected = false; + throw new MideaConnectionException(e); } - throw new MideaConnectionException(e); } // Create streams @@ -177,16 +163,9 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent deviceIsConnected = false; throw new MideaConnectionException(e); } - if (!deviceIsConnected || !connectionMessage) { - logger.info("Connected to IP {}", ipAddress); - resetConnectionMessage(); - } - logger.debug("Connected to IP {}", ipAddress); - deviceIsConnected = true; - resetRetry(); if (version == 3) { - logger.debug("Device {} require authentication, going to authenticate", ipAddress); + logger.debug("Device at IP: {} requires authentication, going to authenticate", ipAddress); try { authenticate(); } catch (MideaAuthenticationException | MideaConnectionException e) { @@ -194,8 +173,13 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent throw e; } } - // requestStatus(getDoPoll()); + + if (!deviceIsConnected) { + logger.info("Connected to IP {}", ipAddress); + } + logger.debug("Connected to IP {}", ipAddress); deviceIsConnected = true; + retry = true; } /** @@ -212,7 +196,7 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE logger.trace("Cloud {}", cloud); if (!isBlank(token) && !isBlank(key) && !"".equals(cloud)) { - logger.debug("Device {} authenticating", ipAddress); + logger.debug("Device at IP: {} authenticating", ipAddress); doV3Handshake(); } else { throw new MideaAuthenticationException("Token, Key and / or cloud provider missing"); @@ -228,13 +212,13 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException { byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST); try { - logger.trace("Device {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request)); + logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request)); write(request); byte[] response = read(); if (response != null && response.length > 0) { - logger.trace("Device {} response for handshake_request length: {}", ipAddress, response.length); + logger.trace("Device at IP: {} response for handshake_request length:{}", ipAddress, response.length); if (response.length == 72) { boolean success = security.tcpKey(Arrays.copyOfRange(response, 8, 72), Utils.hexStringToByteArray(key)); @@ -247,7 +231,6 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio } catch (InterruptedException e) { logger.debug("An interupted error (success) has occured {}", e.getMessage()); } - // requestStatus(getDoPoll()); need to handle } else { throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); } @@ -255,7 +238,7 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio throw new MideaAuthenticationException("Authentication failed!"); } else { logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", response.length); - throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); + throw new MideaAuthenticationException("Unexpected authentication response length"); } } } catch (IOException e) { @@ -290,7 +273,7 @@ private void ensureConnected() throws MideaConnectionException, MideaAuthenticat * Normal device response in 0.75 - 1 second range * If still empty, send the bytes again. If there are bytes, the read method is called. * If the socket times out with no response the command is dropped. There will be another poll - * in the time set by the user (30 seconds min) or the set command can be retried + * in the time set by the user (30 seconds min). A Set command will need to be resent. * * @param command either the set or polling command * @throws MideaAuthenticationException @@ -325,7 +308,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } catch (InterruptedException e) { logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); Thread.currentThread().interrupt(); - // Note, but continue anyway. Command will be dropped + // Note, but continue anyway for second write. } if (inputStream.available() == 0) { @@ -340,7 +323,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal if (version == 3) { Decryption8370Result result = security.decode8370(responseBytes); for (byte[] response : result.getResponses()) { - logger.debug("Response length:{} IP address:{} ", response.length, ipAddress); + logger.debug("Response length: {} IP address: {} ", response.length, ipAddress); if (response.length > 40 + 16) { byte[] data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); @@ -383,12 +366,12 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal default: logger.debug("Invalid response type: {}", data[0x9]); } - logger.trace("Response Type: {} and bodyType:{}", responseType, bodyType2); + logger.trace("Response Type: {} and bodyType: {}", responseType, bodyType2); // The response data from the appliance includes a packet header which we don't want data = Arrays.copyOfRange(data, 10, data.length); byte bodyType = data[0x0]; - logger.trace("Response Type expected: {} and bodyType:{}", responseType, bodyType); + logger.trace("Response Type expected: {} and bodyType: {}", responseType, bodyType); logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", @@ -401,7 +384,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } if (bodyType != -64) { if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from IP Address {}", bodyType, + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); return; } @@ -410,7 +393,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } lastResponse = new Response(data, version, responseType, bodyType); try { - logger.trace("data length is {} version is {} IP address is {}", data.length, + logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, ipAddress); if (callback != null) { callback.updateChannels(lastResponse); @@ -432,42 +415,42 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal Utils.bytesToHex(data)); lastResponse = new Response(data, version, "", (byte) 0x00); - logger.debug("V2 data length is {} version is {} Ip Address is {}", data.length, version, + logger.debug("Data length is {}, version is {}, Ip Address is {}", data.length, version, ipAddress); if (callback != null) { callback.updateChannels(lastResponse); } } else { droppedCommands = droppedCommands + 1; - logger.debug("Problem with reading V2 response, skipping command {} dropped count{}", command, + logger.debug("Problem with reading V2 response, skipping {} skipped count since startup {}", command, droppedCommands); } } return; } else { droppedCommands = droppedCommands + 1; - logger.debug("Problem with reading response, skipping command {} dropped count{}", command, + logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command, droppedCommands); return; } } catch (SocketException e) { - logger.debug("SocketException writing to {}: {}", ipAddress, e.getMessage()); droppedCommands = droppedCommands + 1; - logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); + logger.debug("Socket exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command, + droppedCommands); throw new MideaConnectionException(e); } catch (IOException e) { - logger.debug(" Send IOException writing to {}: {}", ipAddress, e.getMessage()); droppedCommands = droppedCommands + 1; - logger.debug("Socket exception, skipping command {} dropped count{}", command, droppedCommands); + logger.debug("IO exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command, + droppedCommands); throw new MideaConnectionException(e); } } /** * Closes all elements of the connection before starting a new one + * Makes sure writer, inputStream and socket are closed before each command is started */ public synchronized void disconnect() { - // Make sure writer, inputStream and socket are closed before each command is started logger.debug("Disconnecting from device at {}", ipAddress); InputStream inputStream = this.inputStream; @@ -498,7 +481,7 @@ public synchronized void disconnect() { try { int len = inputStream.read(bytes); if (len > 0) { - logger.debug("Response received length: {} Device IP {}", len, ipAddress); + logger.debug("Response received length: {} from device at IP: {}", len, ipAddress); bytes = Arrays.copyOfRange(bytes, 0, len); return bytes; } @@ -527,25 +510,7 @@ public synchronized void write(byte[] buffer) throws IOException { } /** - * Reset Retry controls the short 5 second delay - * Before starting 30 second delays. (More severe Wifi issue) - * It is reset after a successful connection - */ - private void resetRetry() { - retry = true; - } - - /** - * Limit logging of INFO connection messages to - * only when the device was Offline in its prior - * state - */ - private void resetConnectionMessage() { - connectionMessage = true; - } - - /** - * Disconnects from the device + * Disconnects from the AC device * * @param force */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index cfdbe02de67b8..f5341e055d21b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -70,7 +70,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService { private Security security; /** - * Discovery Service + * Discovery Service Uses the default decryption for all devices */ public MideaACDiscoveryService() { super(SUPPORTED_THING_TYPES_UIDS, discoveryTimeoutSeconds, false); @@ -152,7 +152,7 @@ public void discoverThing(String ipAddress, DiscoveryHandler discoveryHandler) { } } } catch (SocketTimeoutException e) { - logger.debug("Discovering poller timeout..."); + logger.trace("Discovering poller timeout..."); } catch (IOException e) { logger.debug("Error during discovery: {}", e.getMessage()); } finally { @@ -243,13 +243,14 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { final String ipAddress = packet.getAddress().getHostAddress(); byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength()); - logger.debug("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data)); + logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data)); if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A") || Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) { logger.trace("Device supported"); - String mSmartId, mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", - mSmartType = ""; + String mSmartId, mSmartip = "", mSmartSN = "", mSmartSSID = "", mSmartType = "", mSmartPort = "", + mSmartVersion = ""; + if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) { mSmartVersion = "2"; } @@ -260,7 +261,7 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { data = Arrays.copyOfRange(data, 8, data.length - 16); } - logger.trace("Version: {}", mSmartVersion); + logger.debug("Version: {}", mSmartVersion); byte[] id = Arrays.copyOfRange(data, 20, 26); logger.trace("Id Bytes: {}", Utils.bytesToHex(id)); @@ -273,10 +274,10 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { logger.debug("Id: '{}'", mSmartId); byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16); - logger.debug("Encrypt data: '{}'", Utils.bytesToHex(encryptData)); + logger.trace("Encrypt data: '{}'", Utils.bytesToHex(encryptData)); byte[] reply = security.aesDecrypt(encryptData); - logger.debug("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply)); + logger.trace("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply)); mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); @@ -343,7 +344,7 @@ private Map collectProperties(String ipAddress, String version, properties.put(CONFIG_IP_ADDRESS, ipAddress); properties.put(CONFIG_IP_PORT, port); properties.put(CONFIG_DEVICEID, id); - properties.put(PROPERTY_VERSION, version); + properties.put(CONFIG_VERSION, version); properties.put(PROPERTY_SN, sn); properties.put(PROPERTY_SSID, ssid); properties.put(PROPERTY_TYPE, type); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index aad6aeb15dce6..b1f8b8af421b7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; +import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -75,11 +76,13 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); private final CloudsDTO clouds; private final boolean imperialUnits; + private boolean isPollRunning = false; private final HttpClient httpClient; private MideaACConfiguration config = new MideaACConfiguration(); private Map properties = new HashMap<>(); - private @Nullable ConnectionManager connectionManager; + // Default parameters are the same as in the MideaACConfiguration class + private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false); private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); private @Nullable ScheduledFuture scheduledTask = null; @@ -113,27 +116,22 @@ public CloudsDTO getClouds() { } /** - * This method handles the Channels that can be set (non-read only) - * First the Routine polling is stopped so there is no conflict - * Then connects and authorizes (if necessary) and returns here to - * create the command set which is then sent to the device. + * This method handles the AC Channels that can be set (non-read only) + * The command set is formed using the previous command to only + * change the item requested and leave the others the same. + * The command set which is then sent to the device via the connectionManager. */ @Override public void handleCommand(ChannelUID channelUID, Command command) { logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString()); ConnectionManager connectionManager = this.connectionManager; - if (connectionManager == null) { - logger.warn("The connection manager was unexpectedly null, please report a bug"); - return; - } + if (command instanceof RefreshType) { try { connectionManager.getStatus(callbackLambda); } catch (MideaAuthenticationException e) { - logger.warn("Unable to proces command: {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (MideaConnectionException | MideaException e) { - logger.warn("Unable to proces command: {}", e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } return; @@ -178,12 +176,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { * Initialize is called on first pass or when a device parameter is changed * The basic check is if the information from Discovery (or the user update) * is valid. Because V2 devices do not require a cloud provider (or token/key) - * The check is for the IP, port and deviceID. This method also resets the dropped - * commands, disconnects the socket and stops the connection monitor (if these were - * running) + * The first check is for the IP, port and deviceID. The second part + * checks the security configuration if required (V3 device). */ @Override public void initialize() { + if (isPollRunning) { + stopScheduler(); + } config = getConfigAs(MideaACConfiguration.class); if (!config.isValid()) { @@ -196,10 +196,13 @@ public void initialize() { try { discoveryService.discoverThing(config.ipAddress, this); + return; } catch (Exception e) { logger.error("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Discovery failure. Check configuration."); + return; } - return; } else { logger.debug("MideaACHandler config of {} is invalid. Check configuration", thing.getUID()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -215,8 +218,10 @@ public void initialize() { logger.info("Retrieving Token and/or Key from cloud"); CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); getTokenKeyCloud(cloudProvider); + return; } else { - logger.warn("Configuration invalid for {}", thing.getUID()); + logger.warn("Configuration invalid for {} and no account info to retrieve from cloud", thing.getUID()); + return; } } else { logger.debug("Security Configuration (V3 Device) valid for {}", thing.getUID()); @@ -228,21 +233,29 @@ public void initialize() { config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); - // startScheduler(2, config.pollingTime, TimeUnit.SECONDS); - scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); + startScheduler(2, config.pollingTime, TimeUnit.SECONDS); } - public void startScheduler(long initialDelay, long delay, TimeUnit unit) { - scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit); - logger.debug("Scheduled task started"); + /** + * Starts the Scheduler for the Polling + * + * @param initialDelay Seconds before first Poll + * @param delay Seconds between Polls + * @param unit Seconds + */ + private void startScheduler(long initialDelay, long delay, TimeUnit unit) { + if (scheduledTask == null) { + isPollRunning = true; + scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit); + logger.debug("Scheduled task started"); + } else { + logger.debug("Scheduler already running"); + } } private void pollJob() { ConnectionManager connectionManager = this.connectionManager; - if (connectionManager == null) { - logger.warn("The connection manager was unexpectedly null, please report a bug"); - return; - } + try { connectionManager.getStatus(callbackLambda); updateStatus(ThingStatus.ONLINE); @@ -315,13 +328,15 @@ public void discovered(DiscoveryResult discoveryResult) { Object propertyIpPort = Objects.requireNonNull(discoveryProps.get(CONFIG_IP_PORT)); configuration.put(CONFIG_IP_PORT, propertyIpPort.toString()); + Object propertyVersion = Objects.requireNonNull(discoveryProps.get(CONFIG_VERSION)); + BigDecimal bigDecimalVersion = new BigDecimal((String) propertyVersion); + logger.trace("Property Version in Handler {}", bigDecimalVersion.intValue()); + configuration.put(CONFIG_VERSION, bigDecimalVersion.intValue()); + updateConfiguration(configuration); properties = editProperties(); - Object propertyVersion = Objects.requireNonNull(discoveryProps.get(PROPERTY_VERSION)); - properties.put(PROPERTY_VERSION, propertyVersion.toString()); - Object propertySN = Objects.requireNonNull(discoveryProps.get(PROPERTY_SN)); properties.put(PROPERTY_SN, propertySN.toString()); @@ -332,10 +347,14 @@ public void discovered(DiscoveryResult discoveryResult) { properties.put(PROPERTY_TYPE, propertyType.toString()); updateProperties(properties); - initialize(); } + /** + * Gets the token and key from the Cloud + * + * @param cloudProvider Cloud Provider account + */ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider); if (cloud != null) { @@ -350,7 +369,7 @@ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { logger.trace("Token: {}", tk.token()); logger.trace("Key: {}", tk.key()); - logger.info("Token and Key obtained from cloud, saving, initializing"); + logger.info("Token and Key obtained from cloud, saving, back to initialize"); initialize(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( @@ -360,22 +379,20 @@ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { } } - public void stopScheduler() { + private void stopScheduler() { ScheduledFuture localScheduledTask = this.scheduledTask; if (localScheduledTask != null && !localScheduledTask.isCancelled()) { localScheduledTask.cancel(true); logger.debug("Scheduled task cancelled."); + isPollRunning = false; scheduledTask = null; } - if (scheduler != null && !scheduler.isShutdown()) { - scheduler.shutdownNow(); - logger.debug("Scheduler service shut down."); - } } @Override public void dispose() { - // stopScheduler(); + stopScheduler(); + connectionManager.dispose(true); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index b521f5aa9c0a1..56615ee2a808e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -68,6 +68,7 @@ public Response(byte[] data, int version, String responseType, byte bodyType) { logger.debug("Swing Mode: {}", getSwingMode()); logger.debug("Sleep Function: {}", getSleepFunction()); logger.debug("Turbo Mode: {}", getTurboMode()); + logger.debug("Eco Mode: {}", getEcoMode()); logger.debug("Indoor Temperature: {}", getIndoorTemperature()); logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); logger.debug("LED Display: {}", getDisplayOn()); @@ -77,7 +78,6 @@ public Response(byte[] data, int version, String responseType, byte bodyType) { logger.trace("Prompt Tone: {}", getPromptTone()); logger.trace("Appliance Error: {}", getApplianceError()); logger.trace("Auxiliary Heat: {}", getAuxHeat()); - logger.trace("Eco Mode: {}", getEcoMode()); logger.trace("Fahrenheit: {}", getFahrenheit()); logger.trace("Humidity: {}", getHumidity()); logger.trace("Alternate Target Temperature {}", getAlternateTargetTemperature()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 98f30a9f964c5..0a449f110256d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -22,7 +22,7 @@ thing-type.config.mideaac.ac.email.description = Email for cloud account chosen thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port -thing-type.config.mideaac.ac.ipPort.description = IP port of the device (for V2: 6444). +thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). thing-type.config.mideaac.ac.password.label = Password @@ -36,7 +36,7 @@ thing-type.config.mideaac.ac.timeout.description = Connecting timeout. Minimum t thing-type.config.mideaac.ac.token.label = Token thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). thing-type.config.mideaac.ac.version.label = AC Version -thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover +thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. # channel types diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 46a1f9a48bbaa..13f29be8fe3ee 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -41,10 +41,10 @@ IP Address of the device. - + ipPort - IP port of the device (for V2: 6444). + IP port of the device. 6444 @@ -106,11 +106,11 @@ After sending a command device will play "ding" tone when command is received and executed. false - + version - Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. Leave blank to discover - 3 + Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. + 0 diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index 4c330036448d7..d0463985edc12 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -31,37 +31,84 @@ public class MideaACConfigurationTest { MideaACConfiguration config = new MideaACConfiguration(); /** - * Test for valid Configs + * Test for valid step 1 Configs */ @Test public void testValidConfigs() { config.ipAddress = "192.168.0.1"; config.ipPort = 6444; config.deviceId = "1234567890"; + config.version = 3; assertTrue(config.isValid()); assertFalse(config.isDiscoveryNeeded()); } /** - * Test for non-valid configs + * Test for non-valid step 1 configs */ @Test public void testnonValidConfigs() { config.ipAddress = "192.168.0.1"; config.ipPort = 0; config.deviceId = "1234567890"; + config.version = 3; assertFalse(config.isValid()); assertTrue(config.isDiscoveryNeeded()); } /** - * Test for bad IP configs + * Test for valid Security Configs + */ + @Test + public void testValidSecurityConfigs() { + config.key = "97c65a4eed4f49fda06a1a51d5cbd61d2c9b81d103ca4ca689f352a07a16fae6"; + config.token = "D24046B597DB9C8A7CA029660BC606F3FD7EBF12693E73B2EF1FFE4C3B7CA00C824E408C9F3CE972CC0D3F8250AD79D0E67B101B47AC2DD84B396E52EA05193F"; + config.cloud = "NetHome Plus"; + assertTrue(config.isV3ConfigValid()); + } + + /** + * Test for Invalid Security Configs + */ + @Test + public void testInvalidSecurityConfigs() { + config.key = "97c65a4eed4f49fda06a1a51d5cbd61d2c9b81d103ca4ca689f352a07a16fae6"; + config.token = "D24046B597DB9C8A7CA029660BC606F3FD7EBF12693E73B2EF1FFE4C3B7CA00C824E408C9F3CE972CC0D3F8250AD79D0E67B101B47AC2DD84B396E52EA05193F"; + config.cloud = ""; + assertFalse(config.isV3ConfigValid()); + } + + /** + * Test for if key and token are obtainable from cloud + */ + @Test + public void testIfTokenAndKeyCanBeObtainedFromCloud() { + config.email = "someemail.com"; + config.password = "somestrongpassword"; + config.cloud = "NetHome Plus"; + assertTrue(config.isTokenKeyObtainable()); + } + + /** + * Test for if key and token cannot be obtaines from cloud + */ + @Test + public void testIfTokenAndKeyCanNotBeObtainedFromCloud() { + config.email = ""; + config.password = "somestrongpassword"; + config.cloud = "NetHome Plus"; + assertFalse(config.isTokenKeyObtainable()); + } + + /** + * Test for bad IP v.4 address */ @Test public void testBadIpConfigs() { config.ipAddress = "192.1680.1"; config.ipPort = 6444; config.deviceId = "1234567890"; + config.version = 3; assertTrue(config.isValid()); assertTrue(config.isDiscoveryNeeded()); } diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 76e9808a6e6fd..74d6794f94332 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -40,6 +40,19 @@ public class MideaACDiscoveryServiceTest { String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", mSmartType = ""; + /** + * Test Version + */ + @Test + public void testVersion() { + if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { + mSmartVersion = "3"; + } else { + mSmartVersion = "2"; + } + assertEquals("3", mSmartVersion); + } + /** * Test Id */ From 1c4ae3e0b029568af99f1b4ea79dafcc1b251423 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 10 Nov 2024 11:25:48 -0500 Subject: [PATCH 04/44] Apply spotless changes forgot to run spotless on the last update Signed-off-by: Bob Eckhoff --- .../internal/connection/ConnectionManager.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index cd837711aa688..a8582127366aa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -422,8 +422,8 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } } else { droppedCommands = droppedCommands + 1; - logger.debug("Problem with reading V2 response, skipping {} skipped count since startup {}", command, - droppedCommands); + logger.debug("Problem with reading V2 response, skipping {} skipped count since startup {}", + command, droppedCommands); } } return; @@ -435,13 +435,13 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } } catch (SocketException e) { droppedCommands = droppedCommands + 1; - logger.debug("Socket exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command, - droppedCommands); + logger.debug("Socket exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, + command, droppedCommands); throw new MideaConnectionException(e); } catch (IOException e) { droppedCommands = droppedCommands + 1; - logger.debug("IO exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, command, - droppedCommands); + logger.debug("IO exception on IP: {}, skipping command {} skipped count since startup {}", ipAddress, + command, droppedCommands); throw new MideaConnectionException(e); } } From 20a6c27d2a4b3b8f0ebf526e0c04db1a6f4aec6d Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 15 Nov 2024 09:34:07 -0500 Subject: [PATCH 05/44] New PR candidate Java doc and possible mdns discovery. Signed-off-by: Bob Eckhoff --- .../internal/MideaACConfiguration.java | 36 +++++++++++++++++++ .../connection/ConnectionManager.java | 1 + .../mideaac/internal/handler/Callback.java | 6 +++- .../src/main/resources/OH-INF/addon/addon.xml | 12 ++++++- 4 files changed, 53 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index b31abacacd020..9d1793056a93b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -23,28 +23,64 @@ @NonNullByDefault public class MideaACConfiguration { + /** + * IP Address + */ public String ipAddress = ""; + /** + * IP Port + */ public int ipPort = 6444; + /** + * Device ID + */ public String deviceId = "0"; + /** + * Cloud Account email + */ public String email = ""; + /** + * Cloud Account Password + */ public String password = ""; + /** + * Cloud Provider + */ public String cloud = ""; + /** + * Token + */ public String token = ""; + /** + * Key + */ public String key = ""; + /** + * Poll Frequency + */ public int pollingTime = 60; + /** + * Socket Timeout + */ public int timeout = 4; + /** + * Prompt tone from indoor unit with a Set Command + */ public boolean promptTone = false; + /** + * AC Version + */ public int version = 0; /** diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index a8582127366aa..ab5b1893590f0 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -140,6 +140,7 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent } retry = false; try { + socket.close(); socket = new Socket(); socket.setSoTimeout(timeout * 1000); socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 98f8464b3e075..7df2505918506 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -21,6 +21,10 @@ */ @NonNullByDefault public interface Callback { - +/** + * Updates channels with the response + * + * @param response Byte response from the device used to update channels + */ void updateChannels(Response response); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml index 4767a09ed3127..1ee28925ed16d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/addon/addon.xml @@ -7,5 +7,15 @@ MideaAC Binding This is the binding for MideaAC. local - + + + mdns + + + mdnsServiceType + _mideaair._tcp.local. + + + + From ca8619f730eb897a2668bfc92bb233336d5fcf18 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 15 Nov 2024 09:37:34 -0500 Subject: [PATCH 06/44] Spotless changes spotless Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/handler/Callback.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 7df2505918506..29d6a0b703e93 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -21,10 +21,10 @@ */ @NonNullByDefault public interface Callback { -/** - * Updates channels with the response - * - * @param response Byte response from the device used to update channels - */ + /** + * Updates channels with the response + * + * @param response Byte response from the device used to update channels + */ void updateChannels(Response response); } From 31f23e2ce94576542c61e53b6fb0ea331e966fbc Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sat, 23 Nov 2024 11:09:48 -0500 Subject: [PATCH 07/44] Make retries more robust Makes retries more robust. Three times for connection and one retry on the command. Signed-off-by: Bob Eckhoff --- .../connection/ConnectionManager.java | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index ab5b1893590f0..7cea3dd053e06 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -70,7 +70,7 @@ public class ConnectionManager { private int droppedCommands = 0; /** - * True allows one short retry after connection problem + * True allows command retry if null response */ private boolean retry = true; @@ -124,36 +124,35 @@ public static boolean isBlank(String str) { public synchronized void connect() throws MideaConnectionException, MideaAuthenticationException { logger.trace("Connecting to {}:{}", ipAddress, ipPort); + int maxTries = 3; + int retryCount = 0; + // Open socket - try { - socket = new Socket(); - socket.setSoTimeout(timeout * 1000); - socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); - } catch (IOException e) { - // Retry addresses most common wifi connection problems- wait 5 seconds and try again - if (retry) { - logger.debug("Retrying Socket, IOException connecting to {}: {}", ipAddress, e.getMessage()); - try { - Thread.sleep(5000); - } catch (InterruptedException ex) { - logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); - } - retry = false; - try { - socket.close(); - socket = new Socket(); - socket.setSoTimeout(timeout * 1000); - socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); - } catch (IOException e2) { - deviceIsConnected = false; - logger.debug("Second try IOException connecting to {}: {}", ipAddress, e2.getMessage()); - throw new MideaConnectionException(e2); + // Retry addresses most common wifi connection problems- wait 5 seconds and try again + while (retryCount < maxTries) { + try { + socket = new Socket(); + socket.setSoTimeout(timeout * 1000); + socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); + break; + } catch (IOException e) { + retryCount++; + if (retryCount < maxTries) { + try { + Thread.sleep(5000); + } catch (InterruptedException ex) { + logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); + } + logger.debug("Socket retry count {}, IOException connecting to {}: {}", retryCount, ipAddress, + e.getMessage()); } - } else { - deviceIsConnected = false; - throw new MideaConnectionException(e); } } + if (retryCount == maxTries) { + deviceIsConnected = false; + logger.info("Failed to connect after {} tries. Try again with next scheduled poll", maxTries); + throw new MideaConnectionException("Failed to connect after maximum tries"); + } // Create streams try { @@ -180,7 +179,6 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent } logger.debug("Connected to IP {}", ipAddress); deviceIsConnected = true; - retry = true; } /** @@ -321,6 +319,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal byte[] responseBytes = read(); if (responseBytes != null) { + retry = true; if (version == 3) { Decryption8370Result result = security.decode8370(responseBytes); for (byte[] response : result.getResponses()) { @@ -412,14 +411,30 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal Utils.bytesToHex(data)); if (data.length > 0) { data = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = data[0x0]; logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); - - lastResponse = new Response(data, version, "", (byte) 0x00); - logger.debug("Data length is {}, version is {}, Ip Address is {}", data.length, version, - ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + if (bodyType != -64) { + if (bodyType == 30) { + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); + return; + } + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, version, "", bodyType); + try { + logger.trace("Data length is {}, version is {}, Ip Address is {}", data.length, version, + ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); + } + } catch (Exception ex) { + logger.warn("Processing response exception: {}", ex.getMessage()); } } else { droppedCommands = droppedCommands + 1; @@ -429,10 +444,17 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } return; } else { - droppedCommands = droppedCommands + 1; - logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command, - droppedCommands); - return; + if (retry) { + logger.debug("Resending Command {}", command); + retry = false; + sendCommand(command, callback); + } else { + droppedCommands = droppedCommands + 1; + logger.info("Problem with reading response, skipping {} skipped count since startup {}", command, + droppedCommands); + retry = true; + return; + } } } catch (SocketException e) { droppedCommands = droppedCommands + 1; From 44f2a8e384dd5ec90d5b5eafcd719cad431ad8dc Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 24 Nov 2024 20:10:59 -0500 Subject: [PATCH 08/44] Align V2 response with V3 reponse Changed V2 response to align with V3 process after extra decoding. Signed-off-by: Bob Eckhoff --- .../connection/ConnectionManager.java | 59 +++++++++---------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 7cea3dd053e06..e281f9920372b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -377,43 +377,44 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToBinary(data)); - if (data.length > 0) { - if (data.length < 21) { - logger.warn("Response data is {} long, minimum is 21!", data.length); - return; - } - if (bodyType != -64) { - if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, - ipAddress); - return; - } - logger.warn("Unexpected response bodyType {}", bodyType); + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + if (bodyType != -64) { + if (bodyType == 30) { + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, + ipAddress); return; } - lastResponse = new Response(data, version, responseType, bodyType); - try { - logger.trace("Data length is {}, version is {}, IP address is {}", data.length, - version, ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); - } - } catch (Exception ex) { - logger.warn("Processing response exception: {}", ex.getMessage()); + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, version, responseType, bodyType); + try { + logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, + ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); } + } catch (Exception ex) { + logger.warn("Processing response exception: {}", ex.getMessage()); } } } } else { - byte[] data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); - // The response data from the appliance includes a packet header which we don't want - logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); - if (data.length > 0) { + if (responseBytes.length > 40 + 16) { + byte[] data = security + .aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); + logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + + // The response data from the appliance includes a packet header which we don't want data = Arrays.copyOfRange(data, 10, data.length); byte bodyType = data[0x0]; logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); + if (data.length < 21) { logger.warn("Response data is {} long, minimum is 21!", data.length); return; @@ -436,10 +437,6 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } catch (Exception ex) { logger.warn("Processing response exception: {}", ex.getMessage()); } - } else { - droppedCommands = droppedCommands + 1; - logger.debug("Problem with reading V2 response, skipping {} skipped count since startup {}", - command, droppedCommands); } } return; @@ -495,7 +492,7 @@ public synchronized void disconnect() { /** * Reads the inputStream byte array * - * @return byte array + * @return byte array or null */ public synchronized byte @Nullable [] read() { byte[] bytes = new byte[512]; From d03fe24c2e8921b1977ddc25d76465d39af1f7cf Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 16 Dec 2024 10:07:17 -0500 Subject: [PATCH 09/44] Change to OH5.0 snapshot Changed version and changed dates (in advance-hopefully ok) Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/pom.xml | 2 +- .../binding/mideaac/internal/MideaACBindingConstants.java | 2 +- .../openhab/binding/mideaac/internal/MideaACConfiguration.java | 2 +- .../openhab/binding/mideaac/internal/MideaACHandlerFactory.java | 2 +- .../main/java/org/openhab/binding/mideaac/internal/Utils.java | 2 +- .../binding/mideaac/internal/connection/CommandHelper.java | 2 +- .../binding/mideaac/internal/connection/ConnectionManager.java | 2 +- .../connection/exception/MideaAuthenticationException.java | 2 +- .../internal/connection/exception/MideaConnectionException.java | 2 +- .../mideaac/internal/connection/exception/MideaException.java | 2 +- .../openhab/binding/mideaac/internal/discovery/Connection.java | 2 +- .../binding/mideaac/internal/discovery/DiscoveryHandler.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryService.java | 2 +- .../java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java | 2 +- .../openhab/binding/mideaac/internal/dto/CloudProviderDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/dto/CloudsDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Callback.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandBase.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandSet.java | 2 +- .../binding/mideaac/internal/handler/MideaACHandler.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Packet.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Response.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Timer.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Crc8.java | 2 +- .../binding/mideaac/internal/security/Decryption8370Result.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Security.java | 2 +- .../org/openhab/binding/mideaac/internal/security/TokenKey.java | 2 +- .../binding/mideaac/internal/MideaACConfigurationTest.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryServiceTest.java | 2 +- .../binding/mideaac/internal/handler/CommandSetTest.java | 2 +- .../openhab/binding/mideaac/internal/handler/ResponseTest.java | 2 +- 31 files changed, 31 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/pom.xml b/bundles/org.openhab.binding.mideaac/pom.xml index a19f12966b8b3..1e12614c8c885 100644 --- a/bundles/org.openhab.binding.mideaac/pom.xml +++ b/bundles/org.openhab.binding.mideaac/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 4.3.0-SNAPSHOT + 5.0.0-SNAPSHOT org.openhab.binding.mideaac diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index c9559a571852b..e334a558cc7ee 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 9d1793056a93b..da8501554681b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 759418a7c29cf..73e6de5d69a11 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index eedad2b658e1c..1d10e038c0ab7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 1b4e58a168990..0866797994e12 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index e281f9920372b..b14b049541626 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java index db8b0ce1d9791..57d8a8bff1dbf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java index efbf5129f8313..c1456a76796fb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java index 44510e573a8a7..95a169c32da2f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java index 44dbc131cc29a..d98d47f918c60 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java index a1d25cd41bbee..2de5d0460b802 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index f5341e055d21b..7cfe567dd1848 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java index 22714ebff8432..e01f5bb811ddb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java index ac92bfd00647f..a38a4781701f6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java index 3b4552e335751..e70094f7162cc 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 29d6a0b703e93..366dd36196f13 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index e81a4cdcb3acd..9bb0e211a8ecb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 6be330c8095af..e389a8504e6da 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index b1f8b8af421b7..6cc514beebaa7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java index 7e9424916830d..42e3db763f680 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 56615ee2a808e..7132b63ef4e87 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java index c213ee489dca5..eb9951d42770c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java index 601d6bb5c7088..4b5a524c9c305 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java index 617dc367aaaca..73663dadbec23 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 8eec22960c516..afd54a936637d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java index 8087638cb9b43..81190692b8baa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index d0463985edc12..f018cd6603e41 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 74d6794f94332..60d5e2dac4f80 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index 3f75ff40869e5..9e872780c59f5 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 1f113499bb9e3..38061bd9e4f93 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From 72bead91cab63cbdacb0ab71cc979f8523077052 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 16 Dec 2024 10:13:00 -0500 Subject: [PATCH 10/44] Changing dates was a bad idea Changing dates early was a bad idea Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/MideaACBindingConstants.java | 2 +- .../openhab/binding/mideaac/internal/MideaACConfiguration.java | 2 +- .../openhab/binding/mideaac/internal/MideaACHandlerFactory.java | 2 +- .../main/java/org/openhab/binding/mideaac/internal/Utils.java | 2 +- .../binding/mideaac/internal/connection/CommandHelper.java | 2 +- .../binding/mideaac/internal/connection/ConnectionManager.java | 2 +- .../connection/exception/MideaAuthenticationException.java | 2 +- .../internal/connection/exception/MideaConnectionException.java | 2 +- .../mideaac/internal/connection/exception/MideaException.java | 2 +- .../openhab/binding/mideaac/internal/discovery/Connection.java | 2 +- .../binding/mideaac/internal/discovery/DiscoveryHandler.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryService.java | 2 +- .../java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java | 2 +- .../openhab/binding/mideaac/internal/dto/CloudProviderDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/dto/CloudsDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Callback.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandBase.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandSet.java | 2 +- .../binding/mideaac/internal/handler/MideaACHandler.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Packet.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Response.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Timer.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Crc8.java | 2 +- .../binding/mideaac/internal/security/Decryption8370Result.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Security.java | 2 +- .../org/openhab/binding/mideaac/internal/security/TokenKey.java | 2 +- .../binding/mideaac/internal/MideaACConfigurationTest.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryServiceTest.java | 2 +- .../binding/mideaac/internal/handler/CommandSetTest.java | 2 +- .../openhab/binding/mideaac/internal/handler/ResponseTest.java | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index e334a558cc7ee..c9559a571852b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index da8501554681b..9d1793056a93b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 73e6de5d69a11..759418a7c29cf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 1d10e038c0ab7..eedad2b658e1c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 0866797994e12..1b4e58a168990 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index b14b049541626..e281f9920372b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java index 57d8a8bff1dbf..db8b0ce1d9791 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java index c1456a76796fb..efbf5129f8313 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java index 95a169c32da2f..44510e573a8a7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java index d98d47f918c60..44dbc131cc29a 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java index 2de5d0460b802..a1d25cd41bbee 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 7cfe567dd1848..f5341e055d21b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java index e01f5bb811ddb..22714ebff8432 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java index a38a4781701f6..ac92bfd00647f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java index e70094f7162cc..3b4552e335751 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 366dd36196f13..29d6a0b703e93 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index 9bb0e211a8ecb..e81a4cdcb3acd 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index e389a8504e6da..6be330c8095af 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 6cc514beebaa7..b1f8b8af421b7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java index 42e3db763f680..7e9424916830d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 7132b63ef4e87..56615ee2a808e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java index eb9951d42770c..c213ee489dca5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java index 4b5a524c9c305..601d6bb5c7088 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java index 73663dadbec23..617dc367aaaca 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index afd54a936637d..8eec22960c516 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java index 81190692b8baa..8087638cb9b43 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index f018cd6603e41..d0463985edc12 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 60d5e2dac4f80..74d6794f94332 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index 9e872780c59f5..3f75ff40869e5 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 38061bd9e4f93..1f113499bb9e3 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2025 Contributors to the openHAB project + * Copyright (c) 2010-2024 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From 0ada337dcbb973da84c9deb79b1ddc66e63ad59a Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 3 Jan 2025 10:21:08 -0500 Subject: [PATCH 11/44] Revert "Changing dates was a bad idea" This reverts commit 44bb90ff99ff470bfd6efd1ddbcdf3dc8f7f31c9. Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/MideaACBindingConstants.java | 2 +- .../openhab/binding/mideaac/internal/MideaACConfiguration.java | 2 +- .../openhab/binding/mideaac/internal/MideaACHandlerFactory.java | 2 +- .../main/java/org/openhab/binding/mideaac/internal/Utils.java | 2 +- .../binding/mideaac/internal/connection/CommandHelper.java | 2 +- .../binding/mideaac/internal/connection/ConnectionManager.java | 2 +- .../connection/exception/MideaAuthenticationException.java | 2 +- .../internal/connection/exception/MideaConnectionException.java | 2 +- .../mideaac/internal/connection/exception/MideaException.java | 2 +- .../openhab/binding/mideaac/internal/discovery/Connection.java | 2 +- .../binding/mideaac/internal/discovery/DiscoveryHandler.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryService.java | 2 +- .../java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java | 2 +- .../openhab/binding/mideaac/internal/dto/CloudProviderDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/dto/CloudsDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Callback.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandBase.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandSet.java | 2 +- .../binding/mideaac/internal/handler/MideaACHandler.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Packet.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Response.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Timer.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Crc8.java | 2 +- .../binding/mideaac/internal/security/Decryption8370Result.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Security.java | 2 +- .../org/openhab/binding/mideaac/internal/security/TokenKey.java | 2 +- .../binding/mideaac/internal/MideaACConfigurationTest.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryServiceTest.java | 2 +- .../binding/mideaac/internal/handler/CommandSetTest.java | 2 +- .../openhab/binding/mideaac/internal/handler/ResponseTest.java | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index c9559a571852b..e334a558cc7ee 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 9d1793056a93b..da8501554681b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 759418a7c29cf..73e6de5d69a11 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index eedad2b658e1c..1d10e038c0ab7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 1b4e58a168990..0866797994e12 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index e281f9920372b..b14b049541626 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java index db8b0ce1d9791..57d8a8bff1dbf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java index efbf5129f8313..c1456a76796fb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java index 44510e573a8a7..95a169c32da2f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java index 44dbc131cc29a..d98d47f918c60 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java index a1d25cd41bbee..2de5d0460b802 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index f5341e055d21b..7cfe567dd1848 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java index 22714ebff8432..e01f5bb811ddb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java index ac92bfd00647f..a38a4781701f6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java index 3b4552e335751..e70094f7162cc 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 29d6a0b703e93..366dd36196f13 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index e81a4cdcb3acd..9bb0e211a8ecb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 6be330c8095af..e389a8504e6da 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index b1f8b8af421b7..6cc514beebaa7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java index 7e9424916830d..42e3db763f680 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 56615ee2a808e..7132b63ef4e87 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java index c213ee489dca5..eb9951d42770c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java index 601d6bb5c7088..4b5a524c9c305 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java index 617dc367aaaca..73663dadbec23 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 8eec22960c516..afd54a936637d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java index 8087638cb9b43..81190692b8baa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index d0463985edc12..f018cd6603e41 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 74d6794f94332..60d5e2dac4f80 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index 3f75ff40869e5..9e872780c59f5 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 1f113499bb9e3..38061bd9e4f93 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2024 Contributors to the openHAB project + * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From dcb15110f02b2b693b9a2af3ab15a52a16d145ce Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 8 Jan 2025 16:37:06 -0500 Subject: [PATCH 12/44] Change to new Headers Change to new Headers Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/MideaACBindingConstants.java | 2 +- .../openhab/binding/mideaac/internal/MideaACConfiguration.java | 2 +- .../openhab/binding/mideaac/internal/MideaACHandlerFactory.java | 2 +- .../main/java/org/openhab/binding/mideaac/internal/Utils.java | 2 +- .../binding/mideaac/internal/connection/CommandHelper.java | 2 +- .../binding/mideaac/internal/connection/ConnectionManager.java | 2 +- .../connection/exception/MideaAuthenticationException.java | 2 +- .../internal/connection/exception/MideaConnectionException.java | 2 +- .../mideaac/internal/connection/exception/MideaException.java | 2 +- .../openhab/binding/mideaac/internal/discovery/Connection.java | 2 +- .../binding/mideaac/internal/discovery/DiscoveryHandler.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryService.java | 2 +- .../java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java | 2 +- .../openhab/binding/mideaac/internal/dto/CloudProviderDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/dto/CloudsDTO.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Callback.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandBase.java | 2 +- .../openhab/binding/mideaac/internal/handler/CommandSet.java | 2 +- .../binding/mideaac/internal/handler/MideaACHandler.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Packet.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Response.java | 2 +- .../org/openhab/binding/mideaac/internal/handler/Timer.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Crc8.java | 2 +- .../binding/mideaac/internal/security/Decryption8370Result.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Security.java | 2 +- .../org/openhab/binding/mideaac/internal/security/TokenKey.java | 2 +- .../binding/mideaac/internal/MideaACConfigurationTest.java | 2 +- .../mideaac/internal/discovery/MideaACDiscoveryServiceTest.java | 2 +- .../binding/mideaac/internal/handler/CommandSetTest.java | 2 +- .../openhab/binding/mideaac/internal/handler/ResponseTest.java | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index e334a558cc7ee..6f5e4e86e7451 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index da8501554681b..4dd9aa2b3a559 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 73e6de5d69a11..7581c619b7fe7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 1d10e038c0ab7..4d524e4a9a3dd 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 0866797994e12..b8283a5993bd8 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index b14b049541626..3c72a16e4be07 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java index 57d8a8bff1dbf..655e70608f14c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java index c1456a76796fb..9f1b6692d6f23 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java index 95a169c32da2f..c2bcbb21893c7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaException.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java index d98d47f918c60..888086eea4cdb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java index 2de5d0460b802..36f4c0a43ef19 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/DiscoveryHandler.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 7cfe567dd1848..bae4ab8954411 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java index e01f5bb811ddb..956af5112631d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java index a38a4781701f6..c1c907b537368 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java index e70094f7162cc..00df03e3ebbe5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 366dd36196f13..a6e85da5f39c0 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index 9bb0e211a8ecb..c796b8b127347 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index e389a8504e6da..843ddcea309d3 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 6cc514beebaa7..5ef9ad4a01e08 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java index 42e3db763f680..873f165082bf9 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Packet.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 7132b63ef4e87..964e4662ca578 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java index eb9951d42770c..d448bd478e64b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Timer.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java index 4b5a524c9c305..e1d136233cc8d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java index 73663dadbec23..f746a91312499 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index afd54a936637d..c113676e478f6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java index 81190692b8baa..b70e2e600cd6a 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index f018cd6603e41..97ebe323d265b 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 60d5e2dac4f80..5d9d98be77130 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index 9e872780c59f5..8e687ce0b37c6 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 38061bd9e4f93..61880766f1f54 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010-2025 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional From c6750ad8a4ca8ec1a5a7a56b1cbe44c5259a91c9 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 19 Feb 2025 17:39:31 -0500 Subject: [PATCH 13/44] Address Comments partial Addressed some PR comments. Some questions outstanding. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 8 +-- .../internal/MideaACConfiguration.java | 6 +-- .../binding/mideaac/internal/Utils.java | 2 - .../connection/ConnectionManager.java | 14 +----- .../internal/handler/MideaACHandler.java | 9 ++-- .../mideaac/internal/security/Security.java | 1 + .../resources/OH-INF/i18n/mideaac.properties | 50 ++++++++----------- .../resources/OH-INF/thing/thing-types.xml | 48 ++++++++---------- 8 files changed, 57 insertions(+), 81 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index cb41f09359950..329fc5723c6d9 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -18,7 +18,7 @@ This binding supports one Thing type `ac`. ## Discovery -Once the Air Conditioner is on the network (WiFi active) the other required parameters can be discovered automatically. +Once the Air Conditioner is on the network (WiFi active) most required parameters will be discovered automatically. An IP broadcast message is sent and every responding unit gets added to the Inbox. As an alternative use the python application msmart-ng from with the msmart-ng discover ipAddress option. @@ -111,10 +111,6 @@ sitemap midea label="Split AC MBR"{ Selection item=fan_speed label="Midea AC Fan Speed" Selection item=operational_mode label="Midea AC Mode" Selection item=swing_mode label="Midea AC Louver Swing Mode" -} + } } ``` - -## Debugging and Tracing - -Switch the log level to TRACE or DEBUG on the UI Settings Page (Add-on Settings) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 4dd9aa2b3a559..9fd42d400b1ea 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -99,7 +99,7 @@ public boolean isValid() { * @return true(discovery needed), false (not needed) */ public boolean isDiscoveryNeeded() { - return ("0".equalsIgnoreCase(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() + return ("0".equals(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() || !Utils.validateIP(ipAddress) || version <= 1); } @@ -110,7 +110,7 @@ public boolean isDiscoveryNeeded() { * @return true (yes they can), false (they cannot) */ public boolean isTokenKeyObtainable() { - return (!email.isBlank() && !password.isBlank() && !"".equals(cloud)); + return (!email.isBlank() && !password.isBlank() && !"".equals(cloud) && !cloud.isBlank()); } /** @@ -119,6 +119,6 @@ public boolean isTokenKeyObtainable() { * @return true (Valid, all items are present) false (key, token and/or provider missing) */ public boolean isV3ConfigValid() { - return (!key.isBlank() && !token.isBlank() && !"".equals(cloud)); + return (!key.isBlank() && !token.isBlank() && !"".equals(cloud) && !cloud.isBlank()); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 4d524e4a9a3dd..c43b948654f79 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -62,10 +62,8 @@ public static String bytesToHex(byte[] bytes) { public static String bytesToBinary(byte[] bytes) { String s1 = ""; for (int j = 0; j < bytes.length; j++) { - s1 = s1.concat(Integer.toBinaryString(bytes[j] & 255 | 256).substring(1)); s1 = s1.concat(" "); - } return s1; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 3c72a16e4be07..c062c19164d3b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -105,16 +105,6 @@ public Response getLastResponse() { return this.lastResponse; } - /** - * Validate if String is blank - * - * @param str string to be evaluated - * @return boolean true or false - */ - public static boolean isBlank(String str) { - return str.trim().isEmpty(); - } - /** * After checking if the key and token need to be updated (Default = 0 Never) * The socket is established with the writer and inputStream (for reading responses) @@ -150,7 +140,7 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent } if (retryCount == maxTries) { deviceIsConnected = false; - logger.info("Failed to connect after {} tries. Try again with next scheduled poll", maxTries); + logger.debug("Failed to connect after {} tries. Try again with next scheduled poll", maxTries); throw new MideaConnectionException("Failed to connect after maximum tries"); } @@ -194,7 +184,7 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE logger.trace("Token: {}", token); logger.trace("Cloud {}", cloud); - if (!isBlank(token) && !isBlank(key) && !"".equals(cloud)) { + if (!token.isBlank() && !key.isBlank() && !"".equals(cloud)) { logger.debug("Device at IP: {} authenticating", ipAddress); doV3Handshake(); } else { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 5ef9ad4a01e08..c224b23700d2d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -84,7 +84,7 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler // Default parameters are the same as in the MideaACConfiguration class private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false); private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - private @Nullable ScheduledFuture scheduledTask = null; + private @Nullable ScheduledFuture scheduledTask; private Callback callbackLambda = (response) -> { this.updateChannels(response); @@ -215,12 +215,12 @@ public void initialize() { if (config.version == 3 && !config.isV3ConfigValid()) { if (config.isTokenKeyObtainable()) { - logger.info("Retrieving Token and/or Key from cloud"); CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); getTokenKeyCloud(cloudProvider); return; } else { - logger.warn("Configuration invalid for {} and no account info to retrieve from cloud", thing.getUID()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Configuration invalid and no account info to retrieve from cloud"); return; } } else { @@ -356,6 +356,7 @@ public void discovered(DiscoveryResult discoveryResult) { * @param cloudProvider Cloud Provider account */ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { + logger.debug("Retrieving Token and/or Key from cloud"); CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider); if (cloud != null) { cloud.setHttpClient(httpClient); @@ -369,7 +370,7 @@ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { logger.trace("Token: {}", tk.token()); logger.trace("Key: {}", tk.key()); - logger.info("Token and Key obtained from cloud, saving, back to initialize"); + logger.debug("Token and Key obtained from cloud, saving, back to initialize"); initialize(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index c113676e478f6..6ee3832bb9334 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -182,6 +182,7 @@ public byte[] encode32Data(byte[] raw) { md.update(combine); return md.digest(); } catch (NoSuchAlgorithmException e) { + logger.warn("Encode32 Data: NoSuchAlgorithmException {}", e.getMessage()); } return new byte[0]; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 0a449f110256d..253f143becdec 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -40,18 +40,13 @@ thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key # channel types -channel-type.mideaac.alternate-target-temperature.label = Alternate Target Temperature -channel-type.mideaac.alternate-target-temperature.description = Alternate Target Temperature (Read Only). -channel-type.mideaac.appliance-error.label = Appliance error -channel-type.mideaac.appliance-error.description = Appliance error (Read Only). -channel-type.mideaac.auxiliary-heat.label = Auxiliary heat -channel-type.mideaac.auxiliary-heat.description = Auxiliary heat (Read Only). -channel-type.mideaac.dropped-commands.label = Dropped Command Monitor -channel-type.mideaac.dropped-commands.description = Commands dropped due to TCP read() issues. -channel-type.mideaac.eco-mode.label = Eco mode +channel-type.mideaac.alternate-target-temperature.label = Alt. Target Temperature +channel-type.mideaac.appliance-error.label = Appliance Error +channel-type.mideaac.auxiliary-heat.label = Auxiliary Heat +channel-type.mideaac.eco-mode.label = Eco Mode channel-type.mideaac.eco-mode.description = Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. -channel-type.mideaac.fan-speed.label = Fan speed -channel-type.mideaac.fan-speed.description = Fan speed: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. +channel-type.mideaac.fan-speed.label = Fan Speed +channel-type.mideaac.fan-speed.description = Fan speeds: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. channel-type.mideaac.fan-speed.state.option.SILENT = SILENT channel-type.mideaac.fan-speed.state.option.LOW = LOW channel-type.mideaac.fan-speed.state.option.MEDIUM = MEDIUM @@ -60,36 +55,35 @@ channel-type.mideaac.fan-speed.state.option.FULL = FULL channel-type.mideaac.fan-speed.state.option.AUTO = AUTO channel-type.mideaac.humidity.label = Humidity channel-type.mideaac.humidity.description = Humidity measured in the room by the indoor unit. -channel-type.mideaac.indoor-temperature.label = Indoor temperature +channel-type.mideaac.indoor-temperature.label = Indoor Temperature channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off -channel-type.mideaac.off-timer.label = OFF Timer -channel-type.mideaac.off-timer.description = OFF Timer (HH:MM) to set. -channel-type.mideaac.on-timer.label = ON Timer -channel-type.mideaac.on-timer.description = ON Timer (HH:MM) to set. -channel-type.mideaac.operational-mode.label = Operational mode -channel-type.mideaac.operational-mode.description = Operational mode: AUTO, COOL, DRY, HEAT. +channel-type.mideaac.off-timer.label = Off Timer +channel-type.mideaac.off-timer.description = Off Timer (HH:MM) to set. +channel-type.mideaac.on-timer.label = On Timer +channel-type.mideaac.on-timer.description = On Timer (HH:MM) to set. +channel-type.mideaac.operational-mode.label = Operational Mode +channel-type.mideaac.operational-mode.description = Operational modes: AUTO, COOL, DRY, HEAT, FAN ONLY. channel-type.mideaac.operational-mode.state.option.AUTO = AUTO channel-type.mideaac.operational-mode.state.option.COOL = COOL channel-type.mideaac.operational-mode.state.option.DRY = DRY channel-type.mideaac.operational-mode.state.option.HEAT = HEAT channel-type.mideaac.operational-mode.state.option.FAN_ONLY = FAN ONLY -channel-type.mideaac.outdoor-temperature.label = Outdoor temperature +channel-type.mideaac.outdoor-temperature.label = Outdoor Temperature channel-type.mideaac.outdoor-temperature.description = Outdoor temperature from the external unit. Not frequent when unit is off channel-type.mideaac.power.label = Power -channel-type.mideaac.power.description = Turn the AC on and off. -channel-type.mideaac.screen-display.label = Screen display +channel-type.mideaac.power.description = Turn the AC on or off. +channel-type.mideaac.screen-display.label = Screen Display channel-type.mideaac.screen-display.description = Status of LEDs on the device. Not all models work on LAN (only IR). No confirmation possible either. -channel-type.mideaac.sleep-function.label = Sleep function +channel-type.mideaac.sleep-function.label = Sleep Function channel-type.mideaac.sleep-function.description = Sleep function ("Moon with a star" icon on IR Remote Controller). -channel-type.mideaac.swing-mode.label = Swing mode -channel-type.mideaac.swing-mode.description = Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support +channel-type.mideaac.swing-mode.label = Swing Mode +channel-type.mideaac.swing-mode.description = Swing modes: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support channel-type.mideaac.swing-mode.state.option.OFF = OFF channel-type.mideaac.swing-mode.state.option.VERTICAL = VERTICAL channel-type.mideaac.swing-mode.state.option.HORIZONTAL = HORIZONTAL channel-type.mideaac.swing-mode.state.option.BOTH = BOTH -channel-type.mideaac.target-temperature.label = Target temperature -channel-type.mideaac.target-temperature.description = Target temperature. -channel-type.mideaac.temperature-unit.label = Temperature unit on LED Display +channel-type.mideaac.target-temperature.label = Target Temperature +channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. -channel-type.mideaac.turbo-mode.label = Turbo mode +channel-type.mideaac.turbo-mode.label = Turbo Mode channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 13f29be8fe3ee..e483d9fd72107 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -119,20 +119,19 @@ Switch - Turn the AC on and off. + Turn the AC on or off. Switch Number:Temperature - - Target temperature. + Temperature String - - Operational mode: AUTO, COOL, DRY, HEAT. + + Operational modes: AUTO, COOL, DRY, HEAT, FAN ONLY. @@ -145,8 +144,8 @@ String - - Fan speed: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. + + Fan speeds: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. @@ -160,8 +159,8 @@ String - - Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support + + Swing modes: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support @@ -173,71 +172,69 @@ Switch - + Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. Switch Switch - + Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. Switch Number:Temperature - + Indoor temperature measured by the internal unit. Not frequent when unit is off Temperature Number:Temperature - + Outdoor temperature from the external unit. Not frequent when unit is off Temperature Switch - + Sleep function ("Moon with a star" icon on IR Remote Controller). Switch Switch - + On = Farenheit on Indoor AC unit LED display, Off = Celsius. Switch Switch - + Status of LEDs on the device. Not all models work on LAN (only IR). No confirmation possible either. Switch Switch - - Appliance error (Read Only). + Switch String - - ON Timer (HH:MM) to set. + + On Timer (HH:MM) to set. String - - OFF Timer (HH:MM) to set. + + Off Timer (HH:MM) to set. Switch - - Auxiliary heat (Read Only). + Switch @@ -250,8 +247,7 @@ Number:Temperature - - Alternate Target Temperature (Read Only). + Temperature From 46a1ca565b8231c33595bccc28034dd7b2f47c27 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Thu, 20 Feb 2025 10:13:05 -0500 Subject: [PATCH 14/44] Additional edits Additional edits. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 26 ++++++++++--------- .../internal/MideaACConfiguration.java | 4 +-- .../connection/ConnectionManager.java | 5 ++-- .../internal/handler/MideaACHandler.java | 9 +++---- 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 329fc5723c6d9..8f30524755d24 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -18,9 +18,11 @@ This binding supports one Thing type `ac`. ## Discovery -Once the Air Conditioner is on the network (WiFi active) most required parameters will be discovered automatically. -An IP broadcast message is sent and every responding unit gets added to the Inbox. -As an alternative use the python application msmart-ng from with the msmart-ng discover ipAddress option. +Once the Air Conditioner is on the network (WiFi active) activating an Inbox scan with this binding will send an IP broadcast message. +Every responding unit gets added to the Inbox. When adding each thing, the required parameters of ipAddress, ipPort, deviceId, pollingTime, +timeout, promptTone and version will be populated with either discovered values or the default settings. A V.2 device will be Online. +A V.3 device will require you to enter the cloud provider, token and key before becoming Online. The token and key can be discovered by entering +your email and password for your cloud account. The email and password are stored securely, but can be deleted after the token and key are entered. ## Binding Configuration @@ -49,7 +51,7 @@ Following channels are available: | Channel | Type | Description | Read only | Advanced | |--:---------------------------|--:-----------------|--:-----------------------------------------------------------------------------------------------------|--:--------|--:-------| -| power | Switch | Turn the AC on and off. | | | +| power | Switch | Turn the AC on or off. | | | | target-temperature | Number:Temperature | Target temperature. | | | | operational-mode | String | Operational mode: OFF (turns off), AUTO, COOL, DRY, HEAT, FAN ONLY | | | | fan-speed | String | Fan speed: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | @@ -103,14 +105,14 @@ Switch temperature_unit "Fahrenheit or Celsius" { ch ```java sitemap midea label="Split AC MBR"{ Frame label="AC Unit" { - Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]" - Text item=indoor_temperature label="Indoor Temperature [%.1f °F]" - Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0 - Switch item=power label="Midea AC Power" - Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"] - Selection item=fan_speed label="Midea AC Fan Speed" - Selection item=operational_mode label="Midea AC Mode" - Selection item=swing_mode label="Midea AC Louver Swing Mode" + Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]" + Text item=indoor_temperature label="Indoor Temperature [%.1f °F]" + Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0 + Switch item=power label="Midea AC Power" + Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"] + Selection item=fan_speed label="Midea AC Fan Speed" + Selection item=operational_mode label="Midea AC Mode" + Selection item=swing_mode label="Midea AC Louver Swing Mode" } } ``` diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 9fd42d400b1ea..b3bfd45be7f29 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -110,7 +110,7 @@ public boolean isDiscoveryNeeded() { * @return true (yes they can), false (they cannot) */ public boolean isTokenKeyObtainable() { - return (!email.isBlank() && !password.isBlank() && !"".equals(cloud) && !cloud.isBlank()); + return (!email.isBlank() && !password.isBlank() && !cloud.isBlank()); } /** @@ -119,6 +119,6 @@ public boolean isTokenKeyObtainable() { * @return true (Valid, all items are present) false (key, token and/or provider missing) */ public boolean isV3ConfigValid() { - return (!key.isBlank() && !token.isBlank() && !"".equals(cloud) && !cloud.isBlank()); + return (!key.isBlank() && !token.isBlank() && !cloud.isBlank()); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index c062c19164d3b..035a251a53ad8 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -226,7 +226,7 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio } else if (Arrays.equals(new String("ERROR").getBytes(), response)) { throw new MideaAuthenticationException("Authentication failed!"); } else { - logger.warn("Authentication reponse unexpected data length ({} instead of 72)!", response.length); + logger.debug("Authentication reponse unexpected data length ({} instead of 72)!", response.length); throw new MideaAuthenticationException("Unexpected authentication response length"); } } @@ -437,7 +437,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal sendCommand(command, callback); } else { droppedCommands = droppedCommands + 1; - logger.info("Problem with reading response, skipping {} skipped count since startup {}", command, + logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command, droppedCommands); retry = true; return; @@ -470,7 +470,6 @@ public synchronized void disconnect() { writer.close(); inputStream.close(); socket.close(); - } catch (IOException e) { logger.warn("IOException closing connection to device at {}: {}", ipAddress, e.getMessage(), e); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index c224b23700d2d..5133da02b0bb6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -168,7 +168,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { connectionManager.sendCommand(CommandHelper.handleOffTimer(command, lastresponse), callbackLambda); } } catch (MideaConnectionException | MideaAuthenticationException e) { - logger.warn("Unable to proces command: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } @@ -187,9 +187,8 @@ public void initialize() { config = getConfigAs(MideaACConfiguration.class); if (!config.isValid()) { - logger.warn("Configuration invalid for {}", thing.getUID()); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration not valid"); if (config.isDiscoveryNeeded()) { - logger.warn("Discovery needed, discovering....{}", thing.getUID()); updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration missing, discovery needed. Discovering..."); MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); @@ -198,13 +197,12 @@ public void initialize() { discoveryService.discoverThing(config.ipAddress, this); return; } catch (Exception e) { - logger.error("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); + logger.debug("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Discovery failure. Check configuration."); return; } } else { - logger.debug("MideaACHandler config of {} is invalid. Check configuration", thing.getUID()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid MideaAC config. Check configuration."); return; @@ -375,7 +373,6 @@ public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); - logger.warn("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error"); } } } From 757a257d803750fffda9604c329ce68402d536ef Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 21 Feb 2025 18:42:24 -0500 Subject: [PATCH 15/44] Update utilities and connection manager Updated utilities to leverage OH core and separated retry timeout from other exceptions. Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/Utils.java | 38 +++++-------------- .../connection/ConnectionManager.java | 27 ++++++++----- .../internal/handler/MideaACHandler.java | 7 ++++ 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index c43b948654f79..5395cc4941296 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.jose4j.base64url.Base64; +import org.openhab.core.util.HexUtils; import com.google.gson.JsonObject; @@ -33,24 +34,16 @@ */ @NonNullByDefault public class Utils { - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - private static final char[] HEX_ARRAY_LOWERCASE = "0123456789abcdef".toCharArray(); static byte[] empty = new byte[0]; /** * Converts byte array to upper case hex string * * @param bytes bytes to convert - * @return string of hex chars + * @return string of upper case hex chars */ public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); + return HexUtils.bytesToHex(bytes); } /** @@ -72,16 +65,10 @@ public static String bytesToBinary(byte[] bytes) { * Converts byte array to lower case hex string * * @param bytes bytes to convert - * @return string of hex chars + * @return string of lower case hex chars */ public static String bytesToHexLowercase(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY_LOWERCASE[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY_LOWERCASE[v & 0x0F]; - } - return new String(hexChars); + return HexUtils.bytesToHex(bytes).toLowerCase(); } /** @@ -99,16 +86,11 @@ public static boolean validateIP(final String ip) { /** * Converts hex string to a byte array * - * @param s string to convert to byte array - * @return byte array + * @param string string to convert to byte array + * @return byte [] array */ - public static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); - } - return data; + public static byte[] hexStringToByteArray(String string) { + return HexUtils.hexToBytes(string); } /** @@ -155,7 +137,7 @@ public static byte[] strxor(byte[] array1, byte[] array2) { } /** - * Create String of the v3 Token + * Create String of the V.3 Token * * @param nbytes number of bytes * @return String diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 035a251a53ad8..bb25dabc52474 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -19,6 +19,7 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; +import java.net.SocketTimeoutException; import java.util.Arrays; import java.util.HexFormat; @@ -111,7 +112,8 @@ public Response getLastResponse() { * The device is considered connected. V2 devices will proceed to send the poll or the * set command. V3 devices will proceed to authenticate */ - public synchronized void connect() throws MideaConnectionException, MideaAuthenticationException { + public synchronized void connect() + throws MideaConnectionException, MideaAuthenticationException, SocketTimeoutException, IOException { logger.trace("Connecting to {}:{}", ipAddress, ipPort); int maxTries = 3; @@ -125,7 +127,7 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent socket.setSoTimeout(timeout * 1000); socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); break; - } catch (IOException e) { + } catch (SocketTimeoutException e) { retryCount++; if (retryCount < maxTries) { try { @@ -133,9 +135,13 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent } catch (InterruptedException ex) { logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); } - logger.debug("Socket retry count {}, IOException connecting to {}: {}", retryCount, ipAddress, + logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retryCount, ipAddress, e.getMessage()); } + } catch (IOException e) { + logger.debug("Socket retry count {}, IOException connecting to {}: {}", retryCount, ipAddress, + e.getMessage()); + break; } } if (retryCount == maxTries) { @@ -165,6 +171,7 @@ public synchronized void connect() throws MideaConnectionException, MideaAuthent } if (!deviceIsConnected) { + // Info logger on first connection after being disconnected logger.info("Connected to IP {}", ipAddress); } logger.debug("Connected to IP {}", ipAddress); @@ -184,7 +191,7 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE logger.trace("Token: {}", token); logger.trace("Cloud {}", cloud); - if (!token.isBlank() && !key.isBlank() && !"".equals(cloud)) { + if (!token.isBlank() && !key.isBlank() && !cloud.isBlank()) { logger.debug("Device at IP: {} authenticating", ipAddress); doV3Handshake(); } else { @@ -245,12 +252,12 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio * @throws MideaException */ public void getStatus(Callback callback) - throws MideaConnectionException, MideaAuthenticationException, MideaException { + throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException { CommandBase requestStatusCommand = new CommandBase(); sendCommand(requestStatusCommand, callback); } - private void ensureConnected() throws MideaConnectionException, MideaAuthenticationException { + private void ensureConnected() throws MideaConnectionException, MideaAuthenticationException, IOException { disconnect(); connect(); } @@ -269,7 +276,7 @@ private void ensureConnected() throws MideaConnectionException, MideaAuthenticat * @throws MideaConnectionException */ public synchronized void sendCommand(CommandBase command, @Nullable Callback callback) - throws MideaConnectionException, MideaAuthenticationException { + throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException { ensureConnected(); if (command instanceof CommandSet) { @@ -388,7 +395,8 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal callback.updateChannels(lastResponse); } } catch (Exception ex) { - logger.warn("Processing response exception: {}", ex.getMessage()); + logger.debug("Processing response exception: {}", ex.getMessage()); + throw new MideaException(ex); } } } @@ -425,7 +433,8 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal callback.updateChannels(lastResponse); } } catch (Exception ex) { - logger.warn("Processing response exception: {}", ex.getMessage()); + logger.debug("Processing response exception: {}", ex.getMessage()); + throw new MideaException(ex); } } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 5133da02b0bb6..dbd43f5eb0367 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -14,6 +14,7 @@ import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*; +import java.io.IOException; import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; @@ -133,6 +134,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (MideaConnectionException | MideaException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } return; } @@ -169,6 +172,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } catch (MideaConnectionException | MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (MideaException | IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } @@ -263,6 +268,8 @@ private void pollJob() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } catch (MideaException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } From 00ce79517adc2eee6d1486d17582af1822748df6 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 23 Feb 2025 19:28:36 -0500 Subject: [PATCH 16/44] Eliminate DTO folder and classes Eliminated DTO folder and classes by addressing null issues in the cloud communications. Plus slight timing issue on retry. Signed-off-by: Bob Eckhoff --- .../internal/MideaACHandlerFactory.java | 6 +-- .../{dto/CloudDTO.java => cloud/Cloud.java} | 39 ++++++++++++------- .../CloudProvider.java} | 14 +++---- .../{dto/CloudsDTO.java => cloud/Clouds.java} | 16 ++++---- .../connection/ConnectionManager.java | 17 ++++++-- .../discovery/MideaACDiscoveryService.java | 4 +- .../internal/handler/MideaACHandler.java | 20 +++++----- .../mideaac/internal/security/Security.java | 6 +-- 8 files changed, 72 insertions(+), 50 deletions(-) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/{dto/CloudDTO.java => cloud/Cloud.java} (90%) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/{dto/CloudProviderDTO.java => cloud/CloudProvider.java} (74%) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/{dto/CloudsDTO.java => cloud/Clouds.java} (73%) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 7581c619b7fe7..79eb4fd9bf022 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -16,7 +16,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mideaac.internal.dto.CloudsDTO; +import org.openhab.binding.mideaac.internal.cloud.Clouds; import org.openhab.binding.mideaac.internal.handler.MideaACHandler; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -40,7 +40,7 @@ public class MideaACHandlerFactory extends BaseThingHandlerFactory { private final HttpClientFactory httpClientFactory; - private final CloudsDTO clouds; + private final Clouds clouds; private final UnitProvider unitProvider; @Override @@ -58,7 +58,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { public MideaACHandlerFactory(@Reference UnitProvider unitProvider, @Reference HttpClientFactory httpClientFactory) { this.httpClientFactory = httpClientFactory; this.unitProvider = unitProvider; - clouds = new CloudsDTO(); + clouds = new Clouds(); } @Override diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java similarity index 90% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index 956af5112631d..c196fd1590834 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.dto; +package org.openhab.binding.mideaac.internal.cloud; import java.nio.ByteOrder; import java.text.SimpleDateFormat; @@ -21,6 +21,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; @@ -39,15 +40,16 @@ import com.google.gson.JsonObject; /** - * The {@link CloudDTO} class connects to the Cloud Provider + * The {@link Cloud} class connects to the Cloud Provider * with user supplied information to retrieve the Security * Token and Key. * * @author Jacek Dobrowolski - Initial contribution * @author Bob Eckhoff - JavaDoc */ -public class CloudDTO { - private final Logger logger = LoggerFactory.getLogger(CloudDTO.class); +@NonNullByDefault +public class Cloud { + private final Logger logger = LoggerFactory.getLogger(Cloud.class); private static final int CLIENT_TYPE = 1; // Android private static final int FORMAT = 2; // JSON @@ -60,7 +62,7 @@ private void setTokenRequested() { } /** - * Token rquested date + * Token requested date * * @return tokenRequestedAt */ @@ -88,7 +90,7 @@ public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } - private String errMsg; + private String errMsg = ""; /** * Gets error message @@ -102,12 +104,12 @@ public String getErrMsg() { private @Nullable String accessToken = ""; private String loginAccount; - private String password; - private CloudProviderDTO cloudProvider; + private String password = ""; + private CloudProvider cloudProvider; private Security security; private @Nullable String loginId; - private String sessionId; + private String sessionId = ""; /** * Parameters for Cloud Provider @@ -116,18 +118,22 @@ public String getErrMsg() { * @param password password * @param cloudProvider Cloud Provider */ - public CloudDTO(String email, String password, CloudProviderDTO cloudProvider) { + public Cloud(String email, String password, CloudProvider cloudProvider) { this.loginAccount = email; this.password = password; this.cloudProvider = cloudProvider; this.security = new Security(cloudProvider); + this.httpClient = new HttpClient(); logger.debug("Cloud provider: {}", cloudProvider.name()); } /** * Set up the initial data payload with the global variable set + * This first message is to confirm the email is in the cloud provider's + * memory. The return is msg "ok", "errorCode":"0", and a loginId to be used + * in the next message. */ - private JsonObject apiRequest(String endpoint, JsonObject args, JsonObject data) { + private @Nullable JsonObject apiRequest(String endpoint, @Nullable JsonObject args, @Nullable JsonObject data) { if (data == null) { data = new JsonObject(); data.addProperty("appId", cloudProvider.appid()); @@ -197,6 +203,7 @@ private JsonObject apiRequest(String endpoint, JsonObject args, JsonObject data) } // POST the endpoint with the payload + @Nullable ContentResponse cr = null; try { cr = request.send(); @@ -247,6 +254,7 @@ private JsonObject apiRequest(String endpoint, JsonObject args, JsonObject data) /** * Performs a user login with the credentials supplied to the constructor + * in the first message * * @return true or false */ @@ -264,6 +272,7 @@ public boolean login() { logger.trace("Using loginId: {}", loginId); logger.trace("Using password: {}", password); + // This is for the MSmartHome. It uses proxied if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { JsonObject newData = new JsonObject(); @@ -283,6 +292,7 @@ public boolean login() { iotData.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); newData.add("iotData", iotData); + @Nullable JsonObject response = apiRequest("/mj/user/login", null, newData); if (response == null) { @@ -290,6 +300,8 @@ public boolean login() { } accessToken = response.getAsJsonObject("mdata").get("accessToken").getAsString(); + + // This for NetHomePlus and MideaAir apps } else { String passwordEncrypted = security.encryptPassword(loginId, password); @@ -311,7 +323,8 @@ public boolean login() { } /** - * Get tokenlist with udpid + * Get token and key with udpid + * Unique Device Product ID * * @param udpid udp id * @return token and key @@ -324,7 +337,7 @@ public TokenKey getToken(String udpid) { JsonObject response = apiRequest("/v1/iot/secure/getToken", args, null); if (response == null) { - return null; + return new TokenKey("", ""); } JsonArray tokenlist = response.getAsJsonArray("tokenlist"); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java similarity index 74% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index c1c907b537368..76c747603b73e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudProviderDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.dto; +package org.openhab.binding.mideaac.internal.cloud; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -31,7 +31,7 @@ * @author Bob Eckhoff - JavaDoc and conversion to record */ @NonNullByDefault -public record CloudProviderDTO(String name, String appkey, String appid, String apiurl, String signkey, String proxied, +public record CloudProvider(String name, String appkey, String appid, String apiurl, String signkey, String proxied, String iotkey, String hmackey) { /** @@ -42,19 +42,19 @@ public record CloudProviderDTO(String name, String appkey, String appid, String * @param name Cloud provider * @return Cloud provider information (appkey, appid, apiurl,signkey, proxied, iotkey, hmackey) */ - public static CloudProviderDTO getCloudProvider(String name) { + public static CloudProvider getCloudProvider(String name) { switch (name) { case "NetHome Plus": - return new CloudProviderDTO("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", + return new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); case "Midea Air": - return new CloudProviderDTO("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", + return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); case "MSmartHome": - return new CloudProviderDTO("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", + return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); } - return new CloudProviderDTO("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + return new CloudProvider("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java similarity index 73% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java index 00df03e3ebbe5..7d027b3169ad5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/dto/CloudsDTO.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.dto; +package org.openhab.binding.mideaac.internal.cloud; import java.util.HashMap; @@ -24,20 +24,20 @@ * @author Bob Eckhoff - JavaDoc */ @NonNullByDefault -public class CloudsDTO { +public class Clouds { - private final HashMap clouds; + private final HashMap clouds; /** * Cloud Provider data */ - public CloudsDTO() { - clouds = new HashMap(); + public Clouds() { + clouds = new HashMap(); } - private CloudDTO add(String email, String password, CloudProviderDTO cloudProvider) { + private Cloud add(String email, String password, CloudProvider cloudProvider) { int hash = (email + password + cloudProvider.name()).hashCode(); - CloudDTO cloud = new CloudDTO(email, password, cloudProvider); + Cloud cloud = new Cloud(email, password, cloudProvider); clouds.put(hash, cloud); return cloud; } @@ -50,7 +50,7 @@ private CloudDTO add(String email, String password, CloudProviderDTO cloudProvid * @param cloudProvider your Cloud Provider * @return parameters for cloud provider */ - public @Nullable CloudDTO get(String email, String password, CloudProviderDTO cloudProvider) { + public @Nullable Cloud get(String email, String password, CloudProvider cloudProvider) { int hash = (email + password + cloudProvider.name()).hashCode(); if (clouds.containsKey(hash)) { return clouds.get(hash); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index bb25dabc52474..e0978509c380c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -26,10 +26,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.cloud.CloudProvider; import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException; import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException; import org.openhab.binding.mideaac.internal.connection.exception.MideaException; -import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; import org.openhab.binding.mideaac.internal.handler.Callback; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.handler.CommandSet; @@ -63,7 +63,7 @@ public class ConnectionManager { private final String cloud; private final String deviceId; private Response lastResponse; - private CloudProviderDTO cloudProvider; + private CloudProvider cloudProvider; private Security security; private final int version; private final boolean promptTone; @@ -89,7 +89,7 @@ public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, this.promptTone = promptTone; this.lastResponse = new Response(HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"), version, "query", (byte) 0xc0); - this.cloudProvider = CloudProviderDTO.getCloudProvider(cloud); + this.cloudProvider = CloudProvider.getCloudProvider(cloud); this.security = new Security(cloudProvider); } @@ -119,6 +119,15 @@ public synchronized void connect() int maxTries = 3; int retryCount = 0; + // If retrying command add delay to avoid connection rejection + if (!retry) { + try { + Thread.sleep(4000); + } catch (InterruptedException ex) { + logger.debug("An interupted error (retry command delay) has occured {}", ex.getMessage()); + } + } + // Open socket // Retry addresses most common wifi connection problems- wait 5 seconds and try again while (retryCount < maxTries) { @@ -505,7 +514,7 @@ public synchronized void disconnect() { } } catch (IOException e) { String message = e.getMessage(); - logger.debug(" Byte read exception {}", message); + logger.debug("Byte read exception {}", message); } return null; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index bae4ab8954411..147678cfde58e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -30,7 +30,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; -import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.openhab.binding.mideaac.internal.cloud.CloudProvider; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.core.config.discovery.AbstractDiscoveryService; @@ -74,7 +74,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService { */ public MideaACDiscoveryService() { super(SUPPORTED_THING_TYPES_UIDS, discoveryTimeoutSeconds, false); - this.security = new Security(CloudProviderDTO.getCloudProvider("")); + this.security = new Security(CloudProvider.getCloudProvider("")); } @Override diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index dbd43f5eb0367..b5f5f216ce642 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -30,6 +30,9 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.mideaac.internal.MideaACConfiguration; +import org.openhab.binding.mideaac.internal.cloud.Cloud; +import org.openhab.binding.mideaac.internal.cloud.CloudProvider; +import org.openhab.binding.mideaac.internal.cloud.Clouds; import org.openhab.binding.mideaac.internal.connection.CommandHelper; import org.openhab.binding.mideaac.internal.connection.ConnectionManager; import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException; @@ -37,9 +40,6 @@ import org.openhab.binding.mideaac.internal.connection.exception.MideaException; import org.openhab.binding.mideaac.internal.discovery.DiscoveryHandler; import org.openhab.binding.mideaac.internal.discovery.MideaACDiscoveryService; -import org.openhab.binding.mideaac.internal.dto.CloudDTO; -import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; -import org.openhab.binding.mideaac.internal.dto.CloudsDTO; import org.openhab.binding.mideaac.internal.security.TokenKey; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; @@ -75,7 +75,7 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler { private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); - private final CloudsDTO clouds; + private final Clouds clouds; private final boolean imperialUnits; private boolean isPollRunning = false; private final HttpClient httpClient; @@ -97,9 +97,9 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler * @param thing Thing * @param unitProvider OH core unit provider * @param httpClient http Client - * @param clouds CloudsDTO + * @param clouds Clouds */ - public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient, CloudsDTO clouds) { + public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient, Clouds clouds) { super(thing); this.thing = thing; this.imperialUnits = unitProvider.getMeasurementSystem() instanceof ImperialUnits; @@ -112,7 +112,7 @@ public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpCli * * @return clouds */ - public CloudsDTO getClouds() { + public Clouds getClouds() { return clouds; } @@ -218,7 +218,7 @@ public void initialize() { if (config.version == 3 && !config.isV3ConfigValid()) { if (config.isTokenKeyObtainable()) { - CloudProviderDTO cloudProvider = CloudProviderDTO.getCloudProvider(config.cloud); + CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); getTokenKeyCloud(cloudProvider); return; } else { @@ -360,9 +360,9 @@ public void discovered(DiscoveryResult discoveryResult) { * * @param cloudProvider Cloud Provider account */ - public void getTokenKeyCloud(CloudProviderDTO cloudProvider) { + public void getTokenKeyCloud(CloudProvider cloudProvider) { logger.debug("Retrieving Token and/or Key from cloud"); - CloudDTO cloud = getClouds().get(config.email, config.password, cloudProvider); + Cloud cloud = getClouds().get(config.email, config.password, cloudProvider); if (cloud != null) { cloud.setHttpClient(httpClient); if (cloud.login()) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 6ee3832bb9334..2e9944e2a6b3e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -36,7 +36,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; -import org.openhab.binding.mideaac.internal.dto.CloudProviderDTO; +import org.openhab.binding.mideaac.internal.cloud.CloudProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,14 +56,14 @@ public class Security { private Logger logger = LoggerFactory.getLogger(Security.class); private IvParameterSpec iv = new IvParameterSpec(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }); - CloudProviderDTO cloudProvider; + CloudProvider cloudProvider; /** * Set Cloud Provider * * @param cloudProvider Name of Cloud provider */ - public Security(CloudProviderDTO cloudProvider) { + public Security(CloudProvider cloudProvider) { this.cloudProvider = cloudProvider; } From 6354dd3410d13c74192f78837b06a185810e4c48 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 23 Feb 2025 19:50:24 -0500 Subject: [PATCH 17/44] Update pom with spotless New standard format? Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mideaac/pom.xml b/bundles/org.openhab.binding.mideaac/pom.xml index 1e12614c8c885..9235943cc3e58 100644 --- a/bundles/org.openhab.binding.mideaac/pom.xml +++ b/bundles/org.openhab.binding.mideaac/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 From fc503456b1a29fb837eeb212817667b006235f7f Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 24 Feb 2025 18:26:13 -0500 Subject: [PATCH 18/44] Change connection log Change connection log to either/or avoiding both on device being connected. Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/connection/ConnectionManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index e0978509c380c..cf87f40e489cf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -182,8 +182,9 @@ public synchronized void connect() if (!deviceIsConnected) { // Info logger on first connection after being disconnected logger.info("Connected to IP {}", ipAddress); + } else { + logger.debug("Connected to IP {}", ipAddress); } - logger.debug("Connected to IP {}", ipAddress); deviceIsConnected = true; } From 3f2aaf15e4852b8c208809dcf44b258899a4f47c Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 21 Mar 2025 11:00:37 -0400 Subject: [PATCH 19/44] Minor tweaks from extended testing Minor tweaks from extended testing Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/connection/ConnectionManager.java | 2 +- .../org/openhab/binding/mideaac/internal/security/Security.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index cf87f40e489cf..13f2e2bc3557e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -122,7 +122,7 @@ public synchronized void connect() // If retrying command add delay to avoid connection rejection if (!retry) { try { - Thread.sleep(4000); + Thread.sleep(5000); } catch (InterruptedException ex) { logger.debug("An interupted error (retry command delay) has occured {}", ex.getMessage()); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 2e9944e2a6b3e..57deafcfdb9ef 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -350,7 +350,7 @@ public Decryption8370Result decode8370(byte[] data) throws IOException { data = Arrays.copyOfRange(data, 0, data.length - padding); } } else { - logger.warn("MsgType: {}", msgtype.toString()); + logger.debug("MsgType: {}", msgtype.toString()); throw new IOException(msgtype.toString() + " response was received"); } From 23eee74ff7318d59276e70063ce275b63769f0b1 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 23 Mar 2025 17:34:55 -0400 Subject: [PATCH 20/44] Add scheduled token and key updates Add capability for scheduled token and key updates. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 36 ++++++------- .../internal/MideaACBindingConstants.java | 1 + .../internal/MideaACConfiguration.java | 5 ++ .../internal/handler/MideaACHandler.java | 50 +++++++++++++------ .../resources/OH-INF/i18n/mideaac.properties | 2 + .../resources/OH-INF/thing/thing-types.xml | 6 +++ 6 files changed, 69 insertions(+), 31 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 8f30524755d24..00b4488065273 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -22,7 +22,8 @@ Once the Air Conditioner is on the network (WiFi active) activating an Inbox sca Every responding unit gets added to the Inbox. When adding each thing, the required parameters of ipAddress, ipPort, deviceId, pollingTime, timeout, promptTone and version will be populated with either discovered values or the default settings. A V.2 device will be Online. A V.3 device will require you to enter the cloud provider, token and key before becoming Online. The token and key can be discovered by entering -your email and password for your cloud account. The email and password are stored securely, but can be deleted after the token and key are entered. +your email and password for your cloud account. The email and password are stored securely, but can be deleted after the token and key are entered, +unless key and token update is activated. ## Binding Configuration @@ -30,20 +31,21 @@ No binding configuration is required. ## Thing Configuration -| Parameter | Required ? | Comment | Default | -|--:----------|--:----------|--:----------------------------------------------------------------|---------| -| ipAddress | Yes | IP Address of the device. | | -| ipPort | Yes | IP port of the device | 6444 | -| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | -| cloud | Yes for V.3 | Cloud Provider name for email and password | | -| email | No | Email for cloud account chosen in Cloud Provider. | | -| password | No | Password for cloud account chosen in Cloud Provider. | | -| token | Yes for V.3 | Secret Token (length 128 HEX) | | -| key | Yes for V.3 | Secret Key (length 64 HEX) | | -| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | -| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | -| promptTone | Yes | "Ding" tone when command is received and executed. | False | -| version | Yes | Version 3 has token, key and cloud requirements. | 0 | +| Parameter | Required ? | Comment | Default | +|--:------------|--:----------|--:----------------------------------------------------------------|---------| +| ipAddress | Yes | IP Address of the device. | | +| ipPort | Yes | IP port of the device | 6444 | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | +| cloud | Yes for V.3 | Cloud Provider name for email and password | | +| email | No | Email for cloud account chosen in Cloud Provider. | | +| password | No | Password for cloud account chosen in Cloud Provider. | | +| token | Yes for V.3 | Secret Token (length 128 HEX) | | +| key | Yes for V.3 | Secret Key (length 64 HEX) | | +| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | +| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | +| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | +| promptTone | Yes | "Ding" tone when command is received and executed. | False | +| version | Yes | Version 3 has token, key and cloud requirements. | 0 | ## Channels @@ -75,13 +77,13 @@ Following channels are available: ### `demo.things` Example ```java -Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="6444", deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, timeout=4, promptTone="false", version="3"] +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="6444", deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] ``` Option to use the built-in binding discovery of ipPort, deviceId, token and key. ```java -Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="", deviceId="", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="", key ="", pollingTime = 60, timeout=4, promptTone="false", version="3"] +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="", deviceId="", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="", key ="", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] ``` ### `demo.items` Example diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index 6f5e4e86e7451..ec04d287edf26 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -83,6 +83,7 @@ public class MideaACBindingConstants { public static final String CONFIG_TOKEN = "token"; public static final String CONFIG_KEY = "key"; public static final String CONFIG_POLLING_TIME = "pollingTime"; + public static final String CONFIG_KEY_TOKEN_UPDATE = "keyTokenUpdate"; public static final String CONFIG_CONNECTING_TIMEOUT = "timeout"; public static final String CONFIG_PROMPT_TONE = "promptTone"; public static final String CONFIG_VERSION = "version"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index b3bfd45be7f29..84445d324afb9 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -68,6 +68,11 @@ public class MideaACConfiguration { */ public int pollingTime = 60; + /** + * Key and Token Update Frequency + */ + public int keyTokenUpdate = 0; + /** * Socket Timeout */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index b5f5f216ce642..6d0dade2dace1 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -78,14 +78,16 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler private final Clouds clouds; private final boolean imperialUnits; private boolean isPollRunning = false; + private boolean isKeyTokenUpdateRunning = false; private final HttpClient httpClient; private MideaACConfiguration config = new MideaACConfiguration(); private Map properties = new HashMap<>(); // Default parameters are the same as in the MideaACConfiguration class private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false); - private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); private @Nullable ScheduledFuture scheduledTask; + private @Nullable ScheduledFuture scheduledKeyTokenUpdate; private Callback callbackLambda = (response) -> { this.updateChannels(response); @@ -189,6 +191,10 @@ public void initialize() { if (isPollRunning) { stopScheduler(); } + if (isKeyTokenUpdateRunning) { + stopTokenKeyUpdate(); + } + config = getConfigAs(MideaACConfiguration.class); if (!config.isValid()) { @@ -236,24 +242,25 @@ public void initialize() { config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); - startScheduler(2, config.pollingTime, TimeUnit.SECONDS); - } - - /** - * Starts the Scheduler for the Polling - * - * @param initialDelay Seconds before first Poll - * @param delay Seconds between Polls - * @param unit Seconds - */ - private void startScheduler(long initialDelay, long delay, TimeUnit unit) { if (scheduledTask == null) { isPollRunning = true; - scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, initialDelay, delay, unit); - logger.debug("Scheduled task started"); + scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); + logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); } else { logger.debug("Scheduler already running"); } + + if (config.keyTokenUpdate != 0) { + if (scheduledKeyTokenUpdate == null) { + isKeyTokenUpdateRunning = true; + scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( + () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, + config.keyTokenUpdate, TimeUnit.DAYS); + logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); + } else { + logger.debug("Token Key Update Scheduler already running"); + } + } } private void pollJob() { @@ -361,6 +368,9 @@ public void discovered(DiscoveryResult discoveryResult) { * @param cloudProvider Cloud Provider account */ public void getTokenKeyCloud(CloudProvider cloudProvider) { + if (isPollRunning) { + stopScheduler(); + } logger.debug("Retrieving Token and/or Key from cloud"); Cloud cloud = getClouds().get(config.email, config.password, cloudProvider); if (cloud != null) { @@ -395,9 +405,21 @@ private void stopScheduler() { } } + private void stopTokenKeyUpdate() { + ScheduledFuture localScheduledTask = this.scheduledKeyTokenUpdate; + + if (localScheduledTask != null && !localScheduledTask.isCancelled()) { + localScheduledTask.cancel(true); + logger.debug("Scheduled Key Token Update cancelled."); + isKeyTokenUpdateRunning = false; + scheduledKeyTokenUpdate = null; + } + } + @Override public void dispose() { stopScheduler(); + stopTokenKeyUpdate(); connectionManager.dispose(true); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 253f143becdec..2e11a6f2e2dec 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -25,6 +25,8 @@ thing-type.config.mideaac.ac.ipPort.label = IP Port thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). +thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update +thing-type.config.mideaac.ac.keyTokenUpdate.description = Frequency to update the Key and Token in Days, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. thing-type.config.mideaac.ac.pollingTime.label = Polling time diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index e483d9fd72107..5c15afdb3ebd9 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -94,6 +94,12 @@ Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. 60 + + keyTokenUpdate + + Frequency to update the Key and Token in Days, default 0 to disable. + 0 + timeout From 88ff93e037b1851932023d2a52d3a8a1c5a5b9ae Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 31 Mar 2025 17:00:47 -0400 Subject: [PATCH 21/44] Validated the automatic token key update steps Completed the automatic token key addition with clean-up, additional testing, and eliminated the Clouds class. Developed additional understanding of the token key update to improve the comments. Corrected an order issue in Cloud Provider. Signed-off-by: Bob Eckhoff --- .../internal/MideaACHandlerFactory.java | 5 +- .../binding/mideaac/internal/Utils.java | 1 + .../binding/mideaac/internal/cloud/Cloud.java | 90 ++++---- .../mideaac/internal/cloud/CloudProvider.java | 19 +- .../mideaac/internal/cloud/Clouds.java | 60 ----- .../connection/ConnectionManager.java | 66 +++--- .../internal/handler/MideaACHandler.java | 82 ++----- .../mideaac/internal/security/Security.java | 11 +- .../mideaac/internal/cloud/CloudTest.java | 216 ++++++++++++++++++ .../MideaACDiscoveryServiceTest.java | 4 +- .../internal/security/SecurityTest.java | 170 ++++++++++++++ 11 files changed, 513 insertions(+), 211 deletions(-) delete mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java index 79eb4fd9bf022..f8defc4ce2611 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACHandlerFactory.java @@ -16,7 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.mideaac.internal.cloud.Clouds; import org.openhab.binding.mideaac.internal.handler.MideaACHandler; import org.openhab.core.i18n.UnitProvider; import org.openhab.core.io.net.http.HttpClientFactory; @@ -40,7 +39,6 @@ public class MideaACHandlerFactory extends BaseThingHandlerFactory { private final HttpClientFactory httpClientFactory; - private final Clouds clouds; private final UnitProvider unitProvider; @Override @@ -58,14 +56,13 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { public MideaACHandlerFactory(@Reference UnitProvider unitProvider, @Reference HttpClientFactory httpClientFactory) { this.httpClientFactory = httpClientFactory; this.unitProvider = unitProvider; - clouds = new Clouds(); } @Override protected @Nullable ThingHandler createHandler(Thing thing) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) { - return new MideaACHandler(thing, unitProvider, httpClientFactory.getCommonHttpClient(), clouds); + return new MideaACHandler(thing, unitProvider, httpClientFactory.getCommonHttpClient()); } return null; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 5395cc4941296..122a2ecfab5f5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -138,6 +138,7 @@ public static byte[] strxor(byte[] array1, byte[] array2) { /** * Create String of the V.3 Token + * String length is the nbytes characters long * * @param nbytes number of bytes * @return String diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index c196fd1590834..1bc7b92540ae6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -55,21 +55,6 @@ public class Cloud { private static final int FORMAT = 2; // JSON private static final String LANGUAGE = "en_US"; - private Date tokenRequestedAt = new Date(); - - private void setTokenRequested() { - tokenRequestedAt = new Date(); - } - - /** - * Token requested date - * - * @return tokenRequestedAt - */ - public Date getTokenRequested() { - return tokenRequestedAt; - } - private HttpClient httpClient; /** @@ -128,10 +113,10 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { } /** - * Set up the initial data payload with the global variable set - * This first message is to confirm the email is in the cloud provider's - * memory. The return is msg "ok", "errorCode":"0", and a loginId to be used - * in the next message. + * This is called during the loginId(), login() and getToken() methods to send a HHTP Post + * to the cloud provider. There are two different messages and formats separated + * by the type of Cloud Provider (proxied or Not). The return is msg "ok", "errorCode":"0" + * There is also information for the next message or the key and token themselves. */ private @Nullable JsonObject apiRequest(String endpoint, @Nullable JsonObject args, @Nullable JsonObject data) { if (data == null) { @@ -144,40 +129,45 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { data.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); } - // Add the method parameters for the endpoint + // For the getLoginId() this adds the email account + // For the login() this adds the email account and the encrpted password + // For the getToken() this adds the udpid if (args != null) { for (Map.Entry entry : args.entrySet()) { data.add(entry.getKey(), entry.getValue().getAsJsonPrimitive()); } } - // Add the login information to the payload - if (!data.has("reqId") && !Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + // This adds the first 16 characters of a 16 byte string + // if Cloud provider uses proxied and wasn't added by the method() + if (!data.has("reqId") && !cloudProvider.proxied().isBlank()) { data.addProperty("reqId", Utils.tokenHex(16)); } String url = cloudProvider.apiurl() + endpoint; + logger.debug("Url for request {}", url); - int time = (int) (new Date().getTime() / 1000); - - String random = String.valueOf(time); - - // Add the sign to the header String json = data.toString(); logger.debug("Request json: {}", json); + int time = (int) (new Date().getTime() / 1000); + String random = String.valueOf(time); + Request request = httpClient.newRequest(url).method(HttpMethod.POST).timeout(15, TimeUnit.SECONDS); // .version(HttpVersion.HTTP_1_1) request.agent("Dalvik/2.1.0 (Linux; U; Android 7.0; SM-G935F Build/NRD90M)"); - if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + if (!cloudProvider.proxied().isBlank()) { request.header("Content-Type", "application/json"); } else { request.header("Content-Type", "application/x-www-form-urlencoded"); } + request.header("secretVersion", "1"); - if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + + // Add the sign to the header, different for proxied + if (!cloudProvider.proxied().isBlank()) { String sign = security.newSign(json, random); request.header("sign", sign); } else { @@ -190,11 +180,14 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { } request.header("random", random); + + // If available, blank if not request.header("accessToken", accessToken); logger.debug("Request headers: {}", request.getHeaders().toString()); - if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + // Different formats for proxied + if (!cloudProvider.proxied().isBlank()) { request.content(new StringContentProvider(json)); } else { String body = Utils.getQueryString(data); @@ -202,7 +195,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { request.content(new StringContentProvider(body)); } - // POST the endpoint with the payload + // POST the payload @Nullable ContentResponse cr = null; try { @@ -239,7 +232,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { return null; } else { logger.debug("Api response ok: {} ({})", code, msg); - if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + if (!cloudProvider.proxied().isBlank()) { return result.get("data").getAsJsonObject(); } else { return result.get("result").getAsJsonObject(); @@ -253,18 +246,21 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { } /** - * Performs a user login with the credentials supplied to the constructor - * in the first message + * First gets the loginId using your email, then gets the session + * Id with the email and encypted password (using the LoginId). Then + * gets the token and key. If loginId and sessionId exist from an earlier + * attempt, it goes directly to getting the token and key * * @return true or false */ public boolean login() { + // First get the loginId using the your email if (loginId == null) { if (!getLoginId()) { return false; } } - // Don't try logging in again, someone beat this thread to it + // No need to login again, skip to getToken() with device Id if (!Objects.isNull(sessionId) && !sessionId.isBlank()) { return true; } @@ -272,8 +268,8 @@ public boolean login() { logger.trace("Using loginId: {}", loginId); logger.trace("Using password: {}", password); - // This is for the MSmartHome. It uses proxied - if (!Objects.isNull(cloudProvider.proxied()) && !cloudProvider.proxied().isBlank()) { + // This is for the MSmartHome (proxied) + if (!cloudProvider.proxied().isBlank()) { JsonObject newData = new JsonObject(); JsonObject data = new JsonObject(); @@ -301,7 +297,7 @@ public boolean login() { accessToken = response.getAsJsonObject("mdata").get("accessToken").getAsString(); - // This for NetHomePlus and MideaAir apps + // This for the non-proxied cloud providers } else { String passwordEncrypted = security.encryptPassword(loginId, password); @@ -323,14 +319,14 @@ public boolean login() { } /** - * Get token and key with udpid - * Unique Device Product ID + * Gets token and key with the device Id modified to udpid + * after SessionId (non-proxied) accessToken are established * - * @param udpid udp id + * @param deviceId The discovered Device Id * @return token and key */ - public TokenKey getToken(String udpid) { - long i = Long.valueOf(udpid); + public TokenKey getToken(String deviceId) { + long i = Long.valueOf(deviceId); JsonObject args = new JsonObject(); args.addProperty("udpid", security.getUdpId(Utils.toIntTo6ByteArray(i, ByteOrder.BIG_ENDIAN))); @@ -345,15 +341,15 @@ public TokenKey getToken(String udpid) { String token = el.getAsJsonPrimitive("token").getAsString(); String key = el.getAsJsonPrimitive("key").getAsString(); - setTokenRequested(); - return new TokenKey(token, key); } /** - * Get the login ID from the email address + * Gets the login ID from your email address + * + * @return loginId (not your email) */ - private boolean getLoginId() { + public boolean getLoginId() { JsonObject args = new JsonObject(); args.addProperty("loginAccount", loginAccount); JsonObject response = apiRequest("/v1/user/login/id/get", args, null); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index 76c747603b73e..1610655e21ed5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -15,7 +15,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link CloudProviderDTO} class contains the information + * The {@link CloudProvider} class contains the information * to allow encryption and decryption for the supported Cloud Providers * * @param name Cloud provider @@ -23,16 +23,16 @@ * @param appid application id * @param apiurl application url * @param signkey sign key for AES - * @param proxied proxy - MSmarthome only * @param iotkey iot key - MSmarthome only * @param hmackey hmac key - MSmarthome only + * @param proxied proxy - MSmarthome only * * @author Jacek Dobrowolski - Initial Contribution * @author Bob Eckhoff - JavaDoc and conversion to record */ @NonNullByDefault -public record CloudProvider(String name, String appkey, String appid, String apiurl, String signkey, String proxied, - String iotkey, String hmackey) { +public record CloudProvider(String name, String appkey, String appid, String apiurl, String signkey, String iotkey, + String hmackey, String proxied) { /** * Cloud provider information for record @@ -40,7 +40,7 @@ public record CloudProvider(String name, String appkey, String appid, String api * V2 Devices do not require a Cloud Provider entry as they only use AES * * @param name Cloud provider - * @return Cloud provider information (appkey, appid, apiurl,signkey, proxied, iotkey, hmackey) + * @return Cloud provider information (appkey, appid, apiurl, signkey, iotkey, hmackey, proxied) */ public static CloudProvider getCloudProvider(String name) { switch (name) { @@ -50,10 +50,19 @@ public static CloudProvider getCloudProvider(String name) { case "Midea Air": return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + // Not in ReadMe yet + // case "Ariston Clima": + // return new CloudProvider("Ariston Clima", "434a209a5ce141c3b726de067835d7f0", "1005", + // "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); case "MSmartHome": return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); + // Future Not sure what to do with "login_key": "ad0ee21d48a64bf49f4fb583ab76e799" + // case "MeijuCloud": // "美的美居" + // return new CloudProvider("MeijuCloud", "46579c15", "900", + // "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=", + // "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "prod_secret123@muc", "PROD_VnoClJI9aikS8dyy", "v5"); } return new CloudProvider("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java deleted file mode 100644 index 7d027b3169ad5..0000000000000 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Clouds.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2010-2025 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.mideaac.internal.cloud; - -import java.util.HashMap; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * The {@link Clouds} class securely stores email and password - * - * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - JavaDoc - */ -@NonNullByDefault -public class Clouds { - - private final HashMap clouds; - - /** - * Cloud Provider data - */ - public Clouds() { - clouds = new HashMap(); - } - - private Cloud add(String email, String password, CloudProvider cloudProvider) { - int hash = (email + password + cloudProvider.name()).hashCode(); - Cloud cloud = new Cloud(email, password, cloudProvider); - clouds.put(hash, cloud); - return cloud; - } - - /** - * Gets user provided cloud provider data - * - * @param email your email - * @param password your password - * @param cloudProvider your Cloud Provider - * @return parameters for cloud provider - */ - public @Nullable Cloud get(String email, String password, CloudProvider cloudProvider) { - int hash = (email + password + cloudProvider.name()).hashCode(); - if (clouds.containsKey(hash)) { - return clouds.get(hash); - } - return add(email, password, cloudProvider); - } -} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 13f2e2bc3557e..32a35e9fae095 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -71,9 +71,9 @@ public class ConnectionManager { private int droppedCommands = 0; /** - * True allows command retry if null response + * True allows command resend if null and timeout response */ - private boolean retry = true; + private boolean resend = true; public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud, String email, String password, String deviceId, int version, boolean promptTone) { @@ -117,43 +117,43 @@ public synchronized void connect() logger.trace("Connecting to {}:{}", ipAddress, ipPort); int maxTries = 3; - int retryCount = 0; + int retrySocket = 0; - // If retrying command add delay to avoid connection rejection - if (!retry) { + // If resending command add delay to avoid connection rejection + if (!resend) { try { Thread.sleep(5000); } catch (InterruptedException ex) { - logger.debug("An interupted error (retry command delay) has occured {}", ex.getMessage()); + logger.debug("An interupted error (resend command delay-connect) has occured {}", ex.getMessage()); } } // Open socket - // Retry addresses most common wifi connection problems- wait 5 seconds and try again - while (retryCount < maxTries) { + // RetrySocket addresses the Timeout exception, others exceptions end the thread + while (retrySocket < maxTries) { try { socket = new Socket(); socket.setSoTimeout(timeout * 1000); socket.connect(new InetSocketAddress(ipAddress, ipPort), timeout * 1000); break; } catch (SocketTimeoutException e) { - retryCount++; - if (retryCount < maxTries) { + retrySocket++; + if (retrySocket < maxTries) { try { Thread.sleep(5000); } catch (InterruptedException ex) { logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); } - logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retryCount, ipAddress, + logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retrySocket, ipAddress, e.getMessage()); } } catch (IOException e) { - logger.debug("Socket retry count {}, IOException connecting to {}: {}", retryCount, ipAddress, + logger.debug("Socket retry count {}, IOException connecting to {}: {}", retrySocket, ipAddress, e.getMessage()); - break; + throw new MideaConnectionException(e); } } - if (retryCount == maxTries) { + if (retrySocket == maxTries) { deviceIsConnected = false; logger.debug("Failed to connect after {} tries. Try again with next scheduled poll", maxTries); throw new MideaConnectionException("Failed to connect after maximum tries"); @@ -210,10 +210,9 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE } /** - * Sends the Handshake Request to the V3 device. Generally quick response - * Without the 1000 ms sleep delay there are problems in sending the Poll/Command - * Suspect that the socket write and read streams need a moment to clear - * as they will be reused in the SendCommand method + * Sends the Handshake Request to the V3 device. Generally quick response. + * After success, but without the 1000 ms sleep delay there are problems. + * Suspect that the device needs a moment to clear before the Poll. */ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException { byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST); @@ -230,8 +229,7 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio Utils.hexStringToByteArray(key)); if (success) { logger.debug("Authentication successful"); - // Altering the sleep caused or can cause write errors problems. Use caution. - // At 500 ms the first write usually fails. Works, but no backup + // Altering the sleep can cause write errors problems. Use caution. try { Thread.sleep(1000); } catch (InterruptedException e) { @@ -277,13 +275,16 @@ private void ensureConnected() throws MideaConnectionException, MideaAuthenticat * make sure to make sure the input stream is empty before sending * the new command and another check if input stream is empty after 1.5 seconds. * Normal device response in 0.75 - 1 second range - * If still empty, send the bytes again. If there are bytes, the read method is called. - * If the socket times out with no response the command is dropped. There will be another poll - * in the time set by the user (30 seconds min). A Set command will need to be resent. + * If still empty, send the bytes again. If the socket times out with no bytes read() + * one resend of the command will be sent. A second failure the command is dropped. + * The scheduler will still send the next poll. * * @param command either the set or polling command - * @throws MideaAuthenticationException + * @param callback communication with the MideaACHandler to update channel status * @throws MideaConnectionException + * @throws MideaAuthenticationException + * @throws MideaException + * @throws IOException */ public synchronized void sendCommand(CommandBase command, @Nullable Callback callback) throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException { @@ -312,21 +313,22 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal try { Thread.sleep(1500); } catch (InterruptedException e) { - logger.debug("An interupted error (retrycommand2) has occured {}", e.getMessage()); + logger.debug("An interupted error (write command2) has occured {}", e.getMessage()); Thread.currentThread().interrupt(); - // Note, but continue anyway for second write. + // Note, but continue anyway for second write if needed. } + // Input stream is checked after 1.5 seconds + // Socket timeout (UI parameter) 2 seconds minimum. if (inputStream.available() == 0) { logger.debug("Input stream empty sending second write {}", command); write(bytes); } - // Socket timeout (UI parameter) 2 seconds minimum up to 10 seconds. byte[] responseBytes = read(); if (responseBytes != null) { - retry = true; + resend = true; if (version == 3) { Decryption8370Result result = security.decode8370(responseBytes); for (byte[] response : result.getResponses()) { @@ -450,15 +452,15 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } return; } else { - if (retry) { + if (resend) { logger.debug("Resending Command {}", command); - retry = false; + resend = false; sendCommand(command, callback); } else { droppedCommands = droppedCommands + 1; logger.debug("Problem with reading response, skipping {} skipped count since startup {}", command, droppedCommands); - retry = true; + resend = true; return; } } @@ -498,7 +500,7 @@ public synchronized void disconnect() { } /** - * Reads the inputStream byte array + * Reads the inputStream byte array (Handshake or command) * * @return byte array or null */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 6d0dade2dace1..56677736010bd 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -32,7 +32,6 @@ import org.openhab.binding.mideaac.internal.MideaACConfiguration; import org.openhab.binding.mideaac.internal.cloud.Cloud; import org.openhab.binding.mideaac.internal.cloud.CloudProvider; -import org.openhab.binding.mideaac.internal.cloud.Clouds; import org.openhab.binding.mideaac.internal.connection.CommandHelper; import org.openhab.binding.mideaac.internal.connection.ConnectionManager; import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException; @@ -69,16 +68,13 @@ * @author Jacek Dobrowolski - Initial contribution * @author Justan Oldman - Last Response added * @author Bob Eckhoff - Longer Polls and OH developer guidelines - * @author Leo Siepel - Refactored class, improved seperation of concerns + * @author Leo Siepel - Refactored class, improved separation of concerns */ @NonNullByDefault public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler { private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); - private final Clouds clouds; private final boolean imperialUnits; - private boolean isPollRunning = false; - private boolean isKeyTokenUpdateRunning = false; private final HttpClient httpClient; private MideaACConfiguration config = new MideaACConfiguration(); @@ -99,23 +95,12 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler * @param thing Thing * @param unitProvider OH core unit provider * @param httpClient http Client - * @param clouds Clouds */ - public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient, Clouds clouds) { + public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient) { super(thing); this.thing = thing; this.imperialUnits = unitProvider.getMeasurementSystem() instanceof ImperialUnits; this.httpClient = httpClient; - this.clouds = clouds; - } - - /** - * Returns Cloud Provider - * - * @return clouds - */ - public Clouds getClouds() { - return clouds; } /** @@ -188,13 +173,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ @Override public void initialize() { - if (isPollRunning) { - stopScheduler(); - } - if (isKeyTokenUpdateRunning) { - stopTokenKeyUpdate(); - } - config = getConfigAs(MideaACConfiguration.class); if (!config.isValid()) { @@ -243,23 +221,17 @@ public void initialize() { config.deviceId, config.version, config.promptTone); if (scheduledTask == null) { - isPollRunning = true; scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); } else { logger.debug("Scheduler already running"); } - if (config.keyTokenUpdate != 0) { - if (scheduledKeyTokenUpdate == null) { - isKeyTokenUpdateRunning = true; - scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( - () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, - config.keyTokenUpdate, TimeUnit.DAYS); - logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); - } else { - logger.debug("Token Key Update Scheduler already running"); - } + if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) { + scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( + () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, + config.keyTokenUpdate, TimeUnit.DAYS); + logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); } } @@ -368,29 +340,27 @@ public void discovered(DiscoveryResult discoveryResult) { * @param cloudProvider Cloud Provider account */ public void getTokenKeyCloud(CloudProvider cloudProvider) { - if (isPollRunning) { + if (scheduledTask != null) { stopScheduler(); } logger.debug("Retrieving Token and/or Key from cloud"); - Cloud cloud = getClouds().get(config.email, config.password, cloudProvider); - if (cloud != null) { - cloud.setHttpClient(httpClient); - if (cloud.login()) { - TokenKey tk = cloud.getToken(config.deviceId); - Configuration configuration = editConfiguration(); - - configuration.put(CONFIG_TOKEN, tk.token()); - configuration.put(CONFIG_KEY, tk.key()); - updateConfiguration(configuration); - - logger.trace("Token: {}", tk.token()); - logger.trace("Key: {}", tk.key()); - logger.debug("Token and Key obtained from cloud, saving, back to initialize"); - initialize(); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format( - "Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); - } + Cloud cloud = new Cloud(config.email, config.password, cloudProvider); + cloud.setHttpClient(httpClient); + if (cloud.login()) { + TokenKey tk = cloud.getToken(config.deviceId); + Configuration configuration = editConfiguration(); + + configuration.put(CONFIG_TOKEN, tk.token()); + configuration.put(CONFIG_KEY, tk.key()); + updateConfiguration(configuration); + + logger.trace("Token: {}", tk.token()); + logger.trace("Key: {}", tk.key()); + logger.debug("Token and Key obtained from cloud, saving, back to initialize"); + initialize(); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String + .format("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); } } @@ -400,7 +370,6 @@ private void stopScheduler() { if (localScheduledTask != null && !localScheduledTask.isCancelled()) { localScheduledTask.cancel(true); logger.debug("Scheduled task cancelled."); - isPollRunning = false; scheduledTask = null; } } @@ -411,7 +380,6 @@ private void stopTokenKeyUpdate() { if (localScheduledTask != null && !localScheduledTask.isCancelled()) { localScheduledTask.cancel(true); logger.debug("Scheduled Key Token Update cancelled."); - isKeyTokenUpdateRunning = false; scheduledKeyTokenUpdate = null; } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 57deafcfdb9ef..16f0134614303 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -59,7 +59,7 @@ public class Security { CloudProvider cloudProvider; /** - * Set Cloud Provider + * Set Cloud Provider to get provider specific keys * * @param cloudProvider Name of Cloud provider */ @@ -488,7 +488,8 @@ private byte[] getRandomBytes(int size) { } /** - * Path to cloud provider + * Encodes the sign for a non-proxy Cloud provider Request + * using the url and the payload into a lower case hex string * * @param url url of cloud provider * @param payload message @@ -513,7 +514,8 @@ private byte[] getRandomBytes(int size) { } /** - * Provides a randown iotKey for Cloud Providers that do not have one + * Encodes the sign for a proxied Cloud provider Request + * using the iotkey and a random lower case hex string * * @param data input data array * @param random random values @@ -608,7 +610,8 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor } /** - * Gets UDPID from byte data + * Gets Udpid from byte data + * This is based on the Device Id * * @param data data array * @return string of lower case bytes diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java new file mode 100644 index 0000000000000..bcc2f5a71db9c --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.cloud; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpMethod; +import org.junit.jupiter.api.Test; + +/** + * The {@link CloudTest} creates messages and + * compares them to the expected result. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CloudTest { + + @Test + public void testLogin() throws Exception { + // Mock HttpClient and ContentResponse + HttpClient mockHttpClient = mock(HttpClient.class); + Request mockRequest = mock(Request.class); + ContentResponse mockResponse = mock(ContentResponse.class); + HttpFields mockHeaders = mock(HttpFields.class); + + // Define behavior of HttpFields + when(mockHeaders.toString()).thenReturn("Mocked Headers"); + + // Mock fluent methods of Request + when(mockHttpClient.newRequest(anyString())).thenReturn(mockRequest); + when(mockRequest.method(HttpMethod.POST)).thenReturn(mockRequest); + when(mockRequest.timeout(anyLong(), any(TimeUnit.class))).thenReturn(mockRequest); + when(mockRequest.content(any(StringContentProvider.class))).thenReturn(mockRequest); + when(mockRequest.getHeaders()).thenReturn(mockHeaders); // Attach mocked headers + when(mockRequest.send()).thenReturn(mockResponse); + + // Define behavior of ContentResponse + when(mockResponse.getStatus()).thenReturn(200); + when(mockResponse.getContentAsString()).thenReturn( + "{\"msg\": \"ok\", \"result\": {\"accessToken\": \"mock-token\", \"sessionId\": \"mock-session-id\"}, \"errorCode\": \"0\"}"); + + // Create a CloudProvider instance (replace arguments with appropriate values) + CloudProvider provider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", + "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + + // Inject the mocked HttpClient into the Cloud class + Cloud cloud = new Cloud("email", "password", provider); + cloud.setHttpClient(mockHttpClient); + + // Set loginId using reflection so that the getLoginId() check doesn't trigger + Field loginIdField = Cloud.class.getDeclaredField("loginId"); + loginIdField.setAccessible(true); + loginIdField.set(cloud, "mock-loginId"); + + // Execute the login method + boolean login = cloud.login(); + + // Assert the result + assertTrue(login); + + // Verify that accessToken is returned + Field accessTokenField = Cloud.class.getDeclaredField("accessToken"); + accessTokenField.setAccessible(true); + assertEquals("mock-token", accessTokenField.get(cloud)); + + // Verify that accessToken is returned + Field sessionIdField = Cloud.class.getDeclaredField("sessionId"); + sessionIdField.setAccessible(true); + assertEquals("mock-session-id", sessionIdField.get(cloud)); + } + + @Test + public void testLoginproxy() throws Exception { + // Mock HttpClient and ContentResponse + HttpClient mockHttpClient = mock(HttpClient.class); + Request mockRequest = mock(Request.class); + ContentResponse mockResponse = mock(ContentResponse.class); + HttpFields mockHeaders = mock(HttpFields.class); + + // Define behavior of HttpFields + when(mockHeaders.toString()).thenReturn("Mocked Headers"); + + // Mock fluent methods of Request + when(mockHttpClient.newRequest(anyString())).thenReturn(mockRequest); + when(mockRequest.method(HttpMethod.POST)).thenReturn(mockRequest); + when(mockRequest.timeout(anyLong(), any(TimeUnit.class))).thenReturn(mockRequest); + when(mockRequest.content(any(StringContentProvider.class))).thenReturn(mockRequest); + when(mockRequest.getHeaders()).thenReturn(mockHeaders); // Attach mocked headers + when(mockRequest.send()).thenReturn(mockResponse); + + // Define behavior of ContentResponse + when(mockResponse.getStatus()).thenReturn(200); + when(mockResponse.getContentAsString()).thenReturn( + "{\"msg\":\"ok\",\"data\":{\"mdata\":{\"accessToken\":\"mock-token\"}},\"errorCode\":\"0\"}"); + + // Create a CloudProvider instance (replace arguments with appropriate values) + CloudProvider provider = new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", + "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", + "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); + + // Inject the mocked HttpClient into the Cloud class + Cloud cloud = new Cloud("email", "password", provider); + cloud.setHttpClient(mockHttpClient); + + // Set loginId using reflection so that the getLoginId() check doesn't trigger + Field loginIdField = Cloud.class.getDeclaredField("loginId"); + loginIdField.setAccessible(true); + loginIdField.set(cloud, "mock-loginId"); + + // Execute the login method + boolean login = cloud.login(); + + // Assert the result + assertTrue(login); + + // Verify that accessToken is returned + Field accessTokenField = Cloud.class.getDeclaredField("accessToken"); + accessTokenField.setAccessible(true); + assertEquals("mock-token", accessTokenField.get(cloud)); + } + + @Test + public void testLoginWithSessionId() throws Exception { + // Create a CloudProvider instance + CloudProvider provider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", + "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + + // Create the Cloud class + Cloud cloud = new Cloud("email", "password", provider); + + // Set loginId using reflection so that the getLoginId() check doesn't trigger + Field loginIdField = Cloud.class.getDeclaredField("loginId"); + loginIdField.setAccessible(true); + loginIdField.set(cloud, "mock-loginId"); + + // Set sessionId using reflection + Field sessionIdField = Cloud.class.getDeclaredField("sessionId"); + sessionIdField.setAccessible(true); + sessionIdField.set(cloud, "mock-session-id"); // Set sessionId to trigger early exit + + // Execute the login method + boolean login = cloud.login(); + + // Assert the result + assertTrue(login); // Validate early exit with sessionId + } + + @Test + public void testGetLoginId() throws Exception { + // Mock HttpClient and dependent objects + HttpClient mockHttpClient = mock(HttpClient.class); + Request mockRequest = mock(Request.class); + ContentResponse mockResponse = mock(ContentResponse.class); + HttpFields mockHeaders = mock(HttpFields.class); + + // Define behavior for HttpFields + when(mockHeaders.toString()).thenReturn("Mocked Headers"); + + // Properly configure the fluent chain for Request + when(mockHttpClient.newRequest(any(String.class))).thenReturn(mockRequest); + when(mockRequest.method(HttpMethod.POST)).thenReturn(mockRequest); + when(mockRequest.timeout(any(Long.class), any(TimeUnit.class))).thenReturn(mockRequest); + when(mockRequest.content(any(StringContentProvider.class))).thenReturn(mockRequest); + when(mockRequest.getHeaders()).thenReturn(mockHeaders); + when(mockRequest.send()).thenReturn(mockResponse); + + // Define behavior for ContentResponse + when(mockResponse.getStatus()).thenReturn(200); + // Include "code": 0 to simulate a successful response + when(mockResponse.getContentAsString()) + .thenReturn("{\"msg\": \"ok\", \"result\": {\"loginId\": \"mock-loginId\"}, \"errorCode\": \"0\"}"); + + // Create a CloudProvider instance + CloudProvider provider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "1017", + "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + + // Inject the mocked HttpClient into the Cloud class + Cloud cloud = new Cloud("email", "password", provider); + cloud.setHttpClient(mockHttpClient); + + // Execute the getLoginId method + boolean getLogin = cloud.getLoginId(); + + // Assert the result + assertTrue(getLogin); // Validate that getLoginId returned true + + // Verify that loginId was set correctly + Field loginIdField = Cloud.class.getDeclaredField("loginId"); + loginIdField.setAccessible(true); + assertEquals("mock-loginId", loginIdField.get(cloud)); // Ensure loginId is correctly set + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 5d9d98be77130..95eebebcc0197 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -36,7 +36,7 @@ public class MideaACDiscoveryServiceTest { byte[] data = HexFormat.of().parseHex( "837000C8200F00005A5A0111B8007A80000000006B0925121D071814C0110800008A0000000000000000018000000000AF55C8897BEA338348DA7FC0B3EF1F1C889CD57C06462D83069558B66AF14A2D66353F52BAECA68AEB4C3948517F276F72D8A3AD4652EFA55466D58975AEB8D948842E20FBDCA6339558C848ECE09211F62B1D8BB9E5C25DBA7BF8E0CC4C77944BDFB3E16E33D88768CC4C3D0658937D0BB19369BF0317B24D3A4DE9E6A13106AFFBBE80328AEA7426CD6BA2AD8439F72B4EE2436CC634040CB976A92A53BCD5"); byte[] reply = HexFormat.of().parseHex( - "F600A8C02C19000030303030303050303030303030305131423838433239353634334243303030300B6E65745F61635F343342430000870002000000000000000000AC00ACAC00000000B88C295643BC150023082122000300000000000000000000000000000000000000000000000000000000000000000000"); + "E600A8C02C19000030303030303050303030303030305131423838433239353634334243303030300B6E65745F61635F343342430000870002000000000000000000AC00ACAC00000000B88C295643BC150023082122000300000000000000000000000000000000000000000000000000000000000000000000"); String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "", mSmartType = ""; @@ -74,7 +74,7 @@ public void testId() { public void testIPAddress() { mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); - assertEquals("192.168.0.246", mSmartip); + assertEquals("192.168.0.230", mSmartip); } /** diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java new file mode 100644 index 0000000000000..d6e981c92f04f --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.security; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.binding.mideaac.internal.cloud.CloudProvider; + +import com.google.gson.JsonObject; + +/** + * The {@link SecurityTest} tests methods and + * compares them to the expected result. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class SecurityTest { + + @Test + public void testGetUdpId() { + // Cloud Provider has no effect. Based on the deviceId + CloudProvider cloudProvider = new CloudProvider("", "", "", "", "", "", "", ""); + Security security = new Security(cloudProvider); + + String deviceId = "151749305185620"; + long deviceIdAsInteger = Long.valueOf(deviceId); + byte[] sixByteArray = Utils.toIntTo6ByteArray(deviceIdAsInteger, ByteOrder.BIG_ENDIAN); + String udpid = security.getUdpId(sixByteArray); + byte[] expectedArray = new byte[] { -118, 3, -29, 110, 53, 84 }; + assertArrayEquals(expectedArray, sixByteArray); + assertEquals("c52a10094ba5dc9866cdf657606d4bbd", udpid); + } + + @Test + public void testencryptIamPassword() { + // Cloud provider appkey is used to encrypt + CloudProvider cloudProvider = new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "", "", "", + "", "", ""); + Security security = new Security(cloudProvider); + + String loginId = "39c5f83c-63d0-4b4b-8f44-2823699857cd"; + String password = "mYPaSsWoRd"; + String encryptedPassword = security.encryptIamPassword(loginId, password); + assertEquals("e1ce0ff005e35c2a4832afba5310ee3f265663319146ba0a2e35974eef54d08a", encryptedPassword); + } + + @Test + public void testencryptPassword() { + // Cloud provider appkey is used to encrypt + CloudProvider cloudProvider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "", "", "", + "", "", ""); + Security security = new Security(cloudProvider); + + String loginId = "0cff1f62-37a2-4500-a4d5-be5924e02823"; + String password = "mYPaSsWoRd"; + String encryptedPassword = security.encryptPassword(loginId, password); + assertEquals("178026cb7c5a299d6abd1cd779d11bf1a47045b09e410db5fe139f7b25c8eb35", encryptedPassword); + } + + @Test + public void testSign1() { + // Cloud provider url used to create the sign (with the endpoint) + CloudProvider cloudProvider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "", + "https://mapp.appsmb.com", "", "", "", ""); + Security security = new Security(cloudProvider); + + String url = "https://mapp.appsmb.com/v1/user/login/id/get"; + + // Create JsonObject using Gson + JsonObject data = new JsonObject(); + data.addProperty("appId", "1017"); + data.addProperty("format", 2); + data.addProperty("clientType", 1); + data.addProperty("language", "en_US"); + data.addProperty("src", "1017"); + data.addProperty("stamp", "20250327172341"); + data.addProperty("loginAccount", "myemail@gmail.com"); + + // Sign the data + String sign = security.sign(url, data); + + // Verify the signature + assertEquals("648ed795a7b0faf566226cc5abc72bd32a51ede0ffbf9649b34c0f25a3508ef4", sign); + } + + @Test + public void testSign2() { + // Cloud provider url used to create the sign (with the endpoint) + CloudProvider cloudProvider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "", + "https://mapp.appsmb.com", "", "", "", ""); + Security security = new Security(cloudProvider); + + String url = "https://mapp.appsmb.com/v1/user/login"; + + // Create JsonObject using Gson + JsonObject data = new JsonObject(); + data.addProperty("appId", "1017"); + data.addProperty("format", 2); + data.addProperty("clientType", 1); + data.addProperty("language", "en_US"); + data.addProperty("src", "1017"); + data.addProperty("stamp", "20250327172342"); + data.addProperty("loginAccount", "myemail@gmail.com"); + data.addProperty("password", "178026cb7c5a299d6abd1cd779d11bf1a47045b09e410db5fe139f7b25c8eb35"); + + // Sign the data + String sign = security.sign(url, data); + + // Verify the signature + assertEquals("43e1031addb18994d686cae8ac3612e75e4862eea67c7a5b9dde19c68508b90b", sign); + } + + @Test + public void testSign3() { + // Cloud provider url used to create the sign (with the endpoint) + CloudProvider cloudProvider = new CloudProvider("NetHome Plus", "3742e9e5842d4ad59c2db887e12449f9", "", + "https://mapp.appsmb.com", "", "", "", ""); + Security security = new Security(cloudProvider); + + String url = "https://mapp.appsmb.com/v1/iot/secure/getToken"; + + // Create JsonObject using Gson + JsonObject data = new JsonObject(); + data.addProperty("appId", "1017"); + data.addProperty("format", 2); + data.addProperty("clientType", 1); + data.addProperty("language", "en_US"); + data.addProperty("src", "1017"); + data.addProperty("stamp", "20250331202017"); + data.addProperty("udpid", "c52a10094ba5dc9866cdf657606d4bbd"); + data.addProperty("sessionId", "a4261dbfd2e748fb89da33a2be3ace3020250325232016876"); + + // Sign the data + String sign = security.sign(url, data); + + // Verify the signature + assertEquals("4cbd636506460ba43251ae254ac1a36457b6e976639cd29b0628818228eefd9d", sign); + } + + @Test + public void testnewSign() { + // Cloud provider that uses newSign ie. proxied + CloudProvider cloudProvider = new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", + "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", + "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); + Security security = new Security(cloudProvider); + + String json = "{\"appId\":\"1010\",\"format\":2,\"clientType\":1,\"language\":\"en_US\",\"src\":\"1010\",\"stamp\":\"20250331151111\",\"loginAccount\":\"myemail@gmail.com\",\"reqId\":\"f2c3e2c3365a4d4f\"}"; + String random = "1742860285"; + String proxysign = security.newSign(json, random); + assertEquals("1afdddedb746d70086dc3c50b35a8ece1e4aadc0030b58e73542528461769a9c", proxysign); + } +} From 4444cf328e77dbf285cff72be123114c62572a4c Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Thu, 3 Apr 2025 15:40:19 -0400 Subject: [PATCH 22/44] Cleanup unused code in Cloud.java Cleanup unused code in Cloud.java Signed-off-by: Bob Eckhoff --- .../binding/mideaac/internal/cloud/Cloud.java | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index 1bc7b92540ae6..d0102511bc301 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -57,15 +57,6 @@ public class Cloud { private HttpClient httpClient; - /** - * Client for Http requests - * - * @return httpClient - */ - public HttpClient getHttpClient() { - return httpClient; - } - /** * Sets Http Client * @@ -77,15 +68,6 @@ public void setHttpClient(HttpClient httpClient) { private String errMsg = ""; - /** - * Gets error message - * - * @return errMsg - */ - public String getErrMsg() { - return errMsg; - } - private @Nullable String accessToken = ""; private String loginAccount; @@ -227,8 +209,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { String msg = result.get("msg").getAsString(); if (code != 0) { errMsg = msg; - handleApiError(code, msg); - logger.warn("Error logging to Cloud: {}", msg); + logger.warn("Error {} logging to Cloud: {}", code, msg); return null; } else { logger.debug("Api response ok: {} ({})", code, msg); @@ -359,8 +340,4 @@ public boolean getLoginId() { loginId = response.get("loginId").getAsString(); return true; } - - private void handleApiError(int asInt, String asString) { - logger.debug("Api error in Cloud class"); - } } From e17c852a001af1c2373ad31233f2a85dc738f039 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 8 Apr 2025 15:35:20 -0400 Subject: [PATCH 23/44] Add the capability command These changes add the get capability command and sends it to the device during initial initiation, reads the results and populates the properties for easy reference. Signed-off-by: Bob Eckhoff --- .../connection/ConnectionManager.java | 11 ++ .../discovery/MideaACDiscoveryService.java | 29 ++- .../mideaac/internal/handler/Callback.java | 17 +- .../handler/CapabilitiesResponse.java | 34 ++++ .../internal/handler/CapabilityParser.java | 178 ++++++++++++++++++ .../internal/handler/CapabilityReaders.java | 119 ++++++++++++ .../mideaac/internal/handler/CommandSet.java | 28 ++- .../internal/handler/MideaACHandler.java | 89 ++++++++- .../mideaac/internal/handler/Reader.java | 35 ++++ .../handler/CapabilityParserTest.java | 178 ++++++++++++++++++ .../internal/security/SecurityTest.java | 15 ++ 11 files changed, 721 insertions(+), 12 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 32a35e9fae095..1610d228c17ac 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -31,6 +31,7 @@ import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException; import org.openhab.binding.mideaac.internal.connection.exception.MideaException; import org.openhab.binding.mideaac.internal.handler.Callback; +import org.openhab.binding.mideaac.internal.handler.CapabilitiesResponse; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.handler.CommandSet; import org.openhab.binding.mideaac.internal.handler.Packet; @@ -386,6 +387,16 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToBinary(data)); + // Handle the capabilities response + if (bodyType == (byte) 0xB5) { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); + } + return; + } + if (data.length < 21) { logger.warn("Response data is {} long, minimum is 21!", data.length); return; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 147678cfde58e..2bdab2fa0fd6a 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -31,6 +31,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.cloud.CloudProvider; +import org.openhab.binding.mideaac.internal.handler.CapabilityParser; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.core.config.discovery.AbstractDiscoveryService; @@ -305,7 +306,8 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { return DiscoveryResultBuilder.create(thingUID).withLabel(thingName) .withRepresentationProperty(CONFIG_IP_ADDRESS).withThingType(THING_TYPE_MIDEAAC) .withProperties(collectProperties(ipAddress, mSmartVersion, mSmartId, mSmartPort, mSmartSN, - mSmartSSID, mSmartType)) + mSmartSSID, mSmartType, new TreeMap<>(), // Placeholder for capabilities + new TreeMap<>())) // Placeholder for numericCapabilities .build(); } else if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) { logger.debug("Midea AC v1 device was detected, supported, but not implemented yet."); @@ -327,7 +329,8 @@ private String createThingName(final byte[] byteIP, String id) { } /** - * Collects properties into a map. + * Collects discovered properties into a map and empty Maps + * for capabilities. * * @param ipAddress IP address of the thing * @param version Version 2 or 3 @@ -339,8 +342,11 @@ private String createThingName(final byte[] byteIP, String id) { * @return Map with properties */ private Map collectProperties(String ipAddress, String version, String id, String port, String sn, - String ssid, String type) { + String ssid, String type, @Nullable Map> capabilities, + @Nullable Map> numericCapabilities) { Map properties = new TreeMap<>(); + + // Basic properties properties.put(CONFIG_IP_ADDRESS, ipAddress); properties.put(CONFIG_IP_PORT, port); properties.put(CONFIG_DEVICEID, id); @@ -349,6 +355,23 @@ private Map collectProperties(String ipAddress, String version, properties.put(PROPERTY_SSID, ssid); properties.put(PROPERTY_TYPE, type); + // Default empty maps for boolean and numeric capabilities + if (capabilities != null) { + capabilities.forEach((capabilityId, capabilityMap) -> { + capabilityMap.forEach((key, value) -> { + properties.put(capabilityId.name() + "_" + key, value); + }); + }); + } + + if (numericCapabilities != null) { + numericCapabilities.forEach((capabilityId, temperatureMap) -> { + temperatureMap.forEach((key, value) -> { + properties.put(capabilityId.name() + "_" + key, value); + }); + }); + } + return properties; } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index a6e85da5f39c0..b48b2ceeabd75 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -15,16 +15,25 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link Response} performs the byte data stream decoding + * The {@link Response} performs the polling byte data stream decoding + * The {@link CapabilitiesResponse} performs the capability byte data stream decoding * * @author Leo Siepel - Initial contribution + * @author Bob Eckhoff - added Capabilities Callback */ @NonNullByDefault public interface Callback { /** - * Updates channels with the response - * - * @param response Byte response from the device used to update channels + * Updates channels with a standard response. + * + * @param response The standard response from the device used to update channels. */ void updateChannels(Response response); + + /** + * Updates channels with a capabilities response. + * + * @param capabilitiesResponse The capabilities response from the device used to update properties. + */ + void updateChannels(CapabilitiesResponse capabilitiesResponse); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java new file mode 100644 index 0000000000000..d484a4e02cfa9 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CapabilityResponse} handles the raw capability message + * from the device + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CapabilitiesResponse { + private final byte[] rawData; + + public CapabilitiesResponse(byte[] rawData) { + this.rawData = rawData; + } + + public byte[] getRawData() { + return rawData; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java new file mode 100644 index 0000000000000..9d95dbd0100ad --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link CapabilityParser} parses the capability Response. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CapabilityParser { + private final Logger logger = LoggerFactory.getLogger(CapabilityParser.class); + private final Map> capabilities = new HashMap<>(); + private final Map> numericCapabilities = new HashMap<>(); + private final int trailingBytes = 2; // We expect exactly 2 trailing bytes + private boolean additionalCapabilities = false; + + public void parse(byte[] payload) { + // Check if the payload is empty or too short to process + if (payload.length < 2 + trailingBytes) { + return; // Exit the method without processing + } + + // The first byte indicates the number of capabilities + int count = payload[1] & 0xFF; // Unsigned byte + int offset = 2; // Start after the count + + while (offset < payload.length - trailingBytes && count-- > 0) { + if (offset + 3 > payload.length - trailingBytes) { + break; // Exit if there's insufficient data + } + + // Size of the capability value + int size = payload[offset + 2] & 0xFF; + if (size == 0) { + offset += 3; // Skip empty capabilities + continue; + } + + // Read the reversed 16-bit capability ID (little-endian) + int rawId = ByteBuffer.wrap(payload, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF; + + // Map the ID to a CapabilityId enum + CapabilityId capabilityId; + try { + capabilityId = CapabilityId.fromId(rawId); + } catch (IllegalArgumentException e) { + logger.debug("Unknown capability ID: {}, Size: {}", rawId, size); + offset += 3 + size; // Skip unknown capability + continue; + } + + // Fetch the first value after the key and size + int value = payload[offset + 3] & 0xFF; + + // Apply predefined capability readers if available + if (CapabilityReaders.hasReader(capabilityId)) { + CapabilityReaders.apply(capabilityId, value, capabilities); + } else if (capabilityId == CapabilityId.TEMPERATURES) { + if (size >= 6) { + numericCapabilities.put(capabilityId, + Map.of("cool_min_temperature", payload[offset + 3] * 0.5, "cool_max_temperature", + payload[offset + 4] * 0.5, "auto_min_temperature", payload[offset + 5] * 0.5, + "auto_max_temperature", payload[offset + 6] * 0.5, "heat_min_temperature", + payload[offset + 7] * 0.5, "heat_max_temperature", payload[offset + 8] * 0.5)); + } + } else if (capabilityId == CapabilityId._UNKNOWN) { + logger.debug("Ignored unknown capability ID: {}, Size: {}", rawId, size); + } else { + logger.debug("Unsupported capability {}, Size: {}", capabilityId, size); + } + + offset += 3 + size; // Advance to the next capability + } + + // Check if additional capability flag exists without interference from CRC + additionalCapabilities = offset < payload.length - trailingBytes; + logger.debug("Additional capabilities {}", additionalCapabilities); + } + + public Map> getCapabilities() { + return capabilities; + } + + public Map> getNumericCapabilities() { + return numericCapabilities; + } + + public boolean hasAdditionalCapabilities() { + return additionalCapabilities; + } + + /** + * From original source, kept notes + */ + public enum CapabilityId { + SWING_UD_ANGLE(0x0009), + SWING_LR_ANGLE(0x000A), + BREEZELESS(0x0018), // AKA "No Wind Sense" + SMART_EYE(0x0030), + WIND_ON_ME(0x0032), + WIND_OFF_ME(0x0033), + SELF_CLEAN(0x0039), // AKA Active Clean + _UNKNOWN(0x0040), // Unknown ID from various logs + BREEZE_AWAY(0x0042), // AKA "Prevent Straight Wind" + BREEZE_CONTROL(0x0043), // AKA "FA No Wind Sense" + RATE_SELECT(0x0048), + FRESH_AIR(0x004B), + PARENT_CONTROL(0x0051), // ?? + PREVENT_STRAIGHT_WIND_SELECT(0x0058), // ?? + WIND_AROUND(0x0059), // ?? + JET_COOL(0x0067), // ?? + PRESET_IECO(0x00E3), + ICHECK(0x0091), // ?? + EMERGENT_HEAT_WIND(0x0093), // ?? + HEAT_PTC_WIND(0x0094), // ?? + CVP(0x0098), // ?? + FAN_SPEED_CONTROL(0x0210), + PRESET_ECO(0x0212), + PRESET_FREEZE_PROTECTION(0x0213), + MODES(0x0214), + SWING_MODES(0x0215), + ENERGY(0x0216), // AKA electricity + FILTER_REMIND(0x0217), + AUX_ELECTRIC_HEAT(0x0219), // AKA PTC + PRESET_TURBO(0x021A), + FILTER_CHECK(0x0221), + ANION(0x021E), + HUMIDITY(0x021F), + FAHRENHEIT(0x0222), + DISPLAY_CONTROL(0x0224), + TEMPERATURES(0x0225), + BUZZER(0x022C), // Reference refers to this as "sound". Is this different then beep? + MAIN_HORIZONTAL_GUIDE_STRIP(0x0230), // ?? + SUP_HORIZONTAL_GUIDE_STRIP(0x0231), // ?? + TWINS_MACHINE(0x0232), // ?? + GUIDE_STRIP_TYPE(0x0233), // ?? + BODY_CHECK(0x0234); // ?? + + private final int id; + + CapabilityId(int id) { + this.id = id; + } + + public int getId() { + return id; + } + + public static CapabilityId fromId(int id) { + for (CapabilityId capability : values()) { + if (capability.id == id) { + return capability; + } + } + throw new IllegalArgumentException("Unknown Capability ID: " + id); + } + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java new file mode 100644 index 0000000000000..b1ad15538023f --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.handler.CapabilityParser.CapabilityId; + +/** + * The {@link CapabilityReaders} reads the raw capability message and + * breaks them into detailed capabilities. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CapabilityReaders { + private static final Map> READERS = new HashMap<>(); + + static { + // Helper to simplify creation of READERS + Function> getValue = (expected) -> (value) -> Objects.equals(value, expected); + + // Add READERS for each capability - Not all are supported + READERS.put(CapabilityId.ANION, List.of(new Reader("anion", getValue.apply(1)))); + READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("aux_electric_heat", getValue.apply(1)))); + READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breeze_away", getValue.apply(1)))); + READERS.put(CapabilityId.BREEZE_CONTROL, List.of(new Reader("breeze_control", getValue.apply(1)))); + READERS.put(CapabilityId.BREEZELESS, List.of(new Reader("breezeless", getValue.apply(1)))); + READERS.put(CapabilityId.BUZZER, List.of(new Reader("buzzer", getValue.apply(1)))); + + READERS.put(CapabilityId.DISPLAY_CONTROL, + List.of(new Reader("display_control", (value) -> List.of(1, 2, 100).contains(value)))); + + READERS.put(CapabilityId.ENERGY, + List.of(new Reader("energy_stats", (value) -> List.of(2, 3, 4, 5).contains(value)), + new Reader("energy_setting", (value) -> List.of(3, 5).contains(value)), + new Reader("energy_bcd", (value) -> List.of(2, 3).contains(value)))); + + READERS.put(CapabilityId.FAHRENHEIT, List.of(new Reader("fahrenheit", getValue.apply(0)))); + + READERS.put(CapabilityId.FAN_SPEED_CONTROL, + List.of(new Reader("fan_silent", getValue.apply(6)), + new Reader("fan_low", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), + new Reader("fan_medium", (value) -> List.of(5, 6, 7).contains(value)), + new Reader("fan_high", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), + new Reader("fan_auto", (value) -> List.of(4, 5, 6).contains(value)), + new Reader("fan_custom", getValue.apply(1)))); + + READERS.put(CapabilityId.FILTER_REMIND, + List.of(new Reader("filter_notice", (value) -> List.of(1, 2, 4).contains(value)), + new Reader("filter_clean", (value) -> List.of(3, 4).contains(value)))); + + READERS.put(CapabilityId.HUMIDITY, + List.of(new Reader("humidity_auto_set", (value) -> List.of(1, 2).contains(value)), + new Reader("humidity_manual_set", (value) -> List.of(2, 3).contains(value)))); + + READERS.put(CapabilityId.MODES, + List.of(new Reader("mode_heat", (value) -> List.of(1, 2, 4, 6, 7, 9, 10, 11, 12, 13).contains(value)), + new Reader("mode_cool", (value) -> !List.of(2, 10, 12).contains(value)), + new Reader("mode_dry", (value) -> List.of(0, 1, 5, 6, 9, 11, 13).contains(value)), + new Reader("mode_auto", (value) -> List.of(0, 1, 2, 7, 8, 9, 13).contains(value)), + new Reader("mode_aux_heat", getValue.apply(9)), + new Reader("mode_aux", (value) -> List.of(9, 10, 11, 13).contains(value)))); + + READERS.put(CapabilityId.PRESET_ECO, List.of(new Reader("eco_cool", (value) -> List.of(1, 2).contains(value)))); + READERS.put(CapabilityId.PRESET_FREEZE_PROTECTION, List.of(new Reader("freeze_protection", getValue.apply(1)))); + READERS.put(CapabilityId.PRESET_IECO, List.of(new Reader("ieco", getValue.apply(1)))); + + READERS.put(CapabilityId.PRESET_TURBO, + List.of(new Reader("turbo_heat", (value) -> List.of(1, 3).contains(value)), + new Reader("turbo_cool", (value) -> value < 2))); + + READERS.put(CapabilityId.RATE_SELECT, List.of(new Reader("rate_select_2_level", getValue.apply(1)), + new Reader("rate_select_5_level", (value) -> List.of(2, 3).contains(value)))); + + READERS.put(CapabilityId.SELF_CLEAN, List.of(new Reader("self_clean", getValue.apply(1)))); + READERS.put(CapabilityId.SMART_EYE, List.of(new Reader("smart_eye", getValue.apply(1)))); + READERS.put(CapabilityId.SWING_LR_ANGLE, List.of(new Reader("swing_horizontal_angle", getValue.apply(1)))); + READERS.put(CapabilityId.SWING_UD_ANGLE, List.of(new Reader("swing_vertical_angle", getValue.apply(1)))); + + READERS.put(CapabilityId.SWING_MODES, + List.of(new Reader("swing_horizontal", (value) -> List.of(1, 3).contains(value)), + new Reader("swing_vertical", (value) -> value < 2))); + + READERS.put(CapabilityId.WIND_OFF_ME, List.of(new Reader("wind_off_me", getValue.apply(1)))); + READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("wind_on_me", getValue.apply(1)))); + } + + public static boolean hasReader(CapabilityId id) { + return READERS.containsKey(id); + } + + public static void apply(CapabilityId id, int value, Map> capabilities) { + Optional.ofNullable(READERS.get(id)).ifPresent(readersList -> { + Map result = readersList.stream() + .map(reader -> Map.entry(reader.name, reader.predicate.test(value))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + capabilities.put(id, result); + }); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 843ddcea309d3..824e96d9aff63 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -248,7 +248,7 @@ public void setFahrenheit(boolean fahrenheitEnabled) { public void setScreenDisplay(boolean screenDisplayToggle) { modifyBytesForDisplayOff(); removeExtraBytes(); - logger.trace(" Set Bytes before crypt {}", Utils.bytesToHex(data)); + logger.trace("Set Bytes before crypt {}", Utils.bytesToHex(data)); } private void modifyBytesForDisplayOff() { @@ -274,6 +274,32 @@ private void removeExtraBytes() { data = newData; } + /** + * Creates the Get capability message + * + * @param getCapabilities + * @return + */ + public void getCapabilities() { + modifyBytesForCapabilities(); + removeExtraCapabilityBytes(); + logger.debug("Set Bytes before encrypt {}", Utils.bytesToHex(data)); + } + + private void modifyBytesForCapabilities() { + data[0x01] = (byte) 0x0E; + data[0x09] = (byte) 0x03; + data[0x0a] = (byte) 0xB5; + data[0x0b] = (byte) 0x02; + data[0x0c] = (byte) 0x00; + } + + private void removeExtraCapabilityBytes() { + byte[] newData = new byte[data.length - 21]; + System.arraycopy(data, 0, newData, 0, newData.length); + data = newData; + } + /** * Add 0.5C to the temperature value. If needed * Target_temperature setter calls this method diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 56677736010bd..6ddc95e3d7613 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -71,7 +71,7 @@ * @author Leo Siepel - Refactored class, improved separation of concerns */ @NonNullByDefault -public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler { +public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler, Callback { private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); private final boolean imperialUnits; @@ -85,8 +85,16 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler private @Nullable ScheduledFuture scheduledTask; private @Nullable ScheduledFuture scheduledKeyTokenUpdate; - private Callback callbackLambda = (response) -> { - this.updateChannels(response); + private final Callback callbackLambda = new Callback() { + @Override + public void updateChannels(Response response) { + MideaACHandler.this.updateChannels(response); + } + + @Override + public void updateChannels(CapabilitiesResponse capabilitiesResponse) { + MideaACHandler.this.updateChannels(capabilitiesResponse); + } }; /** @@ -220,6 +228,25 @@ public void initialize() { config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); + // Initialize connectionManager + connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, + config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, + config.deviceId, config.version, config.promptTone); + + // This checks for one of the added default properties and means the command + // Has already run and doesn't need to be run again + if (!properties.containsKey("mode_fan_only")) { + try { + // Send configuration command and get response + CommandSet initializationCommand = new CommandSet(); + initializationCommand.getCapabilities(); + // Send the command using the connection manager + connectionManager.sendCommand(initializationCommand, this); + // 'this' is passed as the callback because MideaACHandler implements Callback + } catch (Exception e) { + logger.error("Error during capabilities initialization: {}", e.getMessage()); + } + } if (scheduledTask == null) { scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); @@ -262,7 +289,8 @@ private void updateChannel(String channelName, State state) { } } - private void updateChannels(Response response) { + @Override + public void updateChannels(Response response) { updateChannel(CHANNEL_POWER, OnOffType.from(response.getPowerState())); updateChannel(CHANNEL_APPLIANCE_ERROR, OnOffType.from(response.getApplianceError())); updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString())); @@ -300,6 +328,59 @@ private void updateChannels(Response response) { updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); } + @Override + public void updateChannels(CapabilitiesResponse capabilitiesResponse) { + // Handle capabilities responses + CapabilityParser parser = new CapabilityParser(); + parser.parse(capabilitiesResponse.getRawData()); + + properties = editProperties(); + + parser.getCapabilities().forEach((capabilityId, capabilityMap) -> { + capabilityMap.forEach((key, value) -> { + properties.put(key, String.valueOf(value)); + }); + }); + + parser.getNumericCapabilities().forEach((capabilityId, temperatureMap) -> { + temperatureMap.forEach((key, value) -> { + properties.put(key, String.valueOf(value)); + }); + }); + + // Default to false if "display_control" is missing from DISPLAY_CONTROL response + if (!properties.containsKey("display_control")) { + properties.put("display_control", "false - default"); + } + // Default to true if "fan_only" is missing from MODE response + if (!properties.containsKey("mode_fan_only")) { + properties.put("mode_fan_only", "true - default"); + } + // Defaults if FAN_SPEED_CONTROL is missing from response + if (!properties.containsKey("fan_low")) { + properties.put("fan_low", "true - default"); + } + if (!properties.containsKey("fan_medium")) { + properties.put("fan_medium", "true - default"); + } + if (!properties.containsKey("fan_high")) { + properties.put("fan_high", "true - default"); + } + if (!properties.containsKey("fan_auto")) { + properties.put("fan_auto", "true - default"); + } + if (!properties.containsKey("cool_min_temperature")) { + properties.put("min_target_temperature", "17°C / 62°F default"); + } + if (!properties.containsKey("heat_max_temperature")) { + properties.put("max_target_temperature", "30°C / 86°F default"); + } + + updateProperties(properties); + + logger.debug("Capabilities and temperature settings parsed and stored in properties: {}", properties); + } + @Override public void discovered(DiscoveryResult discoveryResult) { logger.debug("Discovered {}", thing.getUID()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java new file mode 100644 index 0000000000000..f8b5a8fb6d636 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link CapabilityReaders} reads the raw capability message and + * breaks them into detailed capabilities. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class Reader { + public final String name; // Name of the capability (e.g., "fan_silent") + public final Predicate predicate; // Condition to check values + + public Reader(String name, Predicate predicate) { + this.name = name; + this.predicate = predicate; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java new file mode 100644 index 0000000000000..51cd2d706a17d --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.mideaac.internal.handler.CapabilityParser.CapabilityId; + +/** + * The {@link CapabilityParser} parses the capability Response. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class CapabilityParserTest { + + @Test + void testParseWithValidPayload() { + // Arrange: Create a valid payload with known capabilities + byte[] payload = new byte[] { (byte) 0xB5, 0x03, // Header and count (3 capabilities) + 0x14, 0x02, 0x01, 0x01, // Capability 1 (0x0214 = MODES, value = 1) + 0x16, 0x02, 0x01, 0x02, // Capability 2 (0x0216 = ENERGY, value = 2) + 0x1A, 0x02, 0x01, 0x03, // Capability 3 (0x021A = PRESET_TURBO, value = 3) + (byte) 0xDE, (byte) 0xDF // CRC Check (trailing bytes) + }; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Check the parsed results + Map> capabilities = parser.getCapabilities(); + assertNotNull(capabilities, "Capabilities map should not be null"); + + // Check individual capabilities + assertTrue(capabilities.containsKey(CapabilityId.MODES), "Should contain MODES capability"); + Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heat_mode")) + .ifPresent(value -> assertEquals(true, value, "MODES - heat_mode should be true")); + + assertTrue(capabilities.containsKey(CapabilityId.ENERGY), "Should contain ENERGY capability"); + Optional.ofNullable(capabilities.get(CapabilityId.ENERGY)).map(modes -> modes.get("energy_stats")) + .ifPresent(value -> assertEquals(true, value, "ENERGY - energy_stats should be true")); + + assertTrue(capabilities.containsKey(CapabilityId.PRESET_TURBO), "Should contain PRESET_TURBO capability"); + Optional.ofNullable(capabilities.get(CapabilityId.PRESET_TURBO)).map(modes -> modes.get("turbo_heat")) + .ifPresent(value -> assertEquals(true, value, "PRESET_TURBO - turbo_heat should be true")); + } + + @Test + void testParseWithEmptyPayload() { + // Arrange: Create an empty payload + byte[] payload = new byte[] {}; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Ensure no capabilities are parsed + assertTrue(parser.getCapabilities().isEmpty(), "Capabilities map should be empty for an empty payload"); + } + + @Test + void testParseWithUnknownCapability() { + // Arrange: Create a payload with an unknown capability + byte[] payload = new byte[] { (byte) 0xB5, 0x01, // Header and count (1 capability) + 0x50, 0x50, 0x01, 0x01 // Unknown capability (0x5050) + }; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Ensure unknown capability is ignored + Map> capabilities = parser.getCapabilities(); + assertTrue(capabilities.isEmpty(), "Capabilities map should not contain unknown capabilities"); + } + + @Test + void testParseWithInvalidSize() { + // Arrange: Create a payload with an invalid size + byte[] payload = new byte[] { (byte) 0xB5, 0x01, // Header and count (1 capability) + 0x14, 0x02, 0x00 // Capability 1 (0x0214 = MODES, size = 0) + }; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Ensure no capabilities are parsed + assertTrue(parser.getCapabilities().isEmpty(), "Capabilities map should be empty for invalid size"); + } + + @Test + void testParseWithTrailingCRC() { + // Arrange: Create a payload with trailing CRC + byte[] payload = new byte[] { (byte) 0xB5, 0x07, // Header and count (7 capabilities) + 0x12, 0x02, 0x01, 0x01, // Capability 1 (0x0212 = PRESET_ECO, value = 1) + 0x13, 0x02, 0x01, 0x01, // Capability 2 (0x0213 = PRESET_FREEZE_PROTECTION, value = 1) + 0x14, 0x02, 0x01, 0x01, // Capability 3 (0x0214 = MODES, value = 1) + 0x15, 0x02, 0x01, 0x01, // Capability 4 (0x0215 = SWING_MODES, value = 1) + 0x16, 0x02, 0x01, 0x01, // Capability 5 (0x0216 = ENERGY, value = 1) + 0x17, 0x02, 0x01, 0x00, // Capability 6 (0x0217 = FILTER_REMIND, value = 0) + 0x1A, 0x02, 0x01, 0x01, // Capability 7 (0x021A = PRESET_TURBO, value = 1) + (byte) 0xDE, (byte) 0xDF // CRC Check (trailing bytes) + }; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Verify capabilities are parsed correctly + Map> capabilities = parser.getCapabilities(); + assertNotNull(capabilities, "Capabilities map should not be null"); + + // Verify specific capabilities + assertTrue(capabilities.containsKey(CapabilityId.PRESET_ECO), "Should contain PRESET_ECO capability"); + Optional.ofNullable(capabilities.get(CapabilityId.PRESET_ECO)).map(modes -> modes.get("eco")) + .ifPresent(value -> assertEquals(true, value, "PRESET_ECO - eco should be true")); + + assertTrue(capabilities.containsKey(CapabilityId.MODES), "Should contain MODES capability"); + Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heat_mode")) + .ifPresent(value -> assertEquals(true, value, "MODES - heat_mode should be true")); + + // Ensure CRC did not cause parsing issues + assertFalse(parser.hasAdditionalCapabilities(), "No additional capabilities"); + } + + @Test + void testParseWithtemperature() { + // Arrange: Create a payload with trailing CRC + byte[] payload = new byte[] { (byte) 0xB5, 0x08, // Header and count (7 capabilities) + 0x12, 0x02, 0x01, 0x01, // Capability 1 (0x0212 = PRESET_ECO, value = 1) + 0x13, 0x02, 0x01, 0x01, // Capability 2 (0x0213 = PRESET_FREEZE_PROTECTION, value = 1) + 0x14, 0x02, 0x01, 0x01, // Capability 3 (0x0214 = MODES, value = 1) + 0x15, 0x02, 0x01, 0x01, // Capability 4 (0x0215 = SWING_MODES, value = 1) + 0x16, 0x02, 0x01, 0x01, // Capability 5 (0x0216 = ENERGY, value = 1) + 0x17, 0x02, 0x01, 0x00, // Capability 6 (0x0217 = FILTER_REMIND, value = 0) + 0x1A, 0x02, 0x01, 0x01, // Capability 7 (0x021A = PRESET_TURBO, value = 1) + 0x25, 0x02, 0x07, 0x20, 0x3c, 0x20, 0x3c, 0x20, 0x3c, 0x00, // Temperature + (byte) 0xDE, (byte) 0xDF // CRC Check (trailing bytes) + }; + + CapabilityParser parser = new CapabilityParser(); + + // Act: Parse the payload + parser.parse(payload); + + // Assert: Verify capabilities are parsed correctly + Map> numericCapabilities = parser.getNumericCapabilities(); + assertNotNull(numericCapabilities, "Capabilities map should not be null"); + + Optional.ofNullable(numericCapabilities.get(CapabilityId.TEMPERATURES)) + .map(modes -> modes.get("cool_min_temperature")).ifPresent(value -> assertEquals(16.0, value)); + Optional.ofNullable(numericCapabilities.get(CapabilityId.TEMPERATURES)) + .map(modes -> modes.get("heat_max_temperature")).ifPresent(value -> assertEquals(30.0, value)); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java index d6e981c92f04f..fc84331dd7b0c 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java @@ -48,6 +48,21 @@ public void testGetUdpId() { assertEquals("c52a10094ba5dc9866cdf657606d4bbd", udpid); } + @Test + public void testGetUdpIdLittle() { + // Cloud Provider has no effect. Based on the deviceId + CloudProvider cloudProvider = new CloudProvider("", "", "", "", "", "", "", ""); + Security security = new Security(cloudProvider); + + String deviceId = "151732605161920"; + long deviceIdAsInteger = Long.valueOf(deviceId); + byte[] sixByteArray = Utils.toIntTo6ByteArray(deviceIdAsInteger, ByteOrder.LITTLE_ENDIAN); + String udpid = security.getUdpId(sixByteArray); + byte[] expectedArray = new byte[] { -64, 17, 8, 0, 0, -118 }; + assertArrayEquals(expectedArray, sixByteArray); + assertEquals("1a795626332686b426df2939df3e9e3a", udpid); + } + @Test public void testencryptIamPassword() { // Cloud provider appkey is used to encrypt From 0024a0b0bc6016e1b85856e4993e0728876cf31d Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 9 Apr 2025 16:07:12 -0400 Subject: [PATCH 24/44] Reorganized files Reorganized files and general clean-up of capabilities. Signed-off-by: Bob Eckhoff --- .../mideaac/internal/cloud/CloudProvider.java | 11 ++--------- .../mideaac/internal/connection/CommandHelper.java | 8 +++++--- .../internal/connection/ConnectionManager.java | 14 +++++++++++++- .../discovery/MideaACDiscoveryService.java | 2 +- .../binding/mideaac/internal/handler/Callback.java | 1 + .../mideaac/internal/handler/CommandBase.java | 2 +- .../mideaac/internal/handler/CommandSet.java | 2 +- .../mideaac/internal/handler/MideaACHandler.java | 3 +++ .../binding/mideaac/internal/handler/Response.java | 2 +- .../{ => capabilities}/CapabilitiesResponse.java | 2 +- .../{ => capabilities}/CapabilityParser.java | 2 +- .../{ => capabilities}/CapabilityReaders.java | 4 ++-- .../handler/{ => capabilities}/Reader.java | 2 +- .../{ => capabilities}/CapabilityParserTest.java | 4 ++-- 14 files changed, 35 insertions(+), 24 deletions(-) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/{ => capabilities}/CapabilitiesResponse.java (92%) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/{ => capabilities}/CapabilityParser.java (98%) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/{ => capabilities}/CapabilityReaders.java (97%) rename bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/{ => capabilities}/Reader.java (93%) rename bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/{ => capabilities}/CapabilityParserTest.java (98%) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index 1610655e21ed5..f3ce1ffa5ad22 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -50,19 +50,12 @@ public static CloudProvider getCloudProvider(String name) { case "Midea Air": return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); - // Not in ReadMe yet - // case "Ariston Clima": - // return new CloudProvider("Ariston Clima", "434a209a5ce141c3b726de067835d7f0", "1005", - // "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); + // Reported in HA version that this cloud has been shutdown. + // The is possible v2 version of security down the road case "MSmartHome": return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); - // Future Not sure what to do with "login_key": "ad0ee21d48a64bf49f4fb583ab76e799" - // case "MeijuCloud": // "美的美居" - // return new CloudProvider("MeijuCloud", "46579c15", "900", - // "https://mp-prod.smartmidea.net/mas/v5/app/proxy?alias=", - // "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "prod_secret123@muc", "PROD_VnoClJI9aikS8dyy", "v5"); } return new CloudProvider("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index b8283a5993bd8..9f0f425cac7ad 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -109,6 +109,7 @@ public static CommandSet handleOperationalMode(Command command, Response lastRes return commandSet; } + // Some devices might support 16.0 degrees C private static float limitTargetTemperatureToRange(float temperatureInCelsius) { if (temperatureInCelsius < 17.0f) { return 17.0f; @@ -147,7 +148,8 @@ public static CommandSet handleTargetTemperature(Command command, Response lastR } /** - * Fan Speeds vary by V2 or V3 and device. This command also turns the power ON + * Fan Speeds vary by V2 or V3 and device - See capabilities. + * This command also turns the power ON * * @param command Fan Speed Auto, Low, High, etc. */ @@ -224,7 +226,7 @@ public static CommandSet handleEcoMode(Command command, Response lastResponse) } /** - * Modes supported depends on the device + * Modes supported depends on the device - See capabilities * Power is turned on when swing mode is changed * * @param command Swing Mode @@ -292,7 +294,7 @@ public static CommandSet handleTurboMode(Command command, Response lastResponse) } /** - * May not be supported via LAN in all models - IR only + * May not be supported via LAN - See capabilities - IR only * * @param command Screen Display Toggle to ON or Off - One command */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 1610d228c17ac..ac6a92c8d2d70 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -31,11 +31,11 @@ import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException; import org.openhab.binding.mideaac.internal.connection.exception.MideaException; import org.openhab.binding.mideaac.internal.handler.Callback; -import org.openhab.binding.mideaac.internal.handler.CapabilitiesResponse; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.handler.CommandSet; import org.openhab.binding.mideaac.internal.handler.Packet; import org.openhab.binding.mideaac.internal.handler.Response; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; import org.openhab.binding.mideaac.internal.security.Decryption8370Result; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.binding.mideaac.internal.security.Security.MsgType; @@ -397,6 +397,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal return; } + // Handle the poll response if (data.length < 21) { logger.warn("Response data is {} long, minimum is 21!", data.length); return; @@ -436,6 +437,17 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); + // Handle the capabilities response + if (bodyType == (byte) 0xB5) { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); + } + return; + } + + // Handle the poll response if (data.length < 21) { logger.warn("Response data is {} long, minimum is 21!", data.length); return; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 2bdab2fa0fd6a..994a911924c80 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -31,8 +31,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.cloud.CloudProvider; -import org.openhab.binding.mideaac.internal.handler.CapabilityParser; import org.openhab.binding.mideaac.internal.handler.CommandBase; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.core.config.discovery.AbstractDiscoveryService; import org.openhab.core.config.discovery.DiscoveryResult; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index b48b2ceeabd75..1321d96338c03 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -13,6 +13,7 @@ package org.openhab.binding.mideaac.internal.handler; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; /** * The {@link Response} performs the polling byte data stream decoding diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index c796b8b127347..58cec4808e4c7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -247,7 +247,7 @@ public static byte[] discover() { */ public CommandBase() { data = new byte[] { (byte) 0xaa, - // request is 0x20; setting is 0x23 + // request is 0x20; setting is 0x23 - This is the message length (byte) 0x20, // device type (byte) 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 824e96d9aff63..67117c2bf6d2f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -290,7 +290,7 @@ private void modifyBytesForCapabilities() { data[0x01] = (byte) 0x0E; data[0x09] = (byte) 0x03; data[0x0a] = (byte) 0xB5; - data[0x0b] = (byte) 0x02; + data[0x0b] = (byte) 0x01; data[0x0c] = (byte) 0x00; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 6ddc95e3d7613..0586cd0b13bc2 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -39,6 +39,8 @@ import org.openhab.binding.mideaac.internal.connection.exception.MideaException; import org.openhab.binding.mideaac.internal.discovery.DiscoveryHandler; import org.openhab.binding.mideaac.internal.discovery.MideaACDiscoveryService; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser; import org.openhab.binding.mideaac.internal.security.TokenKey; import org.openhab.core.config.core.Configuration; import org.openhab.core.config.discovery.DiscoveryResult; @@ -369,6 +371,7 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { if (!properties.containsKey("fan_auto")) { properties.put("fan_auto", "true - default"); } + // Defaults if no TEMPERATURES were in response if (!properties.containsKey("cool_min_temperature")) { properties.put("min_target_temperature", "17°C / 62°F default"); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 964e4662ca578..1864bc5dea1f8 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -85,7 +85,7 @@ public Response(byte[] data, int version, String responseType, byte bodyType) { /** * Trace Log Response and Body Type for V3. V2 set at "" and 0x00 - * This was for future development since only 0xC0 is currently used + * This was for future development */ if (version == 3) { logger.trace("Response and Body Type: {}, {}", responseType, bodyType); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java similarity index 92% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java index d484a4e02cfa9..4cd6c6e012cbc 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilitiesResponse.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.handler; +package org.openhab.binding.mideaac.internal.handler.capabilities; import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java similarity index 98% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java index 9d95dbd0100ad..2e56221385d29 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityParser.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.handler; +package org.openhab.binding.mideaac.internal.handler.capabilities; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java similarity index 97% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java index b1ad15538023f..259179952cba5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CapabilityReaders.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java @@ -10,7 +10,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.handler; +package org.openhab.binding.mideaac.internal.handler.capabilities; import java.util.HashMap; import java.util.List; @@ -22,7 +22,7 @@ import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mideaac.internal.handler.CapabilityParser.CapabilityId; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser.CapabilityId; /** * The {@link CapabilityReaders} reads the raw capability message and diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java similarity index 93% rename from bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java rename to bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java index f8b5a8fb6d636..095a0a21e2609 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Reader.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.handler; +package org.openhab.binding.mideaac.internal.handler.capabilities; import java.util.function.Predicate; diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java similarity index 98% rename from bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java rename to bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java index 51cd2d706a17d..c294535334efc 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CapabilityParserTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java @@ -11,7 +11,7 @@ * SPDX-License-Identifier: EPL-2.0 */ -package org.openhab.binding.mideaac.internal.handler; +package org.openhab.binding.mideaac.internal.handler.capabilities; import static org.junit.jupiter.api.Assertions.*; @@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; -import org.openhab.binding.mideaac.internal.handler.CapabilityParser.CapabilityId; +import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser.CapabilityId; /** * The {@link CapabilityParser} parses the capability Response. From 69ab6e9b709109aa68701a5e8969d56777ca6adc Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 21 Apr 2025 18:05:50 -0400 Subject: [PATCH 25/44] Add default cloud Use the default cloud for faster AC discovery. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 60 +++++++++---------- .../internal/MideaACConfiguration.java | 6 +- .../binding/mideaac/internal/Utils.java | 36 ++++++++--- .../binding/mideaac/internal/cloud/Cloud.java | 2 +- .../mideaac/internal/cloud/CloudProvider.java | 2 +- .../mideaac/internal/security/Security.java | 13 ++-- .../resources/OH-INF/thing/thing-types.xml | 4 +- 7 files changed, 71 insertions(+), 52 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 00b4488065273..a0ea1dc1ea438 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -4,13 +4,13 @@ This binding integrates Air Conditioners that use the Midea protocol. Midea is a An AC device is likely supported if it uses one of the following Android apps or it's iOS equivalent. -| Application | Comment | Options | -|--:-------------------------------------------|--:------------------------------------|--------------| -| Midea Air (com.midea.aircondition.obm) | Full Support of key and token updates | Midea Air | -| NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | -| SmartHome/MSmartHome (com.midea.ai.overseas) | Full Support of key and token updates | MSmartHome | +| Application | Comment | Options | Default | +|----------------------------------------------|------------------------------------------|--------------|---------| +| Midea Air (com.midea.aircondition.obm) | Full Support of key and token updates | Midea Air | | +| NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | Yes | +| SmartHome/MSmartHome (com.midea.ai.overseas) | Note: Reports that this cloud is offline | MSmartHome | | -Note: The Air Conditioner must already be set-up on the WiFi network and have a fixed IP Address with one of the three apps listed above for full discovery and key and token updates. +Note: The Air Conditioner must already be set-up on the WiFi network and have a fixed IP Address to be discovered. ## Supported Things @@ -18,12 +18,10 @@ This binding supports one Thing type `ac`. ## Discovery -Once the Air Conditioner is on the network (WiFi active) activating an Inbox scan with this binding will send an IP broadcast message. -Every responding unit gets added to the Inbox. When adding each thing, the required parameters of ipAddress, ipPort, deviceId, pollingTime, -timeout, promptTone and version will be populated with either discovered values or the default settings. A V.2 device will be Online. -A V.3 device will require you to enter the cloud provider, token and key before becoming Online. The token and key can be discovered by entering -your email and password for your cloud account. The email and password are stored securely, but can be deleted after the token and key are entered, -unless key and token update is activated. +Once the Air Conditioner is on the network (WiFi active) activating the Inbox scan with this binding will send an IP broadcast message. +Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either discovered values +or the default settings. For a V.3 device, if the defaults did not get the token and key enter your cloud provider, email and password. +For security replace the default cloud provider, email and password with your account information. Required if keyTokenUpdate is used. ## Binding Configuration @@ -31,31 +29,31 @@ No binding configuration is required. ## Thing Configuration -| Parameter | Required ? | Comment | Default | -|--:------------|--:----------|--:----------------------------------------------------------------|---------| -| ipAddress | Yes | IP Address of the device. | | -| ipPort | Yes | IP port of the device | 6444 | -| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | -| cloud | Yes for V.3 | Cloud Provider name for email and password | | -| email | No | Email for cloud account chosen in Cloud Provider. | | -| password | No | Password for cloud account chosen in Cloud Provider. | | -| token | Yes for V.3 | Secret Token (length 128 HEX) | | -| key | Yes for V.3 | Secret Key (length 64 HEX) | | -| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | -| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | -| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | -| promptTone | Yes | "Ding" tone when command is received and executed. | False | -| version | Yes | Version 3 has token, key and cloud requirements. | 0 | +| Parameter | Required ? | Comment | Default | +|---------------|-------------|-------------------------------------------------------------------|---------------------------| +| ipAddress | Yes | IP Address of the device. | | +| ipPort | Yes | IP port of the device | 6444 | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | +| cloud | Yes for V.3 | Cloud Provider name for email and password | NetHome Plus | +| email | No | Email for cloud account chosen in Cloud Provider. | nethome+us@mailinator.com | +| password | No | Password for cloud account chosen in Cloud Provider. | password1 | +| token | Yes for V.3 | Secret Token (length 128 HEX) | | +| key | Yes for V.3 | Secret Key (length 64 HEX) | | +| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | +| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | +| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | +| promptTone | Yes | "Ding" tone when command is received and executed. | False | +| version | Yes | Version 3 has token, key and cloud requirements. | 0 | ## Channels Following channels are available: | Channel | Type | Description | Read only | Advanced | -|--:---------------------------|--:-----------------|--:-----------------------------------------------------------------------------------------------------|--:--------|--:-------| +|------------------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------| | power | Switch | Turn the AC on or off. | | | | target-temperature | Number:Temperature | Target temperature. | | | -| operational-mode | String | Operational mode: OFF (turns off), AUTO, COOL, DRY, HEAT, FAN ONLY | | | +| operational-mode | String | Operational mode: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | | | fan-speed | String | Fan speed: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | | swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | | eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | @@ -105,11 +103,11 @@ Switch temperature_unit "Fahrenheit or Celsius" { ch ### `demo.sitemap` Example ```java -sitemap midea label="Split AC MBR"{ +sitemap midea label="Split AC"{ Frame label="AC Unit" { Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]" Text item=indoor_temperature label="Indoor Temperature [%.1f °F]" - Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=63.0 maxValue=78 step=1.0 + Setpoint item=target_temperature label="Target Temperature [%.1f °F]" minValue=62.0 maxValue=86 step=1.0 Switch item=power label="Midea AC Power" Switch item=temperature_unit label= "Temp Unit" mappings=[ON="Fahrenheit", OFF="Celsius"] Selection item=fan_speed label="Midea AC Fan Speed" diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 84445d324afb9..3c4d1f5e4b064 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -41,17 +41,17 @@ public class MideaACConfiguration { /** * Cloud Account email */ - public String email = ""; + public String email = "nethome+us@mailinator.com"; /** * Cloud Account Password */ - public String password = ""; + public String password = "password1"; /** * Cloud Provider */ - public String cloud = ""; + public String cloud = "NetHome Plus"; /** * Token diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 122a2ecfab5f5..063e65a09a9d1 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -12,8 +12,11 @@ */ package org.openhab.binding.mideaac.internal; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Iterator; import java.util.Random; @@ -22,6 +25,8 @@ import org.eclipse.jdt.annotation.Nullable; import org.jose4j.base64url.Base64; import org.openhab.core.util.HexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.gson.JsonObject; @@ -34,6 +39,8 @@ */ @NonNullByDefault public class Utils { + private static Logger logger = LoggerFactory.getLogger(Utils.class); + static byte[] empty = new byte[0]; /** @@ -189,22 +196,37 @@ public static byte[] toIntTo6ByteArray(long i, ByteOrder order) { } /** - * String Builder + * String Builder for Hash * * @param json JSON object * @return string */ - public static String getQueryString(JsonObject json) { + public static String getQueryString(JsonObject json, boolean hash) { StringBuilder sb = new StringBuilder(); Iterator keys = json.keySet().stream().sorted().iterator(); while (keys.hasNext()) { @Nullable String key = keys.next(); - sb.append(key); - sb.append("="); - sb.append(json.get(key).getAsString()); - if (keys.hasNext()) { - sb.append("&"); // To allow for another argument. + String value = json.get(key).getAsString(); + + try { + String encodedKey = URLEncoder.encode(key, StandardCharsets.UTF_8.toString()); + String encodedValue = URLEncoder.encode(value, StandardCharsets.UTF_8.toString()); + + if (hash) { + // For hash generation, preserve + and @ characters in values. + encodedValue = encodedValue.replace("%2B", "+"); + encodedValue = encodedValue.replace("%40", "@"); + } + + // Append the encoded key and value to the query string + sb.append(encodedKey).append("=").append(encodedValue); + + if (keys.hasNext()) { + sb.append("&"); // To allow for another argument. + } + } catch (UnsupportedEncodingException e) { + logger.debug("Error encoding key and value", e); } } return sb.toString(); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index d0102511bc301..679a34aa429aa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -172,7 +172,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { if (!cloudProvider.proxied().isBlank()) { request.content(new StringContentProvider(json)); } else { - String body = Utils.getQueryString(data); + String body = Utils.getQueryString(data, false); logger.debug("Request body: {}", body); request.content(new StringContentProvider(body)); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index f3ce1ffa5ad22..33cfc2012988a 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -51,7 +51,7 @@ public static CloudProvider getCloudProvider(String name) { return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); // Reported in HA version that this cloud has been shutdown. - // The is possible v2 version of security down the road + // There is possible v2 version of security down the road case "MSmartHome": return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 16f0134614303..2959d31355171 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -501,13 +501,13 @@ private byte[] getRandomBytes(int size) { try { path = new URI(url).getPath(); - String query = Utils.getQueryString(payload); + String query = Utils.getQueryString(payload, true); String sign = path + query + cloudProvider.appkey(); logger.trace("sign: {}", sign); return Utils.bytesToHexLowercase(sha256((sign).getBytes(StandardCharsets.US_ASCII))); } catch (URISyntaxException e) { - logger.warn("Syntax error{}", e.getMessage()); + logger.warn("Error parsing URI '{}': {}", url, e.getMessage()); } return null; @@ -564,7 +564,7 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor * * @param loginId Login ID * @param password Login password - * @return string + * @return lower case byte string */ public @Nullable String encryptPassword(@Nullable String loginId, String password) { try { @@ -584,11 +584,11 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor } /** - * Encrypts password for cloud API using MD5 + * Encrypts password for cloud API using MD5 for proxied provider * * @param loginId Login ID * @param password Login password - * @return string + * @return lower case byte string */ public @Nullable String encryptIamPassword(@Nullable String loginId, String password) { try { @@ -598,9 +598,6 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor MessageDigest mdSecond = MessageDigest.getInstance("MD5"); mdSecond.update(Utils.bytesToHexLowercase(md.digest()).getBytes(StandardCharsets.US_ASCII)); - // if self._use_china_server: - // return mdSecond.hexdigest() - String loginHash = loginId + Utils.bytesToHexLowercase(mdSecond.digest()) + cloudProvider.appkey(); return Utils.bytesToHexLowercase(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))); } catch (NoSuchAlgorithmException e) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 5c15afdb3ebd9..c7f182e0fecba 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -64,17 +64,19 @@ true - + NetHome Plus email Email for cloud account chosen in Cloud Provider. + nethome+us@mailinator.com password Password for cloud account chosen in Cloud Provider. + password1 token From a02f6ce165d395a2e221df60d73934e0d52bf813 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 28 Apr 2025 16:13:23 -0400 Subject: [PATCH 26/44] General Cleanup and documentation Conformed capabilities (properties) to OH standards, added documentation. Changed the ReadMe to fully reflect default options. Changed the text configuration to require IP address. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 30 +++---- .../internal/MideaACBindingConstants.java | 53 +++++++++++ .../internal/MideaACConfiguration.java | 7 +- .../binding/mideaac/internal/cloud/Cloud.java | 26 ++---- .../connection/ConnectionManager.java | 8 +- .../discovery/MideaACDiscoveryService.java | 2 + .../internal/handler/MideaACHandler.java | 90 ++++++++++--------- .../capabilities/CapabilityParser.java | 8 +- .../capabilities/CapabilityReaders.java | 74 +++++++-------- .../internal/MideaACConfigurationTest.java | 15 +--- .../mideaac/internal/cloud/CloudTest.java | 12 ++- .../capabilities/CapabilityParserTest.java | 46 +++++----- 12 files changed, 205 insertions(+), 166 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index a0ea1dc1ea438..0729a35cdc41a 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -10,7 +10,7 @@ An AC device is likely supported if it uses one of the following Android apps or | NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | Yes | | SmartHome/MSmartHome (com.midea.ai.overseas) | Note: Reports that this cloud is offline | MSmartHome | | -Note: The Air Conditioner must already be set-up on the WiFi network and have a fixed IP Address to be discovered. +Note: The Air Conditioner must already be set-up on your WiFi network with a fixed IP Address to be discovered. ## Supported Things @@ -18,10 +18,10 @@ This binding supports one Thing type `ac`. ## Discovery -Once the Air Conditioner is on the network (WiFi active) activating the Inbox scan with this binding will send an IP broadcast message. -Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either discovered values -or the default settings. For a V.3 device, if the defaults did not get the token and key enter your cloud provider, email and password. -For security replace the default cloud provider, email and password with your account information. Required if keyTokenUpdate is used. +Once the Air Conditioner is on your network activating the Inbox scan with this binding will send an IP broadcast message. +Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either +discovered values or the default settings. For a V.3 device, in the unlikely event the defaults did not get the token and key, +enter your cloud provider, email and password. The thing properties will show supported AC functions. ## Binding Configuration @@ -37,8 +37,8 @@ No binding configuration is required. | cloud | Yes for V.3 | Cloud Provider name for email and password | NetHome Plus | | email | No | Email for cloud account chosen in Cloud Provider. | nethome+us@mailinator.com | | password | No | Password for cloud account chosen in Cloud Provider. | password1 | -| token | Yes for V.3 | Secret Token (length 128 HEX) | | -| key | Yes for V.3 | Secret Key (length 64 HEX) | | +| token | Yes for V.3 | Secret Token - Retrieved from Cloud | | +| key | Yes for V.3 | Secret Key - Retrieved from Cloud | | | pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | | keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | | timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | @@ -53,8 +53,8 @@ Following channels are available: |------------------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------| | power | Switch | Turn the AC on or off. | | | | target-temperature | Number:Temperature | Target temperature. | | | -| operational-mode | String | Operational mode: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | | -| fan-speed | String | Fan speed: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | +| operational-mode | String | Operational modes: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | | +| fan-speed | String | Fan speeds: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | | swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | | eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | | turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT mode only. | | | @@ -72,19 +72,19 @@ Following channels are available: ## Examples -### `demo.things` Example +### `demo.things` Examples ```java -Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="6444", deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] ``` -Option to use the built-in binding discovery of ipPort, deviceId, token and key. +Minimal IP Address Option to use the built-in defaults. ```java -Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort="", deviceId="", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="", key ="", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] +Thing mideaac:ac:mideaac "myAC" @ "myRoom" [ ipAddress="192.168.0.200"] ``` -### `demo.items` Example +### `demo.items` Examples ```java Switch power "Power" { channel="mideaac:ac:mideaac:power" } @@ -100,7 +100,7 @@ Switch sleep_function "Sleep function" { ch Switch temperature_unit "Fahrenheit or Celsius" { channel="mideaac:ac:mideaac:temperature-unit" } ``` -### `demo.sitemap` Example +### `demo.sitemap` Examples ```java sitemap midea label="Split AC"{ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index ec04d287edf26..67fafe9c9b471 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -88,7 +88,60 @@ public class MideaACBindingConstants { public static final String CONFIG_PROMPT_TONE = "promptTone"; public static final String CONFIG_VERSION = "version"; + // From LAN Discovery public static final String PROPERTY_SN = "sn"; public static final String PROPERTY_SSID = "ssid"; public static final String PROPERTY_TYPE = "type"; + + // Capabilities command + public static final String PROPERTY_ANION = "anion"; + public static final String PROPERTY_AUX_ELECTRIC_HEAT = "auxElectricHeat"; + public static final String PROPERTY_BREEZE_AWAY = "breezeAway"; + public static final String PROPERTY_BREEZE_CONTROL = "breezeControl"; + public static final String PROPERTY_BREEZELESS = "breezeless"; + public static final String PROPERTY_BUZZER = "buzzer"; + public static final String PROPERTY_DISPLAY_CONTROL = "displayControl"; + public static final String PROPERTY_ENERGY_STATS = "energyStats"; + public static final String PROPERTY_ENERGY_SETTING = "energySetting"; + public static final String PROPERTY_ENERGY_BCD = "energyBCD"; + public static final String PROPERTY_FAHRENHEIT = "fahrenheit"; + public static final String PROPERTY_FAN_SPEED_CONTROL_LOW = "fanLow"; + public static final String PROPERTY_FAN_SPEED_CONTROL_MEDIUM = "fanMedium"; + public static final String PROPERTY_FAN_SPEED_CONTROL_HIGH = "fanHigh"; + public static final String PROPERTY_FAN_SPEED_CONTROL_AUTO = "fanAuto"; + public static final String PROPERTY_FAN_SPEED_CONTROL_SILENT = "fanSilent"; + public static final String PROPERTY_FAN_SPEED_CONTROL_CUSTOM = "fanCustom"; + public static final String PROPERTY_FILTER_REMIND_NOTICE = "filterNotice"; + public static final String PROPERTY_FILTER_REMIND_CLEAN = "filterClean"; + public static final String PROPERTY_HUMIDITY_AUTO_SET = "humidityAutoSet"; + public static final String PROPERTY_HUMIDITY_MANUAL_SET = "humidityManualSet"; + public static final String PROPERTY_MODES_AUTO = "modeAuto"; + public static final String PROPERTY_MODES_AUX = "modeAux"; + public static final String PROPERTY_MODES_AUX_HEAT = "modeAuxHeat"; + public static final String PROPERTY_MODES_COOL = "modeCool"; + public static final String PROPERTY_MODES_DRY = "modeDry"; + public static final String PROPERTY_MODES_FAN_ONLY = "modeFanOnly"; + public static final String PROPERTY_MODES_HEAT = "modeHeat"; + public static final String PROPERTY_PRESET_ECO = "ecoCool"; + public static final String PROPERTY_PRESET_FREEZE_PROTECTION = "freezeProtection"; + public static final String PROPERTY_PRESET_IECO = "ieco"; + public static final String PROPERTY_PRESET_TURBO_COOL = "turboCool"; + public static final String PROPERTY_PRESET_TURBO_HEAT = "turboHeat"; + public static final String PROPERTY_RATE_SELECT = "rateSelect5Level"; + public static final String PROPERTY_SELF_CLEAN = "selfClean"; + public static final String PROPERTY_SMART_EYE = "smartEye"; + public static final String PROPERTY_SWING_LR_ANGLE = "swingHorizontalAngle"; + public static final String PROPERTY_SWING_UD_ANGLE = "swingVerticalAngle"; + public static final String PROPERTY_SWING_MODES_HORIZONTAL = "swingHorizontal"; + public static final String PROPERTY_SWING_MODES_VERTICAL = "swingVertical"; + public static final String PROPERTY_TEMPERATURES_MIN_DEFAULT = "minTargetTemperature"; + public static final String PROPERTY_TEMPERATURES_MAX_DEFAULT = "maxTargetTemperature"; + public static final String PROPERTY_TEMPERATURES_COOL_MIN = "coolMinTemperature"; + public static final String PROPERTY_TEMPERATURES_COOL_MAX = "coolMaxTemperature"; + public static final String PROPERTY_TEMPERATURES_AUTO_MIN = "autoMinTemperature"; + public static final String PROPERTY_TEMPERATURES_AUTO_MAX = "autoMaxTemperature"; + public static final String PROPERTY_TEMPERATURES_HEAT_MIN = "heatMinTemperature"; + public static final String PROPERTY_TEMPERATURES_HEAT_MAX = "heatMaxTemperature"; + public static final String PROPERTY_WIND_OFF_ME = "windOffMe"; + public static final String PROPERTY_WIND_ON_ME = "windOnMe"; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 3c4d1f5e4b064..073078b128289 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -99,13 +99,12 @@ public boolean isValid() { } /** - * Check during initialization if discovery is needed + * Check during initialization if discovery is possible. This needs a valid IP * * @return true(discovery needed), false (not needed) */ - public boolean isDiscoveryNeeded() { - return ("0".equals(deviceId) || deviceId.isBlank() || ipPort <= 0 || ipAddress.isBlank() - || !Utils.validateIP(ipAddress) || version <= 1); + public boolean isDiscoveryPossible() { + return (Utils.validateIP(ipAddress)); } /** diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index 679a34aa429aa..d586cefed7fe3 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -41,11 +41,11 @@ /** * The {@link Cloud} class connects to the Cloud Provider - * with user supplied information to retrieve the Security + * with user supplied information (or defaults) to retrieve the Security * Token and Key. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - JavaDoc + * @author Bob Eckhoff - JavaDoc and changed getQueryString for special characters */ @NonNullByDefault public class Cloud { @@ -57,21 +57,12 @@ public class Cloud { private HttpClient httpClient; - /** - * Sets Http Client - * - * @param httpClient Http Client - */ - public void setHttpClient(HttpClient httpClient) { - this.httpClient = httpClient; - } - private String errMsg = ""; private @Nullable String accessToken = ""; private String loginAccount; - private String password = ""; + private String password; private CloudProvider cloudProvider; private Security security; @@ -84,13 +75,14 @@ public void setHttpClient(HttpClient httpClient) { * @param email email * @param password password * @param cloudProvider Cloud Provider + * @param httpClient Used to send posts to the cloud */ - public Cloud(String email, String password, CloudProvider cloudProvider) { + public Cloud(String email, String password, CloudProvider cloudProvider, HttpClient httpClient) { this.loginAccount = email; this.password = password; this.cloudProvider = cloudProvider; this.security = new Security(cloudProvider); - this.httpClient = new HttpClient(); + this.httpClient = httpClient; logger.debug("Cloud provider: {}", cloudProvider.name()); } @@ -227,10 +219,10 @@ public Cloud(String email, String password, CloudProvider cloudProvider) { } /** - * First gets the loginId using your email, then gets the session + * First gets the loginId from the Cloud using the email, then gets the session * Id with the email and encypted password (using the LoginId). Then * gets the token and key. If loginId and sessionId exist from an earlier - * attempt, it goes directly to getting the token and key + * attempt, it goes directly to getting the token and key. * * @return true or false */ @@ -301,7 +293,7 @@ public boolean login() { /** * Gets token and key with the device Id modified to udpid - * after SessionId (non-proxied) accessToken are established + * after SessionId (non-proxied) accessToken is established * * @param deviceId The discovered Device Id * @return token and key diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index ac6a92c8d2d70..29dabc044ebf2 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -108,10 +108,9 @@ public Response getLastResponse() { } /** - * After checking if the key and token need to be updated (Default = 0 Never) * The socket is established with the writer and inputStream (for reading responses) - * The device is considered connected. V2 devices will proceed to send the poll or the - * set command. V3 devices will proceed to authenticate + * V2 devices will proceed to send the poll or the set command. + * V3 devices will proceed to authenticate */ public synchronized void connect() throws MideaConnectionException, MideaAuthenticationException, SocketTimeoutException, IOException { @@ -121,6 +120,7 @@ public synchronized void connect() int retrySocket = 0; // If resending command add delay to avoid connection rejection + // Suspect that the AC device needs a few seconds to clear. if (!resend) { try { Thread.sleep(5000); @@ -130,7 +130,7 @@ public synchronized void connect() } // Open socket - // RetrySocket addresses the Timeout exception, others exceptions end the thread + // RetrySocket addresses the Timeout exception only, others exceptions end the thread. Same as HA python version while (retrySocket < maxTries) { try { socket = new Socket(); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 994a911924c80..aae365050fbab 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -93,6 +93,7 @@ protected void stopScan() { /** * Performs the actual discovery of Midea AC devices (things). + * with unknown IP address. */ private void discoverThings() { try { @@ -127,6 +128,7 @@ private void discoverThings() { /** * Performs the actual discovery of a specific Midea AC device (thing) + * with a known IP address. * * @param ipAddress IP Address * @param discoveryHandler Discovery Handler diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 0586cd0b13bc2..7527819c4acb7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -74,7 +74,6 @@ */ @NonNullByDefault public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler, Callback { - private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class); private final boolean imperialUnits; private final HttpClient httpClient; @@ -175,19 +174,27 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * Initialize is called on first pass or when a device parameter is changed - * The basic check is if the information from Discovery (or the user update) - * is valid. Because V2 devices do not require a cloud provider (or token/key) - * The first check is for the IP, port and deviceID. The second part - * checks the security configuration if required (V3 device). + * To initialize an AC Thing, first check if discovery on your LAN has + * been completed. In the discovery process the IP address, IP port, device ID + * and version are established. Next for V.3 devices, if the token and key are not + * known, the cloud needs to be contacted to get the token and key using either + * the default NetHome Plus cloud or your own cloud account with your password and + * email. LAN discovery DOES NOT include the token key retrieval needed to communicate + * with the AC. V2 devices bypass the cloud connection step because they use a simplier + * hard-coded encryption. Next the Connection Manager is established. Then a command + * is formed and sent to retrieve the AC capabilities if they have not been + * discovered. Capabilities are not returned in the initial LAN Discovery. + * Lastly the routine polling and token key update frequency are set. + * */ @Override public void initialize() { config = getConfigAs(MideaACConfiguration.class); + // Check for valid discovery configurations and discover again if not if (!config.isValid()) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration not valid"); - if (config.isDiscoveryNeeded()) { + if (config.isDiscoveryPossible()) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration missing, discovery needed. Discovering..."); MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); @@ -196,20 +203,20 @@ public void initialize() { discoveryService.discoverThing(config.ipAddress, this); return; } catch (Exception e) { - logger.debug("Discovery failure for {}: {}", thing.getUID(), e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Discovery failure. Check configuration."); + "Discovery failure. Check IP Address."); return; } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid MideaAC config. Check configuration."); + "Invalid MideaAC config. Check IP Address."); return; } } else { - logger.debug("Non-security Configuration valid for {}", thing.getUID()); + logger.debug("Valid discovery for {}", thing.getUID()); } + // Check for valid token and key and/or contact cloud account to get them if (config.version == 3 && !config.isV3ConfigValid()) { if (config.isTokenKeyObtainable()) { CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); @@ -221,34 +228,29 @@ public void initialize() { return; } } else { - logger.debug("Security Configuration (V3 Device) valid for {}", thing.getUID()); + logger.debug("Valid security for V.3 device {}", thing.getUID()); } updateStatus(ThingStatus.UNKNOWN); + // Initialize connectionManager for communication with the AC connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); - // Initialize connectionManager - connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, - config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, - config.deviceId, config.version, config.promptTone); - - // This checks for one of the added default properties and means the command - // Has already run and doesn't need to be run again - if (!properties.containsKey("mode_fan_only")) { + // Form and send capabilities command if not already present in properties + if (!properties.containsKey("modeFanOnly")) { try { - // Send configuration command and get response CommandSet initializationCommand = new CommandSet(); initializationCommand.getCapabilities(); - // Send the command using the connection manager connectionManager.sendCommand(initializationCommand, this); - // 'this' is passed as the callback because MideaACHandler implements Callback } catch (Exception e) { - logger.error("Error during capabilities initialization: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "AC capabilities not returned"); } } + + // Establish routine polling per configuration or defaults if (scheduledTask == null) { scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); @@ -256,11 +258,14 @@ public void initialize() { logger.debug("Scheduler already running"); } + // Establish token key update frequency, if not disabled if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) { scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, config.keyTokenUpdate, TimeUnit.DAYS); logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); + } else { + logger.debug("Token Key Scheduler already running"); } } @@ -330,9 +335,9 @@ public void updateChannels(Response response) { updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); } + // Handle capabilities responses @Override public void updateChannels(CapabilitiesResponse capabilitiesResponse) { - // Handle capabilities responses CapabilityParser parser = new CapabilityParser(); parser.parse(capabilitiesResponse.getRawData()); @@ -351,32 +356,32 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { }); // Default to false if "display_control" is missing from DISPLAY_CONTROL response - if (!properties.containsKey("display_control")) { - properties.put("display_control", "false - default"); + if (!properties.containsKey("displayControl")) { + properties.put("displayControl", "false - default"); } // Default to true if "fan_only" is missing from MODE response - if (!properties.containsKey("mode_fan_only")) { - properties.put("mode_fan_only", "true - default"); + if (!properties.containsKey("modeFanOnly")) { + properties.put("modeFanOnly", "true - default"); } // Defaults if FAN_SPEED_CONTROL is missing from response - if (!properties.containsKey("fan_low")) { - properties.put("fan_low", "true - default"); + if (!properties.containsKey("fanLow")) { + properties.put("fanLow", "true - default"); } - if (!properties.containsKey("fan_medium")) { - properties.put("fan_medium", "true - default"); + if (!properties.containsKey("fanMedium")) { + properties.put("fanMedium", "true - default"); } - if (!properties.containsKey("fan_high")) { - properties.put("fan_high", "true - default"); + if (!properties.containsKey("fanHigh")) { + properties.put("fanHigh", "true - default"); } - if (!properties.containsKey("fan_auto")) { - properties.put("fan_auto", "true - default"); + if (!properties.containsKey("fanAuto")) { + properties.put("fanAuto", "true - default"); } // Defaults if no TEMPERATURES were in response - if (!properties.containsKey("cool_min_temperature")) { - properties.put("min_target_temperature", "17°C / 62°F default"); + if (!properties.containsKey("coolMinTemperature")) { + properties.put("minTargetTemperature", "17°C / 62°F default"); } - if (!properties.containsKey("heat_max_temperature")) { - properties.put("max_target_temperature", "30°C / 86°F default"); + if (!properties.containsKey("heatMaxTemperature")) { + properties.put("maxTargetTemperature", "30°C / 86°F default"); } updateProperties(properties); @@ -428,8 +433,7 @@ public void getTokenKeyCloud(CloudProvider cloudProvider) { stopScheduler(); } logger.debug("Retrieving Token and/or Key from cloud"); - Cloud cloud = new Cloud(config.email, config.password, cloudProvider); - cloud.setHttpClient(httpClient); + Cloud cloud = new Cloud(config.email, config.password, cloudProvider, httpClient); if (cloud.login()) { TokenKey tk = cloud.getToken(config.deviceId); Configuration configuration = editConfiguration(); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java index 2e56221385d29..ea0e4855aac19 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java @@ -78,10 +78,10 @@ public void parse(byte[] payload) { } else if (capabilityId == CapabilityId.TEMPERATURES) { if (size >= 6) { numericCapabilities.put(capabilityId, - Map.of("cool_min_temperature", payload[offset + 3] * 0.5, "cool_max_temperature", - payload[offset + 4] * 0.5, "auto_min_temperature", payload[offset + 5] * 0.5, - "auto_max_temperature", payload[offset + 6] * 0.5, "heat_min_temperature", - payload[offset + 7] * 0.5, "heat_max_temperature", payload[offset + 8] * 0.5)); + Map.of("coolMinTemperature", payload[offset + 3] * 0.5, "coolMaxTemperature", + payload[offset + 4] * 0.5, "autoMinTemperature", payload[offset + 5] * 0.5, + "autoMaxTemperature", payload[offset + 6] * 0.5, "heatMinTemperature", + payload[offset + 7] * 0.5, "heatMaxTemperature", payload[offset + 8] * 0.5)); } } else if (capabilityId == CapabilityId._UNKNOWN) { logger.debug("Ignored unknown capability ID: {}, Size: {}", rawId, size); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java index 259179952cba5..546924eeb1447 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java @@ -40,68 +40,68 @@ public class CapabilityReaders { // Add READERS for each capability - Not all are supported READERS.put(CapabilityId.ANION, List.of(new Reader("anion", getValue.apply(1)))); - READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("aux_electric_heat", getValue.apply(1)))); - READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breeze_away", getValue.apply(1)))); - READERS.put(CapabilityId.BREEZE_CONTROL, List.of(new Reader("breeze_control", getValue.apply(1)))); + READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("auxElectricHeat", getValue.apply(1)))); + READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breezeAway", getValue.apply(1)))); + READERS.put(CapabilityId.BREEZE_CONTROL, List.of(new Reader("breezeControl", getValue.apply(1)))); READERS.put(CapabilityId.BREEZELESS, List.of(new Reader("breezeless", getValue.apply(1)))); READERS.put(CapabilityId.BUZZER, List.of(new Reader("buzzer", getValue.apply(1)))); READERS.put(CapabilityId.DISPLAY_CONTROL, - List.of(new Reader("display_control", (value) -> List.of(1, 2, 100).contains(value)))); + List.of(new Reader("displayControl", (value) -> List.of(1, 2, 100).contains(value)))); READERS.put(CapabilityId.ENERGY, - List.of(new Reader("energy_stats", (value) -> List.of(2, 3, 4, 5).contains(value)), - new Reader("energy_setting", (value) -> List.of(3, 5).contains(value)), - new Reader("energy_bcd", (value) -> List.of(2, 3).contains(value)))); + List.of(new Reader("energyStats", (value) -> List.of(2, 3, 4, 5).contains(value)), + new Reader("energySetting", (value) -> List.of(3, 5).contains(value)), + new Reader("energyBCD", (value) -> List.of(2, 3).contains(value)))); READERS.put(CapabilityId.FAHRENHEIT, List.of(new Reader("fahrenheit", getValue.apply(0)))); READERS.put(CapabilityId.FAN_SPEED_CONTROL, - List.of(new Reader("fan_silent", getValue.apply(6)), - new Reader("fan_low", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), - new Reader("fan_medium", (value) -> List.of(5, 6, 7).contains(value)), - new Reader("fan_high", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), - new Reader("fan_auto", (value) -> List.of(4, 5, 6).contains(value)), - new Reader("fan_custom", getValue.apply(1)))); + List.of(new Reader("fanSilent", getValue.apply(6)), + new Reader("fanLow", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), + new Reader("fanMedium", (value) -> List.of(5, 6, 7).contains(value)), + new Reader("fanHigh", (value) -> List.of(3, 4, 5, 6, 7).contains(value)), + new Reader("fanAuto", (value) -> List.of(4, 5, 6).contains(value)), + new Reader("fanCustom", getValue.apply(1)))); READERS.put(CapabilityId.FILTER_REMIND, - List.of(new Reader("filter_notice", (value) -> List.of(1, 2, 4).contains(value)), - new Reader("filter_clean", (value) -> List.of(3, 4).contains(value)))); + List.of(new Reader("filterNotice", (value) -> List.of(1, 2, 4).contains(value)), + new Reader("filterClean", (value) -> List.of(3, 4).contains(value)))); READERS.put(CapabilityId.HUMIDITY, - List.of(new Reader("humidity_auto_set", (value) -> List.of(1, 2).contains(value)), - new Reader("humidity_manual_set", (value) -> List.of(2, 3).contains(value)))); + List.of(new Reader("humidityAutoSet", (value) -> List.of(1, 2).contains(value)), + new Reader("humidityManualSet", (value) -> List.of(2, 3).contains(value)))); READERS.put(CapabilityId.MODES, - List.of(new Reader("mode_heat", (value) -> List.of(1, 2, 4, 6, 7, 9, 10, 11, 12, 13).contains(value)), - new Reader("mode_cool", (value) -> !List.of(2, 10, 12).contains(value)), - new Reader("mode_dry", (value) -> List.of(0, 1, 5, 6, 9, 11, 13).contains(value)), - new Reader("mode_auto", (value) -> List.of(0, 1, 2, 7, 8, 9, 13).contains(value)), - new Reader("mode_aux_heat", getValue.apply(9)), - new Reader("mode_aux", (value) -> List.of(9, 10, 11, 13).contains(value)))); - - READERS.put(CapabilityId.PRESET_ECO, List.of(new Reader("eco_cool", (value) -> List.of(1, 2).contains(value)))); - READERS.put(CapabilityId.PRESET_FREEZE_PROTECTION, List.of(new Reader("freeze_protection", getValue.apply(1)))); + List.of(new Reader("modeHeat", (value) -> List.of(1, 2, 4, 6, 7, 9, 10, 11, 12, 13).contains(value)), + new Reader("modeCool", (value) -> !List.of(2, 10, 12).contains(value)), + new Reader("modeDry", (value) -> List.of(0, 1, 5, 6, 9, 11, 13).contains(value)), + new Reader("modeAuto", (value) -> List.of(0, 1, 2, 7, 8, 9, 13).contains(value)), + new Reader("modeAuxHeat", getValue.apply(9)), + new Reader("modeAux", (value) -> List.of(9, 10, 11, 13).contains(value)))); + + READERS.put(CapabilityId.PRESET_ECO, List.of(new Reader("ecoCool", (value) -> List.of(1, 2).contains(value)))); + READERS.put(CapabilityId.PRESET_FREEZE_PROTECTION, List.of(new Reader("freezeProtection", getValue.apply(1)))); READERS.put(CapabilityId.PRESET_IECO, List.of(new Reader("ieco", getValue.apply(1)))); READERS.put(CapabilityId.PRESET_TURBO, - List.of(new Reader("turbo_heat", (value) -> List.of(1, 3).contains(value)), - new Reader("turbo_cool", (value) -> value < 2))); + List.of(new Reader("turboHeat", (value) -> List.of(1, 3).contains(value)), + new Reader("turboCool", (value) -> value < 2))); READERS.put(CapabilityId.RATE_SELECT, List.of(new Reader("rate_select_2_level", getValue.apply(1)), - new Reader("rate_select_5_level", (value) -> List.of(2, 3).contains(value)))); + new Reader("rateSelect5Level", (value) -> List.of(2, 3).contains(value)))); - READERS.put(CapabilityId.SELF_CLEAN, List.of(new Reader("self_clean", getValue.apply(1)))); - READERS.put(CapabilityId.SMART_EYE, List.of(new Reader("smart_eye", getValue.apply(1)))); - READERS.put(CapabilityId.SWING_LR_ANGLE, List.of(new Reader("swing_horizontal_angle", getValue.apply(1)))); - READERS.put(CapabilityId.SWING_UD_ANGLE, List.of(new Reader("swing_vertical_angle", getValue.apply(1)))); + READERS.put(CapabilityId.SELF_CLEAN, List.of(new Reader("selfClean", getValue.apply(1)))); + READERS.put(CapabilityId.SMART_EYE, List.of(new Reader("smartEye", getValue.apply(1)))); + READERS.put(CapabilityId.SWING_LR_ANGLE, List.of(new Reader("swingHorizontalAngle", getValue.apply(1)))); + READERS.put(CapabilityId.SWING_UD_ANGLE, List.of(new Reader("swingVerticalAngle", getValue.apply(1)))); READERS.put(CapabilityId.SWING_MODES, - List.of(new Reader("swing_horizontal", (value) -> List.of(1, 3).contains(value)), - new Reader("swing_vertical", (value) -> value < 2))); + List.of(new Reader("swingHorizontal", (value) -> List.of(1, 3).contains(value)), + new Reader("swingVertical", (value) -> value < 2))); - READERS.put(CapabilityId.WIND_OFF_ME, List.of(new Reader("wind_off_me", getValue.apply(1)))); - READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("wind_on_me", getValue.apply(1)))); + READERS.put(CapabilityId.WIND_OFF_ME, List.of(new Reader("windOffMe", getValue.apply(1)))); + READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("windOnMe", getValue.apply(1)))); } public static boolean hasReader(CapabilityId id) { diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index 97ebe323d265b..2a5b14cb810a4 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -40,7 +40,7 @@ public void testValidConfigs() { config.deviceId = "1234567890"; config.version = 3; assertTrue(config.isValid()); - assertFalse(config.isDiscoveryNeeded()); + assertTrue(config.isDiscoveryPossible()); } /** @@ -53,7 +53,7 @@ public void testnonValidConfigs() { config.deviceId = "1234567890"; config.version = 3; assertFalse(config.isValid()); - assertTrue(config.isDiscoveryNeeded()); + assertTrue(config.isDiscoveryPossible()); } /** @@ -110,16 +110,7 @@ public void testBadIpConfigs() { config.deviceId = "1234567890"; config.version = 3; assertTrue(config.isValid()); - assertTrue(config.isDiscoveryNeeded()); - } - - /** - * Test to return cloud provider - */ - @Test - public void testCloudProvider() { - config.cloud = "NetHome Plus"; - assertEquals(config.cloud, "NetHome Plus"); + assertFalse(config.isDiscoveryPossible()); } /** diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java index bcc2f5a71db9c..4db9dcf66d6c8 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java @@ -68,8 +68,7 @@ public void testLogin() throws Exception { "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); // Inject the mocked HttpClient into the Cloud class - Cloud cloud = new Cloud("email", "password", provider); - cloud.setHttpClient(mockHttpClient); + Cloud cloud = new Cloud("email", "password", provider, mockHttpClient); // Set loginId using reflection so that the getLoginId() check doesn't trigger Field loginIdField = Cloud.class.getDeclaredField("loginId"); @@ -123,8 +122,7 @@ public void testLoginproxy() throws Exception { "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); // Inject the mocked HttpClient into the Cloud class - Cloud cloud = new Cloud("email", "password", provider); - cloud.setHttpClient(mockHttpClient); + Cloud cloud = new Cloud("email", "password", provider, mockHttpClient); // Set loginId using reflection so that the getLoginId() check doesn't trigger Field loginIdField = Cloud.class.getDeclaredField("loginId"); @@ -150,7 +148,8 @@ public void testLoginWithSessionId() throws Exception { "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); // Create the Cloud class - Cloud cloud = new Cloud("email", "password", provider); + HttpClient mockHttpClient = mock(HttpClient.class); + Cloud cloud = new Cloud("email", "password", provider, mockHttpClient); // Set loginId using reflection so that the getLoginId() check doesn't trigger Field loginIdField = Cloud.class.getDeclaredField("loginId"); @@ -199,8 +198,7 @@ public void testGetLoginId() throws Exception { "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); // Inject the mocked HttpClient into the Cloud class - Cloud cloud = new Cloud("email", "password", provider); - cloud.setHttpClient(mockHttpClient); + Cloud cloud = new Cloud("email", "password", provider, mockHttpClient); // Execute the getLoginId method boolean getLogin = cloud.getLoginId(); diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java index c294535334efc..a2619b562e3bf 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java @@ -47,20 +47,20 @@ void testParseWithValidPayload() { // Assert: Check the parsed results Map> capabilities = parser.getCapabilities(); - assertNotNull(capabilities, "Capabilities map should not be null"); + assertNotNull(capabilities); // Check individual capabilities - assertTrue(capabilities.containsKey(CapabilityId.MODES), "Should contain MODES capability"); - Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heat_mode")) - .ifPresent(value -> assertEquals(true, value, "MODES - heat_mode should be true")); + assertTrue(capabilities.containsKey(CapabilityId.MODES)); + Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heatMode")) + .ifPresent(value -> assertEquals(true, value)); - assertTrue(capabilities.containsKey(CapabilityId.ENERGY), "Should contain ENERGY capability"); - Optional.ofNullable(capabilities.get(CapabilityId.ENERGY)).map(modes -> modes.get("energy_stats")) - .ifPresent(value -> assertEquals(true, value, "ENERGY - energy_stats should be true")); + assertTrue(capabilities.containsKey(CapabilityId.ENERGY)); + Optional.ofNullable(capabilities.get(CapabilityId.ENERGY)).map(modes -> modes.get("energyStats")) + .ifPresent(value -> assertEquals(true, value)); - assertTrue(capabilities.containsKey(CapabilityId.PRESET_TURBO), "Should contain PRESET_TURBO capability"); - Optional.ofNullable(capabilities.get(CapabilityId.PRESET_TURBO)).map(modes -> modes.get("turbo_heat")) - .ifPresent(value -> assertEquals(true, value, "PRESET_TURBO - turbo_heat should be true")); + assertTrue(capabilities.containsKey(CapabilityId.PRESET_TURBO)); + Optional.ofNullable(capabilities.get(CapabilityId.PRESET_TURBO)).map(modes -> modes.get("turboHeat")) + .ifPresent(value -> assertEquals(true, value)); } @Test @@ -74,7 +74,7 @@ void testParseWithEmptyPayload() { parser.parse(payload); // Assert: Ensure no capabilities are parsed - assertTrue(parser.getCapabilities().isEmpty(), "Capabilities map should be empty for an empty payload"); + assertTrue(parser.getCapabilities().isEmpty()); } @Test @@ -91,7 +91,7 @@ void testParseWithUnknownCapability() { // Assert: Ensure unknown capability is ignored Map> capabilities = parser.getCapabilities(); - assertTrue(capabilities.isEmpty(), "Capabilities map should not contain unknown capabilities"); + assertTrue(capabilities.isEmpty()); } @Test @@ -107,7 +107,7 @@ void testParseWithInvalidSize() { parser.parse(payload); // Assert: Ensure no capabilities are parsed - assertTrue(parser.getCapabilities().isEmpty(), "Capabilities map should be empty for invalid size"); + assertTrue(parser.getCapabilities().isEmpty()); } @Test @@ -131,19 +131,19 @@ void testParseWithTrailingCRC() { // Assert: Verify capabilities are parsed correctly Map> capabilities = parser.getCapabilities(); - assertNotNull(capabilities, "Capabilities map should not be null"); + assertNotNull(capabilities); // Verify specific capabilities - assertTrue(capabilities.containsKey(CapabilityId.PRESET_ECO), "Should contain PRESET_ECO capability"); + assertTrue(capabilities.containsKey(CapabilityId.PRESET_ECO)); Optional.ofNullable(capabilities.get(CapabilityId.PRESET_ECO)).map(modes -> modes.get("eco")) - .ifPresent(value -> assertEquals(true, value, "PRESET_ECO - eco should be true")); + .ifPresent(value -> assertEquals(true, value)); - assertTrue(capabilities.containsKey(CapabilityId.MODES), "Should contain MODES capability"); - Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heat_mode")) - .ifPresent(value -> assertEquals(true, value, "MODES - heat_mode should be true")); + assertTrue(capabilities.containsKey(CapabilityId.MODES)); + Optional.ofNullable(capabilities.get(CapabilityId.MODES)).map(modes -> modes.get("heatMode")) + .ifPresent(value -> assertEquals(true, value)); // Ensure CRC did not cause parsing issues - assertFalse(parser.hasAdditionalCapabilities(), "No additional capabilities"); + assertFalse(parser.hasAdditionalCapabilities()); } @Test @@ -168,11 +168,11 @@ void testParseWithtemperature() { // Assert: Verify capabilities are parsed correctly Map> numericCapabilities = parser.getNumericCapabilities(); - assertNotNull(numericCapabilities, "Capabilities map should not be null"); + assertNotNull(numericCapabilities); Optional.ofNullable(numericCapabilities.get(CapabilityId.TEMPERATURES)) - .map(modes -> modes.get("cool_min_temperature")).ifPresent(value -> assertEquals(16.0, value)); + .map(modes -> modes.get("coolMinTemperature")).ifPresent(value -> assertEquals(16.0, value)); Optional.ofNullable(numericCapabilities.get(CapabilityId.TEMPERATURES)) - .map(modes -> modes.get("heat_max_temperature")).ifPresent(value -> assertEquals(30.0, value)); + .map(modes -> modes.get("heatMaxTemperature")).ifPresent(value -> assertEquals(30.0, value)); } } From 4303e3b86f4e7694cf0436f66137a846fc6c5601 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 29 Apr 2025 11:03:58 -0400 Subject: [PATCH 27/44] Document clarifications General review for document clarifications Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 2 +- .../internal/MideaACBindingConstants.java | 6 +- .../internal/MideaACConfiguration.java | 2 +- .../binding/mideaac/internal/Utils.java | 2 +- .../binding/mideaac/internal/cloud/Cloud.java | 2 +- .../mideaac/internal/cloud/CloudProvider.java | 5 +- .../internal/connection/CommandHelper.java | 2 +- .../connection/ConnectionManager.java | 22 ++++- .../MideaAuthenticationException.java | 3 +- .../exception/MideaConnectionException.java | 3 +- .../internal/discovery/Connection.java | 82 ------------------- .../discovery/MideaACDiscoveryService.java | 22 +++-- .../mideaac/internal/handler/CommandSet.java | 2 +- .../capabilities/CapabilitiesResponse.java | 5 ++ .../capabilities/CapabilityParser.java | 11 +++ .../capabilities/CapabilityReaders.java | 13 ++- .../internal/handler/capabilities/Reader.java | 8 +- .../MideaACDiscoveryServiceTest.java | 2 +- 18 files changed, 87 insertions(+), 107 deletions(-) delete mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 0729a35cdc41a..13aa54b34207d 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -19,7 +19,7 @@ This binding supports one Thing type `ac`. ## Discovery Once the Air Conditioner is on your network activating the Inbox scan with this binding will send an IP broadcast message. -Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either +Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either discovered values or the default settings. For a V.3 device, in the unlikely event the defaults did not get the token and key, enter your cloud provider, email and password. The thing properties will show supported AC functions. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index 67fafe9c9b471..acc02cd00aed7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -27,7 +27,7 @@ * used across the whole binding. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - OH naming conventions + * @author Bob Eckhoff - OH naming conventions and capability properties */ @NonNullByDefault public class MideaACBindingConstants { @@ -88,12 +88,12 @@ public class MideaACBindingConstants { public static final String CONFIG_PROMPT_TONE = "promptTone"; public static final String CONFIG_VERSION = "version"; - // From LAN Discovery + // Properties from LAN Discovery public static final String PROPERTY_SN = "sn"; public static final String PROPERTY_SSID = "ssid"; public static final String PROPERTY_TYPE = "type"; - // Capabilities command + // Capabilities properties public static final String PROPERTY_ANION = "anion"; public static final String PROPERTY_AUX_ELECTRIC_HEAT = "auxElectricHeat"; public static final String PROPERTY_BREEZE_AWAY = "breezeAway"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 073078b128289..5c30cc38ece51 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -18,7 +18,7 @@ * The {@link MideaACConfiguration} class contains fields mapping thing configuration parameters. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - OH addons changes + * @author Bob Eckhoff - OH addons changes, modified checks and defaults */ @NonNullByDefault public class MideaACConfiguration { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 063e65a09a9d1..335e3536c163e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -35,7 +35,7 @@ * which are used across the whole binding. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - JavaDoc + * @author Bob Eckhoff - JavaDoc, reversed array and refined query String */ @NonNullByDefault public class Utils { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index d586cefed7fe3..98d6d3bf1bb85 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -320,7 +320,7 @@ public TokenKey getToken(String deviceId) { /** * Gets the login ID from your email address * - * @return loginId (not your email) + * @return true or false */ public boolean getLoginId() { JsonObject args = new JsonObject(); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index 33cfc2012988a..b6f1cd311a76b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -28,7 +28,7 @@ * @param proxied proxy - MSmarthome only * * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - JavaDoc and conversion to record + * @author Bob Eckhoff - JavaDoc and conversion to record, NetHome Plus as default */ @NonNullByDefault public record CloudProvider(String name, String appkey, String appid, String apiurl, String signkey, String iotkey, @@ -51,12 +51,13 @@ public static CloudProvider getCloudProvider(String name) { return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); // Reported in HA version that this cloud has been shutdown. - // There is possible v2 version of security down the road + // There is a possible v2 version of security down the road case "MSmartHome": return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "meicloud", "PROD_VnoClJI9aikS8dyy", "v5"); } + // Blank is okay for version 2 as a hard coded signkey is used for all devices return new CloudProvider("", "", "", "", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 9f0f425cac7ad..d79e96f711383 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -125,7 +125,7 @@ private static float limitTargetTemperatureToRange(float temperatureInCelsius) { * Device only uses Celsius in 0.5 degree increments * Fahrenheit is rounded to fit (example * setting to 64 F is 18 C but will result in 64.4 F display in OH) - * The evaporator only displays 2 digits, so will show 64. + * The evaporator (inside unit) only displays 2 digits, so will show 64. * * @param command Target Temperature */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 29dabc044ebf2..27387e18dd974 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -76,6 +76,21 @@ public class ConnectionManager { */ private boolean resend = true; + /** + * Connection manager configuration + * + * @param ipAddress Device IP + * @param ipPort Device Port + * @param timeout Socket timeout + * @param key Security key V.3 + * @param token Security token V.3 + * @param cloud Cloud Provider + * @param email Cloud Provider login email + * @param password Cloud Provider login password + * @param deviceId Device ID + * @param version Device version + * @param promptTone Tone after command true or false + */ public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud, String email, String password, String deviceId, int version, boolean promptTone) { this.deviceIsConnected = false; @@ -111,6 +126,11 @@ public Response getLastResponse() { * The socket is established with the writer and inputStream (for reading responses) * V2 devices will proceed to send the poll or the set command. * V3 devices will proceed to authenticate + * + * @throws MideaConnectionException + * @throws MideaAuthenticationException + * @throws SocketTimeoutException + * @throws IOException */ public synchronized void connect() throws MideaConnectionException, MideaAuthenticationException, SocketTimeoutException, IOException { @@ -565,7 +585,7 @@ public synchronized void write(byte[] buffer) throws IOException { /** * Disconnects from the AC device * - * @param force + * @param force true or false */ public void dispose(boolean force) { disconnect(); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java index 655e70608f14c..50b72f0889274 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaAuthenticationException.java @@ -15,7 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link MideaAuthenticationException} represents a binding specific {@link Exception}. + * The {@link MideaAuthenticationException} represents a binding + * Authentication specific {@link Exception}. * * @author Leo Siepel - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java index 9f1b6692d6f23..96be6b98b1836 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/exception/MideaConnectionException.java @@ -15,7 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link MideaConnectionException} represents a binding specific {@link Exception}. + * The {@link MideaConnectionException} represents a binding specific + * Connection {@link Exception}. * * @author Leo Siepel - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java deleted file mode 100644 index 888086eea4cdb..0000000000000 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/Connection.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2010-2025 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.mideaac.internal.discovery; - -import java.io.Closeable; -import java.io.IOException; -import java.net.DatagramPacket; -import java.net.DatagramSocket; -import java.net.InetAddress; -import java.net.SocketException; -import java.net.UnknownHostException; - -import org.eclipse.jdt.annotation.NonNullByDefault; - -/** - * The {@link Connection} Manages the discovery connection to a Midea AC. - * - * @author Jacek Dobrowolski - Initial contribution - */ -@NonNullByDefault -public class Connection implements Closeable { - - /** - * UDP port1 to send command. - */ - public static final int MIDEAAC_SEND_PORT1 = 6445; - /** - * UDP port2 to send command. - */ - public static final int MIDEAAC_SEND_PORT2 = 20086; - /** - * UDP port devices send discover replies back. - */ - public static final int MIDEAAC_RECEIVE_PORT = 6440; - - private final InetAddress iNetAddress; - private final DatagramSocket socket; - - /** - * Initializes a connection to the given IP address. - * - * @param ipAddress IP address of the connection - * @throws UnknownHostException if ipAddress could not be resolved. - * @throws SocketException if no Datagram socket connection could be made. - */ - public Connection(String ipAddress) throws SocketException, UnknownHostException { - iNetAddress = InetAddress.getByName(ipAddress); - socket = new DatagramSocket(); - } - - /** - * Sends the 9 bytes command to the Midea AC device. - * - * @param command the 9 bytes command - * @throws IOException Connection to the LED failed - */ - public void sendCommand(byte[] command) throws IOException { - { - DatagramPacket sendPkt = new DatagramPacket(command, command.length, iNetAddress, MIDEAAC_SEND_PORT1); - socket.send(sendPkt); - } - { - DatagramPacket sendPkt = new DatagramPacket(command, command.length, iNetAddress, MIDEAAC_SEND_PORT2); - socket.send(sendPkt); - } - } - - @Override - public void close() { - socket.close(); - } -} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index aae365050fbab..76d6ce4a29043 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -57,6 +57,18 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService { private final int receiveJobTimeout = 20000; private final int udpPacketTimeout = receiveJobTimeout - 50; private final String mideaacNamePrefix = "MideaAC"; + /** + * UDP port1 to send command. + */ + public static final int MIDEAAC_SEND_PORT1 = 6445; + /** + * UDP port2 to send command. + */ + public static final int MIDEAAC_SEND_PORT2 = 20086; + /** + * UDP port devices send discover replies back. + */ + public static final int MIDEAAC_RECEIVE_PORT = 6440; private final Logger logger = LoggerFactory.getLogger(MideaACDiscoveryService.class); @@ -185,7 +197,7 @@ public void startDiscoverSocket(String ipAddress, @Nullable DiscoveryHandler dis throws SocketException, IOException { logger.trace("Discovering: {}", ipAddress); this.discoveryHandler = discoveryHandler; - discoverSocket = new DatagramSocket(new InetSocketAddress(Connection.MIDEAAC_RECEIVE_PORT)); + discoverSocket = new DatagramSocket(new InetSocketAddress(MIDEAAC_RECEIVE_PORT)); DatagramSocket discoverSocket = this.discoverSocket; if (discoverSocket != null) { discoverSocket.setBroadcast(true); @@ -193,15 +205,15 @@ public void startDiscoverSocket(String ipAddress, @Nullable DiscoveryHandler dis final InetAddress broadcast = InetAddress.getByName(ipAddress); { final DatagramPacket discoverPacket = new DatagramPacket(CommandBase.discover(), - CommandBase.discover().length, broadcast, Connection.MIDEAAC_SEND_PORT1); + CommandBase.discover().length, broadcast, MIDEAAC_SEND_PORT1); discoverSocket.send(discoverPacket); - logger.trace("Broadcast discovery package sent to port: {}", Connection.MIDEAAC_SEND_PORT1); + logger.trace("Broadcast discovery package sent to port: {}", MIDEAAC_SEND_PORT1); } { final DatagramPacket discoverPacket = new DatagramPacket(CommandBase.discover(), - CommandBase.discover().length, broadcast, Connection.MIDEAAC_SEND_PORT2); + CommandBase.discover().length, broadcast, MIDEAAC_SEND_PORT2); discoverSocket.send(discoverPacket); - logger.trace("Broadcast discovery package sent to port: {}", Connection.MIDEAAC_SEND_PORT2); + logger.trace("Broadcast discovery package sent to port: {}", MIDEAAC_SEND_PORT2); } } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 67117c2bf6d2f..e7be5d68690aa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -25,7 +25,7 @@ * bit(s) the set them to the commanded value. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - Add Java Docs, minor fixes + * @author Bob Eckhoff - Add Java Docs, Timer Display LED and capabilities */ @NonNullByDefault public class CommandSet extends CommandBase { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java index 4cd6c6e012cbc..04afe1958f798 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilitiesResponse.java @@ -24,6 +24,11 @@ public class CapabilitiesResponse { private final byte[] rawData; + /** + * Initialization + * + * @param rawData as bytes + */ public CapabilitiesResponse(byte[] rawData) { this.rawData = rawData; } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java index ea0e4855aac19..76710479092f5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java @@ -162,10 +162,21 @@ public enum CapabilityId { this.id = id; } + /** + * Gets ID + * + * @return Id from enum + */ public int getId() { return id; } + /** + * Gets String name of capability + * + * @param id of enum + * @return String Name + */ public static CapabilityId fromId(int id) { for (CapabilityId capability : values()) { if (capability.id == id) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java index 546924eeb1447..7272a8edaa372 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java @@ -38,7 +38,7 @@ public class CapabilityReaders { // Helper to simplify creation of READERS Function> getValue = (expected) -> (value) -> Objects.equals(value, expected); - // Add READERS for each capability - Not all are supported + // Add READERS for each capability - Not all are supported by all AC devices READERS.put(CapabilityId.ANION, List.of(new Reader("anion", getValue.apply(1)))); READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("auxElectricHeat", getValue.apply(1)))); READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breezeAway", getValue.apply(1)))); @@ -104,10 +104,21 @@ public class CapabilityReaders { READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("windOnMe", getValue.apply(1)))); } + /** + * Validates if Reader exists for the capability + * @param id capability id + * @return true or false + */ public static boolean hasReader(CapabilityId id) { return READERS.containsKey(id); } + /** + * Applies the appropriate Reader + * @param id id + * @param value value from reader + * @param capabilities summary + */ public static void apply(CapabilityId id, int value, Map> capabilities) { Optional.ofNullable(READERS.get(id)).ifPresent(readersList -> { Map result = readersList.stream() diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java index 095a0a21e2609..12a29c9479c3c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/Reader.java @@ -18,15 +18,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault; /** - * The {@link CapabilityReaders} reads the raw capability message and - * breaks them into detailed capabilities. + * The {@link Reader} reads the raw capability message and + * breaks them down for further parsing. * * @author Bob Eckhoff - Initial contribution */ @NonNullByDefault public class Reader { - public final String name; // Name of the capability (e.g., "fan_silent") - public final Predicate predicate; // Condition to check values + public final String name; + public final Predicate predicate; public Reader(String name, Predicate predicate) { this.name = name; diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 95eebebcc0197..497eb7ffcfe47 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -106,7 +106,7 @@ public void testSSID() { } /** - * Test Type + * Test Type - ac */ @Test public void testType() { From e87fe49ba9b9ffede294ed8f8e8c59e79fc514bc Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 29 Apr 2025 11:13:01 -0400 Subject: [PATCH 28/44] spotless missed Forgot to run spotless first Signed-off-by: Bob Eckhoff --- .../internal/handler/capabilities/CapabilityReaders.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java index 7272a8edaa372..ad06f8dd49701 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityReaders.java @@ -106,6 +106,7 @@ public class CapabilityReaders { /** * Validates if Reader exists for the capability + * * @param id capability id * @return true or false */ @@ -115,6 +116,7 @@ public static boolean hasReader(CapabilityId id) { /** * Applies the appropriate Reader + * * @param id id * @param value value from reader * @param capabilities summary From 12ca6dac806ceef7df6f73f2417a5af44f21e525 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 7 May 2025 15:13:08 -0400 Subject: [PATCH 29/44] Add capability follow-up and energy Poll Added capability follow-up command and energy polling, command and reporting. Streamlined response() coding between version 2 and 3. Eliminated alternate target temp for now. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 10 +- .../internal/MideaACBindingConstants.java | 9 +- .../internal/MideaACConfiguration.java | 12 ++ .../connection/ConnectionManager.java | 182 ++++++------------ .../mideaac/internal/handler/Callback.java | 7 + .../mideaac/internal/handler/CommandSet.java | 68 ++++++- .../internal/handler/EnergyResponse.java | 118 ++++++++++++ .../internal/handler/MideaACHandler.java | 83 +++++++- .../mideaac/internal/handler/Response.java | 110 ++--------- .../capabilities/CapabilityParser.java | 9 +- .../resources/OH-INF/i18n/mideaac.properties | 11 +- .../resources/OH-INF/thing/thing-types.xml | 41 +++- .../internal/handler/CommandSetTest.java | 73 ++++++- .../internal/handler/EnergyResponseTest.java | 85 ++++++++ .../internal/handler/ResponseTest.java | 12 +- .../capabilities/CapabilityParserTest.java | 3 +- 16 files changed, 574 insertions(+), 259 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 13aa54b34207d..cdb3025f8ca0e 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -41,9 +41,11 @@ No binding configuration is required. | key | Yes for V.3 | Secret Key - Retrieved from Cloud | | | pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | | keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | +| energyPoll | Yes | Frequency to poll running energy stats (if supported-disable = 0) | 0 | | timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | -| promptTone | Yes | "Ding" tone when command is received and executed. | False | +| promptTone | Yes | "Ding" tone when command is received and executed. | false | | version | Yes | Version 3 has token, key and cloud requirements. | 0 | +| energyDecode | Yes | Devices use two different methods to report energy. Compare. | true ## Channels @@ -66,16 +68,18 @@ Following channels are available: | off-timer | String | Sets the future time to turn off the AC. | | Yes | | screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | | humidity | Number | If device supports, the indoor humidity. | Yes | Yes | +| kilowatt-hours | Number | If device supports, cumulative KWH usage | Yes | Yes | +| amperes | Number | If device supports, current amperage usage | Yes | Yes | +| watts | Number | If device supports, wattage | Yes | Yes | | appliance-error | Switch | If device supports, appliance error | Yes | Yes | | auxiliary-heat | Switch | If device supports, auxiliary heat | Yes | Yes | -| alternate-target-temperature | Number:Temperature | Alternate Target Temperature - not currently used | Yes | Yes | ## Examples ### `demo.things` Examples ```java -Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, timeout=4, promptTone="false", version="3"] +Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, energyPoll = 0, timeout=4, promptTone="false", version="3", energyDecode="true"] ``` Minimal IP Address Option to use the built-in defaults. diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index acc02cd00aed7..0784494f46307 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -60,9 +60,10 @@ public class MideaACBindingConstants { public static final String CHANNEL_INDOOR_TEMPERATURE = "indoor-temperature"; public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature"; public static final String CHANNEL_HUMIDITY = "humidity"; - public static final String CHANNEL_ALTERNATE_TARGET_TEMPERATURE = "alternate-target-temperature"; public static final String CHANNEL_SCREEN_DISPLAY = "screen-display"; - public static final String DROPPED_COMMANDS = "dropped-commands"; + public static final String CHANNEL_KILOWATT_HOURS = "kilowatt-hours"; + public static final String CHANNEL_AMPERES = "amperes"; + public static final String CHANNEL_WATTS = "watts"; public static final Unit API_TEMPERATURE_UNIT = SIUnits.CELSIUS; @@ -84,16 +85,18 @@ public class MideaACBindingConstants { public static final String CONFIG_KEY = "key"; public static final String CONFIG_POLLING_TIME = "pollingTime"; public static final String CONFIG_KEY_TOKEN_UPDATE = "keyTokenUpdate"; + public static final String CONFIG_ENERGY_POLL = "energyPoll"; public static final String CONFIG_CONNECTING_TIMEOUT = "timeout"; public static final String CONFIG_PROMPT_TONE = "promptTone"; public static final String CONFIG_VERSION = "version"; + public static final String CONFIG_ENERGY_DECODE = "energyDecode"; // Properties from LAN Discovery public static final String PROPERTY_SN = "sn"; public static final String PROPERTY_SSID = "ssid"; public static final String PROPERTY_TYPE = "type"; - // Capabilities properties + // Capabilities properties discoverable public static final String PROPERTY_ANION = "anion"; public static final String PROPERTY_AUX_ELECTRIC_HEAT = "auxElectricHeat"; public static final String PROPERTY_BREEZE_AWAY = "breezeAway"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 5c30cc38ece51..fcc552fdbdb39 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -68,6 +68,12 @@ public class MideaACConfiguration { */ public int pollingTime = 60; + /** + * Energy Update Frequency while running + * (if supported) + */ + public int energyPoll = 0; + /** * Key and Token Update Frequency */ @@ -88,6 +94,12 @@ public class MideaACConfiguration { */ public int version = 0; + /** + * Choose between Energy Decoding methods + * true = BCD, false = binary + */ + public boolean energyDecode = true; + /** * Check during initialization that the params are valid * diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 27387e18dd974..cb729062ae3b5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -33,6 +33,7 @@ import org.openhab.binding.mideaac.internal.handler.Callback; import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.handler.CommandSet; +import org.openhab.binding.mideaac.internal.handler.EnergyResponse; import org.openhab.binding.mideaac.internal.handler.Packet; import org.openhab.binding.mideaac.internal.handler.Response; import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; @@ -104,7 +105,7 @@ public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, this.version = version; this.promptTone = promptTone; this.lastResponse = new Response(HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"), - version, "query", (byte) 0xc0); + version); this.cloudProvider = CloudProvider.getCloudProvider(cloud); this.security = new Security(cloudProvider); } @@ -350,148 +351,81 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal if (responseBytes != null) { resend = true; + byte[] data = null; if (version == 3) { Decryption8370Result result = security.decode8370(responseBytes); for (byte[] response : result.getResponses()) { logger.debug("Response length: {} IP address: {} ", response.length, ipAddress); if (response.length > 40 + 16) { - byte[] data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); - + data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); - byte bodyType2 = data[0xa]; - - // data[3]: Device Type - 0xAC = AC - // https://github.com/georgezhao2010/midea_ac_lan/blob/06fc4b582a012bbbfd6bd5942c92034270eca0eb/custom_components/midea_ac_lan/midea_devices.py#L96 - - // data[9]: MessageType - set, query, notify1, notify2, exception, querySN, exception2, - // querySubtype - // https://github.com/georgezhao2010/midea_ac_lan/blob/30d0ff5ff14f150da10b883e97b2f280767aa89a/custom_components/midea_ac_lan/midea/core/message.py#L22-L29 - String responseType = ""; - switch (data[0x9]) { - case 0x02: - responseType = "set"; - break; - case 0x03: - responseType = "query"; - break; - case 0x04: - responseType = "notify1"; - break; - case 0x05: - responseType = "notify2"; - break; - case 0x06: - responseType = "exception"; - break; - case 0x07: - responseType = "querySN"; - break; - case 0x0A: - responseType = "exception2"; - break; - case 0x09: // Helyesen: 0xA0 - responseType = "querySubtype"; - break; - default: - logger.debug("Invalid response type: {}", data[0x9]); - } - logger.trace("Response Type: {} and bodyType: {}", responseType, bodyType2); - - // The response data from the appliance includes a packet header which we don't want - data = Arrays.copyOfRange(data, 10, data.length); - byte bodyType = data[0x0]; - logger.trace("Response Type expected: {} and bodyType: {}", responseType, bodyType); - logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToHex(data)); - logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToBinary(data)); - - // Handle the capabilities response - if (bodyType == (byte) 0xB5) { - logger.debug("Capabilities response detected with bodyType 0xB5."); - CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); - if (callback != null) { - callback.updateChannels(capabilitiesResponse); - } - return; - } - - // Handle the poll response - if (data.length < 21) { - logger.warn("Response data is {} long, minimum is 21!", data.length); - return; - } - if (bodyType != -64) { - if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, - ipAddress); - return; - } - logger.warn("Unexpected response bodyType {}", bodyType); - return; - } - lastResponse = new Response(data, version, responseType, bodyType); - try { - logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, - ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); - } - } catch (Exception ex) { - logger.debug("Processing response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } } } - } else { + } else if (version == 2) { if (responseBytes.length > 40 + 16) { - byte[] data = security - .aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); + data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); - - // The response data from the appliance includes a packet header which we don't want - data = Arrays.copyOfRange(data, 10, data.length); - byte bodyType = data[0x0]; - logger.trace("V2 Bytes decoded and stripped without header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); - - // Handle the capabilities response - if (bodyType == (byte) 0xB5) { - logger.debug("Capabilities response detected with bodyType 0xB5."); - CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); - if (callback != null) { - callback.updateChannels(capabilitiesResponse); - } - return; + } + } + // The response data from the appliance includes a packet header which we don't want + if (data != null && data.length > 10) { + data = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = data[0x0]; + logger.trace("Response bodyType: {}", bodyType); + logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", + data.length, Utils.bytesToBinary(data)); + + // Handle the capabilities response + if (bodyType == (byte) 0xB5) { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); } + return; + } - // Handle the poll response - if (data.length < 21) { - logger.warn("Response data is {} long, minimum is 21!", data.length); - return; + // Handle the Energy Response + if (bodyType == (byte) 0xC1) { + logger.debug("Energy response detected with bodyType 0xC1."); + EnergyResponse energyUpdate = new EnergyResponse(data); + if (callback != null) { + callback.updateChannels(energyUpdate); } - if (bodyType != -64) { - if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); - return; - } - logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + + // Handle the poll response + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + if (bodyType != -64) { + if (bodyType == 30) { + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); return; } - lastResponse = new Response(data, version, "", bodyType); - try { - logger.trace("Data length is {}, version is {}, Ip Address is {}", data.length, version, - ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); - } - } catch (Exception ex) { - logger.debug("Processing response exception: {}", ex.getMessage()); - throw new MideaException(ex); + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, version); + try { + logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, + ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); } + } catch (Exception ex) { + logger.debug("Processing response exception: {}", ex.getMessage()); + throw new MideaException(ex); } + return; + } else { + logger.warn("Decryption failed or insufficient data length to strike header"); } return; } else { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 1321d96338c03..8e75afb523a3d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -37,4 +37,11 @@ public interface Callback { * @param capabilitiesResponse The capabilities response from the device used to update properties. */ void updateChannels(CapabilitiesResponse capabilitiesResponse); + + /** + * Updates channels with a Energy response. + * + * @param capabilitiesResponse The capabilities response from the device used to update properties. + */ + void updateChannels(EnergyResponse energyResponse); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index e7be5d68690aa..9ba8cd8c3e01e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -248,7 +248,7 @@ public void setFahrenheit(boolean fahrenheitEnabled) { public void setScreenDisplay(boolean screenDisplayToggle) { modifyBytesForDisplayOff(); removeExtraBytes(); - logger.trace("Set Bytes before crypt {}", Utils.bytesToHex(data)); + logger.trace("Set Display Bytes before encrypt {}", Utils.bytesToHex(data)); } private void modifyBytesForDisplayOff() { @@ -275,15 +275,14 @@ private void removeExtraBytes() { } /** - * Creates the Get capability message + * Creates the Initial Get capability message * - * @param getCapabilities - * @return + * @return Capability message */ public void getCapabilities() { modifyBytesForCapabilities(); removeExtraCapabilityBytes(); - logger.debug("Set Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Capability Bytes before encrypt {}", Utils.bytesToHex(data)); } private void modifyBytesForCapabilities() { @@ -300,6 +299,32 @@ private void removeExtraCapabilityBytes() { data = newData; } + /** + * Creates the Additional Get capability message + * + * @return Additional Capability message + */ + public void getAdditionalCapabilities() { + modifyBytesForAdditionalCapabilities(); + removeExtraAdditionalCapabilityBytes(); + logger.trace("Set Additional Capability Bytes before encrypt {}", Utils.bytesToHex(data)); + } + + private void modifyBytesForAdditionalCapabilities() { + data[0x01] = (byte) 0x0F; + data[0x09] = (byte) 0x03; + data[0x0a] = (byte) 0xB5; + data[0x0b] = (byte) 0x01; + data[0x0c] = (byte) 0x01; + data[0x0d] = (byte) 0x01; + } + + private void removeExtraAdditionalCapabilityBytes() { + byte[] newData = new byte[data.length - 20]; + System.arraycopy(data, 0, newData, 0, newData.length); + data = newData; + } + /** * Add 0.5C to the temperature value. If needed * Target_temperature setter calls this method @@ -422,4 +447,37 @@ public int getOffTimer() { public int getOffTimer2() { return ((data[0x10] & (byte) 0x0f)) & 0x0f; } + + /** + * Energy detail polling + * Response will be C1, not C0 + * + */ + public void energyPoll() { + modifyBytesForEnergyPoll(); + removeExtraEnergyPollBytes(); + logger.trace("Set Energy Bytes before encrypt {}", Utils.bytesToHex(data)); + } + + private void modifyBytesForEnergyPoll() { + data[0x01] = (byte) 0x20; + data[0x09] = (byte) 0x03; + data[0x0a] = (byte) 0x41; + data[0x0b] = (byte) 0x21; + data[0x0c] = (byte) 0x01; + data[0x0d] = (byte) 0x44; + data[0x0e] = (byte) 0x00; + data[0x0f] = (byte) 0x00; + data[0x10] = (byte) 0x00; + data[0x11] = (byte) 0x00; + data[0x12] = (byte) 0x00; + data[0x13] = (byte) 0x00; + data[0x14] = (byte) 0x00; + } + + private void removeExtraEnergyPollBytes() { + byte[] newData = new byte[data.length - 3]; + System.arraycopy(data, 0, newData, 0, newData.length); + data = newData; + } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java new file mode 100644 index 0000000000000..ce121d53ea3b0 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EnergyResponse} handles the energy messages + * from the device + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class EnergyResponse { + private final byte[] rawData; + private Logger logger = LoggerFactory.getLogger(EnergyResponse.class); + + /** + * Initialization + * + * @param rawData as bytes + */ + public EnergyResponse(byte[] rawData) { + this.rawData = rawData; + + if (logger.isDebugEnabled()) { + logger.debug("Total Kilowatt Hours: {}", getKilowattHours()); + logger.debug("Current Amperes: {}", getAmperes()); + logger.debug("Power Watts: {}", getWatts()); + logger.debug("Total Kilowatt Hours BCD: {}", getKilowattHoursBCD()); + logger.debug("Current Amperes BCD: {}", getAmperesBCD()); + logger.debug("Power Watts BCD: {}", getWattsBCD()); + } + } + + /** + * Kilowatt Hours using binary + * + * @return kilowatt Hours + */ + public double getKilowattHours() { + return ByteBuffer.wrap(rawData, 4, 4).getInt() / 100.00; + } + + /** + * Amperes in use using binary + * + * @return amperes + */ + public double getAmperes() { + return ByteBuffer.wrap(rawData, 12, 4).getInt() / 10.0; + } + + /** + * Watts in use using binary + * + * @return watts + */ + public double getWatts() { + return ((rawData[16] & 0xFF) << 16 | (rawData[17] & 0xFF) << 8 | (rawData[18] & 0xFF)) / 10.0; + } + + /** + * Kilowatt Hours using BCD + * + * @return kilowatt Hours + */ + public double getKilowattHoursBCD() { + double kilowattHours = 0.0; + kilowattHours = bcdToDecimal(rawData, 4, 4) / 100.00; + return kilowattHours; + } + + /** + * Amperes using BCD + * + * @return amperes + */ + public double getAmperesBCD() { + double amperes = 0.0; + amperes = bcdToDecimal(rawData, 12, 4) / 10.0; + return amperes; + } + + /** + * Watts Using BCD + * + * @return watts + */ + public double getWattsBCD() { + double watts = 0.0; + watts = bcdToDecimal(rawData, 16, 3) / 10.0; + return watts; + } + + private long bcdToDecimal(byte[] data, int offset, int length) { + long decimalValue = 0; + for (int i = 0; i < length; i++) { + int byteValue = data[offset + i] & 0xFF; // Ensure byte is unsigned + decimalValue = decimalValue * 100 + ((byteValue >> 4) * 10) + (byteValue & 0x0F); + } + return decimalValue; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 7527819c4acb7..8a20ce1b082e7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -82,9 +82,10 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler private Map properties = new HashMap<>(); // Default parameters are the same as in the MideaACConfiguration class private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false); - private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); private @Nullable ScheduledFuture scheduledTask; private @Nullable ScheduledFuture scheduledKeyTokenUpdate; + private @Nullable ScheduledFuture scheduledEnergyUpdate; private final Callback callbackLambda = new Callback() { @Override @@ -96,6 +97,11 @@ public void updateChannels(Response response) { public void updateChannels(CapabilitiesResponse capabilitiesResponse) { MideaACHandler.this.updateChannels(capabilitiesResponse); } + + @Override + public void updateChannels(EnergyResponse energyUpdate) { + MideaACHandler.this.updateChannels(energyUpdate); + } }; /** @@ -248,6 +254,18 @@ public void initialize() { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "AC capabilities not returned"); } + CapabilityParser parser = new CapabilityParser(); + logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); + if (parser.hasAdditionalCapabilities()) { + try { + CommandSet initializationCommand = new CommandSet(); + initializationCommand.getAdditionalCapabilities(); + connectionManager.sendCommand(initializationCommand, this); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "AC additional capabilities not returned"); + } + } } // Establish routine polling per configuration or defaults @@ -265,7 +283,39 @@ public void initialize() { config.keyTokenUpdate, TimeUnit.DAYS); logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); } else { - logger.debug("Token Key Scheduler already running"); + logger.debug("Token Key Scheduler already running or disabled"); + } + + // Establish Energy polling, if not disabled. Online AC only + if (config.energyPoll != 0 && scheduledEnergyUpdate == null) { + scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 30, config.energyPoll, + TimeUnit.SECONDS); + logger.debug("Scheduled Energy Update started, Poll Time {} seconds", config.energyPoll); + } else { + logger.debug("Energy Scheduler already running or disabled"); + } + } + + private void energyUpdate() { + ConnectionManager connectionManager = this.connectionManager; + Response response = connectionManager.getLastResponse(); + + if (response.getPowerState()) { + try { + CommandSet energyUpdate = new CommandSet(); + energyUpdate.energyPoll(); + connectionManager.sendCommand(energyUpdate, this); + } catch (MideaAuthenticationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (MideaConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (MideaException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } else { + logger.trace("AC is off, skipping energy update."); } } @@ -315,8 +365,6 @@ public void updateChannels(Response response) { QuantityType targetTemperature = new QuantityType(response.getTargetTemperature(), SIUnits.CELSIUS); - QuantityType alternateTemperature = new QuantityType( - response.getAlternateTargetTemperature(), SIUnits.CELSIUS); QuantityType outdoorTemperature = new QuantityType(response.getOutdoorTemperature(), SIUnits.CELSIUS); QuantityType indoorTemperature = new QuantityType(response.getIndoorTemperature(), @@ -324,13 +372,11 @@ public void updateChannels(Response response) { if (imperialUnits) { targetTemperature = Objects.requireNonNull(targetTemperature.toUnit(ImperialUnits.FAHRENHEIT)); - alternateTemperature = Objects.requireNonNull(alternateTemperature.toUnit(ImperialUnits.FAHRENHEIT)); indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); outdoorTemperature = Objects.requireNonNull(outdoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); } updateChannel(CHANNEL_TARGET_TEMPERATURE, targetTemperature); - updateChannel(CHANNEL_ALTERNATE_TARGET_TEMPERATURE, alternateTemperature); updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature); updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); } @@ -389,6 +435,20 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { logger.debug("Capabilities and temperature settings parsed and stored in properties: {}", properties); } + // Handle Energy response update + @Override + public void updateChannels(EnergyResponse energyUpdate) { + if (config.energyDecode) { + updateChannel(CHANNEL_KILOWATT_HOURS, new DecimalType(energyUpdate.getKilowattHoursBCD())); + updateChannel(CHANNEL_AMPERES, new DecimalType(energyUpdate.getAmperesBCD())); + updateChannel(CHANNEL_WATTS, new DecimalType(energyUpdate.getWattsBCD())); + } else { + updateChannel(CHANNEL_KILOWATT_HOURS, new DecimalType(energyUpdate.getKilowattHours())); + updateChannel(CHANNEL_AMPERES, new DecimalType(energyUpdate.getAmperes())); + updateChannel(CHANNEL_WATTS, new DecimalType(energyUpdate.getWatts())); + } + } + @Override public void discovered(DiscoveryResult discoveryResult) { logger.debug("Discovered {}", thing.getUID()); @@ -472,10 +532,21 @@ private void stopTokenKeyUpdate() { } } + private void stopEnergyUpdate() { + ScheduledFuture localScheduledTask = this.scheduledEnergyUpdate; + + if (localScheduledTask != null && !localScheduledTask.isCancelled()) { + localScheduledTask.cancel(true); + logger.debug("Scheduled Energy Update cancelled."); + scheduledEnergyUpdate = null; + } + } + @Override public void dispose() { stopScheduler(); stopTokenKeyUpdate(); + stopEnergyUpdate(); connectionManager.dispose(true); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 1864bc5dea1f8..2616e7a8d628e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -37,8 +37,6 @@ public class Response { private Logger logger = LoggerFactory.getLogger(Response.class); private final int version; - String responseType; - byte bodyType; private int getVersion() { return version; @@ -49,14 +47,10 @@ private int getVersion() { * * @param data byte array from device * @param version version of the device - * @param responseType response type - * @param bodyType Body type */ - public Response(byte[] data, int version, String responseType, byte bodyType) { + public Response(byte[] data, int version) { this.data = data; this.version = version; - this.bodyType = bodyType; - this.responseType = responseType; if (logger.isDebugEnabled()) { logger.debug("Power State: {}", getPowerState()); @@ -80,29 +74,6 @@ public Response(byte[] data, int version, String responseType, byte bodyType) { logger.trace("Auxiliary Heat: {}", getAuxHeat()); logger.trace("Fahrenheit: {}", getFahrenheit()); logger.trace("Humidity: {}", getHumidity()); - logger.trace("Alternate Target Temperature {}", getAlternateTargetTemperature()); - } - - /** - * Trace Log Response and Body Type for V3. V2 set at "" and 0x00 - * This was for future development - */ - if (version == 3) { - logger.trace("Response and Body Type: {}, {}", responseType, bodyType); - if ("notify2".equals(responseType) && bodyType == -95) { // 0xA0 = -95 - logger.trace("Response Handler: XA0Message"); - } else if ("notify1".equals(responseType) && bodyType == -91) { // 0xA1 = -91 - logger.trace("Response Handler: XA1Message"); - } else if (("notify2".equals(responseType) || "set".equals(responseType) || "query".equals(responseType)) - && (bodyType == 0xB0 || bodyType == 0xB1 || bodyType == 0xB5)) { - logger.trace("Response Handler: XBXMessage"); - } else if (("set".equals(responseType) || "query".equals(responseType)) && bodyType == -64) { // 0xC0 = -64 - logger.trace("Response Handler: XCOMessage"); - } else if ("query".equals(responseType) && bodyType == 0xC1) { - logger.trace("Response Handler: XC1Message"); - } else { - logger.trace("Response Handler: _general_"); - } } } @@ -277,62 +248,22 @@ public Float getIndoorTemperature() { double indoorTempInteger; double indoorTempDecimal; - if (data[0] == (byte) 0xc0) { - if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) < -19) { - return (float) -19; - } - if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) > 50) { - return (float) 50; - } else { - indoorTempInteger = (float) ((Byte.toUnsignedInt(data[11]) - 50f) / 2.0f); - } - - indoorTempDecimal = (float) ((data[15] & 0x0F) * 0.1f); - - if (Byte.toUnsignedInt(data[11]) > 49) { - return (float) (indoorTempInteger + indoorTempDecimal); - } else { - return (float) (indoorTempInteger - indoorTempDecimal); - } + if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) < -19) { + return (float) -19; + } + if (((Byte.toUnsignedInt(data[11]) - 50) / 2.0) > 50) { + return (float) 50; + } else { + indoorTempInteger = (float) ((Byte.toUnsignedInt(data[11]) - 50f) / 2.0f); } - /** - * Not observed or tested, but left in from original author - * This was for future development since only 0xC0 is currently used - */ - if (data[0] == (byte) 0xa0 || data[0] == (byte) 0xa1) { - if (data[0] == (byte) 0xa0) { - if ((data[1] >> 2) - 4 == 0) { - indoorTempInteger = -1; - } else { - indoorTempInteger = (data[1] >> 2) + 12; - } - - if (((data[1] >> 1) & 0x01) == 1) { - indoorTempDecimal = 0.5f; - } else { - indoorTempDecimal = 0; - } - } - if (data[0] == (byte) 0xa1) { - if (((Byte.toUnsignedInt(data[13]) - 50) / 2.0f) < -19) { - return (float) -19; - } - if (((Byte.toUnsignedInt(data[13]) - 50) / 2.0f) > 50) { - return (float) 50; - } else { - indoorTempInteger = (float) (Byte.toUnsignedInt(data[13]) - 50f) / 2.0f; - } - indoorTempDecimal = (data[18] & 0x0f) * 0.1f; + indoorTempDecimal = (float) ((data[15] & 0x0F) * 0.1f); - if (Byte.toUnsignedInt(data[13]) > 49) { - return (float) (indoorTempInteger + indoorTempDecimal); - } else { - return (float) (indoorTempInteger - indoorTempDecimal); - } - } + if (Byte.toUnsignedInt(data[11]) > 49) { + return (float) (indoorTempInteger + indoorTempDecimal); + } else { + return (float) (indoorTempInteger - indoorTempDecimal); } - return empty; } /** @@ -355,21 +286,10 @@ public Float getOutdoorTemperature() { return 0.0f; } - /** - * Returns the Alternative Target Temperature (not used) - * - * @return Alternate target Temperature - */ - public Float getAlternateTargetTemperature() { - if ((data[13] & 0x1f) != 0) { - return (data[13] & 0x1f) + 12.0f + (((data[0x02] & 0x10) > 0) ? 0.5f : 0.0f); - } else { - return 0.0f; - } - } - /** * Returns status of Device LEDs + * This is not affected when the IR controller turns + * them off * * @return LEDs on (true) or (false) */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java index 76710479092f5..06952f60fa0f4 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java @@ -92,8 +92,13 @@ public void parse(byte[] payload) { offset += 3 + size; // Advance to the next capability } - // Check if additional capability flag exists without interference from CRC - additionalCapabilities = offset < payload.length - trailingBytes; + // Check for additional capability without interference from CRC8 and chksum + if (offset + 2 <= payload.length - trailingBytes) { + // Extract the two-byte additional capability flag + int additionalCapabilityFlag = ((payload[offset] & 0xFF) << 8) | (payload[offset + 1] & 0xFF); + // Check if the flag matches 0x0100 + additionalCapabilities = (additionalCapabilityFlag == 0x0100); + } logger.debug("Additional capabilities {}", additionalCapabilities); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 2e11a6f2e2dec..fccefed972265 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -19,6 +19,10 @@ thing-type.config.mideaac.ac.deviceId.label = Device ID thing-type.config.mideaac.ac.deviceId.description = ID of the device. Leave 0 to do ID discovery. thing-type.config.mideaac.ac.email.label = Email thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. +thing-type.config.mideaac.ac.energyDecode.label = Alternative Energy Method +thing-type.config.mideaac.ac.energyDecode.description = Devices decode Energy reports differently. Confirm with other measurements what to use. +thing-type.config.mideaac.ac.energyPoll.label = Energy Poll +thing-type.config.mideaac.ac.energyPoll.description = Energy poll when AC is On, default 0 to disable (if not supported). thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port @@ -42,7 +46,8 @@ thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key # channel types -channel-type.mideaac.alternate-target-temperature.label = Alt. Target Temperature +channel-type.mideaac.amperes.label = Amperes +channel-type.mideaac.amperes.description = Amperes (current) reported by the indoor unit. channel-type.mideaac.appliance-error.label = Appliance Error channel-type.mideaac.auxiliary-heat.label = Auxiliary Heat channel-type.mideaac.eco-mode.label = Eco Mode @@ -59,6 +64,8 @@ channel-type.mideaac.humidity.label = Humidity channel-type.mideaac.humidity.description = Humidity measured in the room by the indoor unit. channel-type.mideaac.indoor-temperature.label = Indoor Temperature channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off +channel-type.mideaac.kilowatt-hours.label = Kilowatt Hours +channel-type.mideaac.kilowatt-hours.description = kilowatt Hours reported by the indoor unit. channel-type.mideaac.off-timer.label = Off Timer channel-type.mideaac.off-timer.description = Off Timer (HH:MM) to set. channel-type.mideaac.on-timer.label = On Timer @@ -89,3 +96,5 @@ channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. channel-type.mideaac.turbo-mode.label = Turbo Mode channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. +channel-type.mideaac.watts.label = Watts +channel-type.mideaac.watts.description = Watts reported by the indoor unit. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index c7f182e0fecba..c21a6600e5a63 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -29,8 +29,10 @@ + + + - ipAddress @@ -96,6 +98,12 @@ Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. 60 + + energyPoll + + Energy poll when AC is On, default 0 to disable (if not supported). + 0 + keyTokenUpdate @@ -120,6 +128,12 @@ Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. 0 + + energyDecode + + Devices decode Energy reports differently. Confirm with other measurements what to use. + true + @@ -253,10 +267,25 @@ Humidity - - Number:Temperature - - Temperature - + + Number + + kilowatt Hours reported by the indoor unit. + Energy + + + + Number + + Amperes (current) reported by the indoor unit. + Energy + + + + Number + + Watts reported by the indoor unit. + Energy + diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index 8e687ce0b37c6..c1a64f4a2f38b 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -223,8 +223,8 @@ public void testSetScreenDisplayOff() { assertEquals((byte) 0x20, commandSet.data[0x01]); assertEquals((byte) 0x03, commandSet.data[0x09]); assertEquals((byte) 0x41, commandSet.data[0x0a]); - assertEquals((byte) 0x02, commandSet.data[0x0b] & 0x02); // Check if bit 1 is set - assertEquals((byte) 0x00, commandSet.data[0x0b] & 0x80); // Check if bit 7 is cleared + assertEquals((byte) 0x02, commandSet.data[0x0b] & 0x02); // Check if bit 2 is set + assertEquals((byte) 0x00, commandSet.data[0x0b] & 0x80); // Check if bit 8 is cleared assertEquals((byte) 0x00, commandSet.data[0x0c]); assertEquals((byte) 0xff, commandSet.data[0x0d]); assertEquals((byte) 0x02, commandSet.data[0x0e]); @@ -238,4 +238,73 @@ public void testSetScreenDisplayOff() { // Check the length of the data array assertEquals(31, commandSet.data.length); } + + /** + * Energy poll command Test + * + */ + @Test + public void testEnergyPoll() { + CommandSet commandSet = new CommandSet(); + commandSet.energyPoll(); + + // Check the modified bytes + assertEquals((byte) 0x20, commandSet.data[0x01]); + assertEquals((byte) 0x03, commandSet.data[0x09]); + assertEquals((byte) 0x41, commandSet.data[0x0a]); + assertEquals((byte) 0x21, commandSet.data[0x0b]); + assertEquals((byte) 0x01, commandSet.data[0x0c]); + assertEquals((byte) 0x44, commandSet.data[0x0d]); + assertEquals((byte) 0x00, commandSet.data[0x0e]); + assertEquals((byte) 0x00, commandSet.data[0x0f]); + assertEquals((byte) 0x00, commandSet.data[0x10]); + assertEquals((byte) 0x00, commandSet.data[0x11]); + assertEquals((byte) 0x00, commandSet.data[0x12]); + assertEquals((byte) 0x00, commandSet.data[0x13]); + assertEquals((byte) 0x00, commandSet.data[0x14]); + + // Check the length of the data array + assertEquals(31, commandSet.data.length); + } + + /** + * Capabilities Command Test + * + */ + @Test + public void testCapabilities() { + CommandSet commandSet = new CommandSet(); + commandSet.getCapabilities(); + + // Check the modified bytes + assertEquals((byte) 0x0e, commandSet.data[0x01]); + assertEquals((byte) 0x03, commandSet.data[0x09]); + assertEquals((byte) 0xB5, commandSet.data[0x0a]); + assertEquals((byte) 0x01, commandSet.data[0x0b]); + assertEquals((byte) 0x00, commandSet.data[0x0c]); + + // Check the length of the data array + assertEquals(13, commandSet.data.length); + } + + /** + * Additional Capabilities Command Test + * + */ + @Test + public void testAdditionalCapabilities() { + CommandSet commandSet = new CommandSet(); + commandSet.getAdditionalCapabilities(); + + // Check the modified bytes + assertEquals((byte) 0x0f, commandSet.data[0x01]); + assertEquals((byte) 0x03, commandSet.data[0x09]); + assertEquals((byte) 0xB5, commandSet.data[0x0a]); + assertEquals((byte) 0x01, commandSet.data[0x0b]); + assertEquals((byte) 0x01, commandSet.data[0x0c]); + assertEquals((byte) 0x01, commandSet.data[0x0d]); + + // Check the length of the data array + assertEquals(14, commandSet.data.length); + } } diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java new file mode 100644 index 0000000000000..1d45dba6b9dcf --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * The {@link EnergyResponseTest} tests the Energy response methods + * from the device from a test string + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class EnergyResponseTest { + byte[] dataEnergy = HexFormat.of().parseHex("c1210144000005e00000000000000006000aeb000000487a5e"); + byte[] dataEnergy2 = HexFormat.of().parseHex("C1210144000246540000000000000000000000001953"); + EnergyResponse responseEnergy = new EnergyResponse(dataEnergy); + EnergyResponse responseEnergy2 = new EnergyResponse(dataEnergy2); + + /** + * Test Energy Kilowatt Hours + */ + @Test + public void testGetKilowattHours() { + double kilowattHours = responseEnergy.getKilowattHours(); + assertEquals(15.04, kilowattHours); + } + + @Test + public void testGetKilowattHours2() { + double kilowattHours = responseEnergy2.getKilowattHours(); + assertEquals(1490.76, kilowattHours); + } + + @Test + public void testGetKilowattHours2BCD() { + double kilowattHours = responseEnergy2.getKilowattHoursBCD(); + assertEquals(246.54, kilowattHours); + } + + /** + * Test amperes + */ + @Test + public void testAmperes() { + double amperes = responseEnergy.getAmperes(); + assertEquals(0.6, amperes); + } + + @Test + public void testAmperesBCD() { + double amperes = responseEnergy.getAmperesBCD(); + assertEquals(0.6, amperes); + } + + /** + * Test watts + */ + @Test + public void testGetWatts() { + double watts = responseEnergy.getWatts(); + assertEquals(279.5, watts); + } + + @Test + public void testGetWattsBCD() { + double watts = responseEnergy.getWattsBCD(); + assertEquals(115.1, watts); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 61880766f1f54..7bec3f7f128bc 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -31,9 +31,7 @@ public class ResponseTest { byte[] data = HexFormat.of().parseHex("C00042668387123C00000460FF0C7000000000320000F9ECDB"); private int version = 3; - String responseType = "query"; - byte bodyType = (byte) 0xC0; - Response response = new Response(data, version, responseType, bodyType); + Response response = new Response(data, version); /** * Power State Test @@ -186,12 +184,4 @@ public void testDisplayOn() { public void testGetHumidity() { assertEquals(50, response.getHumidity()); } - - /** - * Alternate Target temperature Test - */ - @Test - public void testAlternateTargetTemperature() { - assertEquals(24, response.getAlternateTargetTemperature()); - } } diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java index a2619b562e3bf..5deb47838b531 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java @@ -121,6 +121,7 @@ void testParseWithTrailingCRC() { 0x16, 0x02, 0x01, 0x01, // Capability 5 (0x0216 = ENERGY, value = 1) 0x17, 0x02, 0x01, 0x00, // Capability 6 (0x0217 = FILTER_REMIND, value = 0) 0x1A, 0x02, 0x01, 0x01, // Capability 7 (0x021A = PRESET_TURBO, value = 1) + 0x01, 0x00, // extra capabilities - run command (byte) 0xDE, (byte) 0xDF // CRC Check (trailing bytes) }; @@ -143,7 +144,7 @@ void testParseWithTrailingCRC() { .ifPresent(value -> assertEquals(true, value)); // Ensure CRC did not cause parsing issues - assertFalse(parser.hasAdditionalCapabilities()); + assertTrue(parser.hasAdditionalCapabilities()); } @Test From 5e16f7d05051868f5298bd6143d1e67e7300c5c5 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Thu, 8 May 2025 17:49:56 -0400 Subject: [PATCH 30/44] Add energy Polling Refresh Add energy Polling Refresh, convert frequency to minutes. Edits for clarity Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 8 ++++---- .../internal/MideaACConfiguration.java | 20 ++++++++++++------- .../binding/mideaac/internal/cloud/Cloud.java | 9 ++++----- .../mideaac/internal/cloud/CloudProvider.java | 6 +++--- .../connection/ConnectionManager.java | 2 +- .../discovery/MideaACDiscoveryService.java | 2 +- .../mideaac/internal/handler/Callback.java | 7 ++++--- .../internal/handler/MideaACHandler.java | 16 ++++++++++----- .../capabilities/CapabilityParser.java | 4 +++- .../security/Decryption8370Result.java | 2 +- .../mideaac/internal/security/TokenKey.java | 2 +- .../resources/OH-INF/i18n/mideaac.properties | 16 +++++++-------- .../resources/OH-INF/thing/thing-types.xml | 18 ++++++++--------- .../internal/MideaACConfigurationTest.java | 3 ++- .../mideaac/internal/cloud/CloudTest.java | 4 ++-- .../MideaACDiscoveryServiceTest.java | 16 +++++++-------- .../internal/handler/CommandSetTest.java | 4 ++-- .../internal/handler/EnergyResponseTest.java | 9 +++++---- .../internal/handler/ResponseTest.java | 4 ++-- .../capabilities/CapabilityParserTest.java | 7 ++++--- .../internal/security/SecurityTest.java | 4 ++-- 21 files changed, 90 insertions(+), 73 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index cdb3025f8ca0e..0faba6f2e61cb 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -39,10 +39,10 @@ No binding configuration is required. | password | No | Password for cloud account chosen in Cloud Provider. | password1 | | token | Yes for V.3 | Secret Token - Retrieved from Cloud | | | key | Yes for V.3 | Secret Key - Retrieved from Cloud | | -| pollingTime | Yes | Polling time in seconds. Minimum time is 30 seconds. | 60 | -| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 | -| energyPoll | Yes | Frequency to poll running energy stats (if supported-disable = 0) | 0 | -| timeout | Yes | Connecting timeout. Minimum time is 2 second, maximum 10 seconds. | 4 | +| pollingTime | Yes | Polling frequency. Minimum is 30. | 60 seconds | +| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 days | +| energyPoll | Yes | Frequency to poll running energy stats (if supported-disable = 0) | 0 minutes | +| timeout | Yes | Connecting timeout. Minimum time is 2, maximum 10. | 4 seconds | | promptTone | Yes | "Ding" tone when command is received and executed. | false | | version | Yes | Version 3 has token, key and cloud requirements. | 0 | | energyDecode | Yes | Devices use two different methods to report energy. Compare. | true diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index fcc552fdbdb39..9e0854499fdc3 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.mideaac.internal; +import java.util.regex.Pattern; + import org.eclipse.jdt.annotation.NonNullByDefault; /** @@ -54,33 +56,33 @@ public class MideaACConfiguration { public String cloud = "NetHome Plus"; /** - * Token + * Token 128 hex length */ public String token = ""; /** - * Key + * Key 64 hex length */ public String key = ""; /** - * Poll Frequency + * Poll Frequency - seconds */ public int pollingTime = 60; /** * Energy Update Frequency while running - * (if supported) + * (if supported) in minutes */ public int energyPoll = 0; /** - * Key and Token Update Frequency + * Key and Token Update Frequency in days */ public int keyTokenUpdate = 0; /** - * Socket Timeout + * Socket Timeout in seconds */ public int timeout = 4; @@ -135,6 +137,10 @@ public boolean isTokenKeyObtainable() { * @return true (Valid, all items are present) false (key, token and/or provider missing) */ public boolean isV3ConfigValid() { - return (!key.isBlank() && !token.isBlank() && !cloud.isBlank()); + return isHexString(key, 64) && isHexString(token, 128) && !cloud.isBlank(); + } + + private boolean isHexString(String str, int length) { + return str.length() == length && Pattern.matches("[0-9a-fA-F]{" + length + "}", str); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index 98d6d3bf1bb85..cc3edb2f286e8 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -46,6 +46,7 @@ * * @author Jacek Dobrowolski - Initial contribution * @author Bob Eckhoff - JavaDoc and changed getQueryString for special characters + * to allow for default email with a "+" */ @NonNullByDefault public class Cloud { @@ -241,8 +242,8 @@ public boolean login() { logger.trace("Using loginId: {}", loginId); logger.trace("Using password: {}", password); - // This is for the MSmartHome (proxied) if (!cloudProvider.proxied().isBlank()) { + // This is for the MSmartHome (proxied) cloud JsonObject newData = new JsonObject(); JsonObject data = new JsonObject(); @@ -263,15 +264,13 @@ public boolean login() { @Nullable JsonObject response = apiRequest("/mj/user/login", null, newData); - if (response == null) { return false; } accessToken = response.getAsJsonObject("mdata").get("accessToken").getAsString(); - - // This for the non-proxied cloud providers } else { + // This for the non-proxied cloud providers String passwordEncrypted = security.encryptPassword(loginId, password); JsonObject data = new JsonObject(); @@ -295,7 +294,7 @@ public boolean login() { * Gets token and key with the device Id modified to udpid * after SessionId (non-proxied) accessToken is established * - * @param deviceId The discovered Device Id + * @param deviceId The AC Device ID to be modified * @return token and key */ public TokenKey getToken(String deviceId) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java index b6f1cd311a76b..2a357241313ac 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/CloudProvider.java @@ -35,7 +35,7 @@ public record CloudProvider(String name, String appkey, String appid, String api String hmackey, String proxied) { /** - * Cloud provider information for record + * Cloud provider information * All providers use the same signkey for AES encryption and Decryption. * V2 Devices do not require a Cloud Provider entry as they only use AES * @@ -50,8 +50,8 @@ public static CloudProvider getCloudProvider(String name) { case "Midea Air": return new CloudProvider("Midea Air", "ff0cf6f5f0c3471de36341cab3f7a9af", "1117", "https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", ""); - // Reported in HA version that this cloud has been shutdown. - // There is a possible v2 version of security down the road + // Reported in HA version of the Midea binding that this cloud has been shutdown. + // There is a possible v2 version of security down the road? case "MSmartHome": return new CloudProvider("MSmartHome", "ac21b9f9cbfe4ca5a88562ef25e2b768", "1010", "https://mp-prod.appsmb.com/mas/v5/app/proxy?alias=", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index cb729062ae3b5..62cf9adeda1cf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -51,7 +51,7 @@ * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command * * This gets around the issue that any command needs to be within 30 seconds of the authorization - * in testing this only adds 50 ms, but allows polls at longer intervals + * in testing this only adds 50 ms, but allows Scheduled polls at longer intervals. */ @NonNullByDefault public class ConnectionManager { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index 76d6ce4a29043..ca69f49fcb481 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -47,7 +47,7 @@ * The {@link MideaACDiscoveryService} service for Midea AC. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - OH naming conventions + * @author Bob Eckhoff - OH naming conventions and Capabilities capture */ @NonNullByDefault @Component(service = DiscoveryService.class, configurationPid = "discovery.mideaac") diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 8e75afb523a3d..56476120e1c25 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -18,9 +18,10 @@ /** * The {@link Response} performs the polling byte data stream decoding * The {@link CapabilitiesResponse} performs the capability byte data stream decoding - * + * The {@link EnergyResponse} performs the energy byte stream data decoding + * * @author Leo Siepel - Initial contribution - * @author Bob Eckhoff - added Capabilities Callback + * @author Bob Eckhoff - added Capabilities and Energy Callbacks */ @NonNullByDefault public interface Callback { @@ -41,7 +42,7 @@ public interface Callback { /** * Updates channels with a Energy response. * - * @param capabilitiesResponse The capabilities response from the device used to update properties. + * @param energyResponse The Energy response from the device used to update properties. */ void updateChannels(EnergyResponse energyResponse); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 8a20ce1b082e7..1e400896ddbf7 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -123,6 +123,7 @@ public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpCli * The command set is formed using the previous command to only * change the item requested and leave the others the same. * The command set which is then sent to the device via the connectionManager. + * For a Refresh both regular and energy polls are triggerred. */ @Override public void handleCommand(ChannelUID channelUID, Command command) { @@ -132,6 +133,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { try { connectionManager.getStatus(callbackLambda); + // Read only Energy channels not updated with routine poll + CommandSet energyUpdate = new CommandSet(); + energyUpdate.energyPoll(); + connectionManager.sendCommand(energyUpdate, this); } catch (MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (MideaConnectionException | MideaException e) { @@ -190,7 +195,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { * hard-coded encryption. Next the Connection Manager is established. Then a command * is formed and sent to retrieve the AC capabilities if they have not been * discovered. Capabilities are not returned in the initial LAN Discovery. - * Lastly the routine polling and token key update frequency are set. + * Lastly the routine polling, token key update and Energy polling frequency are set. * */ @Override @@ -286,10 +291,10 @@ public void initialize() { logger.debug("Token Key Scheduler already running or disabled"); } - // Establish Energy polling, if not disabled. Online AC only + // Establish Energy polling, if not disabled. if (config.energyPoll != 0 && scheduledEnergyUpdate == null) { - scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 30, config.energyPoll, - TimeUnit.SECONDS); + scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 1, config.energyPoll, + TimeUnit.MINUTES); logger.debug("Scheduled Energy Update started, Poll Time {} seconds", config.energyPoll); } else { logger.debug("Energy Scheduler already running or disabled"); @@ -300,6 +305,7 @@ private void energyUpdate() { ConnectionManager connectionManager = this.connectionManager; Response response = connectionManager.getLastResponse(); + // Only runs if device is ON to reduce traffic if (response.getPowerState()) { try { CommandSet energyUpdate = new CommandSet(); @@ -435,7 +441,7 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { logger.debug("Capabilities and temperature settings parsed and stored in properties: {}", properties); } - // Handle Energy response update + // Handle Energy response updates - Config flags sets what decoding to use @Override public void updateChannels(EnergyResponse energyUpdate) { if (config.energyDecode) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java index 06952f60fa0f4..224758cecec80 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParser.java @@ -22,7 +22,9 @@ import org.slf4j.LoggerFactory; /** - * The {@link CapabilityParser} parses the capability Response. + * The {@link CapabilityParser} parses the capability Response into + * boolean and number responses. Decodes and reads the number responses (Temps) + * and decodes the boolean responses for the {@link CapabilityReaders} * * @author Bob Eckhoff - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java index f746a91312499..1044a6ec60a27 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Decryption8370Result.java @@ -20,7 +20,7 @@ * The {@link Decryption8370Result} Protocol. V3 Only * * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - JavaDoc + * @author Bob Eckhoff - JavaDoc additions */ @NonNullByDefault public class Decryption8370Result { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java index b70e2e600cd6a..ff279c514da99 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/TokenKey.java @@ -21,7 +21,7 @@ * @param key For coding/decoding messages * * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - JavaDoc and OH addons review + * @author Bob Eckhoff - JavaDoc and convert to record */ @NonNullByDefault public record TokenKey(String token, String key) { diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index fccefed972265..4e56ad7a498f5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -21,24 +21,24 @@ thing-type.config.mideaac.ac.email.label = Email thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. thing-type.config.mideaac.ac.energyDecode.label = Alternative Energy Method thing-type.config.mideaac.ac.energyDecode.description = Devices decode Energy reports differently. Confirm with other measurements what to use. -thing-type.config.mideaac.ac.energyPoll.label = Energy Poll -thing-type.config.mideaac.ac.energyPoll.description = Energy poll when AC is On, default 0 to disable (if not supported). +thing-type.config.mideaac.ac.energyPoll.label = Energy Poll in minutes +thing-type.config.mideaac.ac.energyPoll.description = Energy poll when AC is On, default 0 (disabled in case not supported). thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). -thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update -thing-type.config.mideaac.ac.keyTokenUpdate.description = Frequency to update the Key and Token in Days, default 0 to disable. +thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update in Days +thing-type.config.mideaac.ac.keyTokenUpdate.description = Frequency to update the Key and Token, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.pollingTime.label = Polling time -thing-type.config.mideaac.ac.pollingTime.description = Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. +thing-type.config.mideaac.ac.pollingTime.label = Polling time in seconds +thing-type.config.mideaac.ac.pollingTime.description = Polling time. Minimum time is 30, default 60. thing-type.config.mideaac.ac.promptTone.label = Prompt tone thing-type.config.mideaac.ac.promptTone.description = After sending a command device will play "ding" tone when command is received and executed. -thing-type.config.mideaac.ac.timeout.label = Timeout -thing-type.config.mideaac.ac.timeout.description = Connecting timeout. Minimum time is 2 second, maximum 10 seconds (4 seconds default). +thing-type.config.mideaac.ac.timeout.label = Socket timeout in seconds +thing-type.config.mideaac.ac.timeout.description = Connecting socket timeout. Minimum is 2, maximum is 10 (4 is default). thing-type.config.mideaac.ac.token.label = Token thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). thing-type.config.mideaac.ac.version.label = AC Version diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index c21a6600e5a63..5a3b02a9961aa 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -94,26 +94,26 @@ pollingTime - - Polling time in seconds. Minimum time is 30 seconds, default 60 seconds. + + Polling time. Minimum time is 30, default 60. 60 - + energyPoll - - Energy poll when AC is On, default 0 to disable (if not supported). + + Energy poll when AC is On, default 0 (disabled in case not supported). 0 keyTokenUpdate - - Frequency to update the Key and Token in Days, default 0 to disable. + + Frequency to update the Key and Token, default 0 to disable. 0 timeout - - Connecting timeout. Minimum time is 2 second, maximum 10 seconds (4 seconds default). + + Connecting socket timeout. Minimum is 2, maximum is 10 (4 is default). 4 diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java index 2a5b14cb810a4..c7e92ba1c9a9a 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/MideaACConfigurationTest.java @@ -21,7 +21,8 @@ import org.openhab.binding.mideaac.internal.security.TokenKey; /** - * Testing of the {@link MideaACConfigurationTest} Configuration + * Testing of the {@link MideaACConfigurationTest} Midea AC + * Configuration methods * * @author Robert Eckhoff - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java index 4db9dcf66d6c8..ad553321cad5f 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/cloud/CloudTest.java @@ -31,8 +31,8 @@ import org.junit.jupiter.api.Test; /** - * The {@link CloudTest} creates messages and - * compares them to the expected result. + * The {@link CloudTest} tests the methods in the Cloud + * class with mock responses. * * @author Bob Eckhoff - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index 497eb7ffcfe47..f2dca0768a7c7 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -26,7 +26,7 @@ /** * The {@link MideaACDiscoveryServiceTest} tests the discovery byte arrays * (reply string already decrypted - See SecurityTest) - * to extract the correct device information + * against the methods in the MideaACDiscoveryService for correctness * * @author Robert Eckhoff - Initial contribution */ @@ -41,7 +41,7 @@ public class MideaACDiscoveryServiceTest { mSmartType = ""; /** - * Test Version + * Test AC Version */ @Test public void testVersion() { @@ -54,7 +54,7 @@ public void testVersion() { } /** - * Test Id + * Test AC Id */ @Test public void testId() { @@ -68,7 +68,7 @@ public void testId() { } /** - * Test IP address of device + * Test IP address of AC device */ @Test public void testIPAddress() { @@ -78,7 +78,7 @@ public void testIPAddress() { } /** - * Test Device Port + * Test AC Device Port */ @Test public void testPort() { @@ -88,7 +88,7 @@ public void testPort() { } /** - * Test serial Number + * Test AC serial Number */ @Test public void testSN() { @@ -97,7 +97,7 @@ public void testSN() { } /** - * Test SSID - SN converted + * Test AC response SSID Conversion */ @Test public void testSSID() { @@ -106,7 +106,7 @@ public void testSSID() { } /** - * Test Type - ac + * Test Type - Only ac supported */ @Test public void testType() { diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index c1a64f4a2f38b..e2fa6192b5915 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -21,8 +21,8 @@ import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; /** - * The {@link CommandSetTest} compares example SET commands with the - * expected results. + * The {@link CommandSetTest} tests the methods in the CommandSet class + * for correctness. * * @author Bob Eckhoff - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java index 1d45dba6b9dcf..b76d88e313f67 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java @@ -21,7 +21,8 @@ /** * The {@link EnergyResponseTest} tests the Energy response methods - * from the device from a test string + * from the device using test strings. There are two decoding + * algorithms supported by different Midea ACs. * * @author Bob Eckhoff - Initial contribution */ @@ -33,7 +34,7 @@ public class EnergyResponseTest { EnergyResponse responseEnergy2 = new EnergyResponse(dataEnergy2); /** - * Test Energy Kilowatt Hours + * Test Energy Kilowatt Hours 3 tests */ @Test public void testGetKilowattHours() { @@ -54,7 +55,7 @@ public void testGetKilowattHours2BCD() { } /** - * Test amperes + * Test amperes 2 tests */ @Test public void testAmperes() { @@ -69,7 +70,7 @@ public void testAmperesBCD() { } /** - * Test watts + * Test watts 2 tests */ @Test public void testGetWatts() { diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 7bec3f7f128bc..b9a019e457813 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -20,8 +20,8 @@ import org.junit.jupiter.api.Test; /** - * The {@link ResponseTest} extracts the AC device response and - * compares them to the expected result. + * The {@link ResponseTest} tests the methods in the Response class + * against an example response string. * * @author Bob Eckhoff - Initial contribution */ diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java index 5deb47838b531..c66d97b53310d 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java @@ -23,7 +23,8 @@ import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser.CapabilityId; /** - * The {@link CapabilityParser} parses the capability Response. + * The {@link CapabilityParserTest} tests the methods in the + * CapabilityParser against test payloads. * * @author Bob Eckhoff - Initial contribution */ @@ -112,7 +113,7 @@ void testParseWithInvalidSize() { @Test void testParseWithTrailingCRC() { - // Arrange: Create a payload with trailing CRC + // Arrange: Create a payload with extra capabilities byte[] payload = new byte[] { (byte) 0xB5, 0x07, // Header and count (7 capabilities) 0x12, 0x02, 0x01, 0x01, // Capability 1 (0x0212 = PRESET_ECO, value = 1) 0x13, 0x02, 0x01, 0x01, // Capability 2 (0x0213 = PRESET_FREEZE_PROTECTION, value = 1) @@ -149,7 +150,7 @@ void testParseWithTrailingCRC() { @Test void testParseWithtemperature() { - // Arrange: Create a payload with trailing CRC + // Arrange: Create a payload with Temperature bytes byte[] payload = new byte[] { (byte) 0xB5, 0x08, // Header and count (7 capabilities) 0x12, 0x02, 0x01, 0x01, // Capability 1 (0x0212 = PRESET_ECO, value = 1) 0x13, 0x02, 0x01, 0x01, // Capability 2 (0x0213 = PRESET_FREEZE_PROTECTION, value = 1) diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java index fc84331dd7b0c..a31e9185f51d2 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/security/SecurityTest.java @@ -25,8 +25,8 @@ import com.google.gson.JsonObject; /** - * The {@link SecurityTest} tests methods and - * compares them to the expected result. + * The {@link SecurityTest} tests methods and compares + * them to the expected result with sample data. * * @author Bob Eckhoff - Initial contribution */ From e96426be480c878e60522029d778d2bfa330403b Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 9 May 2025 17:59:50 -0400 Subject: [PATCH 31/44] Improve config descriptions Improve config descriptions and inline documentation Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 38 +++++++++---------- .../connection/ConnectionManager.java | 28 +++++++++----- .../discovery/MideaACDiscoveryService.java | 4 +- .../internal/handler/MideaACHandler.java | 16 +++++--- .../resources/OH-INF/i18n/mideaac.properties | 20 +++++----- .../resources/OH-INF/thing/thing-types.xml | 20 +++++----- 6 files changed, 71 insertions(+), 55 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 0faba6f2e61cb..a0ee916141125 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -19,9 +19,9 @@ This binding supports one Thing type `ac`. ## Discovery Once the Air Conditioner is on your network activating the Inbox scan with this binding will send an IP broadcast message. -Every responding unit gets added to the Inbox. When adding each thing, the required parameters of will be populated with either +Every responding unit gets added to the Inbox. When adding each thing, the required parameters will be populated with either discovered values or the default settings. For a V.3 device, in the unlikely event the defaults did not get the token and key, -enter your cloud provider, email and password. The thing properties will show supported AC functions. +enter your cloud provider, email and password. The thing properties dropdown on the Thing UI page will show supported AC functions. ## Binding Configuration @@ -29,23 +29,23 @@ No binding configuration is required. ## Thing Configuration -| Parameter | Required ? | Comment | Default | -|---------------|-------------|-------------------------------------------------------------------|---------------------------| -| ipAddress | Yes | IP Address of the device. | | -| ipPort | Yes | IP port of the device | 6444 | -| deviceId | Yes | ID of the device. Leave 0 to do ID discovery (length 6 bytes). | 0 | -| cloud | Yes for V.3 | Cloud Provider name for email and password | NetHome Plus | -| email | No | Email for cloud account chosen in Cloud Provider. | nethome+us@mailinator.com | -| password | No | Password for cloud account chosen in Cloud Provider. | password1 | -| token | Yes for V.3 | Secret Token - Retrieved from Cloud | | -| key | Yes for V.3 | Secret Key - Retrieved from Cloud | | -| pollingTime | Yes | Polling frequency. Minimum is 30. | 60 seconds | -| keyTokenUpdate| No | Frequency to update key and Token in days (disable = 0) | 0 days | -| energyPoll | Yes | Frequency to poll running energy stats (if supported-disable = 0) | 0 minutes | -| timeout | Yes | Connecting timeout. Minimum time is 2, maximum 10. | 4 seconds | -| promptTone | Yes | "Ding" tone when command is received and executed. | false | -| version | Yes | Version 3 has token, key and cloud requirements. | 0 | -| energyDecode | Yes | Devices use two different methods to report energy. Compare. | true +| Parameter | Required ? | Comment | Default | +|---------------|-------------|--------------------------------------------------------------|---------------------------| +| ipAddress | Yes | IP Address of the device. | | +| ipPort | Yes | IP port of the device | 6444 | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery. | 0 | +| cloud | Yes for V.3 | Your Cloud Provider name (or default). | NetHome Plus | +| email | No | Email for your cloud account (or default). | nethome+us@mailinator.com | +| password | No | Password for your cloud account (or default). | password1 | +| token | Yes for V.3 | Secret Token - Retrieved from cloud | | +| key | Yes for V.3 | Secret Key - Retrieved from cloud | | +| pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | +| keyTokenUpdate| No | Frequency to update key and token from cloud in days | 0 days (disabled) | +| energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | +| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | +| promptTone | Yes | "Ding" tone when command is received and executed. | false | +| version | Yes | Version 3 has token, key and cloud requirements. | 0 | +| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true ## Channels diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 62cf9adeda1cf..a38dede7a7572 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -381,20 +381,30 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal // Handle the capabilities response if (bodyType == (byte) 0xB5) { - logger.debug("Capabilities response detected with bodyType 0xB5."); - CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); - if (callback != null) { - callback.updateChannels(capabilitiesResponse); + try { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); + } + } catch (Exception ex) { + logger.debug("Capability response exception: {}", ex.getMessage()); + throw new MideaException(ex); } return; } // Handle the Energy Response if (bodyType == (byte) 0xC1) { - logger.debug("Energy response detected with bodyType 0xC1."); - EnergyResponse energyUpdate = new EnergyResponse(data); - if (callback != null) { - callback.updateChannels(energyUpdate); + try { + logger.debug("Energy response detected with bodyType 0xC1."); + EnergyResponse energyUpdate = new EnergyResponse(data); + if (callback != null) { + callback.updateChannels(energyUpdate); + } + } catch (Exception ex) { + logger.debug("Energy response exception: {}", ex.getMessage()); + throw new MideaException(ex); } return; } @@ -420,7 +430,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal callback.updateChannels(lastResponse); } } catch (Exception ex) { - logger.debug("Processing response exception: {}", ex.getMessage()); + logger.debug("Poll response exception: {}", ex.getMessage()); throw new MideaException(ex); } return; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index ca69f49fcb481..a3e89937b2fdf 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -129,9 +129,9 @@ private void discoverThings() { } } } catch (SocketTimeoutException e) { - logger.debug("Discovering poller timeout..."); + logger.debug("Discovery poll timeout"); } catch (IOException e) { - logger.debug("Error during discovery: {}", e.getMessage()); + logger.debug("Exception during discovery - no issue if socket closed: {}", e.getMessage()); } finally { closeDiscoverSocket(); removeOlderResults(getTimestampOfLastScan()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 1e400896ddbf7..dc809f93391b9 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -230,12 +230,18 @@ public void initialize() { // Check for valid token and key and/or contact cloud account to get them if (config.version == 3 && !config.isV3ConfigValid()) { if (config.isTokenKeyObtainable()) { - CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); - getTokenKeyCloud(cloudProvider); - return; + try { + CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); + getTokenKeyCloud(cloudProvider); + return; + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Token and key could not be obtained from Cloud"); + return; + } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Configuration invalid and no account info to retrieve from cloud"); + "No account info to retrieve from cloud"); return; } } else { @@ -295,7 +301,7 @@ public void initialize() { if (config.energyPoll != 0 && scheduledEnergyUpdate == null) { scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 1, config.energyPoll, TimeUnit.MINUTES); - logger.debug("Scheduled Energy Update started, Poll Time {} seconds", config.energyPoll); + logger.debug("Scheduled Energy Update started, Poll Time {} minutes", config.energyPoll); } else { logger.debug("Energy Scheduler already running or disabled"); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 4e56ad7a498f5..a4001830c334b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -19,28 +19,28 @@ thing-type.config.mideaac.ac.deviceId.label = Device ID thing-type.config.mideaac.ac.deviceId.description = ID of the device. Leave 0 to do ID discovery. thing-type.config.mideaac.ac.email.label = Email thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.energyDecode.label = Alternative Energy Method -thing-type.config.mideaac.ac.energyDecode.description = Devices decode Energy reports differently. Confirm with other measurements what to use. -thing-type.config.mideaac.ac.energyPoll.label = Energy Poll in minutes -thing-type.config.mideaac.ac.energyPoll.description = Energy poll when AC is On, default 0 (disabled in case not supported). +thing-type.config.mideaac.ac.energyDecode.label = Energy response decode method +thing-type.config.mideaac.ac.energyDecode.description = Binary-Coded Decimal (BCD) = true. Big-endian = false. +thing-type.config.mideaac.ac.energyPoll.label = Energy poll frequency in minutes +thing-type.config.mideaac.ac.energyPoll.description = Energy poll (when AC is On), default 0, disabled. (in case not supported). thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key -thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). -thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update in Days -thing-type.config.mideaac.ac.keyTokenUpdate.description = Frequency to update the Key and Token, default 0 to disable. +thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). +thing-type.config.mideaac.ac.keyTokenUpdate.label = Key token update frequency in days +thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.pollingTime.label = Polling time in seconds -thing-type.config.mideaac.ac.pollingTime.description = Polling time. Minimum time is 30, default 60. +thing-type.config.mideaac.ac.pollingTime.label = Poll frequency in seconds +thing-type.config.mideaac.ac.pollingTime.description = Poll device status. Minimum is 30, default 60. thing-type.config.mideaac.ac.promptTone.label = Prompt tone thing-type.config.mideaac.ac.promptTone.description = After sending a command device will play "ding" tone when command is received and executed. thing-type.config.mideaac.ac.timeout.label = Socket timeout in seconds thing-type.config.mideaac.ac.timeout.description = Connecting socket timeout. Minimum is 2, maximum is 10 (4 is default). thing-type.config.mideaac.ac.token.label = Token -thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email and password for Cloud to retrieve it). +thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). thing-type.config.mideaac.ac.version.label = AC Version thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 5a3b02a9961aa..e925e7839a2e2 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -84,30 +84,30 @@ token Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not - known, enter email and password for Cloud to retrieve it). + known, enter email, password for Cloud account to retrieve it). key Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not - known, enter email and password for Cloud to retrieve it). + known, enter email, password for Cloud account to retrieve it). pollingTime - - Polling time. Minimum time is 30, default 60. + + Poll device status. Minimum is 30, default 60. 60 energyPoll - - Energy poll when AC is On, default 0 (disabled in case not supported). + + Energy poll (when AC is On), default 0, disabled. (in case not supported). 0 keyTokenUpdate - - Frequency to update the Key and Token, default 0 to disable. + + Update the Key and Token from the cloud, default 0 to disable. 0 @@ -130,8 +130,8 @@ energyDecode - - Devices decode Energy reports differently. Confirm with other measurements what to use. + + Binary-Coded Decimal (BCD) = true. Big-endian = false. true From d1a4eb8da8cb62ea19b077b35cc1b57e64b07197 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 20 May 2025 15:09:09 -0400 Subject: [PATCH 32/44] Correct LED command and change energy polling Correct LED command (misread python byte) and change energy polling, even when device is off. There is slight wattage when off to capture. Signed-off-by: Bob Eckhoff --- .../mideaac/internal/handler/CommandSet.java | 3 +- .../internal/handler/MideaACHandler.java | 30 ++++++++----------- .../internal/handler/CommandSetTest.java | 3 +- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 9ba8cd8c3e01e..76847efedf7c0 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -255,8 +255,7 @@ private void modifyBytesForDisplayOff() { data[0x01] = (byte) 0x20; data[0x09] = (byte) 0x03; data[0x0a] = (byte) 0x41; - data[0x0b] |= 0x02; // Set - data[0x0b] &= ~(byte) 0x80; // Clear + data[0x0b] = (byte) 0x61; // Includes beep 0x40 data[0x0c] = (byte) 0x00; data[0x0d] = (byte) 0xff; data[0x0e] = (byte) 0x02; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index dc809f93391b9..3e9be7ae4b063 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -309,25 +309,19 @@ public void initialize() { private void energyUpdate() { ConnectionManager connectionManager = this.connectionManager; - Response response = connectionManager.getLastResponse(); - // Only runs if device is ON to reduce traffic - if (response.getPowerState()) { - try { - CommandSet energyUpdate = new CommandSet(); - energyUpdate.energyPoll(); - connectionManager.sendCommand(energyUpdate, this); - } catch (MideaAuthenticationException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); - } catch (MideaConnectionException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (MideaException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); - } - } else { - logger.trace("AC is off, skipping energy update."); + try { + CommandSet energyUpdate = new CommandSet(); + energyUpdate.energyPoll(); + connectionManager.sendCommand(energyUpdate, this); + } catch (MideaAuthenticationException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } catch (MideaConnectionException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (MideaException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); } } diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java index e2fa6192b5915..acd37155b8869 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/CommandSetTest.java @@ -223,8 +223,7 @@ public void testSetScreenDisplayOff() { assertEquals((byte) 0x20, commandSet.data[0x01]); assertEquals((byte) 0x03, commandSet.data[0x09]); assertEquals((byte) 0x41, commandSet.data[0x0a]); - assertEquals((byte) 0x02, commandSet.data[0x0b] & 0x02); // Check if bit 2 is set - assertEquals((byte) 0x00, commandSet.data[0x0b] & 0x80); // Check if bit 8 is cleared + assertEquals((byte) 0x61, commandSet.data[0x0b]); assertEquals((byte) 0x00, commandSet.data[0x0c]); assertEquals((byte) 0xff, commandSet.data[0x0d]); assertEquals((byte) 0x02, commandSet.data[0x0e]); From 64b451e82a7346e58c023ab5aee8c983ce1d7375 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 9 Jul 2025 18:36:25 -0400 Subject: [PATCH 33/44] Add target humidity support for Midea AC binding Introduces a new 'target humidity' channel and associated logic for setting and handling target humidity in dry mode. Adds handling for unsolicited humidity reports (0xA0), updates the command and response processing, extends the callback interface, and provides tests for the new functionality. Updates i18n and thing-type definitions to reflect the new channel. Signed-off-by: Bob Eckhoff --- .../internal/MideaACBindingConstants.java | 1 + .../internal/connection/CommandHelper.java | 30 +++ .../connection/ConnectionManager.java | 173 +++++++++++------- .../mideaac/internal/handler/Callback.java | 7 + .../mideaac/internal/handler/CommandSet.java | 10 + .../internal/handler/HumidityResponse.java | 111 +++++++++++ .../internal/handler/MideaACHandler.java | 16 +- .../mideaac/internal/handler/Response.java | 10 +- .../resources/OH-INF/i18n/mideaac.properties | 8 +- .../resources/OH-INF/thing/thing-types.xml | 14 +- .../handler/HumidityResponseTest.java | 83 +++++++++ .../internal/handler/ResponseTest.java | 2 +- 12 files changed, 384 insertions(+), 81 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/HumidityResponse.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/HumidityResponseTest.java diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index 0784494f46307..a7fdbe1f0468f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -59,6 +59,7 @@ public class MideaACBindingConstants { public static final String CHANNEL_TURBO_MODE = "turbo-mode"; public static final String CHANNEL_INDOOR_TEMPERATURE = "indoor-temperature"; public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature"; + public static final String CHANNEL_TARGET_HUMIDITY = "target-humidity"; public static final String CHANNEL_HUMIDITY = "humidity"; public static final String CHANNEL_SCREEN_DISPLAY = "screen-display"; public static final String CHANNEL_KILOWATT_HOURS = "kilowatt-hours"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index d79e96f711383..ab9daa10bdfa6 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -437,4 +437,34 @@ public static CommandSet handleOffTimer(Command command, Response lastResponse) return commandSet; } + + // Limit Humidity to range + private static int limitHumidityToRange(int humidity) { + if (humidity < 32) { + return 32; + } + if (humidity > 80) { + return 80; + } + + return humidity; + } + + /** + * Sets the Target Humidity for Dry Mode + * + * @param command Target Humidity + */ + public static CommandSet handleTargetHumidity(Command command, Response lastResponse) { + CommandSet commandSet = CommandSet.fromResponse(lastResponse); + + if (command instanceof DecimalType decimalCommand) { + int humidity = decimalCommand.intValue(); + commandSet.setTargetHumidity(limitHumidityToRange(humidity)); + } else { + logger.debug("Unknown target humidity command: {}", command); + } + + return commandSet; + } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index a38dede7a7572..cb12b5ad76498 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -34,6 +34,7 @@ import org.openhab.binding.mideaac.internal.handler.CommandBase; import org.openhab.binding.mideaac.internal.handler.CommandSet; import org.openhab.binding.mideaac.internal.handler.EnergyResponse; +import org.openhab.binding.mideaac.internal.handler.HumidityResponse; import org.openhab.binding.mideaac.internal.handler.Packet; import org.openhab.binding.mideaac.internal.handler.Response; import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; @@ -351,91 +352,45 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal if (responseBytes != null) { resend = true; - byte[] data = null; + int processed = 0; if (version == 3) { Decryption8370Result result = security.decode8370(responseBytes); for (byte[] response : result.getResponses()) { + byte[] data = null; logger.debug("Response length: {} IP address: {} ", response.length, ipAddress); if (response.length > 40 + 16) { data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); } + // The response data from the appliance includes a packet header which we don't want + if (data != null && data.length > 10) { + byte[] processedData = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = processedData[0x0]; + handleResponse(processedData, bodyType, callback); + processed++; + if (processed >= 2) { + break; + } + } else { + logger.warn("Decryption failed or insufficient data length to strike header"); + } } } else if (version == 2) { + byte[] data = null; if (responseBytes.length > 40 + 16) { data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); } - } - // The response data from the appliance includes a packet header which we don't want - if (data != null && data.length > 10) { - data = Arrays.copyOfRange(data, 10, data.length); - byte bodyType = data[0x0]; - logger.trace("Response bodyType: {}", bodyType); - logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); - logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", - data.length, Utils.bytesToBinary(data)); - - // Handle the capabilities response - if (bodyType == (byte) 0xB5) { - try { - logger.debug("Capabilities response detected with bodyType 0xB5."); - CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); - if (callback != null) { - callback.updateChannels(capabilitiesResponse); - } - } catch (Exception ex) { - logger.debug("Capability response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } - return; - } - - // Handle the Energy Response - if (bodyType == (byte) 0xC1) { - try { - logger.debug("Energy response detected with bodyType 0xC1."); - EnergyResponse energyUpdate = new EnergyResponse(data); - if (callback != null) { - callback.updateChannels(energyUpdate); - } - } catch (Exception ex) { - logger.debug("Energy response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } - return; - } - - // Handle the poll response - if (data.length < 21) { - logger.warn("Response data is {} long, minimum is 21!", data.length); - return; - } - if (bodyType != -64) { - if (bodyType == 30) { - logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); - return; - } - logger.warn("Unexpected response bodyType {}", bodyType); - return; - } - lastResponse = new Response(data, version); - try { - logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, - ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); - } - } catch (Exception ex) { - logger.debug("Poll response exception: {}", ex.getMessage()); - throw new MideaException(ex); + // The response data from the appliance includes a packet header which we don't want + if (data != null && data.length > 10) { + byte[] processedData = Arrays.copyOfRange(data, 10, data.length); + byte bodyType = processedData[0x0]; + handleResponse(processedData, bodyType, callback); + } else { + logger.warn("Decryption failed or insufficient data length to strike header"); } - return; - } else { - logger.warn("Decryption failed or insufficient data length to strike header"); } return; } else { @@ -464,6 +419,88 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } } + private void handleResponse(byte[] data, byte bodyType, @Nullable Callback callback) throws MideaException { + logger.trace("Response bodyType: {}", bodyType); + logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, + Utils.bytesToHex(data)); + logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, + Utils.bytesToBinary(data)); + + // Validate the proper length + if (data.length < 21) { + logger.warn("Response data is {} long, minimum is 21!", data.length); + return; + } + + // Scan for error code + if (bodyType == (byte) 0x1E) { + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); + return; + } + + // Handle the capabilities response + if (bodyType == (byte) 0xB5) { + try { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); + } + } catch (Exception ex) { + logger.debug("Capability response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + return; + } + + // Handle the Energy Response + if (bodyType == (byte) 0xC1) { + try { + logger.debug("Energy response detected with bodyType 0xC1."); + EnergyResponse energyUpdate = new EnergyResponse(data); + if (callback != null) { + callback.updateChannels(energyUpdate); + } + } catch (Exception ex) { + logger.debug("Energy response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + return; + } + + // Handle notify2 message (bodyType 0xA0) + if (bodyType == (byte) 0xA0) { + try { + logger.debug("Response detected with bodyType 0xA0. Data length: {}", data.length); + HumidityResponse humidityResponse = new HumidityResponse(data); + if (callback != null) { + callback.updateChannels(humidityResponse); + } else { + logger.debug("Callback is null for unsolicited humidity response, channels not updated."); + } + } catch (Exception ex) { + logger.debug("Unsolicited response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + return; + } + + if (bodyType != (byte) 0xC0) { + logger.warn("Unexpected response bodyType {}", bodyType); + return; + } + lastResponse = new Response(data, version); + try { + logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); + } + } catch (Exception ex) { + logger.debug("Poll response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + } + /** * Closes all elements of the connection before starting a new one * Makes sure writer, inputStream and socket are closed before each command is started diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 56476120e1c25..80fde48c5cc1e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -45,4 +45,11 @@ public interface Callback { * @param energyResponse The Energy response from the device used to update properties. */ void updateChannels(EnergyResponse energyResponse); + + /** + * Updates channels with a Humidity response. + * + * @param humidityResponse The unsolicited Humidity response from the device used to update properties. + */ + void updateChannels(HumidityResponse humidityResponse); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 76847efedf7c0..2148cd0435286 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -70,6 +70,7 @@ public static CommandSet fromResponse(Response response) { commandSet.setSleepMode(response.getSleepFunction()); commandSet.setOnTimer(response.getOnTimerData()); commandSet.setOffTimer(response.getOffTimerData()); + commandSet.setTargetHumidity(response.getTargetHumidity()); return commandSet; } @@ -479,4 +480,13 @@ private void removeExtraEnergyPollBytes() { System.arraycopy(data, 0, newData, 0, newData.length); data = newData; } + + /** + * Sets the Target Humidity for Dry Mode + * + * @param targetHumidity + */ + public void setTargetHumidity(int humidity) { + data[0x1D] |= humidity; + } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/HumidityResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/HumidityResponse.java new file mode 100644 index 0000000000000..b2115c51ea6cc --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/HumidityResponse.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mideaac.internal.handler.CommandBase.FanSpeed; +import org.openhab.binding.mideaac.internal.handler.CommandBase.OperationalMode; +import org.openhab.binding.mideaac.internal.handler.CommandBase.SwingMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link HumidityResponse} handles the unsolicited 0xA0 humidity report messages + * from the device + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class HumidityResponse { + private final byte[] rawData; + private Logger logger = LoggerFactory.getLogger(HumidityResponse.class); + private int version = 3; + + /** + * Initialization + * + * @param rawData as bytes + */ + public HumidityResponse(byte[] rawData) { + this.rawData = rawData; + if (logger.isDebugEnabled()) { + logger.debug("Humidity: {}", getHumidity()); + logger.debug("Power from 0xA0 {}", getPowerState()); + logger.debug("Operational Mode from 0xA0 {}", getOperationalMode()); + logger.debug("Target Temperature from 0xA0 {}", getTargetTemperature()); + logger.debug("Fan Speed from 0xA0 {}", getFanSpeed()); + logger.debug("Swing Mode from 0xA0 {}", getSwingMode()); + } + } + + /** + * Reported Room Humidity from 0xA0 message type + * + * @return humidity + */ + public int getHumidity() { + return (rawData[13] & (byte) 0x7f); + } + + /** + * Power from 0xA0 message type + * not used in channels + * + * @return power status + */ + public boolean getPowerState() { + return (rawData[0x01] & 0x1) > 0; + } + + /** + * Cool, Heat, Fan Only, etc. from 0xA0 message type + * Not used in Channels + * + * @return Cool, Heat, Fan Only, etc. + */ + public OperationalMode getOperationalMode() { + return OperationalMode.fromId((rawData[0x02] & 0xe0) >> 5); + } + + /** + * Target Temperature from 0xA0 message type - Different + * Not used in Channels + * + * @return current setpoint in degrees C + */ + public float getTargetTemperature() { + return ((rawData[0x01] & 0x3E) >> 1) + 12.0f + (((rawData[0x01] & 0x40) >> 6 > 0) ? 0.5f : 0.0f); + } + + /** + * Low, Medium, High, Auto etc. See Command Base class + * From message 0XA0; assumed version 3 for this message + * Not Used in Channels + * + * @return Low, Medium, High, Auto etc. + */ + public FanSpeed getFanSpeed() { + return FanSpeed.fromId(rawData[0x03] & 0x7f, version); + } + + /** + * Status of the vertical and/or horzontal louver + * From message 0XA0; assumed version 3 for this message + * Not Used in Channels + * + * @return Vertical, Horizontal, Off, Both + */ + public SwingMode getSwingMode() { + return SwingMode.fromId(rawData[0x07] & 0x3f, version); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index 3e9be7ae4b063..c0baa8d029689 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -102,6 +102,11 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { public void updateChannels(EnergyResponse energyUpdate) { MideaACHandler.this.updateChannels(energyUpdate); } + + @Override + public void updateChannels(HumidityResponse humidityResponse) { + MideaACHandler.this.updateChannels(humidityResponse); + } }; /** @@ -176,6 +181,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { connectionManager.sendCommand(CommandHelper.handleOnTimer(command, lastresponse), callbackLambda); } else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) { connectionManager.sendCommand(CommandHelper.handleOffTimer(command, lastresponse), callbackLambda); + } else if (channelUID.getId().equals(CHANNEL_TARGET_HUMIDITY)) { + connectionManager.sendCommand(CommandHelper.handleTargetHumidity(command, lastresponse), + callbackLambda); } } catch (MideaConnectionException | MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); @@ -367,7 +375,7 @@ public void updateChannels(Response response) { updateChannel(CHANNEL_SLEEP_FUNCTION, OnOffType.from(response.getSleepFunction())); updateChannel(CHANNEL_TURBO_MODE, OnOffType.from(response.getTurboMode())); updateChannel(CHANNEL_SCREEN_DISPLAY, OnOffType.from(response.getDisplayOn())); - updateChannel(CHANNEL_HUMIDITY, new DecimalType(response.getHumidity())); + updateChannel(CHANNEL_TARGET_HUMIDITY, new DecimalType(response.getTargetHumidity())); QuantityType targetTemperature = new QuantityType(response.getTargetTemperature(), SIUnits.CELSIUS); @@ -455,6 +463,12 @@ public void updateChannels(EnergyResponse energyUpdate) { } } + // Handle Humidity response from room + @Override + public void updateChannels(HumidityResponse humidityResponse) { + updateChannel(CHANNEL_HUMIDITY, new DecimalType(humidityResponse.getHumidity())); + } + @Override public void discovered(DiscoveryResult discoveryResult) { logger.debug("Discovered {}", thing.getUID()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 2616e7a8d628e..17bf2e9ff58c5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -66,6 +66,7 @@ public Response(byte[] data, int version) { logger.debug("Indoor Temperature: {}", getIndoorTemperature()); logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); logger.debug("LED Display: {}", getDisplayOn()); + logger.debug("Target Humidity: {}", getTargetHumidity()); } if (logger.isTraceEnabled()) { @@ -73,7 +74,6 @@ public Response(byte[] data, int version) { logger.trace("Appliance Error: {}", getApplianceError()); logger.trace("Auxiliary Heat: {}", getAuxHeat()); logger.trace("Fahrenheit: {}", getFahrenheit()); - logger.trace("Humidity: {}", getHumidity()); } } @@ -298,12 +298,12 @@ public boolean getDisplayOn() { } /** - * Not observed with units being tested - * From reference Document + * This appears to be the target humidity for Dry mode * - * @return humidity + * + * @return Target Humidity */ - public int getHumidity() { + public int getTargetHumidity() { return (data[19] & (byte) 0x7f); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index a4001830c334b..38828f673bee5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -21,8 +21,8 @@ thing-type.config.mideaac.ac.email.label = Email thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. thing-type.config.mideaac.ac.energyDecode.label = Energy response decode method thing-type.config.mideaac.ac.energyDecode.description = Binary-Coded Decimal (BCD) = true. Big-endian = false. -thing-type.config.mideaac.ac.energyPoll.label = Energy poll frequency in minutes -thing-type.config.mideaac.ac.energyPoll.description = Energy poll (when AC is On), default 0, disabled. (in case not supported). +thing-type.config.mideaac.ac.energyPoll.label = Energy poll in minutes +thing-type.config.mideaac.ac.energyPoll.description = Energy polling frequency. default 0 ; (in case not supported). thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port @@ -61,7 +61,7 @@ channel-type.mideaac.fan-speed.state.option.HIGH = HIGH channel-type.mideaac.fan-speed.state.option.FULL = FULL channel-type.mideaac.fan-speed.state.option.AUTO = AUTO channel-type.mideaac.humidity.label = Humidity -channel-type.mideaac.humidity.description = Humidity measured in the room by the indoor unit. +channel-type.mideaac.humidity.description = Humidity in room (if supported). channel-type.mideaac.indoor-temperature.label = Indoor Temperature channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off channel-type.mideaac.kilowatt-hours.label = Kilowatt Hours @@ -91,6 +91,8 @@ channel-type.mideaac.swing-mode.state.option.OFF = OFF channel-type.mideaac.swing-mode.state.option.VERTICAL = VERTICAL channel-type.mideaac.swing-mode.state.option.HORIZONTAL = HORIZONTAL channel-type.mideaac.swing-mode.state.option.BOTH = BOTH +channel-type.mideaac.target-humidity.label = Target Humidity +channel-type.mideaac.target-humidity.description = Set Target Humidity for Dry Mode (if supported). channel-type.mideaac.target-temperature.label = Target Temperature channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index e925e7839a2e2..7033c1af50ac3 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -28,6 +28,7 @@ + @@ -100,8 +101,8 @@ energyPoll - - Energy poll (when AC is On), default 0, disabled. (in case not supported). + + Energy polling frequency. default 0 ; (in case not supported). 0 @@ -260,10 +261,17 @@ Switch + + Number + + Set Target Humidity for Dry Mode (if supported). + Humidity + + Number - Humidity measured in the room by the indoor unit. + Humidity in room (if supported). Humidity diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/HumidityResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/HumidityResponseTest.java new file mode 100644 index 0000000000000..55db706aa490f --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/HumidityResponseTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * The {@link HumidityResponseTest} tests the methods in the HumidityResponse class + * against an example response string. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class HumidityResponseTest { + @org.jupnp.registry.event.Before + + // From OH forum; Midea topic July 2025 + byte[] data = HexFormat.of().parseHex("A01240660000003C00000000003600000000000000004234EA"); + HumidityResponse response = new HumidityResponse(data); + + /** + * Humidity Test + */ + @Test + public void testGetHumidity() { + assertEquals(54, response.getHumidity()); + } + + /** + * Power State + */ + @Test + public void testPowerState() { + assertEquals(false, response.getPowerState()); + } + + /** + * Target Temperature from 0XA0 Message + */ + @Test + public void testTargetTemperature() { + assertEquals(21.0, response.getTargetTemperature()); + } + + /** + * Operational mode from 0XA0 Message + */ + @Test + public void testOperationalMode() { + assertEquals(CommandBase.OperationalMode.COOL, response.getOperationalMode()); + } + + /** + * Fan Speed from 0XA0 Message + */ + @Test + public void testFanSpeed() { + assertEquals(CommandBase.FanSpeed.AUTO3, response.getFanSpeed()); + } + + /** + * Swing Mode from 0XA0 Message + */ + @Test + public void testSwingMode() { + assertEquals(CommandBase.SwingMode.VERTICAL3, response.getSwingMode()); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index b9a019e457813..9ffa28cede15f 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -182,6 +182,6 @@ public void testDisplayOn() { */ @Test public void testGetHumidity() { - assertEquals(50, response.getHumidity()); + assertEquals(50, response.getTargetHumidity()); } } From 885150a28cb88beb5823efd10ad873d2868bef32 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 14 Jul 2025 17:41:48 -0400 Subject: [PATCH 34/44] Add support for maximum humidity and filter status channels Replaces 'target-humidity' with 'maximum-humidity' channel and adds 'filter-status' channel for Midea AC binding. Implements handling for new unsolicited temperature (0xA1) and humidity (0xA0, 0xC1) responses, updates command and response processing, and extends tests for new features. Updates documentation and i18n resources to reflect these changes. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 48 +++--- .../internal/MideaACBindingConstants.java | 3 +- .../internal/connection/CommandHelper.java | 6 +- .../connection/ConnectionManager.java | 147 ++++++++++-------- .../discovery/MideaACDiscoveryService.java | 2 +- .../mideaac/internal/handler/Callback.java | 30 +++- .../mideaac/internal/handler/CommandSet.java | 42 ++++- .../internal/handler/EnergyResponse.java | 29 +++- .../internal/handler/MideaACHandler.java | 54 ++++++- .../mideaac/internal/handler/Response.java | 21 ++- .../internal/handler/TemperatureResponse.java | 98 ++++++++++++ .../resources/OH-INF/i18n/mideaac.properties | 5 +- .../resources/OH-INF/thing/thing-types.xml | 15 +- .../internal/handler/EnergyResponseTest.java | 10 ++ .../internal/handler/ResponseTest.java | 10 +- .../handler/TemperatureResponseTest.java | 68 ++++++++ 16 files changed, 463 insertions(+), 125 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java create mode 100644 bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponseTest.java diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index a0ee916141125..d632ff0cfb037 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -51,28 +51,30 @@ No binding configuration is required. Following channels are available: -| Channel | Type | Description | Read only | Advanced | -|------------------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------| -| power | Switch | Turn the AC on or off. | | | -| target-temperature | Number:Temperature | Target temperature. | | | -| operational-mode | String | Operational modes: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | | -| fan-speed | String | Fan speeds: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | -| swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | -| eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | -| turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT mode only. | | | -| sleep-function | Switch | Sleep function ("Moon with a star" icon on IR Remote Controller). | | | -| indoor-temperature | Number:Temperature | Indoor temperature measured in the room, where internal unit is installed. | Yes | | -| outdoor-temperature | Number:Temperature | Outdoor temperature by external unit. Some units do not report reading when off. | Yes | | -| temperature-unit | Switch | Sets the LED display on the evaporator to Fahrenheit (true) or Celsius (false). | | Yes | -| on-timer | String | Sets the future time to turn on the AC. | | Yes | -| off-timer | String | Sets the future time to turn off the AC. | | Yes | -| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | -| humidity | Number | If device supports, the indoor humidity. | Yes | Yes | -| kilowatt-hours | Number | If device supports, cumulative KWH usage | Yes | Yes | -| amperes | Number | If device supports, current amperage usage | Yes | Yes | -| watts | Number | If device supports, wattage | Yes | Yes | -| appliance-error | Switch | If device supports, appliance error | Yes | Yes | -| auxiliary-heat | Switch | If device supports, auxiliary heat | Yes | Yes | +| Channel | Type | Description | Read only | Advanced | +|----------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------| +| power | Switch | Turn the AC on or off. | | | +| target-temperature | Number:Temperature | Target temperature. | | | +| operational-mode | String | Operational modes: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | | +| fan-speed | String | Fan speeds: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | | +| swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | +| eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | +| turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT mode only. | | | +| sleep-function | Switch | Sleep function ("Moon with a star" icon on IR Remote Controller). | | | +| indoor-temperature | Number:Temperature | Indoor temperature measured in the room, where internal unit is installed. | Yes | | +| outdoor-temperature | Number:Temperature | Outdoor temperature by external unit. Some units do not report reading when off. | Yes | | +| temperature-unit | Switch | Sets the LED display on the evaporator to Fahrenheit (true) or Celsius (false). | | Yes | +| on-timer | String | Sets the future time to turn on the AC. | | Yes | +| off-timer | String | Sets the future time to turn off the AC. | | Yes | +| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | +| maximum-humidity | Number | If device supports, allows setting the maximum humidity in DRY mode | | Yes +| humidity | Number | If device supports, the indoor room humidity. | Yes | Yes | +| kilowatt-hours | Number | If device supports, cumulative KWH usage | Yes | Yes | +| amperes | Number | If device supports, current amperage usage | Yes | Yes | +| watts | Number | If device supports, current wattage reading | Yes | Yes | +| appliance-error | Switch | If device supports, appliance error notification | Yes | Yes | +| filter-status | Switch | If device supports, notification that filter needs cleaning | Yes | Yes | +| auxiliary-heat | Switch | If device supports, auxiliary heat (On or Off) | Yes | Yes | ## Examples @@ -107,7 +109,7 @@ Switch temperature_unit "Fahrenheit or Celsius" { ch ### `demo.sitemap` Examples ```java -sitemap midea label="Split AC"{ +sitemap midea label="Midea AC"{ Frame label="AC Unit" { Text item=outdoor_temperature label="Outdoor Temperature [%.1f °F]" Text item=indoor_temperature label="Indoor Temperature [%.1f °F]" diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index a7fdbe1f0468f..f83a423aa1b0d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -59,9 +59,10 @@ public class MideaACBindingConstants { public static final String CHANNEL_TURBO_MODE = "turbo-mode"; public static final String CHANNEL_INDOOR_TEMPERATURE = "indoor-temperature"; public static final String CHANNEL_OUTDOOR_TEMPERATURE = "outdoor-temperature"; - public static final String CHANNEL_TARGET_HUMIDITY = "target-humidity"; + public static final String CHANNEL_MAXIMUM_HUMIDITY = "maximum-humidity"; public static final String CHANNEL_HUMIDITY = "humidity"; public static final String CHANNEL_SCREEN_DISPLAY = "screen-display"; + public static final String CHANNEL_FILTER_STATUS = "filter-status"; public static final String CHANNEL_KILOWATT_HOURS = "kilowatt-hours"; public static final String CHANNEL_AMPERES = "amperes"; public static final String CHANNEL_WATTS = "watts"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index ab9daa10bdfa6..7c5f4d24d2bbb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -455,14 +455,14 @@ private static int limitHumidityToRange(int humidity) { * * @param command Target Humidity */ - public static CommandSet handleTargetHumidity(Command command, Response lastResponse) { + public static CommandSet handleMaximumHumidity(Command command, Response lastResponse) { CommandSet commandSet = CommandSet.fromResponse(lastResponse); if (command instanceof DecimalType decimalCommand) { int humidity = decimalCommand.intValue(); - commandSet.setTargetHumidity(limitHumidityToRange(humidity)); + commandSet.setMaximumHumidity(limitHumidityToRange(humidity)); } else { - logger.debug("Unknown target humidity command: {}", command); + logger.debug("Unknown maximum humidity command: {}", command); } return commandSet; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index cb12b5ad76498..d723e132a9e17 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -37,6 +37,7 @@ import org.openhab.binding.mideaac.internal.handler.HumidityResponse; import org.openhab.binding.mideaac.internal.handler.Packet; import org.openhab.binding.mideaac.internal.handler.Response; +import org.openhab.binding.mideaac.internal.handler.TemperatureResponse; import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse; import org.openhab.binding.mideaac.internal.security.Decryption8370Result; import org.openhab.binding.mideaac.internal.security.Security; @@ -49,7 +50,8 @@ * indoor AC unit evaporator. * * @author Jacek Dobrowolski - Initial Contribution - * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command + * @author Bob Eckhoff - Revised logic to reconnect with security before each poll or command. Also added additional + * command processing. * * This gets around the issue that any command needs to be within 30 seconds of the authorization * in testing this only adds 50 ms, but allows Scheduled polls at longer intervals. @@ -421,9 +423,9 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal private void handleResponse(byte[] data, byte bodyType, @Nullable Callback callback) throws MideaException { logger.trace("Response bodyType: {}", bodyType); - logger.trace("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, + logger.debug("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToHex(data)); - logger.debug("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, + logger.trace("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToBinary(data)); // Validate the proper length @@ -432,72 +434,93 @@ private void handleResponse(byte[] data, byte bodyType, @Nullable Callback callb return; } - // Scan for error code - if (bodyType == (byte) 0x1E) { - logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); - return; - } + switch (bodyType) { + case (byte) 0x1E: + logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress); + return; - // Handle the capabilities response - if (bodyType == (byte) 0xB5) { - try { - logger.debug("Capabilities response detected with bodyType 0xB5."); - CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); - if (callback != null) { - callback.updateChannels(capabilitiesResponse); + case (byte) 0xB5: + try { + logger.debug("Capabilities response detected with bodyType 0xB5."); + CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data); + if (callback != null) { + callback.updateChannels(capabilitiesResponse); + } + } catch (Exception ex) { + logger.debug("Capability response exception: {}", ex.getMessage()); + throw new MideaException(ex); } - } catch (Exception ex) { - logger.debug("Capability response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } - return; - } + return; - // Handle the Energy Response - if (bodyType == (byte) 0xC1) { - try { - logger.debug("Energy response detected with bodyType 0xC1."); - EnergyResponse energyUpdate = new EnergyResponse(data); - if (callback != null) { - callback.updateChannels(energyUpdate); + case (byte) 0xC1: + try { + logger.debug("Energy/Humidity response detected with bodyType 0xC1."); + EnergyResponse energyUpdate = new EnergyResponse(data); + // Separate Humidity from Energy + if (callback != null) { + if (data[3] == (byte) 0x45) { + callback.updateHumidityFromEnergy(energyUpdate); + } else { + callback.updateChannels(energyUpdate); + } + } + } catch (Exception ex) { + logger.debug("Energy/Humidity response exception: {}", ex.getMessage()); + throw new MideaException(ex); } - } catch (Exception ex) { - logger.debug("Energy response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } - return; - } + return; - // Handle notify2 message (bodyType 0xA0) - if (bodyType == (byte) 0xA0) { - try { - logger.debug("Response detected with bodyType 0xA0. Data length: {}", data.length); - HumidityResponse humidityResponse = new HumidityResponse(data); - if (callback != null) { - callback.updateChannels(humidityResponse); - } else { - logger.debug("Callback is null for unsolicited humidity response, channels not updated."); + case (byte) 0xA0: + try { + logger.debug("Response detected with bodyType 0xA0. Data length: {}", data.length); + HumidityResponse humidityResponse = new HumidityResponse(data); + if (callback != null) { + callback.updateChannels(humidityResponse); + } else { + logger.debug("Callback is null for unsolicited 0xA0 humidity response, channels not updated."); + } + } catch (Exception ex) { + logger.debug("Unsolicited 0xA0 response exception: {}", ex.getMessage()); + throw new MideaException(ex); } - } catch (Exception ex) { - logger.debug("Unsolicited response exception: {}", ex.getMessage()); - throw new MideaException(ex); - } - return; - } + return; - if (bodyType != (byte) 0xC0) { - logger.warn("Unexpected response bodyType {}", bodyType); - return; - } - lastResponse = new Response(data, version); - try { - logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, ipAddress); - if (callback != null) { - callback.updateChannels(lastResponse); - } - } catch (Exception ex) { - logger.debug("Poll response exception: {}", ex.getMessage()); - throw new MideaException(ex); + case (byte) 0xA1: + try { + logger.debug("Response detected with bodyType 0xA1. Data length: {}", data.length); + TemperatureResponse temperatureResponse = new TemperatureResponse(data); + if (callback != null) { + callback.updateChannels(temperatureResponse); + } else { + logger.debug( + "Callback is null for unsolicited 0xA1 temperature response, channels not updated."); + } + } catch (Exception ex) { + logger.debug("Unsolicited 0xA1 response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + return; + + case (byte) 0xA5: + // A500000000 0000000000 0000000000 0000000000 0000005F13 FF00000000 0000236C7C + logger.debug("Not fully documented, possible filter runtime in minutes"); + return; + + case (byte) 0xC0: + lastResponse = new Response(data, version); + try { + logger.trace("Data length is {}, version is {}, IP address is {}", data.length, version, ipAddress); + if (callback != null) { + callback.updateChannels(lastResponse); + } + } catch (Exception ex) { + logger.debug("Poll response exception: {}", ex.getMessage()); + throw new MideaException(ex); + } + return; + + default: + logger.warn("Unexpected response bodyType {}", bodyType); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index a3e89937b2fdf..b42c95fe8ecc4 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -53,7 +53,7 @@ @Component(service = DiscoveryService.class, configurationPid = "discovery.mideaac") public class MideaACDiscoveryService extends AbstractDiscoveryService { - private static int discoveryTimeoutSeconds = 5; + private static int discoveryTimeoutSeconds = 10; private final int receiveJobTimeout = 20000; private final int udpPacketTimeout = receiveJobTimeout - 50; private final String mideaacNamePrefix = "MideaAC"; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java index 80fde48c5cc1e..cad9e8a70f102 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Callback.java @@ -19,37 +19,53 @@ * The {@link Response} performs the polling byte data stream decoding * The {@link CapabilitiesResponse} performs the capability byte data stream decoding * The {@link EnergyResponse} performs the energy byte stream data decoding + * The {@link HumidityResponse} performs decoding of unsolicited message 0xA0 + * The {@link TemperatureResponse} performs decoding of unsolicited message 0xA1 * * @author Leo Siepel - Initial contribution - * @author Bob Eckhoff - added Capabilities and Energy Callbacks + * @author Bob Eckhoff - added additional Callbacks after Response */ @NonNullByDefault public interface Callback { /** - * Updates channels with a standard response. + * Updates channels with a standard response (0xC0). * * @param response The standard response from the device used to update channels. */ void updateChannels(Response response); /** - * Updates channels with a capabilities response. + * Updates channels with a capabilities response (0xB5). * * @param capabilitiesResponse The capabilities response from the device used to update properties. */ void updateChannels(CapabilitiesResponse capabilitiesResponse); /** - * Updates channels with a Energy response. + * Updates channels with a Energy response (0xC1 - 0x44). * - * @param energyResponse The Energy response from the device used to update properties. + * @param energyResponse The Energy response from the device used to update energy. */ void updateChannels(EnergyResponse energyResponse); /** - * Updates channels with a Humidity response. + * Updates humidity with a Energy response (0xC1 - 0x45). * - * @param humidityResponse The unsolicited Humidity response from the device used to update properties. + * @param energyResponse The Energy response from a humidity Poll used to update humidity. + */ + void updateHumidityFromEnergy(EnergyResponse energyResponse); + + /** + * Updates channels with an unsolicted Humidity Response (0xA0). + * + * @param humidityResponse The unsolicited (0xA0) response from the device used to update properties. */ void updateChannels(HumidityResponse humidityResponse); + + /** + * Updates channels with an unsolicited Temperature response (0xA1). + * + * @param temperatureResponse The unsolicited (0xA1) response from the device used to update properties. + */ + void updateChannels(TemperatureResponse temperatureResponse); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 2148cd0435286..cd1ece35642e5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -70,7 +70,7 @@ public static CommandSet fromResponse(Response response) { commandSet.setSleepMode(response.getSleepFunction()); commandSet.setOnTimer(response.getOnTimerData()); commandSet.setOffTimer(response.getOffTimerData()); - commandSet.setTargetHumidity(response.getTargetHumidity()); + commandSet.setMaximumHumidity(response.getMaximumHumidity()); return commandSet; } @@ -482,11 +482,45 @@ private void removeExtraEnergyPollBytes() { } /** - * Sets the Target Humidity for Dry Mode + * Humidity detail polling + * Response will be C1, not C0 + * + */ + public void humidityPoll() { + modifyBytesForHumidityPoll(); + removeExtraHumidityPollBytes(); + logger.trace("Set Humidity Poll Bytes before encrypt {}", Utils.bytesToHex(data)); + } + + private void modifyBytesForHumidityPoll() { + data[0x01] = (byte) 0x20; + data[0x09] = (byte) 0x03; + data[0x0a] = (byte) 0x41; + data[0x0b] = (byte) 0x21; + data[0x0c] = (byte) 0x01; + data[0x0d] = (byte) 0x45; + data[0x0e] = (byte) 0x00; + data[0x0f] = (byte) 0x00; + data[0x10] = (byte) 0x00; + data[0x11] = (byte) 0x00; + data[0x12] = (byte) 0x00; + data[0x13] = (byte) 0x00; + data[0x14] = (byte) 0x00; + } + + private void removeExtraHumidityPollBytes() { + byte[] newData = new byte[data.length - 3]; + System.arraycopy(data, 0, newData, 0, newData.length); + data = newData; + } + + /** + * Sets the Maximum Humidity for Dry Mode * - * @param targetHumidity + * @param humidity */ - public void setTargetHumidity(int humidity) { + public void setMaximumHumidity(int humidity) { + data[0x1D] &= ~(byte) 0xff; data[0x1D] |= humidity; } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java index ce121d53ea3b0..05ab2035b97f2 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/EnergyResponse.java @@ -38,15 +38,32 @@ public EnergyResponse(byte[] rawData) { this.rawData = rawData; if (logger.isDebugEnabled()) { - logger.debug("Total Kilowatt Hours: {}", getKilowattHours()); - logger.debug("Current Amperes: {}", getAmperes()); - logger.debug("Power Watts: {}", getWatts()); - logger.debug("Total Kilowatt Hours BCD: {}", getKilowattHoursBCD()); - logger.debug("Current Amperes BCD: {}", getAmperesBCD()); - logger.debug("Power Watts BCD: {}", getWattsBCD()); + if (isHumidityResponse()) { + logger.debug("Humidity from Poll: {}", getHumidity()); + } else { + logger.debug("Total Kilowatt Hours: {}", getKilowattHours()); + logger.debug("Current Amperes: {}", getAmperes()); + logger.debug("Power Watts: {}", getWatts()); + logger.debug("Total Kilowatt Hours BCD: {}", getKilowattHoursBCD()); + logger.debug("Current Amperes BCD: {}", getAmperesBCD()); + logger.debug("Power Watts BCD: {}", getWattsBCD()); + } } } + private boolean isHumidityResponse() { + return rawData.length > 3 && rawData[3] == (byte) 0x45; + } + + /** + * Humidity reading if Byte[3] == (byte) 0x45 + * + * @return humidity + */ + public int getHumidity() { + return rawData[4]; + } + /** * Kilowatt Hours using binary * diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index c0baa8d029689..eb05fbe0e415e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -69,7 +69,7 @@ * * @author Jacek Dobrowolski - Initial contribution * @author Justan Oldman - Last Response added - * @author Bob Eckhoff - Longer Polls and OH developer guidelines + * @author Bob Eckhoff - Longer Polls, OH developer guidelines added other messages * @author Leo Siepel - Refactored class, improved separation of concerns */ @NonNullByDefault @@ -103,10 +103,20 @@ public void updateChannels(EnergyResponse energyUpdate) { MideaACHandler.this.updateChannels(energyUpdate); } + @Override + public void updateHumidityFromEnergy(EnergyResponse energyUpdate) { + MideaACHandler.this.updateHumidityFromEnergy(energyUpdate); + } + @Override public void updateChannels(HumidityResponse humidityResponse) { MideaACHandler.this.updateChannels(humidityResponse); } + + @Override + public void updateChannels(TemperatureResponse temperatureResponse) { + MideaACHandler.this.updateChannels(temperatureResponse); + } }; /** @@ -138,10 +148,14 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (command instanceof RefreshType) { try { connectionManager.getStatus(callbackLambda); - // Read only Energy channels not updated with routine poll + // Read only Energy and Humidity channels not updated with routine poll CommandSet energyUpdate = new CommandSet(); energyUpdate.energyPoll(); connectionManager.sendCommand(energyUpdate, this); + CommandSet humidityUpdate = new CommandSet(); + humidityUpdate.humidityPoll(); + connectionManager.sendCommand(humidityUpdate, this); + } catch (MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (MideaConnectionException | MideaException e) { @@ -181,8 +195,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { connectionManager.sendCommand(CommandHelper.handleOnTimer(command, lastresponse), callbackLambda); } else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) { connectionManager.sendCommand(CommandHelper.handleOffTimer(command, lastresponse), callbackLambda); - } else if (channelUID.getId().equals(CHANNEL_TARGET_HUMIDITY)) { - connectionManager.sendCommand(CommandHelper.handleTargetHumidity(command, lastresponse), + } else if (channelUID.getId().equals(CHANNEL_MAXIMUM_HUMIDITY)) { + connectionManager.sendCommand(CommandHelper.handleMaximumHumidity(command, lastresponse), callbackLambda); } } catch (MideaConnectionException | MideaAuthenticationException e) { @@ -375,7 +389,8 @@ public void updateChannels(Response response) { updateChannel(CHANNEL_SLEEP_FUNCTION, OnOffType.from(response.getSleepFunction())); updateChannel(CHANNEL_TURBO_MODE, OnOffType.from(response.getTurboMode())); updateChannel(CHANNEL_SCREEN_DISPLAY, OnOffType.from(response.getDisplayOn())); - updateChannel(CHANNEL_TARGET_HUMIDITY, new DecimalType(response.getTargetHumidity())); + updateChannel(CHANNEL_FILTER_STATUS, OnOffType.from(response.getFilterStatus())); + updateChannel(CHANNEL_MAXIMUM_HUMIDITY, new DecimalType(response.getMaximumHumidity())); QuantityType targetTemperature = new QuantityType(response.getTargetTemperature(), SIUnits.CELSIUS); @@ -463,12 +478,39 @@ public void updateChannels(EnergyResponse energyUpdate) { } } - // Handle Humidity response from room + // Handle Humidity from energy poll command (0xC1) + @Override + public void updateHumidityFromEnergy(EnergyResponse energyUpdate) { + updateChannel(CHANNEL_HUMIDITY, new DecimalType(energyUpdate.getHumidity())); + } + + // Handle unsolicted Humidity response in room (0xA0) @Override public void updateChannels(HumidityResponse humidityResponse) { updateChannel(CHANNEL_HUMIDITY, new DecimalType(humidityResponse.getHumidity())); } + // Handle unsolicted Temperature response in room (0xA1) Humidity only for now + @Override + public void updateChannels(TemperatureResponse temperatureResponse) { + updateChannel(CHANNEL_HUMIDITY, new DecimalType(temperatureResponse.getHumidity())); + + // QuantityType outdoorTemperature = new + // QuantityType(temperatureResponse.getOutdoorTemperature(), + // SIUnits.CELSIUS); + // QuantityType indoorTemperature = new + // QuantityType(temperatureResponse.getIndoorTemperature(), + // SIUnits.CELSIUS); + + // if (imperialUnits) { + // indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + // outdoorTemperature = Objects.requireNonNull(outdoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + // } + + // updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature); + // updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); + } + @Override public void discovered(DiscoveryResult discoveryResult) { logger.debug("Discovered {}", thing.getUID()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 17bf2e9ff58c5..38e8710784d97 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -66,7 +66,6 @@ public Response(byte[] data, int version) { logger.debug("Indoor Temperature: {}", getIndoorTemperature()); logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); logger.debug("LED Display: {}", getDisplayOn()); - logger.debug("Target Humidity: {}", getTargetHumidity()); } if (logger.isTraceEnabled()) { @@ -74,6 +73,8 @@ public Response(byte[] data, int version) { logger.trace("Appliance Error: {}", getApplianceError()); logger.trace("Auxiliary Heat: {}", getAuxHeat()); logger.trace("Fahrenheit: {}", getFahrenheit()); + logger.trace("Maximum Humidity in Dry mode: {}", getMaximumHumidity()); + logger.trace("Filter Notification: {}", getFilterStatus()); } } @@ -286,6 +287,16 @@ public Float getOutdoorTemperature() { return 0.0f; } + /** + * Returns Filter Status + * Documents show two possibles, hedged with both + * + * @return Filter needs changing = true + */ + public boolean getFilterStatus() { + return ((data[13] & (byte) 0x20) != 0) || ((data[13] & (byte) 0x40) != 0); + } + /** * Returns status of Device LEDs * This is not affected when the IR controller turns @@ -298,12 +309,12 @@ public boolean getDisplayOn() { } /** - * This appears to be the target humidity for Dry mode - * + * This returns the maximum humidity for Dry mode, if supported + * Possibly an add-on sensor is required in some cases * - * @return Target Humidity + * @return Maximum Humidity in Dry Mode */ - public int getTargetHumidity() { + public int getMaximumHumidity() { return (data[19] & (byte) 0x7f); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java new file mode 100644 index 0000000000000..3aedecfde1b2d --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link TemperatureResponse} handles the unsolicited 0xA1 temperature report messages + * from the device + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class TemperatureResponse { + private final byte[] rawData; + private Logger logger = LoggerFactory.getLogger(TemperatureResponse.class); + + /** + * Initialization + * + * @param rawData as bytes + */ + public TemperatureResponse(byte[] rawData) { + this.rawData = rawData; + if (logger.isDebugEnabled()) { + logger.debug("Humidity from 0xA1: {}", getHumidity()); + logger.debug("Indoor Temperature from 0xA1: {}", getIndoorTemperature()); + logger.debug("Outdoor Temperature from 0xA1: {}", getOutdoorTemperature()); + logger.debug("Current Work Time (minutes) from 0xA1: {}", getCurrentWorkTime()); + } + } + + /** + * Reported Room Humidity from 0xA1 message type + * + * @return humidity + */ + public int getHumidity() { + return (rawData[17] & (byte) 0x7f); + } + + /** + * Current Work Time from 0xA1 message type + * not validated from test + * + * @return CurrentWorkTime + */ + public int getCurrentWorkTime() { + return ((((rawData[9] & 0xFF) << 8) & 0xFF00) | (rawData[10] & 0x00FF)) * 60 * 24 + (rawData[11] & 0xFF) * 60 + + (rawData[12] & 0xFF); + } + + /** + * Reported indoor temperature from 0xA1 message type + * + * @return indoor temperature + */ + public Float getIndoorTemperature() { + Float indoorTemperatureValue = (float) 0.0; + Float smallIndoorTemperatureValue = (float) 0.0; + Float indoorTemperature = (float) 0.0; + if ((rawData[13] & 0xFF) != 0x00 && (rawData[13] & 0xFF) != 0xFF) { + indoorTemperatureValue = (float) ((rawData[13] & 0xFF) - 50.0f) / 2.0f; + smallIndoorTemperatureValue = (float) (rawData[18] & 0x0F); + indoorTemperature = indoorTemperatureValue + smallIndoorTemperatureValue / 10f; + } + return indoorTemperature; + } + + /** + * Reported Outdoor Temperature from 0xA1 message type + * + * @return outdoor temperature + */ + public Float getOutdoorTemperature() { + Float outdoorTemperatureValue = (float) 0.0; + Float smallOutdoorTemperatureValue = (float) 0.0; + Float outdoorTemperature = (float) 0.0; + if ((rawData[14] & 0xFF) != 0x00 && (rawData[14] & 0xFF) != 0xFF) { + outdoorTemperatureValue = (float) ((rawData[14] & 0xFF) - 50.0f) / 2.0f; + smallOutdoorTemperatureValue = (float) ((rawData[18] & 0xFF) >>> 4); + outdoorTemperature = outdoorTemperatureValue + smallOutdoorTemperatureValue / 10.0f; + } + return outdoorTemperature; + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 38828f673bee5..d6c1967d4e9bb 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -60,12 +60,15 @@ channel-type.mideaac.fan-speed.state.option.MEDIUM = MEDIUM channel-type.mideaac.fan-speed.state.option.HIGH = HIGH channel-type.mideaac.fan-speed.state.option.FULL = FULL channel-type.mideaac.fan-speed.state.option.AUTO = AUTO +channel-type.mideaac.filter-status.label = Filter needs cleaning channel-type.mideaac.humidity.label = Humidity channel-type.mideaac.humidity.description = Humidity in room (if supported). channel-type.mideaac.indoor-temperature.label = Indoor Temperature channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off channel-type.mideaac.kilowatt-hours.label = Kilowatt Hours channel-type.mideaac.kilowatt-hours.description = kilowatt Hours reported by the indoor unit. +channel-type.mideaac.maximum-humidity.label = Maximum Humidity +channel-type.mideaac.maximum-humidity.description = Set Maximum Humidity level for Dry Mode (if supported). channel-type.mideaac.off-timer.label = Off Timer channel-type.mideaac.off-timer.description = Off Timer (HH:MM) to set. channel-type.mideaac.on-timer.label = On Timer @@ -91,8 +94,6 @@ channel-type.mideaac.swing-mode.state.option.OFF = OFF channel-type.mideaac.swing-mode.state.option.VERTICAL = VERTICAL channel-type.mideaac.swing-mode.state.option.HORIZONTAL = HORIZONTAL channel-type.mideaac.swing-mode.state.option.BOTH = BOTH -channel-type.mideaac.target-humidity.label = Target Humidity -channel-type.mideaac.target-humidity.description = Set Target Humidity for Dry Mode (if supported). channel-type.mideaac.target-temperature.label = Target Temperature channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 7033c1af50ac3..fb87347912fdd 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -28,12 +28,13 @@ - + + ipAddress @@ -245,6 +246,12 @@ Switch + + Switch + + Switch + + String @@ -261,10 +268,10 @@ Switch - + Number - - Set Target Humidity for Dry Mode (if supported). + + Set Maximum Humidity level for Dry Mode (if supported). Humidity diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java index b76d88e313f67..a06bd5f87f0e0 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/EnergyResponseTest.java @@ -30,8 +30,18 @@ public class EnergyResponseTest { byte[] dataEnergy = HexFormat.of().parseHex("c1210144000005e00000000000000006000aeb000000487a5e"); byte[] dataEnergy2 = HexFormat.of().parseHex("C1210144000246540000000000000000000000001953"); + byte[] dataEnergy3 = HexFormat.of().parseHex("C1210145350000000000000000000000000000001953"); EnergyResponse responseEnergy = new EnergyResponse(dataEnergy); EnergyResponse responseEnergy2 = new EnergyResponse(dataEnergy2); + EnergyResponse responseEnergy3 = new EnergyResponse(dataEnergy3); + + /** + * Test Humidity response + */ + @Test + public void testGetHumidity() { + assertEquals(53, responseEnergy3.getHumidity()); + } /** * Test Energy Kilowatt Hours 3 tests diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java index 9ffa28cede15f..cdedb5146ae74 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/ResponseTest.java @@ -177,11 +177,19 @@ public void testDisplayOn() { assertEquals(false, response.getDisplayOn()); } + /** + * Filter Status Test + */ + @Test + public void testFilterStatus() { + assertEquals(false, response.getFilterStatus()); + } + /** * Humidity Test */ @Test public void testGetHumidity() { - assertEquals(50, response.getTargetHumidity()); + assertEquals(50, response.getMaximumHumidity()); } } diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponseTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponseTest.java new file mode 100644 index 0000000000000..dcd1629d9e1e2 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponseTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.handler; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HexFormat; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * The {@link TemperatureResponseTest} tests the methods in the HumidityResponse class + * against an example response string. + * + * @author Bob Eckhoff - Initial contribution + */ +@NonNullByDefault +public class TemperatureResponseTest { + @org.jupnp.registry.event.Before + + // From OH forum; Midea topic July 2025 + byte[] data = HexFormat.of().parseHex("A10000000000000000000A0A0A64FF000031050000000000000000000000003CD5DB"); + + TemperatureResponse response = new TemperatureResponse(data); + + /** + * Humidity Test 0XA1 + */ + @Test + public void testGetHumidity() { + assertEquals(49, response.getHumidity()); + } + + /** + * Indoor Temperature Test 0xA1 + */ + @Test + public void testGetIndoorTemperature() { + assertEquals(25.5, response.getIndoorTemperature(), 0.0001); + } + + /** + * Outdoor Temperature Test 0xA1 + */ + @Test + public void testGetOutdoorTemperature() { + assertEquals(0.0, response.getOutdoorTemperature(), 0.0001); + } + + /** + * Get Current work time in minutes 0xA1 + */ + @Test + public void testGetCurrentWorkTime() { + assertEquals(15010, response.getCurrentWorkTime()); + } +} From 48fb70ee54d76bd9ddda989797ef58570f091bcb Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 15 Jul 2025 17:01:18 -0400 Subject: [PATCH 35/44] Refactor and update Midea AC binding documentation and code Improved documentation in README and JavaDoc comments, clarified terminology (e.g., 'Maximum Humidity'), and updated test method naming for consistency. Fixed CRC8 calculation logic for correctness. Updated thing-types.xml to use correct state patterns for energy channels. Removed outdated example from README and added clarifying notes about channel support. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 6 +++--- .../java/org/openhab/binding/mideaac/internal/Utils.java | 2 +- .../mideaac/internal/connection/CommandHelper.java | 4 ++-- .../binding/mideaac/internal/handler/CommandSet.java | 2 +- .../binding/mideaac/internal/handler/MideaACHandler.java | 1 + .../openhab/binding/mideaac/internal/security/Crc8.java | 8 +------- .../src/main/resources/OH-INF/thing/thing-types.xml | 6 +++--- .../handler/capabilities/CapabilityParserTest.java | 2 +- 8 files changed, 13 insertions(+), 18 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index d632ff0cfb037..8830c7cbeffde 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -4,7 +4,7 @@ This binding integrates Air Conditioners that use the Midea protocol. Midea is a An AC device is likely supported if it uses one of the following Android apps or it's iOS equivalent. -| Application | Comment | Options | Default | +| Cloud Provider | Comment | Options | Default | |----------------------------------------------|------------------------------------------|--------------|---------| | Midea Air (com.midea.aircondition.obm) | Full Support of key and token updates | Midea Air | | | NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | Yes | @@ -21,7 +21,7 @@ This binding supports one Thing type `ac`. Once the Air Conditioner is on your network activating the Inbox scan with this binding will send an IP broadcast message. Every responding unit gets added to the Inbox. When adding each thing, the required parameters will be populated with either discovered values or the default settings. For a V.3 device, in the unlikely event the defaults did not get the token and key, -enter your cloud provider, email and password. The thing properties dropdown on the Thing UI page will show supported AC functions. +enter your cloud provider, email and password. ## Binding Configuration @@ -50,6 +50,7 @@ No binding configuration is required. ## Channels Following channels are available: +Note: After discovery, the thing properties dropdown on the Thing UI page will show what channels and modes your device supports. | Channel | Type | Description | Read only | Advanced | |----------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------| @@ -99,7 +100,6 @@ String operational_mode "Operational Mode" { ch String fan_speed "Fan Speed" { channel="mideaac:ac:mideaac:fan-speed" } String swing_mode "Swing Mode" { channel="mideaac:ac:mideaac:swing-mode" } Number:Temperature indoor_temperature "Indoor Temperature [%.1f °F]" { channel="mideaac:ac:mideaac:indoor-temperature" } -Number:Temperature outdoor_temperature "Current Temperature [%.1f °F]" { channel="mideaac:ac:mideaac:outdoor-temperature" } Switch eco_mode "Eco Mode" { channel="mideaac:ac:mideaac:eco-mode" } Switch turbo_mode "Turbo Mode" { channel="mideaac:ac:mideaac:turbo-mode" } Switch sleep_function "Sleep function" { channel="mideaac:ac:mideaac:sleep-function" } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 335e3536c163e..161996335b1f3 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -35,7 +35,7 @@ * which are used across the whole binding. * * @author Jacek Dobrowolski - Initial contribution - * @author Bob Eckhoff - JavaDoc, reversed array and refined query String + * @author Bob Eckhoff - JavaDoc, reversed array and refined query String method */ @NonNullByDefault public class Utils { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java index 7c5f4d24d2bbb..8e881d21614ca 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/CommandHelper.java @@ -451,9 +451,9 @@ private static int limitHumidityToRange(int humidity) { } /** - * Sets the Target Humidity for Dry Mode + * Sets the Maximum Humidity for Dry Mode * - * @param command Target Humidity + * @param command Maximum Humidity */ public static CommandSet handleMaximumHumidity(Command command, Response lastResponse) { CommandSet commandSet = CommandSet.fromResponse(lastResponse); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index cd1ece35642e5..0b174e393b3c8 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -275,7 +275,7 @@ private void removeExtraBytes() { } /** - * Creates the Initial Get capability message + * Creates the Initial Get Capability message * * @return Capability message */ diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index eb05fbe0e415e..a3d2e293e7761 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -491,6 +491,7 @@ public void updateChannels(HumidityResponse humidityResponse) { } // Handle unsolicted Temperature response in room (0xA1) Humidity only for now + // Temperatures are updated via the poll @Override public void updateChannels(TemperatureResponse temperatureResponse) { updateChannel(CHANNEL_HUMIDITY, new DecimalType(temperatureResponse.getHumidity())); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java index e1d136233cc8d..6b24ccb0bbbd2 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Crc8.java @@ -64,13 +64,7 @@ public class Crc8 { public static int calculate(byte[] bytes) { int crcValue = 0; for (byte m : bytes) { - int k = (byte) (crcValue ^ m); - if (k > 256) { - k -= 256; - } - if (k < 0) { - k += 256; - } + int k = (crcValue ^ m) & 0xFF; crcValue = CRC8_854_TABLE[k]; } return crcValue; diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index fb87347912fdd..f8bd1cd7de935 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -287,20 +287,20 @@ kilowatt Hours reported by the indoor unit. Energy - + Number Amperes (current) reported by the indoor unit. Energy - + Number Watts reported by the indoor unit. Energy - + diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java index c66d97b53310d..fbda33e3538a8 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/handler/capabilities/CapabilityParserTest.java @@ -149,7 +149,7 @@ void testParseWithTrailingCRC() { } @Test - void testParseWithtemperature() { + void testParseWithTemperature() { // Arrange: Create a payload with Temperature bytes byte[] payload = new byte[] { (byte) 0xB5, 0x08, // Header and count (7 capabilities) 0x12, 0x02, 0x01, 0x01, // Capability 1 (0x0212 = PRESET_ECO, value = 1) From dfe2fc064b60034e24d39d3dc01585affdaad290 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 16 Jul 2025 16:00:55 -0400 Subject: [PATCH 36/44] Minor clean-ups Minor cleanups of Temperature response and energy formats. Signed-off-by: Bob Eckhoff --- .../internal/handler/MideaACHandler.java | 26 +++++++++---------- .../mideaac/internal/handler/Response.java | 2 +- .../internal/handler/TemperatureResponse.java | 2 +- .../resources/OH-INF/thing/thing-types.xml | 6 ++--- 4 files changed, 17 insertions(+), 19 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index a3d2e293e7761..ddfbbac4a8149 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -490,26 +490,24 @@ public void updateChannels(HumidityResponse humidityResponse) { updateChannel(CHANNEL_HUMIDITY, new DecimalType(humidityResponse.getHumidity())); } - // Handle unsolicted Temperature response in room (0xA1) Humidity only for now - // Temperatures are updated via the poll + // Handle unsolicted Temperature and Humidity response in room (0xA1) + // Temperatures are also updated via the poll @Override public void updateChannels(TemperatureResponse temperatureResponse) { updateChannel(CHANNEL_HUMIDITY, new DecimalType(temperatureResponse.getHumidity())); - // QuantityType outdoorTemperature = new - // QuantityType(temperatureResponse.getOutdoorTemperature(), - // SIUnits.CELSIUS); - // QuantityType indoorTemperature = new - // QuantityType(temperatureResponse.getIndoorTemperature(), - // SIUnits.CELSIUS); + QuantityType outdoorTemperature = new QuantityType( + temperatureResponse.getOutdoorTemperature(), SIUnits.CELSIUS); + QuantityType indoorTemperature = new QuantityType( + temperatureResponse.getIndoorTemperature(), SIUnits.CELSIUS); - // if (imperialUnits) { - // indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); - // outdoorTemperature = Objects.requireNonNull(outdoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); - // } + if (imperialUnits) { + indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + outdoorTemperature = Objects.requireNonNull(outdoorTemperature.toUnit(ImperialUnits.FAHRENHEIT)); + } - // updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature); - // updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); + updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature); + updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature); } @Override diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java index 38e8710784d97..8322cd583c39f 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/Response.java @@ -65,10 +65,10 @@ public Response(byte[] data, int version) { logger.debug("Eco Mode: {}", getEcoMode()); logger.debug("Indoor Temperature: {}", getIndoorTemperature()); logger.debug("Outdoor Temperature: {}", getOutdoorTemperature()); - logger.debug("LED Display: {}", getDisplayOn()); } if (logger.isTraceEnabled()) { + logger.trace("LED Display: {}", getDisplayOn()); logger.trace("Prompt Tone: {}", getPromptTone()); logger.trace("Appliance Error: {}", getApplianceError()); logger.trace("Auxiliary Heat: {}", getAuxHeat()); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java index 3aedecfde1b2d..d8c53ee6cf102 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/TemperatureResponse.java @@ -38,7 +38,7 @@ public TemperatureResponse(byte[] rawData) { logger.debug("Humidity from 0xA1: {}", getHumidity()); logger.debug("Indoor Temperature from 0xA1: {}", getIndoorTemperature()); logger.debug("Outdoor Temperature from 0xA1: {}", getOutdoorTemperature()); - logger.debug("Current Work Time (minutes) from 0xA1: {}", getCurrentWorkTime()); + logger.debug("Current Work Time (minutes) from 0xA1: {}", getCurrentWorkTime()); // Not a channel } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index f8bd1cd7de935..e981827989e74 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -287,20 +287,20 @@ kilowatt Hours reported by the indoor unit. Energy - + Number Amperes (current) reported by the indoor unit. Energy - + Number Watts reported by the indoor unit. Energy - + From 7108254290f040bbd818c620261c5eb9c02a7785 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 23 Jul 2025 09:00:47 -0400 Subject: [PATCH 37/44] Update palm to 5.1 Update pom to 5.1 Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.openhab.binding.mideaac/pom.xml b/bundles/org.openhab.binding.mideaac/pom.xml index 9235943cc3e58..7e353bfbd94b5 100644 --- a/bundles/org.openhab.binding.mideaac/pom.xml +++ b/bundles/org.openhab.binding.mideaac/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 5.0.0-SNAPSHOT + 5.1.0-SNAPSHOT org.openhab.binding.mideaac From 1e7383a48f86c6eafab1ee8d3ec2808759da6175 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Thu, 24 Jul 2025 13:14:44 -0400 Subject: [PATCH 38/44] Add tags to channels Saw that there was a project to add tags to channels, so tried to follow examples from other bindings. Don't have sematic model deployed, so wanted to add to the review process. Signed-off-by: Bob Eckhoff --- .../resources/OH-INF/thing/thing-types.xml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index e981827989e74..4f4bbc3a5c956 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -145,17 +145,29 @@ Turn the AC on or off. Switch + + Switch + Power + Number:Temperature Temperature + + Setpoint + Temperature + String Operational modes: AUTO, COOL, DRY, HEAT, FAN ONLY. + + Control + Mode + @@ -170,6 +182,10 @@ String Fan speeds: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. + + Control + Airflow + @@ -185,6 +201,10 @@ String Swing modes: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support + + Control + Tilt + @@ -199,6 +219,10 @@ Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. Switch + + Switch + Airconditioning + Switch @@ -206,12 +230,20 @@ Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. Switch + + Switch + Airconditioning + Number:Temperature Indoor temperature measured by the internal unit. Not frequent when unit is off Temperature + + Measurement + Temperature + @@ -219,6 +251,10 @@ Outdoor temperature from the external unit. Not frequent when unit is off Temperature + + Measurement + Temperature + @@ -226,12 +262,20 @@ Sleep function ("Moon with a star" icon on IR Remote Controller). Switch + + Switch + Airconditioning + Switch On = Farenheit on Indoor AC unit LED display, Off = Celsius. Switch + + Switch + Enabled + Switch @@ -239,33 +283,57 @@ Status of LEDs on the device. Not all models work on LAN (only IR). No confirmation possible either. Switch + + Switch + Brightness + Switch Switch + + Alarm + Airconditioning + Switch Switch + + Alarm + Airflow + String On Timer (HH:MM) to set. + + Control + Duration + String Off Timer (HH:MM) to set. + + Control + Duration + Switch Switch + + Status + Heating + @@ -273,6 +341,10 @@ Set Maximum Humidity level for Dry Mode (if supported). Humidity + + Control + Humidity + @@ -280,6 +352,10 @@ Humidity in room (if supported). Humidity + + Measurement + Humidity + @@ -287,6 +363,10 @@ kilowatt Hours reported by the indoor unit. Energy + + Measurement + Energy + @@ -294,6 +374,10 @@ Amperes (current) reported by the indoor unit. Energy + + Measurement + Energy + @@ -301,6 +385,10 @@ Watts reported by the indoor unit. Energy + + Measurement + Energy + From 7b778af1dd4f4f0a38eb65b5cbc4e16fa9516ba5 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 24 Aug 2025 17:47:27 -0400 Subject: [PATCH 39/44] Add CODEOWNER for mideaac binding Assigned @apella12 as the code owner for /bundles/org.openhab.binding.mideaac/ in the CODEOWNERS file. Signed-off-by: Bob Eckhoff --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 215180a43ae49..7ca2d5d362f02 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -232,6 +232,7 @@ /bundles/org.openhab.binding.meteostick/ @cdjackson /bundles/org.openhab.binding.metofficedatahub/ @dag81 /bundles/org.openhab.binding.mffan/ @mark-brooks-180 +/bundles/org.openhab.binding.mideaac/ @apella12 /bundles/org.openhab.binding.miele/ @kgoderis @jlaur /bundles/org.openhab.binding.mielecloud/ @BjoernLange /bundles/org.openhab.binding.mihome/ @pboos From 2af62c049288cb44de8ad195c36702882dcb9942 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 31 Oct 2025 20:08:01 -0400 Subject: [PATCH 40/44] Address some of the October review comments Renames energy-related channels (kilowatt-hours, amperes, watts) to energy-consumption, current-draw, and power-consumption across code, documentation, and UI resources for clarity. Refactors hex string conversion to use HexUtils from openHAB core, removing redundant methods from Utils. Improves error handling and introduces LoginFailedException for cloud login failures. Updates related tests and documentation to match new channel names. End of day WIP Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 6 +-- .../internal/MideaACBindingConstants.java | 6 +-- .../internal/MideaACConfiguration.java | 4 +- .../binding/mideaac/internal/Utils.java | 36 ---------------- .../binding/mideaac/internal/cloud/Cloud.java | 15 ++++--- .../internal/cloud/LoginFailedException.java | 32 ++++++++++++++ .../connection/ConnectionManager.java | 17 +++++--- .../discovery/MideaACDiscoveryService.java | 21 ++++----- .../mideaac/internal/handler/CommandBase.java | 7 ++- .../mideaac/internal/handler/CommandSet.java | 12 +++--- .../internal/handler/MideaACHandler.java | 15 +++---- .../mideaac/internal/security/Security.java | 43 ++++++++++--------- .../resources/OH-INF/i18n/mideaac.properties | 14 +++--- .../resources/OH-INF/thing/thing-types.xml | 14 +++--- .../MideaACDiscoveryServiceTest.java | 5 ++- 15 files changed, 125 insertions(+), 122 deletions(-) create mode 100644 bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/LoginFailedException.java diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 8830c7cbeffde..db0318c5f7d04 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -70,9 +70,9 @@ Note: After discovery, the thing properties dropdown on the Thing UI page will | screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | | maximum-humidity | Number | If device supports, allows setting the maximum humidity in DRY mode | | Yes | humidity | Number | If device supports, the indoor room humidity. | Yes | Yes | -| kilowatt-hours | Number | If device supports, cumulative KWH usage | Yes | Yes | -| amperes | Number | If device supports, current amperage usage | Yes | Yes | -| watts | Number | If device supports, current wattage reading | Yes | Yes | +| energy-consumption | Number | If device supports, cumulative Kilowatt-Hours usage | Yes | Yes | +| current-draw | Number | If device supports, instantaneous amperage usage | Yes | Yes | +| power-consumption | Number | If device supports, instantaneous wattage reading | Yes | Yes | | appliance-error | Switch | If device supports, appliance error notification | Yes | Yes | | filter-status | Switch | If device supports, notification that filter needs cleaning | Yes | Yes | | auxiliary-heat | Switch | If device supports, auxiliary heat (On or Off) | Yes | Yes | diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java index f83a423aa1b0d..95147b85a7cfc 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACBindingConstants.java @@ -63,9 +63,9 @@ public class MideaACBindingConstants { public static final String CHANNEL_HUMIDITY = "humidity"; public static final String CHANNEL_SCREEN_DISPLAY = "screen-display"; public static final String CHANNEL_FILTER_STATUS = "filter-status"; - public static final String CHANNEL_KILOWATT_HOURS = "kilowatt-hours"; - public static final String CHANNEL_AMPERES = "amperes"; - public static final String CHANNEL_WATTS = "watts"; + public static final String CHANNEL_ENERGY_CONSUMPTION = "energy-consumption"; + public static final String CHANNEL_CURRENT_DRAW = "current-draw"; + public static final String CHANNEL_POWER_CONSUMPTION = "power-consumption"; public static final Unit API_TEMPERATURE_UNIT = SIUnits.CELSIUS; diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 9e0854499fdc3..9b63e2f25f5cc 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -118,7 +118,7 @@ public boolean isValid() { * @return true(discovery needed), false (not needed) */ public boolean isDiscoveryPossible() { - return (Utils.validateIP(ipAddress)); + return Utils.validateIP(ipAddress); } /** @@ -128,7 +128,7 @@ public boolean isDiscoveryPossible() { * @return true (yes they can), false (they cannot) */ public boolean isTokenKeyObtainable() { - return (!email.isBlank() && !password.isBlank() && !cloud.isBlank()); + return !email.isBlank() && !password.isBlank() && !cloud.isBlank(); } /** diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java index 161996335b1f3..3f89559caca3e 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/Utils.java @@ -43,16 +43,6 @@ public class Utils { static byte[] empty = new byte[0]; - /** - * Converts byte array to upper case hex string - * - * @param bytes bytes to convert - * @return string of upper case hex chars - */ - public static String bytesToHex(byte[] bytes) { - return HexUtils.bytesToHex(bytes); - } - /** * Converts byte array to binary string * @@ -68,16 +58,6 @@ public static String bytesToBinary(byte[] bytes) { return s1; } - /** - * Converts byte array to lower case hex string - * - * @param bytes bytes to convert - * @return string of lower case hex chars - */ - public static String bytesToHexLowercase(byte[] bytes) { - return HexUtils.bytesToHex(bytes).toLowerCase(); - } - /** * Validates the IP address format * @@ -143,22 +123,6 @@ public static byte[] strxor(byte[] array1, byte[] array2) { return result; } - /** - * Create String of the V.3 Token - * String length is the nbytes characters long - * - * @param nbytes number of bytes - * @return String - */ - public static String tokenHex(int nbytes) { - Random r = new Random(); - StringBuffer sb = new StringBuffer(); - for (int n = 0; n < nbytes; n++) { - sb.append(Integer.toHexString(r.nextInt())); - } - return sb.toString().substring(0, nbytes); - } - /** * Create URL safe token * diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index cc3edb2f286e8..179f2d4896453 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -31,6 +31,7 @@ import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.binding.mideaac.internal.security.TokenKey; +import org.openhab.core.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -116,7 +117,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli // This adds the first 16 characters of a 16 byte string // if Cloud provider uses proxied and wasn't added by the method() if (!data.has("reqId") && !cloudProvider.proxied().isBlank()) { - data.addProperty("reqId", Utils.tokenHex(16)); + data.addProperty("reqId", StringUtils.getRandomHex(16)); } String url = cloudProvider.apiurl() + endpoint; @@ -176,11 +177,13 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli try { cr = request.send(); } catch (InterruptedException e) { - logger.warn("an interupted error has occurred{}", e.getMessage()); + Thread.currentThread().interrupt(); // Restore interrupt flag + logger.warn("Request interrupted: {}", e.getMessage()); + return null; // Return quickly } catch (TimeoutException e) { - logger.warn("a timeout error has occurred{}", e.getMessage()); + logger.warn("Request timed out: {}", e.getMessage()); } catch (ExecutionException e) { - logger.warn("an execution error has occurred{}", e.getMessage()); + logger.warn("Request execution failed: {}", e.getMessage()); } if (cr != null) { @@ -203,7 +206,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli if (code != 0) { errMsg = msg; logger.warn("Error {} logging to Cloud: {}", code, msg); - return null; + throw new LoginFailedException("Login failed with error code " + code + ": " + msg); } else { logger.debug("Api response ok: {} ({})", code, msg); if (!cloudProvider.proxied().isBlank()) { @@ -257,7 +260,7 @@ public boolean login() { iotData.addProperty("loginAccount", loginAccount); iotData.addProperty("password", security.encryptPassword(loginId, password)); iotData.addProperty("pushToken", Utils.tokenUrlsafe(120)); - iotData.addProperty("reqId", Utils.tokenHex(16)); + iotData.addProperty("reqId", StringUtils.getRandomHex(16)); iotData.addProperty("src", cloudProvider.appid()); iotData.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())); newData.add("iotData", iotData); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/LoginFailedException.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/LoginFailedException.java new file mode 100644 index 0000000000000..b6e71bc4cd719 --- /dev/null +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/LoginFailedException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2025 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.mideaac.internal.cloud; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link LoginFailedException} is used to indicate login failures to Midea + * cloud services. + * + * @author Bob Eckhoff - Initial contribution + */ + +@NonNullByDefault +public class LoginFailedException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public LoginFailedException(String message) { + super(message); + } +} diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index d723e132a9e17..4b1afd587923d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -42,6 +42,7 @@ import org.openhab.binding.mideaac.internal.security.Decryption8370Result; import org.openhab.binding.mideaac.internal.security.Security; import org.openhab.binding.mideaac.internal.security.Security.MsgType; +import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -168,6 +169,8 @@ public synchronized void connect() Thread.sleep(5000); } catch (InterruptedException ex) { logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); + Thread.currentThread().interrupt(); + throw new MideaConnectionException("Socket connection interrupted"); } logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retrySocket, ipAddress, e.getMessage()); @@ -242,7 +245,7 @@ public void authenticate() throws MideaConnectionException, MideaAuthenticationE private void doV3Handshake() throws MideaConnectionException, MideaAuthenticationException { byte[] request = security.encode8370(Utils.hexStringToByteArray(token), MsgType.MSGTYPE_HANDSHAKE_REQUEST); try { - logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, Utils.bytesToHex(request)); + logger.trace("Device at IP: {} writing handshake_request: {}", ipAddress, HexUtils.bytesToHex(request)); write(request); byte[] response = read(); @@ -315,8 +318,8 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException { ensureConnected(); - if (command instanceof CommandSet) { - ((CommandSet) command).setPromptTone(promptTone); + if (command instanceof CommandSet cmdSet) { + cmdSet.setPromptTone(promptTone); } Packet packet = new Packet(command, deviceId, security); packet.compose(); @@ -340,7 +343,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } catch (InterruptedException e) { logger.debug("An interupted error (write command2) has occured {}", e.getMessage()); Thread.currentThread().interrupt(); - // Note, but continue anyway for second write if needed. + throw new MideaConnectionException("Command interrupted during wait"); } // Input stream is checked after 1.5 seconds @@ -363,7 +366,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal if (response.length > 40 + 16) { data = security.aesDecrypt(Arrays.copyOfRange(response, 40, response.length - 16)); logger.trace("Bytes in HEX, decoded and with header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); + HexUtils.bytesToHex(data)); } // The response data from the appliance includes a packet header which we don't want if (data != null && data.length > 10) { @@ -383,7 +386,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal if (responseBytes.length > 40 + 16) { data = security.aesDecrypt(Arrays.copyOfRange(responseBytes, 40, responseBytes.length - 16)); logger.trace("V2 Bytes decoded with header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); + HexUtils.bytesToHex(data)); } // The response data from the appliance includes a packet header which we don't want if (data != null && data.length > 10) { @@ -424,7 +427,7 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal private void handleResponse(byte[] data, byte bodyType, @Nullable Callback callback) throws MideaException { logger.trace("Response bodyType: {}", bodyType); logger.debug("Bytes in HEX, decoded and stripped without header: length: {}, data: {}", data.length, - Utils.bytesToHex(data)); + HexUtils.bytesToHex(data)); logger.trace("Bytes in BINARY, decoded and stripped without header: length: {}, data: {}", data.length, Utils.bytesToBinary(data)); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java index b42c95fe8ecc4..0299cba68f11a 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryService.java @@ -39,6 +39,7 @@ import org.openhab.core.config.discovery.DiscoveryResultBuilder; import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.thing.ThingUID; +import org.openhab.core.util.HexUtils; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -258,28 +259,28 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { final String ipAddress = packet.getAddress().getHostAddress(); byte[] data = Arrays.copyOfRange(packet.getData(), 0, packet.getLength()); - logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, Utils.bytesToHex(data)); + logger.trace("Midea AC discover data ({}) from {}: '{}'", data.length, ipAddress, HexUtils.bytesToHex(data)); - if (data.length >= 104 && (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A") - || Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) { + if (data.length >= 104 && (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A") + || HexUtils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A"))) { logger.trace("Device supported"); String mSmartId, mSmartip = "", mSmartSN = "", mSmartSSID = "", mSmartType = "", mSmartPort = "", mSmartVersion = ""; - if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) { + if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("5A5A")) { mSmartVersion = "2"; } - if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { + if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { mSmartVersion = "3"; } - if (Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { + if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { data = Arrays.copyOfRange(data, 8, data.length - 16); } logger.debug("Version: {}", mSmartVersion); byte[] id = Arrays.copyOfRange(data, 20, 26); - logger.trace("Id Bytes: {}", Utils.bytesToHex(id)); + logger.trace("Id Bytes: {}", HexUtils.bytesToHex(id)); byte[] idReverse = Utils.reverse(id); @@ -289,10 +290,10 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { logger.debug("Id: '{}'", mSmartId); byte[] encryptData = Arrays.copyOfRange(data, 40, data.length - 16); - logger.trace("Encrypt data: '{}'", Utils.bytesToHex(encryptData)); + logger.trace("Encrypt data: '{}'", HexUtils.bytesToHex(encryptData)); byte[] reply = security.aesDecrypt(encryptData); - logger.trace("Length: {}, Reply: '{}'", reply.length, Utils.bytesToHex(reply)); + logger.trace("Length: {}, Reply: '{}'", reply.length, HexUtils.bytesToHex(reply)); mSmartip = Byte.toUnsignedInt(reply[3]) + "." + Byte.toUnsignedInt(reply[2]) + "." + Byte.toUnsignedInt(reply[1]) + "." + Byte.toUnsignedInt(reply[0]); @@ -323,7 +324,7 @@ public DiscoveryResult discoveryPacketReceived(DatagramPacket packet) { mSmartSSID, mSmartType, new TreeMap<>(), // Placeholder for capabilities new TreeMap<>())) // Placeholder for numericCapabilities .build(); - } else if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) { + } else if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 6)).equals("3C3F786D6C20")) { logger.debug("Midea AC v1 device was detected, supported, but not implemented yet."); return null; } else { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java index 58cec4808e4c7..8255268dce14c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandBase.java @@ -16,8 +16,8 @@ import java.util.Arrays; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.security.Crc8; +import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -281,7 +281,7 @@ public CommandBase() { * Pulls the elements of the Base command together */ public void compose() { - logger.trace("Base Bytes before crypt {}", Utils.bytesToHex(data)); + logger.trace("Base Bytes before crypt {}", HexUtils.bytesToHex(data)); byte crc8 = (byte) Crc8.calculate(Arrays.copyOfRange(data, 10, data.length)); byte[] newData1 = new byte[data.length + 1]; System.arraycopy(data, 0, newData1, 0, data.length); @@ -308,7 +308,6 @@ private static byte checksum(byte[] bytes) { for (byte value : bytes) { sum = (byte) (sum + value); } - sum = (byte) ((255 - (sum % 256)) + 1); - return (byte) sum; + return (byte) ((255 - (sum % 256)) + 1); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java index 0b174e393b3c8..786853ee623b1 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/CommandSet.java @@ -13,8 +13,8 @@ package org.openhab.binding.mideaac.internal.handler; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.handler.Timer.TimerData; +import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -249,7 +249,7 @@ public void setFahrenheit(boolean fahrenheitEnabled) { public void setScreenDisplay(boolean screenDisplayToggle) { modifyBytesForDisplayOff(); removeExtraBytes(); - logger.trace("Set Display Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Display Bytes before encrypt {}", HexUtils.bytesToHex(data)); } private void modifyBytesForDisplayOff() { @@ -282,7 +282,7 @@ private void removeExtraBytes() { public void getCapabilities() { modifyBytesForCapabilities(); removeExtraCapabilityBytes(); - logger.trace("Set Capability Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Capability Bytes before encrypt {}", HexUtils.bytesToHex(data)); } private void modifyBytesForCapabilities() { @@ -307,7 +307,7 @@ private void removeExtraCapabilityBytes() { public void getAdditionalCapabilities() { modifyBytesForAdditionalCapabilities(); removeExtraAdditionalCapabilityBytes(); - logger.trace("Set Additional Capability Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Additional Capability Bytes before encrypt {}", HexUtils.bytesToHex(data)); } private void modifyBytesForAdditionalCapabilities() { @@ -456,7 +456,7 @@ public int getOffTimer2() { public void energyPoll() { modifyBytesForEnergyPoll(); removeExtraEnergyPollBytes(); - logger.trace("Set Energy Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Energy Bytes before encrypt {}", HexUtils.bytesToHex(data)); } private void modifyBytesForEnergyPoll() { @@ -489,7 +489,7 @@ private void removeExtraEnergyPollBytes() { public void humidityPoll() { modifyBytesForHumidityPoll(); removeExtraHumidityPollBytes(); - logger.trace("Set Humidity Poll Bytes before encrypt {}", Utils.bytesToHex(data)); + logger.trace("Set Humidity Poll Bytes before encrypt {}", HexUtils.bytesToHex(data)); } private void modifyBytesForHumidityPoll() { diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index ddfbbac4a8149..c3aee93c299fe 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -155,7 +155,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { CommandSet humidityUpdate = new CommandSet(); humidityUpdate.humidityPoll(); connectionManager.sendCommand(humidityUpdate, this); - } catch (MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); } catch (MideaConnectionException | MideaException e) { @@ -254,7 +253,7 @@ public void initialize() { if (config.isTokenKeyObtainable()) { try { CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); - getTokenKeyCloud(cloudProvider); + Executors.newSingleThreadExecutor().submit(() -> getTokenKeyCloud(cloudProvider)); return; } catch (Exception e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -468,13 +467,13 @@ public void updateChannels(CapabilitiesResponse capabilitiesResponse) { @Override public void updateChannels(EnergyResponse energyUpdate) { if (config.energyDecode) { - updateChannel(CHANNEL_KILOWATT_HOURS, new DecimalType(energyUpdate.getKilowattHoursBCD())); - updateChannel(CHANNEL_AMPERES, new DecimalType(energyUpdate.getAmperesBCD())); - updateChannel(CHANNEL_WATTS, new DecimalType(energyUpdate.getWattsBCD())); + updateChannel(CHANNEL_ENERGY_CONSUMPTION, new DecimalType(energyUpdate.getKilowattHoursBCD())); + updateChannel(CHANNEL_CURRENT_DRAW, new DecimalType(energyUpdate.getAmperesBCD())); + updateChannel(CHANNEL_POWER_CONSUMPTION, new DecimalType(energyUpdate.getWattsBCD())); } else { - updateChannel(CHANNEL_KILOWATT_HOURS, new DecimalType(energyUpdate.getKilowattHours())); - updateChannel(CHANNEL_AMPERES, new DecimalType(energyUpdate.getAmperes())); - updateChannel(CHANNEL_WATTS, new DecimalType(energyUpdate.getWatts())); + updateChannel(CHANNEL_ENERGY_CONSUMPTION, new DecimalType(energyUpdate.getKilowattHours())); + updateChannel(CHANNEL_CURRENT_DRAW, new DecimalType(energyUpdate.getAmperes())); + updateChannel(CHANNEL_POWER_CONSUMPTION, new DecimalType(energyUpdate.getWatts())); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java index 2959d31355171..cfc8488bcdb26 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/security/Security.java @@ -37,6 +37,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.mideaac.internal.Utils; import org.openhab.binding.mideaac.internal.cloud.CloudProvider; +import org.openhab.core.util.HexUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -283,12 +284,12 @@ public byte[] encode8370(byte[] data, MsgType msgtype) { byte[] finalData = new byte[dataBuffer.remaining()]; dataBuffer.get(finalData); - logger.trace("Header: {}", Utils.bytesToHex(finalHeader)); + logger.trace("Header: {}", HexUtils.bytesToHex(finalHeader)); if (msgtype == MsgType.MSGTYPE_ENCRYPTED_RESPONSE || msgtype == MsgType.MSGTYPE_ENCRYPTED_REQUEST) { byte[] sign = sha256(Utils.concatenateArrays(finalHeader, finalData)); - logger.trace("Sign: {}", Utils.bytesToHex(sign)); - logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); + logger.trace("Sign: {}", HexUtils.bytesToHex(sign)); + logger.trace("TcpKey: {}", HexUtils.bytesToHex(tcpKey)); finalData = Utils.concatenateArrays(aesCbcEncrypt(finalData, tcpKey), sign); } @@ -309,7 +310,7 @@ public Decryption8370Result decode8370(byte[] data) throws IOException { return new Decryption8370Result(new ArrayList(), data); } byte[] header = Arrays.copyOfRange(data, 0, 6); - logger.trace("Header: {}", Utils.bytesToHex(header)); + logger.trace("Header: {}", HexUtils.bytesToHex(header)); if (header[0] != (byte) 0x83 || header[1] != (byte) 0x70) { logger.warn("Not an 8370 message"); return new Decryption8370Result(new ArrayList(), data); @@ -336,10 +337,10 @@ public Decryption8370Result decode8370(byte[] data) throws IOException { data = aesCbcDecrypt(data, tcpKey); byte[] signLocal = sha256(Utils.concatenateArrays(header, data)); - logger.trace("Sign: {}", Utils.bytesToHex(sign)); - logger.trace("SignLocal: {}", Utils.bytesToHex(signLocal)); - logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); - logger.trace("Data: {}", Utils.bytesToHex(data)); + logger.trace("Sign: {}", HexUtils.bytesToHex(sign)); + logger.trace("SignLocal: {}", HexUtils.bytesToHex(signLocal)); + logger.trace("TcpKey: {}", HexUtils.bytesToHex(tcpKey)); + logger.trace("Data: {}", HexUtils.bytesToHex(data)); if (!Arrays.equals(sign, signLocal)) { logger.warn("Sign does not match"); @@ -384,17 +385,17 @@ public boolean tcpKey(byte[] response, byte key[]) { byte[] plain = aesCbcDecrypt(payload, key); byte[] signLocal = sha256(plain); - logger.trace("Payload: {}", Utils.bytesToHex(payload)); - logger.trace("Sign: {}", Utils.bytesToHex(sign)); - logger.trace("SignLocal: {}", Utils.bytesToHex(signLocal)); - logger.trace("Plain: {}", Utils.bytesToHex(plain)); + logger.trace("Payload: {}", HexUtils.bytesToHex(payload)); + logger.trace("Sign: {}", HexUtils.bytesToHex(sign)); + logger.trace("SignLocal: {}", HexUtils.bytesToHex(signLocal)); + logger.trace("Plain: {}", HexUtils.bytesToHex(plain)); if (!Arrays.equals(sign, signLocal)) { logger.warn("Sign does not match"); return false; } tcpKey = Utils.strxor(plain, key); - logger.trace("TcpKey: {}", Utils.bytesToHex(tcpKey)); + logger.trace("TcpKey: {}", HexUtils.bytesToHex(tcpKey)); return true; } @@ -505,7 +506,7 @@ private byte[] getRandomBytes(int size) { String sign = path + query + cloudProvider.appkey(); logger.trace("sign: {}", sign); - return Utils.bytesToHexLowercase(sha256((sign).getBytes(StandardCharsets.US_ASCII))); + return HexUtils.bytesToHex(sha256(sign.getBytes(StandardCharsets.US_ASCII))).toLowerCase(); } catch (URISyntaxException e) { logger.warn("Error parsing URI '{}': {}", url, e.getMessage()); } @@ -556,7 +557,7 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm); Mac mac = Mac.getInstance(algorithm); mac.init(secretKeySpec); - return Utils.bytesToHexLowercase(mac.doFinal(data.getBytes())); + return HexUtils.bytesToHex(mac.doFinal(data.getBytes())).toLowerCase(); } /** @@ -573,10 +574,10 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor m.update(password.getBytes(StandardCharsets.US_ASCII)); // Create the login hash with the loginID + password hash + appKey, then hash it all AGAIN - String loginHash = loginId + Utils.bytesToHexLowercase(m.digest()) + cloudProvider.appkey(); + String loginHash = loginId + HexUtils.bytesToHex(m.digest()).toLowerCase() + cloudProvider.appkey(); m = MessageDigest.getInstance("SHA-256"); m.update(loginHash.getBytes(StandardCharsets.US_ASCII)); - return Utils.bytesToHexLowercase(m.digest()); + return HexUtils.bytesToHex(m.digest()).toLowerCase(); } catch (NoSuchAlgorithmException e) { logger.warn("encryptPassword error: NoSuchAlgorithmException: {}", e.getMessage()); } @@ -596,10 +597,10 @@ public String hmac(String data, String key, String algorithm) throws NoSuchAlgor md.update(password.getBytes(StandardCharsets.US_ASCII)); MessageDigest mdSecond = MessageDigest.getInstance("MD5"); - mdSecond.update(Utils.bytesToHexLowercase(md.digest()).getBytes(StandardCharsets.US_ASCII)); + mdSecond.update((HexUtils.bytesToHex(md.digest()).toLowerCase()).getBytes(StandardCharsets.US_ASCII)); - String loginHash = loginId + Utils.bytesToHexLowercase(mdSecond.digest()) + cloudProvider.appkey(); - return Utils.bytesToHexLowercase(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))); + String loginHash = loginId + HexUtils.bytesToHex(mdSecond.digest()).toLowerCase() + cloudProvider.appkey(); + return HexUtils.bytesToHex(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))).toLowerCase(); } catch (NoSuchAlgorithmException e) { logger.warn("encryptIamPasswordt error: NoSuchAlgorithmException: {}", e.getMessage()); } @@ -623,6 +624,6 @@ public String getUdpId(byte[] data) { b3[i] = (byte) (b1[i] ^ b2[i]); i++; } - return Utils.bytesToHexLowercase(b3); + return HexUtils.bytesToHex(b3).toLowerCase(); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index d6c1967d4e9bb..9122f46e68dde 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -33,7 +33,7 @@ thing-type.config.mideaac.ac.keyTokenUpdate.label = Key token update frequency i thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.pollingTime.label = Poll frequency in seconds +thing-type.config.mideaac.ac.pollingTime.label = Poll Frequency in Seconds thing-type.config.mideaac.ac.pollingTime.description = Poll device status. Minimum is 30, default 60. thing-type.config.mideaac.ac.promptTone.label = Prompt tone thing-type.config.mideaac.ac.promptTone.description = After sending a command device will play "ding" tone when command is received and executed. @@ -46,12 +46,14 @@ thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key # channel types -channel-type.mideaac.amperes.label = Amperes -channel-type.mideaac.amperes.description = Amperes (current) reported by the indoor unit. channel-type.mideaac.appliance-error.label = Appliance Error channel-type.mideaac.auxiliary-heat.label = Auxiliary Heat +channel-type.mideaac.current-draw.label = Amperes +channel-type.mideaac.current-draw.description = Amperes (current) reported by the indoor unit. channel-type.mideaac.eco-mode.label = Eco Mode channel-type.mideaac.eco-mode.description = Eco mode, Cool only, Temp: min. 24C, Fan: AUTO. +channel-type.mideaac.energy-consumption.label = Kilowatt Hours +channel-type.mideaac.energy-consumption.description = kilowatt Hours reported by the indoor unit. channel-type.mideaac.fan-speed.label = Fan Speed channel-type.mideaac.fan-speed.description = Fan speeds: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. channel-type.mideaac.fan-speed.state.option.SILENT = SILENT @@ -65,8 +67,6 @@ channel-type.mideaac.humidity.label = Humidity channel-type.mideaac.humidity.description = Humidity in room (if supported). channel-type.mideaac.indoor-temperature.label = Indoor Temperature channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off -channel-type.mideaac.kilowatt-hours.label = Kilowatt Hours -channel-type.mideaac.kilowatt-hours.description = kilowatt Hours reported by the indoor unit. channel-type.mideaac.maximum-humidity.label = Maximum Humidity channel-type.mideaac.maximum-humidity.description = Set Maximum Humidity level for Dry Mode (if supported). channel-type.mideaac.off-timer.label = Off Timer @@ -82,6 +82,8 @@ channel-type.mideaac.operational-mode.state.option.HEAT = HEAT channel-type.mideaac.operational-mode.state.option.FAN_ONLY = FAN ONLY channel-type.mideaac.outdoor-temperature.label = Outdoor Temperature channel-type.mideaac.outdoor-temperature.description = Outdoor temperature from the external unit. Not frequent when unit is off +channel-type.mideaac.power-consumption.label = Watts +channel-type.mideaac.power-consumption.description = Watts reported by the indoor unit. channel-type.mideaac.power.label = Power channel-type.mideaac.power.description = Turn the AC on or off. channel-type.mideaac.screen-display.label = Screen Display @@ -99,5 +101,3 @@ channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. channel-type.mideaac.turbo-mode.label = Turbo Mode channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. -channel-type.mideaac.watts.label = Watts -channel-type.mideaac.watts.description = Watts reported by the indoor unit. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 4f4bbc3a5c956..0d5fd96ed55c5 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -30,9 +30,9 @@ - - - + + + @@ -96,7 +96,7 @@ pollingTime - + Poll device status. Minimum is 30, default 60. 60 @@ -358,7 +358,7 @@ - + Number kilowatt Hours reported by the indoor unit. @@ -369,7 +369,7 @@ - + Number Amperes (current) reported by the indoor unit. @@ -380,7 +380,7 @@ - + Number Watts reported by the indoor unit. diff --git a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java index f2dca0768a7c7..4f28c5f940ad2 100644 --- a/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java +++ b/bundles/org.openhab.binding.mideaac/src/test/java/org/openhab/binding/mideaac/internal/discovery/MideaACDiscoveryServiceTest.java @@ -22,6 +22,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.junit.jupiter.api.Test; import org.openhab.binding.mideaac.internal.Utils; +import org.openhab.core.util.HexUtils; /** * The {@link MideaACDiscoveryServiceTest} tests the discovery byte arrays @@ -45,7 +46,7 @@ public class MideaACDiscoveryServiceTest { */ @Test public void testVersion() { - if (Utils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { + if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 0, 2)).equals("8370")) { mSmartVersion = "3"; } else { mSmartVersion = "2"; @@ -58,7 +59,7 @@ public void testVersion() { */ @Test public void testId() { - if (Utils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { + if (HexUtils.bytesToHex(Arrays.copyOfRange(data, 8, 10)).equals("5A5A")) { data = Arrays.copyOfRange(data, 8, data.length - 16); } byte[] id = Utils.reverse(Arrays.copyOfRange(data, 20, 26)); From d6be125b9dc1d27b91873d35ced45018f87bd275 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 2 Nov 2025 11:24:01 -0500 Subject: [PATCH 41/44] Review update 2. i18n still WIP Added 'Advanced' column to README and marked advanced parameters in thing-types.xml. Changed keyTokenUpdate unit from days to hours and updated related labels, descriptions, and scheduling logic. Improved MideaACHandler to run discovery and cloud token/key retrieval asynchronously, updating thing status to UNKNOWN during pending operations. Updated i18n strings for clarity and consistency. Changed cloud login error logging from warn to debug. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 34 ++++++------- .../binding/mideaac/internal/cloud/Cloud.java | 2 +- .../internal/handler/MideaACHandler.java | 48 +++++++++++-------- .../resources/OH-INF/i18n/mideaac.properties | 35 +++++++++----- .../resources/OH-INF/thing/thing-types.xml | 45 ++++++++--------- 5 files changed, 91 insertions(+), 73 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index db0318c5f7d04..f6e22d610e620 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -29,23 +29,23 @@ No binding configuration is required. ## Thing Configuration -| Parameter | Required ? | Comment | Default | -|---------------|-------------|--------------------------------------------------------------|---------------------------| -| ipAddress | Yes | IP Address of the device. | | -| ipPort | Yes | IP port of the device | 6444 | -| deviceId | Yes | ID of the device. Leave 0 to do ID discovery. | 0 | -| cloud | Yes for V.3 | Your Cloud Provider name (or default). | NetHome Plus | -| email | No | Email for your cloud account (or default). | nethome+us@mailinator.com | -| password | No | Password for your cloud account (or default). | password1 | -| token | Yes for V.3 | Secret Token - Retrieved from cloud | | -| key | Yes for V.3 | Secret Key - Retrieved from cloud | | -| pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | -| keyTokenUpdate| No | Frequency to update key and token from cloud in days | 0 days (disabled) | -| energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | -| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | -| promptTone | Yes | "Ding" tone when command is received and executed. | false | -| version | Yes | Version 3 has token, key and cloud requirements. | 0 | -| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true +| Parameter | Required ? | Comment | Default | Advanced | +|---------------|-------------|--------------------------------------------------------------|---------------------------|----------| +| ipAddress | Yes | IP Address of the device. | | | +| ipPort | Yes | IP port of the device | 6444 | Yes | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery. | 0 | Yes | +| cloud | Yes for V.3 | Your Cloud Provider name (or default). | NetHome Plus | | +| email | No | Email for your cloud account (or default). | nethome+us@mailinator.com | | +| password | No | Password for your cloud account (or default). | password1 | | +| token | Yes for V.3 | Secret Token - Retrieved from cloud | | Yes | +| key | Yes for V.3 | Secret Key - Retrieved from cloud | | Yes | +| pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | | +| keyTokenUpdate| No | Frequency to update key and token from cloud in hours | 0 hours (disabled) | Yes | +| energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | | +| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | | +| promptTone | Yes | "Ding" tone when command is received and executed. | false | | +| version | Yes | Version 3 has token, key and cloud requirements. | 0 | Yes | +| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true | Yes | ## Channels diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java index 179f2d4896453..404405b6ff050 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/cloud/Cloud.java @@ -205,7 +205,7 @@ public Cloud(String email, String password, CloudProvider cloudProvider, HttpCli String msg = result.get("msg").getAsString(); if (code != 0) { errMsg = msg; - logger.warn("Error {} logging to Cloud: {}", code, msg); + logger.debug("Error {} logging to Cloud: {}", code, msg); throw new LoginFailedException("Login failed with error code " + code + ": " + msg); } else { logger.debug("Api response ok: {} ({})", code, msg); diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index c3aee93c299fe..c03c1b2210a2c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -225,20 +225,27 @@ public void initialize() { // Check for valid discovery configurations and discover again if not if (!config.isValid()) { + // Mark thing as unknown while asynchronous discovery/login is running to avoid race conditions updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration not valid"); + if (config.isDiscoveryPossible()) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration missing, discovery needed. Discovering..."); MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); - try { - discoveryService.discoverThing(config.ipAddress, this); - return; - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Discovery failure. Check IP Address."); - return; - } + // Run discovery asynchronously so initialize() can return quickly, but keep the thing in UNKNOWN + // until the discovery attempt finishes and updates the thing status accordingly. + scheduler.execute(() -> { + try { + // perform the potentially blocking discovery/login work here + discoveryService.discoverThing(config.ipAddress, this); + } catch (Exception e) { + // discovery failed — update status to OFFLINE with an explanation + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Discovery failure. Check IP Address."); + } + }); + return; } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid MideaAC config. Check IP Address."); @@ -251,15 +258,18 @@ public void initialize() { // Check for valid token and key and/or contact cloud account to get them if (config.version == 3 && !config.isV3ConfigValid()) { if (config.isTokenKeyObtainable()) { - try { - CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); - Executors.newSingleThreadExecutor().submit(() -> getTokenKeyCloud(cloudProvider)); - return; - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Token and key could not be obtained from Cloud"); - return; - } + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, + "Retrieving token and key from cloud"); + scheduler.execute(() -> { + try { + CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); + getTokenKeyCloud(cloudProvider); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Token and key could not be obtained from Cloud"); + } + }); + return; } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No account info to retrieve from cloud"); @@ -312,8 +322,8 @@ public void initialize() { if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) { scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, - config.keyTokenUpdate, TimeUnit.DAYS); - logger.debug("Token Key Update Scheduler started, update interval {} days", config.keyTokenUpdate); + config.keyTokenUpdate, TimeUnit.HOURS); + logger.debug("Token Key Update Scheduler started, update interval {} hours", config.keyTokenUpdate); } else { logger.debug("Token Key Scheduler already running or disabled"); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 9122f46e68dde..10b41d4c10531 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -19,26 +19,26 @@ thing-type.config.mideaac.ac.deviceId.label = Device ID thing-type.config.mideaac.ac.deviceId.description = ID of the device. Leave 0 to do ID discovery. thing-type.config.mideaac.ac.email.label = Email thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.energyDecode.label = Energy response decode method +thing-type.config.mideaac.ac.energyDecode.label = Energy Response Decode Method thing-type.config.mideaac.ac.energyDecode.description = Binary-Coded Decimal (BCD) = true. Big-endian = false. -thing-type.config.mideaac.ac.energyPoll.label = Energy poll in minutes -thing-type.config.mideaac.ac.energyPoll.description = Energy polling frequency. default 0 ; (in case not supported). +thing-type.config.mideaac.ac.energyPoll.label = Energy Poll +thing-type.config.mideaac.ac.energyPoll.description = Energy polling in minutes. default 0 ; (in case not supported). thing-type.config.mideaac.ac.ipAddress.label = IP Address thing-type.config.mideaac.ac.ipAddress.description = IP Address of the device. thing-type.config.mideaac.ac.ipPort.label = IP Port thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). -thing-type.config.mideaac.ac.keyTokenUpdate.label = Key token update frequency in days -thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud, default 0 to disable. +thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update +thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud in hours, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. -thing-type.config.mideaac.ac.pollingTime.label = Poll Frequency in Seconds -thing-type.config.mideaac.ac.pollingTime.description = Poll device status. Minimum is 30, default 60. -thing-type.config.mideaac.ac.promptTone.label = Prompt tone +thing-type.config.mideaac.ac.pollingTime.label = Poll Frequency +thing-type.config.mideaac.ac.pollingTime.description = Base device poll in seconds. Minimum is 30, default 60. +thing-type.config.mideaac.ac.promptTone.label = Prompt Tone thing-type.config.mideaac.ac.promptTone.description = After sending a command device will play "ding" tone when command is received and executed. -thing-type.config.mideaac.ac.timeout.label = Socket timeout in seconds -thing-type.config.mideaac.ac.timeout.description = Connecting socket timeout. Minimum is 2, maximum is 10 (4 is default). +thing-type.config.mideaac.ac.timeout.label = Socket Timeout +thing-type.config.mideaac.ac.timeout.description = Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default). thing-type.config.mideaac.ac.token.label = Token thing-type.config.mideaac.ac.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). thing-type.config.mideaac.ac.version.label = AC Version @@ -62,7 +62,7 @@ channel-type.mideaac.fan-speed.state.option.MEDIUM = MEDIUM channel-type.mideaac.fan-speed.state.option.HIGH = HIGH channel-type.mideaac.fan-speed.state.option.FULL = FULL channel-type.mideaac.fan-speed.state.option.AUTO = AUTO -channel-type.mideaac.filter-status.label = Filter needs cleaning +channel-type.mideaac.filter-status.label = Filter Needs Cleaning channel-type.mideaac.humidity.label = Humidity channel-type.mideaac.humidity.description = Humidity in room (if supported). channel-type.mideaac.indoor-temperature.label = Indoor Temperature @@ -101,3 +101,16 @@ channel-type.mideaac.temperature-unit.label = Temperature Unit F/C channel-type.mideaac.temperature-unit.description = On = Farenheit on Indoor AC unit LED display, Off = Celsius. channel-type.mideaac.turbo-mode.label = Turbo Mode channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. Only works in COOL and HEAT mode. + +# thing status descriptions + +unknown.configuration_pending = Configuration not valid. +unknown.configuration_pending = Configuration missing, discovery needed. Discovering... +offline.configuration_error = Discovery failure. Check IP Address. +offline.configuration_error = Invalid MideaAC config. Check IP Address. +unknown.configuration_pending = Retrieving token and key from cloud. +offline.configuration_error = Token and key could not be obtained from Cloud. +offline.configuration_error = No account info to retrieve from cloud +offline.communication_error = AC capabilities not returned +offline.communication_error = AC additional capabilities not returned +offline.configuration_error = Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 0d5fd96ed55c5..b471206e6ff5b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -41,24 +41,23 @@ - ipAddress + network-address IP Address of the device. - ipPort IP port of the device. 6444 + true - deviceId ID of the device. Leave 0 to do ID discovery. 0 + true - cloud Cloud Provider name for email and password. @@ -83,58 +82,54 @@ password1 - token Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). + true - key Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). + true - pollingTime - - Poll device status. Minimum is 30, default 60. + + Base device poll in seconds. Minimum is 30, default 60. 60 - energyPoll - - Energy polling frequency. default 0 ; (in case not supported). + + Energy polling in minutes. default 0 ; (in case not supported). 0 - - keyTokenUpdate - - Update the Key and Token from the cloud, default 0 to disable. + + + Update the Key and Token from the cloud in hours, default 0 to disable. 0 + true - timeout - - Connecting socket timeout. Minimum is 2, maximum is 10 (4 is default). + + Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default). 4 - promptTone - + After sending a command device will play "ding" tone when command is received and executed. false - version Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. 0 + true - energyDecode - + Binary-Coded Decimal (BCD) = true. Big-endian = false. true + true @@ -300,7 +295,7 @@ Switch - + Switch Alarm From 592d9b4d8ba32fcb7a01f75903b1d2f93cb49d68 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 4 Nov 2025 15:41:22 -0500 Subject: [PATCH 42/44] Improve configuration validation, error handling andi18n messages Refined configuration parameter validation and error messaging for MideaAC binding. Updated default AC version to 3, clarified keyTokenUpdate interval, improved asynchronous device/cloud parameter retrieval, and enhanced connection error handling. Updated documentation and i18n resources for clearer user guidance. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 34 +++---- .../internal/MideaACConfiguration.java | 5 +- .../connection/ConnectionManager.java | 22 +++-- .../internal/handler/MideaACHandler.java | 88 +++++++++++-------- .../resources/OH-INF/i18n/mideaac.properties | 16 ++-- .../resources/OH-INF/thing/thing-types.xml | 2 +- 6 files changed, 89 insertions(+), 78 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index f6e22d610e620..1399dc8c554e9 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -29,23 +29,23 @@ No binding configuration is required. ## Thing Configuration -| Parameter | Required ? | Comment | Default | Advanced | -|---------------|-------------|--------------------------------------------------------------|---------------------------|----------| -| ipAddress | Yes | IP Address of the device. | | | -| ipPort | Yes | IP port of the device | 6444 | Yes | -| deviceId | Yes | ID of the device. Leave 0 to do ID discovery. | 0 | Yes | -| cloud | Yes for V.3 | Your Cloud Provider name (or default). | NetHome Plus | | -| email | No | Email for your cloud account (or default). | nethome+us@mailinator.com | | -| password | No | Password for your cloud account (or default). | password1 | | -| token | Yes for V.3 | Secret Token - Retrieved from cloud | | Yes | -| key | Yes for V.3 | Secret Key - Retrieved from cloud | | Yes | -| pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | | -| keyTokenUpdate| No | Frequency to update key and token from cloud in hours | 0 hours (disabled) | Yes | -| energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | | -| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | | -| promptTone | Yes | "Ding" tone when command is received and executed. | false | | -| version | Yes | Version 3 has token, key and cloud requirements. | 0 | Yes | -| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true | Yes | +| Parameter | Required ? | Comment | Default | Advanced | +|---------------|-------------|-------------------------------------------------------------------|---------------------------|----------| +| ipAddress | Yes | IP Address of the device. | | | +| ipPort | Yes | IP port of the device | 6444 | Yes | +| deviceId | Yes | ID of the device. Leave 0 to do ID discovery. | 0 | Yes | +| cloud | Yes for V.3 | Your Cloud Provider name (or default). | NetHome Plus | | +| email | No | Email for your cloud account (or default). | nethome+us@mailinator.com | | +| password | No | Password for your cloud account (or default). | password1 | | +| token | Yes for V.3 | Secret Token - Retrieved from cloud | | Yes | +| key | Yes for V.3 | Secret Key - Retrieved from cloud | | Yes | +| pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | | +| keyTokenUpdate| No | Frequency to update key-token from cloud in hours. Minimum is 24 | 0 hours (disabled) | Yes | +| energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | | +| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | | +| promptTone | Yes | "Ding" tone when command is received and executed. | false | | +| version | Yes | Version 3 has token, key and cloud requirements. | 3 | Yes | +| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true | Yes | ## Channels diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java index 9b63e2f25f5cc..ada8cb7381bad 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/MideaACConfiguration.java @@ -77,7 +77,8 @@ public class MideaACConfiguration { public int energyPoll = 0; /** - * Key and Token Update Frequency in days + * Key and Token Update Frequency in hours + * 0 to disable. Minimum 24 hours best practice if used */ public int keyTokenUpdate = 0; @@ -94,7 +95,7 @@ public class MideaACConfiguration { /** * AC Version */ - public int version = 0; + public int version = 3; /** * Choose between Energy Decoding methods diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java index 4b1afd587923d..8decc90ecca35 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/connection/ConnectionManager.java @@ -144,13 +144,14 @@ public synchronized void connect() int maxTries = 3; int retrySocket = 0; - // If resending command add delay to avoid connection rejection - // Suspect that the AC device needs a few seconds to clear. + // If resending command add delay. Device needs time to clear. if (!resend) { try { Thread.sleep(5000); } catch (InterruptedException ex) { - logger.debug("An interupted error (resend command delay-connect) has occured {}", ex.getMessage()); + logger.debug("An interupted sleep error (resend command delay) has occured {}", ex.getMessage()); + Thread.currentThread().interrupt(); + throw new MideaConnectionException(ex); } } @@ -165,12 +166,13 @@ public synchronized void connect() } catch (SocketTimeoutException e) { retrySocket++; if (retrySocket < maxTries) { + // Device needs time to clear after timeout exception. try { Thread.sleep(5000); } catch (InterruptedException ex) { - logger.debug("An interupted error (socket retry) has occured {}", ex.getMessage()); + logger.debug("An interupted sleep error (socket retry delay) has occured {}", ex.getMessage()); Thread.currentThread().interrupt(); - throw new MideaConnectionException("Socket connection interrupted"); + throw new MideaConnectionException(ex); } logger.debug("Socket retry count {}, Socket timeout connecting to {}: {}", retrySocket, ipAddress, e.getMessage()); @@ -257,11 +259,13 @@ private void doV3Handshake() throws MideaConnectionException, MideaAuthenticatio Utils.hexStringToByteArray(key)); if (success) { logger.debug("Authentication successful"); - // Altering the sleep can cause write errors problems. Use caution. + // Reducing the sleep can cause write failures. Device needs time to clear. try { Thread.sleep(1000); } catch (InterruptedException e) { - logger.debug("An interupted error (success) has occured {}", e.getMessage()); + logger.debug("An interupted sleep error (Authentication) has occured {}", e.getMessage()); + Thread.currentThread().interrupt(); + throw new MideaConnectionException(e); } } else { throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration."); @@ -343,13 +347,13 @@ public synchronized void sendCommand(CommandBase command, @Nullable Callback cal } catch (InterruptedException e) { logger.debug("An interupted error (write command2) has occured {}", e.getMessage()); Thread.currentThread().interrupt(); - throw new MideaConnectionException("Command interrupted during wait"); + throw new MideaConnectionException(e); } // Input stream is checked after 1.5 seconds // Socket timeout (UI parameter) 2 seconds minimum. if (inputStream.available() == 0) { - logger.debug("Input stream empty sending second write {}", command); + logger.debug("Input stream still empty sending second write {}", command); write(bytes); } diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index c03c1b2210a2c..a37dfbfbe8bc9 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -222,97 +222,109 @@ public void handleCommand(ChannelUID channelUID, Command command) { @Override public void initialize() { config = getConfigAs(MideaACConfiguration.class); + updateStatus(ThingStatus.UNKNOWN); // Check for valid discovery configurations and discover again if not + // Mostly needed for partial textual configurations. UI discovery should be valid if (!config.isValid()) { - // Mark thing as unknown while asynchronous discovery/login is running to avoid race conditions - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, "Configuration not valid"); + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + "Required configuration parameter(s) are missing or invalid."); if (config.isDiscoveryPossible()) { + // Mark thing as UNKNOWN with message while attempting discovery. updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, - "Configuration missing, discovery needed. Discovering..."); + "Retriving required parameters from device or the cloud."); MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); - // Run discovery asynchronously so initialize() can return quickly, but keep the thing in UNKNOWN - // until the discovery attempt finishes and updates the thing status accordingly. + // Run discovery asynchronously and end this initialization thread. + // If successful, initialize() will be called again. scheduler.execute(() -> { try { - // perform the potentially blocking discovery/login work here discoveryService.discoverThing(config.ipAddress, this); } catch (Exception e) { - // discovery failed — update status to OFFLINE with an explanation - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Discovery failure. Check IP Address."); + // Required parameter discovery failed + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Could not retrieve required parameters from device or the cloud."); } }); return; } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid MideaAC config. Check IP Address."); + "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); return; } } else { - logger.debug("Valid discovery for {}", thing.getUID()); + logger.debug("Discovery parameters are valid for {}", thing.getUID()); } // Check for valid token and key and/or contact cloud account to get them if (config.version == 3 && !config.isV3ConfigValid()) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, + "Required configuration parameter(s) are missing or invalid."); + if (config.isTokenKeyObtainable()) { + // Mark thing as UNKNOWN with message while attempting token/key retrieval. updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, - "Retrieving token and key from cloud"); + "Retriving required parameters from device or the cloud."); + + // Run token and key retrieval asynchronously and end this initialization thread. + // If successful, initialize() will be called again. scheduler.execute(() -> { try { CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); getTokenKeyCloud(cloudProvider); } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Token and key could not be obtained from Cloud"); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Could not retrieve required parameters from device or the cloud."); } }); return; } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "No account info to retrieve from cloud"); + "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); return; } } else { - logger.debug("Valid security for V.3 device {}", thing.getUID()); + logger.debug("Valid token and key for V.3 device {}", thing.getUID()); } - updateStatus(ThingStatus.UNKNOWN); - // Initialize connectionManager for communication with the AC connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); - // Form and send capabilities command if not already present in properties + // Form and send capabilities command if not already present in properties (async) if (!properties.containsKey("modeFanOnly")) { - try { - CommandSet initializationCommand = new CommandSet(); - initializationCommand.getCapabilities(); - connectionManager.sendCommand(initializationCommand, this); - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "AC capabilities not returned"); - } - CapabilityParser parser = new CapabilityParser(); - logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); - if (parser.hasAdditionalCapabilities()) { + scheduler.execute(() -> { try { CommandSet initializationCommand = new CommandSet(); - initializationCommand.getAdditionalCapabilities(); - connectionManager.sendCommand(initializationCommand, this); + initializationCommand.getCapabilities(); + this.connectionManager.sendCommand(initializationCommand, this); + + // Check if additional capabilities should be fetched + CapabilityParser parser = new CapabilityParser(); + logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); + if (parser.hasAdditionalCapabilities()) { + // Attempt to fetch additional capabilities after a short delay + scheduler.schedule(() -> { + try { + CommandSet additionalCommand = new CommandSet(); + additionalCommand.getAdditionalCapabilities(); + this.connectionManager.sendCommand(additionalCommand, this); + } catch (Exception e) { + logger.debug("AC additional capabilities not returned {}", e.getMessage()); + } + }, 2, TimeUnit.SECONDS); + } } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "AC additional capabilities not returned"); + logger.debug("AC capabilities not returned {}", e.getMessage()); } - } + }); } // Establish routine polling per configuration or defaults if (scheduledTask == null) { - scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); + scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 5, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); } else { logger.debug("Scheduler already running"); @@ -577,8 +589,8 @@ public void getTokenKeyCloud(CloudProvider cloudProvider) { logger.debug("Token and Key obtained from cloud, saving, back to initialize"); initialize(); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String - .format("Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error")); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 10b41d4c10531..76fba09ee181b 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -30,7 +30,7 @@ thing-type.config.mideaac.ac.ipPort.description = IP port of the device. thing-type.config.mideaac.ac.key.label = Key thing-type.config.mideaac.ac.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it). thing-type.config.mideaac.ac.keyTokenUpdate.label = Key Token Update -thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud in hours, default 0 to disable. +thing-type.config.mideaac.ac.keyTokenUpdate.description = Update the Key and Token from the cloud in hours, min 24h, default 0 to disable. thing-type.config.mideaac.ac.password.label = Password thing-type.config.mideaac.ac.password.description = Password for cloud account chosen in Cloud Provider. thing-type.config.mideaac.ac.pollingTime.label = Poll Frequency @@ -104,13 +104,7 @@ channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air a # thing status descriptions -unknown.configuration_pending = Configuration not valid. -unknown.configuration_pending = Configuration missing, discovery needed. Discovering... -offline.configuration_error = Discovery failure. Check IP Address. -offline.configuration_error = Invalid MideaAC config. Check IP Address. -unknown.configuration_pending = Retrieving token and key from cloud. -offline.configuration_error = Token and key could not be obtained from Cloud. -offline.configuration_error = No account info to retrieve from cloud -offline.communication_error = AC capabilities not returned -offline.communication_error = AC additional capabilities not returned -offline.configuration_error = Can't retrieve Token and Key from Cloud; email, password and/or cloud parameter error +unknown.configuration_error = Required configuration parameter(s) are missing or invalid. +unknown.configuration_pending = Retrieving required parameters from device or the cloud. +offline.communication_error = Could not retrieve required parameters from device or the cloud. +offline.configuration_error = Invalid configuration parameters provided. Retrieval from device or cloud not possible. diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index b471206e6ff5b..385bcfadd298c 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -105,7 +105,7 @@ - Update the Key and Token from the cloud in hours, default 0 to disable. + Update the Key and Token from the cloud in hours, min 24h, default 0 to disable. 0 true From 4b6d55469e482e5e8dabc2a302e4897af948bc39 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 5 Nov 2025 09:27:01 -0500 Subject: [PATCH 43/44] Improve AC thing initialization documentation and i18n keys Refactored MideaACHandler initialization to clarify discovery and token/key retrieval steps, using more specific status messages and i18n keys. Updated i18n properties for granular error reporting. Marked 'timeout' and 'version' parameters as advanced in thing-types.xml and set version default to 3. Updated README to reflect 'timeout' as advanced. Signed-off-by: Bob Eckhoff --- bundles/org.openhab.binding.mideaac/README.md | 2 +- .../internal/handler/MideaACHandler.java | 89 ++++++++++--------- .../resources/OH-INF/i18n/mideaac.properties | 12 ++- .../resources/OH-INF/thing/thing-types.xml | 3 +- 4 files changed, 60 insertions(+), 46 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/README.md b/bundles/org.openhab.binding.mideaac/README.md index 1399dc8c554e9..69d356672db87 100644 --- a/bundles/org.openhab.binding.mideaac/README.md +++ b/bundles/org.openhab.binding.mideaac/README.md @@ -42,7 +42,7 @@ No binding configuration is required. | pollingTime | Yes | Frequency to Poll AC Status in seconds. Minimum is 30. | 60 seconds | | | keyTokenUpdate| No | Frequency to update key-token from cloud in hours. Minimum is 24 | 0 hours (disabled) | Yes | | energyPoll | Yes | Frequency to poll energy data (if supported) | 0 minutes (disabled) | | -| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | | +| timeout | Yes | Socket connection timeout in seconds. Min. is 2, max. 10. | 4 seconds | Yes | | promptTone | Yes | "Ding" tone when command is received and executed. | false | | | version | Yes | Version 3 has token, key and cloud requirements. | 3 | Yes | | energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true | Yes | diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index a37dfbfbe8bc9..d88bfca44f2a0 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -206,66 +206,68 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * To initialize an AC Thing, first check if discovery on your LAN has - * been completed. In the discovery process the IP address, IP port, device ID - * and version are established. Next for V.3 devices, if the token and key are not - * known, the cloud needs to be contacted to get the token and key using either - * the default NetHome Plus cloud or your own cloud account with your password and - * email. LAN discovery DOES NOT include the token key retrieval needed to communicate - * with the AC. V2 devices bypass the cloud connection step because they use a simplier - * hard-coded encryption. Next the Connection Manager is established. Then a command - * is formed and sent to retrieve the AC capabilities if they have not been - * discovered. Capabilities are not returned in the initial LAN Discovery. - * Lastly the routine polling, token key update and Energy polling frequency are set. - * + * To initialize an AC Thing, 1) Checks if discovery using the UI or your textual + * configuration has all the required parameters (IP address, IP port, device ID + * and version). If not, attempt LAN discovery (mostly for incomplete textual configs). + * 2) For V.3 devices, if the token and key are not known, attempt to retrieve + * them from the cloud using your email and password or the defaults. V2 devices + * bypass the cloud connection step because they use a simplier hard-coded encryption. + * 3) Initialize the Connection Manager to manage commands. At this point the device + * is fully functional. + * 4) The first command(s) retrieve the AC capabilities. These capabilities are not + * returned in the initial LAN Discovery. + * 5) Lastly the routine polling, Energy polling and token key update command + * frequencies are set. */ @Override public void initialize() { config = getConfigAs(MideaACConfiguration.class); - updateStatus(ThingStatus.UNKNOWN); - // Check for valid discovery configurations and discover again if not + // 1. Check for valid discovery configurations and discover again if not // Mostly needed for partial textual configurations. UI discovery should be valid if (!config.isValid()) { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, - "Required configuration parameter(s) are missing or invalid."); + // Mark thing as OFFLINE with message about discovery error. + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration_error_discovery"); if (config.isDiscoveryPossible()) { - // Mark thing as UNKNOWN with message while attempting discovery. - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, - "Retriving required parameters from device or the cloud."); + // Keep thing OFFLINE with message about attempting discovery. + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration_pending_discovery"); MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); - // Run discovery asynchronously and end this initialization thread. + // Kick off discovery asynchronously and end this initialization thread. // If successful, initialize() will be called again. scheduler.execute(() -> { try { discoveryService.discoverThing(config.ipAddress, this); } catch (Exception e) { - // Required parameter discovery failed + // Required parameter discovery failed due to communication error. updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not retrieve required parameters from device or the cloud."); + "@text/offline.communication_error_discovery"); } }); return; } else { + // Required parameters are missing and cannot be discovered. updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); + "@text/offline.configuration_error_invalid_discovery"); return; } } else { logger.debug("Discovery parameters are valid for {}", thing.getUID()); } - // Check for valid token and key and/or contact cloud account to get them + // 2. Check for valid token and key and/or contact cloud account to get them if (config.version == 3 && !config.isV3ConfigValid()) { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_ERROR, - "Required configuration parameter(s) are missing or invalid."); + // Mark thing as OFFLINE with message about token/key error. + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration_error_token"); if (config.isTokenKeyObtainable()) { - // Mark thing as UNKNOWN with message while attempting token/key retrieval. - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING, - "Retriving required parameters from device or the cloud."); + // Keep thing OFFLINE with message about attempting token/key retrieval. + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.configuration_pending_token"); // Run token and key retrieval asynchronously and end this initialization thread. // If successful, initialize() will be called again. @@ -275,25 +277,26 @@ public void initialize() { getTokenKeyCloud(cloudProvider); } catch (Exception e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Could not retrieve required parameters from device or the cloud."); + "@text/offline.communication_error_token"); } }); return; } else { + // Token and/or key are missing or invalid and cannot be obtained from cloud. updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); + "@text/offline.configuration_error_invalid_token "); return; } } else { logger.debug("Valid token and key for V.3 device {}", thing.getUID()); } - // Initialize connectionManager for communication with the AC + // 3. Initialize connectionManager with valid configuration parameters for communication with the AC connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); - // Form and send capabilities command if not already present in properties (async) + // 4. Send capabilities command (async) as first command if not previously retrieved (use default key as marker) if (!properties.containsKey("modeFanOnly")) { scheduler.execute(() -> { try { @@ -301,11 +304,10 @@ public void initialize() { initializationCommand.getCapabilities(); this.connectionManager.sendCommand(initializationCommand, this); - // Check if additional capabilities should be fetched + // Check if additional capabilities are available and fetch them if so CapabilityParser parser = new CapabilityParser(); logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); if (parser.hasAdditionalCapabilities()) { - // Attempt to fetch additional capabilities after a short delay scheduler.schedule(() -> { try { CommandSet additionalCommand = new CommandSet(); @@ -317,20 +319,26 @@ public void initialize() { }, 2, TimeUnit.SECONDS); } } catch (Exception e) { + // Will not affect AC device readiness, just log the issue logger.debug("AC capabilities not returned {}", e.getMessage()); } }); } - // Establish routine polling per configuration or defaults + // Set status to UNKNOWN first before going ONLINE (will be updated in pollJob to ONLINE) + // With the initial delay and the time for authentication and communication, the ac will + // be unknown for 4-5 seconds. + updateStatus(ThingStatus.UNKNOWN); + + // 5a. Establish routine polling per configuration or defaults if (scheduledTask == null) { - scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 5, config.pollingTime, TimeUnit.SECONDS); + scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); } else { logger.debug("Scheduler already running"); } - // Establish token key update frequency, if not disabled + // 5b. Establish token key update frequency, if not disabled if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) { scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, @@ -340,7 +348,7 @@ public void initialize() { logger.debug("Token Key Scheduler already running or disabled"); } - // Establish Energy polling, if not disabled. + // 5c. Establish Energy polling, if not disabled. if (config.energyPoll != 0 && scheduledEnergyUpdate == null) { scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 1, config.energyPoll, TimeUnit.MINUTES); @@ -373,6 +381,7 @@ private void pollJob() { try { connectionManager.getStatus(callbackLambda); + // If we reach here, the device is online. updateStatus(ThingStatus.ONLINE); } catch (MideaAuthenticationException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); @@ -590,7 +599,7 @@ public void getTokenKeyCloud(CloudProvider cloudProvider) { initialize(); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Invalid configuration parameters provided. Retrieval from device or cloud not possible."); + "@text/offline.configuration_error_invalid_token "); } } diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 76fba09ee181b..1e394ec1f5165 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -104,7 +104,11 @@ channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air a # thing status descriptions -unknown.configuration_error = Required configuration parameter(s) are missing or invalid. -unknown.configuration_pending = Retrieving required parameters from device or the cloud. -offline.communication_error = Could not retrieve required parameters from device or the cloud. -offline.configuration_error = Invalid configuration parameters provided. Retrieval from device or cloud not possible. +offline.configuration_error_discovery = Required discovery parameter(s) are missing or invalid +offline.configuration_pending_discovery = Retrieving required parameters via device discovery +offline.communication_error_discovery = Could not retrieve required parameters via device discovery +offline.configuration_error_invalid_discovery = Invalid discovery configuration parameters +offline.configuration_error_token = Required cloud parameter(s) are missing or invalid +offline.configuration_pending_token = Retrieving required parameters from the cloud provider +offline.communication_error_token = Could not retrieve required parameters from the cloud +offline.configuration_error_invalid_token = Invalid cloud configuration parameters provided diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml index 385bcfadd298c..2701a40881fb1 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/thing/thing-types.xml @@ -113,6 +113,7 @@ Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default). 4 + true @@ -122,7 +123,7 @@ Version 3 requires Token, Key and Cloud provider. Version 2 doesn't. - 0 + 3 true From fe1dafc58449cf61d094604c445aa1acb7133514 Mon Sep 17 00:00:00 2001 From: Leo Siepel Date: Wed, 5 Nov 2025 22:07:29 +0100 Subject: [PATCH 44/44] Refactor initialize Signed-off-by: Leo Siepel --- .../internal/handler/MideaACHandler.java | 229 ++++++++++-------- .../resources/OH-INF/i18n/mideaac.properties | 2 - 2 files changed, 128 insertions(+), 103 deletions(-) diff --git a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java index d88bfca44f2a0..6a6037bcc92ca 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java +++ b/bundles/org.openhab.binding.mideaac/src/main/java/org/openhab/binding/mideaac/internal/handler/MideaACHandler.java @@ -206,131 +206,158 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * To initialize an AC Thing, 1) Checks if discovery using the UI or your textual - * configuration has all the required parameters (IP address, IP port, device ID - * and version). If not, attempt LAN discovery (mostly for incomplete textual configs). - * 2) For V.3 devices, if the token and key are not known, attempt to retrieve - * them from the cloud using your email and password or the defaults. V2 devices - * bypass the cloud connection step because they use a simplier hard-coded encryption. - * 3) Initialize the Connection Manager to manage commands. At this point the device - * is fully functional. - * 4) The first command(s) retrieve the AC capabilities. These capabilities are not - * returned in the initial LAN Discovery. - * 5) Lastly the routine polling, Energy polling and token key update command - * frequencies are set. + * Initializes the handler by performing the following steps: + *
    + *
  1. Retrieves the configuration for the handler.
  2. + *
  3. Ensures the discovery or configuration is valid. If not, starts the discovery process and exits early.
  4. + *
  5. Ensures the token and key for V3 devices are available. If not, starts the retrieval process and exits + * early.
  6. + *
  7. Updates the thing's status to {@link ThingStatus#UNKNOWN}.
  8. + *
  9. Initializes the connection manager using the configuration.
  10. + *
  11. Requests device capabilities if they are missing.
  12. + *
  13. Starts any necessary schedulers for the handler.
  14. + *
*/ @Override public void initialize() { config = getConfigAs(MideaACConfiguration.class); - // 1. Check for valid discovery configurations and discover again if not - // Mostly needed for partial textual configurations. UI discovery should be valid - if (!config.isValid()) { - // Mark thing as OFFLINE with message about discovery error. + // 1) Ensure discovery/config is valid or start discovery and exit early + if (!ensureConfigOrStartDiscovery()) { + return; + } + + // 2) Ensure token/key for V3 devices or start retrieval and exit early + if (!ensureTokenKeyOrStartRetrieval()) { + return; + } + + updateStatus(ThingStatus.UNKNOWN); + + initConnectionManagerFromConfig(); + + requestCapabilitiesIfMissing(); + + startSchedulers(); + } + + /** + * Ensure we have all configuration needed to reach the device. If incomplete + * but discoverable, + * trigger discovery asynchronously and return false to stop current + * initialization. + */ + private boolean ensureConfigOrStartDiscovery() { + if (config.isValid()) { + logger.debug("Discovery parameters are valid for {}", thing.getUID()); + return true; + } + + if (!config.isDiscoveryPossible()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.configuration_error_discovery"); + "@text/offline.configuration_error_invalid_discovery"); + return false; + } - if (config.isDiscoveryPossible()) { + MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); + + // Kick off discovery asynchronously and end this initialization thread. + + scheduler.execute(() -> { + try { // Keep thing OFFLINE with message about attempting discovery. updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.configuration_pending_discovery"); - MideaACDiscoveryService discoveryService = new MideaACDiscoveryService(); - - // Kick off discovery asynchronously and end this initialization thread. - // If successful, initialize() will be called again. - scheduler.execute(() -> { - try { - discoveryService.discoverThing(config.ipAddress, this); - } catch (Exception e) { - // Required parameter discovery failed due to communication error. - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.communication_error_discovery"); - } - }); - return; - } else { - // Required parameters are missing and cannot be discovered. - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.configuration_error_invalid_discovery"); - return; + discoveryService.discoverThing(config.ipAddress, this); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication_error_discovery"); } - } else { - logger.debug("Discovery parameters are valid for {}", thing.getUID()); + }); + + return false; + } + + /** + * Ensure token/key are available for V3 devices. If retrievable from cloud, + * trigger async + * retrieval and return false to stop current initialization. + */ + private boolean ensureTokenKeyOrStartRetrieval() { + if (config.version != 3 || config.isV3ConfigValid()) { + logger.debug("Valid token and key for V.3 device {}", thing.getUID()); + return true; } - // 2. Check for valid token and key and/or contact cloud account to get them - if (config.version == 3 && !config.isV3ConfigValid()) { - // Mark thing as OFFLINE with message about token/key error. + if (!config.isTokenKeyObtainable()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.configuration_error_token"); + "@text/offline.configuration_error_invalid_token"); + return false; + } - if (config.isTokenKeyObtainable()) { - // Keep thing OFFLINE with message about attempting token/key retrieval. + scheduler.execute(() -> { + try { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/offline.configuration_pending_token"); - - // Run token and key retrieval asynchronously and end this initialization thread. - // If successful, initialize() will be called again. - scheduler.execute(() -> { - try { - CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); - getTokenKeyCloud(cloudProvider); - } catch (Exception e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "@text/offline.communication_error_token"); - } - }); - return; - } else { - // Token and/or key are missing or invalid and cannot be obtained from cloud. - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "@text/offline.configuration_error_invalid_token "); - return; + CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud); + getTokenKeyCloud(cloudProvider); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication_error_token"); } - } else { - logger.debug("Valid token and key for V.3 device {}", thing.getUID()); - } + }); - // 3. Initialize connectionManager with valid configuration parameters for communication with the AC + return false; + } + + /** Initialize the connection manager from the current configuration. */ + private void initConnectionManagerFromConfig() { connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress, config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password, config.deviceId, config.version, config.promptTone); + } - // 4. Send capabilities command (async) as first command if not previously retrieved (use default key as marker) - if (!properties.containsKey("modeFanOnly")) { - scheduler.execute(() -> { - try { - CommandSet initializationCommand = new CommandSet(); - initializationCommand.getCapabilities(); - this.connectionManager.sendCommand(initializationCommand, this); - - // Check if additional capabilities are available and fetch them if so - CapabilityParser parser = new CapabilityParser(); - logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); - if (parser.hasAdditionalCapabilities()) { - scheduler.schedule(() -> { - try { - CommandSet additionalCommand = new CommandSet(); - additionalCommand.getAdditionalCapabilities(); - this.connectionManager.sendCommand(additionalCommand, this); - } catch (Exception e) { - logger.debug("AC additional capabilities not returned {}", e.getMessage()); - } - }, 2, TimeUnit.SECONDS); - } - } catch (Exception e) { - // Will not affect AC device readiness, just log the issue - logger.debug("AC capabilities not returned {}", e.getMessage()); - } - }); + /** + * Send capabilities command(s) if we don't yet have them stored in properties. + */ + private void requestCapabilitiesIfMissing() { + if (properties.containsKey("modeFanOnly")) { + return; } - // Set status to UNKNOWN first before going ONLINE (will be updated in pollJob to ONLINE) - // With the initial delay and the time for authentication and communication, the ac will - // be unknown for 4-5 seconds. - updateStatus(ThingStatus.UNKNOWN); + scheduler.execute(() -> { + try { + CommandSet initializationCommand = new CommandSet(); + initializationCommand.getCapabilities(); + this.connectionManager.sendCommand(initializationCommand, this); + + // Check if additional capabilities are available and fetch them if so + CapabilityParser parser = new CapabilityParser(); + logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities()); + if (parser.hasAdditionalCapabilities()) { + scheduler.schedule(() -> { + try { + CommandSet additionalCommand = new CommandSet(); + additionalCommand.getAdditionalCapabilities(); + this.connectionManager.sendCommand(additionalCommand, this); + } catch (Exception e) { + logger.debug("AC additional capabilities not returned {}", e.getMessage()); + } + }, 2, TimeUnit.SECONDS); + } + } catch (Exception e) { + // Will not affect AC device readiness, just log the issue + logger.debug("AC capabilities not returned {}", e.getMessage()); + } + }); + } - // 5a. Establish routine polling per configuration or defaults + /** + * Start routine, token refresh and energy schedulers according to + * configuration. + */ + private void startSchedulers() { + // Routine polling if (scheduledTask == null) { scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS); logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime); @@ -338,7 +365,7 @@ public void initialize() { logger.debug("Scheduler already running"); } - // 5b. Establish token key update frequency, if not disabled + // Token/key update if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) { scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay( () -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate, @@ -348,7 +375,7 @@ public void initialize() { logger.debug("Token Key Scheduler already running or disabled"); } - // 5c. Establish Energy polling, if not disabled. + // Energy polling if (config.energyPoll != 0 && scheduledEnergyUpdate == null) { scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 1, config.energyPoll, TimeUnit.MINUTES); diff --git a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties index 1e394ec1f5165..26031658b078d 100644 --- a/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties +++ b/bundles/org.openhab.binding.mideaac/src/main/resources/OH-INF/i18n/mideaac.properties @@ -104,11 +104,9 @@ channel-type.mideaac.turbo-mode.description = Turbo mode, "Boost" in Midea Air a # thing status descriptions -offline.configuration_error_discovery = Required discovery parameter(s) are missing or invalid offline.configuration_pending_discovery = Retrieving required parameters via device discovery offline.communication_error_discovery = Could not retrieve required parameters via device discovery offline.configuration_error_invalid_discovery = Invalid discovery configuration parameters -offline.configuration_error_token = Required cloud parameter(s) are missing or invalid offline.configuration_pending_token = Retrieving required parameters from the cloud provider offline.communication_error_token = Could not retrieve required parameters from the cloud offline.configuration_error_invalid_token = Invalid cloud configuration parameters provided