From 925fc2860ea0a34c1ba3dc27db8b38e32fda66e7 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Tue, 29 Oct 2024 17:02:02 -0400 Subject: [PATCH 01/12] 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 170f8ce163d..3d3fdf4074a 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1101,6 +1101,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 00000000000..38d625e3492 --- /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 00000000000..47450dadf04 --- /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 00000000000..a19f12966b8 --- /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 00000000000..e7f89f4dcb8 --- /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 00000000000..35d419e3f27 --- /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 00000000000..12667638da4 --- /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 00000000000..bea42f4ff56 --- /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 00000000000..eedad2b658e --- /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 00000000000..44dbc131cc2 --- /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 00000000000..a1d25cd41bb --- /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 00000000000..cfdbe02de67 --- /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 00000000000..22714ebff84 --- /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 00000000000..ac92bfd0064 --- /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 00000000000..3b4552e3357 --- /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 00000000000..e81a4cdcb3a --- /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 00000000000..6be330c8095 --- /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 00000000000..444d13ee74b --- /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 00000000000..022ae685f28 --- /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 00000000000..b521f5aa9c0 --- /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 00000000000..c213ee489dc --- /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 00000000000..601d6bb5c70 --- /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 00000000000..617dc367aaa --- /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 00000000000..8eec22960c5 --- /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 00000000000..8087638cb9b --- /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 00000000000..4767a09ed31 --- /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 00000000000..98f30a9f964 --- /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 00000000000..a803250bcb9 --- /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 00000000000..a728420af66 --- /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 00000000000..76e9808a6e6 --- /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 00000000000..3f75ff40869 --- /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 00000000000..1f113499bb9 --- /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 68102f6c939..4a3349de42f 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -256,6 +256,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 a2159bed2ad387c38225d63ff23c325ff2afc677 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 6 Nov 2024 11:08:59 -0500 Subject: [PATCH 02/12] 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 | 1455 +++-------------- .../mideaac/internal/handler/Packet.java | 13 +- .../resources/OH-INF/thing/thing-types.xml | 8 - .../internal/MideaACConfigurationTest.java | 6 +- 13 files changed, 1356 insertions(+), 1294 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 47450dadf04..206126153a1 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 12667638da4..e19e4333502 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 class MideaACConfiguration { * @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 bea42f4ff56..759418a7c29 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 @@ import org.osgi.service.component.annotations.Reference; @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 class MideaACHandlerFactory extends BaseThingHandlerFactory { */ @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 00000000000..61d00d9f02c --- /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 00000000000..accbb93b820 --- /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 00000000000..db8b0ce1d97 --- /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 00000000000..efbf5129f83 --- /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 00000000000..44510e573a8 --- /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 00000000000..98f8464b3e0 --- /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 444d13ee74b..aad6aeb15dc 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 @@ 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.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 @@ import org.slf4j.LoggerFactory; * @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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler @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 - */ + ConnectionManager connectionManager = this.connectionManager; + if (connectionManager == null) { + logger.warn("The connection manager was unexpectedly null, please report a bug"); + return; + } 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; + 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); - } - - 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; + 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); } + } catch (MideaConnectionException | MideaAuthenticationException e) { + logger.warn("Unable to proces command: {}", 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); - } - } - } else { - logger.debug("Command must be of type StringType: {}", command); - commandSet.setOffTimer(false, hours, minutes); - } - - getConnectionManager().sendCommandAndMonitor(commandSet); } /** @@ -675,18 +184,8 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler */ @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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler initialize(); } - /** - * Manage the ONLINE/OFFLINE statuses of the thing with problems (or lack thereof) - */ - private void markOnline() { - if (!isOnline()) { - updateStatus(ThingStatus.ONLINE); - } - } + 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 markOffline() { - if (isOnline()) { - updateStatus(ThingStatus.OFFLINE); - } - } + configuration.put(CONFIG_TOKEN, tk.token()); + configuration.put(CONFIG_KEY, tk.key()); + updateConfiguration(configuration); - 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()); + 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"); } - 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); + public void stopScheduler() { + ScheduledFuture localScheduledTask = this.scheduledTask; + + 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."); + } } - 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; - } - } + // 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 022ae685f28..7e942491683 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 java.time.LocalDateTime; 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 @@ import org.openhab.binding.mideaac.internal.Utils; 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 class Packet { 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 class Packet { 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 a803250bcb9..46a1f9a48bb 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 a728420af66..4c330036448 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 class MideaACConfigurationTest { @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 class MideaACConfigurationTest { @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 99ed3d50001b7c9073fd29aafc8b9ce96f6632b3 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 10 Nov 2024 11:15:10 -0500 Subject: [PATCH 03/12] 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 206126153a1..cb41f093599 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 35d419e3f27..c9559a57185 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 e19e4333502..b31abacacd0 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 class MideaACConfiguration { */ 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 61d00d9f02c..1b4e58a1689 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 class CommandHelper { 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 accbb93b820..cd837711aa6 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { 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 @@ public class ConnectionManager { } 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 @@ public class ConnectionManager { 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 @@ public class ConnectionManager { * 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 class ConnectionManager { } 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { } 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 class ConnectionManager { } 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 class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { } /** - * 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 cfdbe02de67..f5341e055d2 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 class MideaACDiscoveryService extends AbstractDiscoveryService { } } } 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 class MideaACDiscoveryService extends AbstractDiscoveryService { 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 class MideaACDiscoveryService extends AbstractDiscoveryService { 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 class MideaACDiscoveryService extends AbstractDiscoveryService { 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 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService { 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 aad6aeb15dc..b1f8b8af421 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 @@ package org.openhab.binding.mideaac.internal.handler; 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler } /** - * 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler * 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler 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 class MideaACHandler extends BaseThingHandler implements DiscoveryHandler } } - 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 b521f5aa9c0..56615ee2a80 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 class Response { 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 class Response { 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 98f30a9f964..0a449f11025 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 46a1f9a48bb..13f29be8fe3 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 4c330036448..d0463985edc 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 76e9808a6e6..74d6794f943 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 2a73de9d069dc51e203ee46ff1b32ed35df254a0 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 10 Nov 2024 11:25:48 -0500 Subject: [PATCH 04/12] 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 cd837711aa6..a8582127366 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 class ConnectionManager { } } 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 class ConnectionManager { } } 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 32c26658110d8264904ed4f3ab73cfb05223414a Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 15 Nov 2024 09:34:07 -0500 Subject: [PATCH 05/12] 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 b31abacacd0..9d1793056a9 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 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @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 a8582127366..ab5b1893590 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 class ConnectionManager { } 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 98f8464b3e0..7df25059185 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 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @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 4767a09ed31..1ee28925ed1 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 23ec6aeb7e82536f9d03877036b2224d9020886a Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 15 Nov 2024 09:37:34 -0500 Subject: [PATCH 06/12] 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 7df25059185..29d6a0b703e 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 @@ import org.eclipse.jdt.annotation.NonNullByDefault; */ @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 18d67a08c5bf141c12cf9feac930a86eaad232c6 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sat, 23 Nov 2024 11:09:48 -0500 Subject: [PATCH 07/12] 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 ab5b1893590..7cea3dd053e 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 class ConnectionManager { 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 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()); } - 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); - } - } 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 class ConnectionManager { } logger.debug("Connected to IP {}", ipAddress); deviceIsConnected = true; - retry = true; } /** @@ -321,6 +319,7 @@ public class ConnectionManager { 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 class ConnectionManager { 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 class ConnectionManager { } 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 736551b6131f05f27ec1f5837b92e20817d6b1c2 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Sun, 24 Nov 2024 20:10:59 -0500 Subject: [PATCH 08/12] 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 7cea3dd053e..e281f992037 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 class ConnectionManager { 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); + 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; } - 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()); + 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 class ConnectionManager { } 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 class ConnectionManager { /** * Reads the inputStream byte array * - * @return byte array + * @return byte array or null */ public synchronized byte @Nullable [] read() { byte[] bytes = new byte[512]; From 0993ebc13c2f0c833b9cc21c47be780c8008db46 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 16 Dec 2024 10:07:17 -0500 Subject: [PATCH 09/12] 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 a19f12966b8..1e12614c8c8 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 c9559a57185..e334a558cc7 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 9d1793056a9..da850155468 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 759418a7c29..73e6de5d69a 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 eedad2b658e..1d10e038c0a 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 1b4e58a1689..0866797994e 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 e281f992037..b14b0495416 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 db8b0ce1d97..57d8a8bff1d 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 efbf5129f83..c1456a76796 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 44510e573a8..95a169c32da 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 44dbc131cc2..d98d47f918c 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 a1d25cd41bb..2de5d0460b8 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 f5341e055d2..7cfe567dd18 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 22714ebff84..e01f5bb811d 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 ac92bfd0064..a38a4781701 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 3b4552e3357..e70094f7162 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 29d6a0b703e..366dd36196f 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 e81a4cdcb3a..9bb0e211a8e 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 6be330c8095..e389a8504e6 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 b1f8b8af421..6cc514beeba 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 7e942491683..42e3db763f6 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 56615ee2a80..7132b63ef4e 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 c213ee489dc..eb9951d4277 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 601d6bb5c70..4b5a524c9c3 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 617dc367aaa..73663dadbec 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 8eec22960c5..afd54a93663 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 8087638cb9b..81190692b8b 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 d0463985edc..f018cd6603e 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 74d6794f943..60d5e2dac4f 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 3f75ff40869..9e872780c59 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 1f113499bb9..38061bd9e4f 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 2c3b4c90b7ffebf6819cadaae49107fc1159464e Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Mon, 16 Dec 2024 10:13:00 -0500 Subject: [PATCH 10/12] 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 e334a558cc7..c9559a57185 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 da850155468..9d1793056a9 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 73e6de5d69a..759418a7c29 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 1d10e038c0a..eedad2b658e 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 0866797994e..1b4e58a1689 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 b14b0495416..e281f992037 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 57d8a8bff1d..db8b0ce1d97 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 c1456a76796..efbf5129f83 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 95a169c32da..44510e573a8 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 d98d47f918c..44dbc131cc2 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 2de5d0460b8..a1d25cd41bb 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 7cfe567dd18..f5341e055d2 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 e01f5bb811d..22714ebff84 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 a38a4781701..ac92bfd0064 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 e70094f7162..3b4552e3357 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 366dd36196f..29d6a0b703e 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 9bb0e211a8e..e81a4cdcb3a 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 e389a8504e6..6be330c8095 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 6cc514beeba..b1f8b8af421 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 42e3db763f6..7e942491683 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 7132b63ef4e..56615ee2a80 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 eb9951d4277..c213ee489dc 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 4b5a524c9c3..601d6bb5c70 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 73663dadbec..617dc367aaa 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 afd54a93663..8eec22960c5 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 81190692b8b..8087638cb9b 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 f018cd6603e..d0463985edc 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 60d5e2dac4f..74d6794f943 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 9e872780c59..3f75ff40869 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 38061bd9e4f..1f113499bb9 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 071c1e73918b2d9068b82f3cbbc06f2db8cb6117 Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Fri, 3 Jan 2025 10:21:08 -0500 Subject: [PATCH 11/12] 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 c9559a57185..e334a558cc7 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 9d1793056a9..da850155468 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 759418a7c29..73e6de5d69a 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 eedad2b658e..1d10e038c0a 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 1b4e58a1689..0866797994e 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 e281f992037..b14b0495416 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 db8b0ce1d97..57d8a8bff1d 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 efbf5129f83..c1456a76796 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 44510e573a8..95a169c32da 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 44dbc131cc2..d98d47f918c 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 a1d25cd41bb..2de5d0460b8 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 f5341e055d2..7cfe567dd18 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 22714ebff84..e01f5bb811d 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 ac92bfd0064..a38a4781701 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 3b4552e3357..e70094f7162 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 29d6a0b703e..366dd36196f 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 e81a4cdcb3a..9bb0e211a8e 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 6be330c8095..e389a8504e6 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 b1f8b8af421..6cc514beeba 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 7e942491683..42e3db763f6 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 56615ee2a80..7132b63ef4e 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 c213ee489dc..eb9951d4277 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 601d6bb5c70..4b5a524c9c3 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 617dc367aaa..73663dadbec 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 8eec22960c5..afd54a93663 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 8087638cb9b..81190692b8b 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 d0463985edc..f018cd6603e 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 74d6794f943..60d5e2dac4f 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 3f75ff40869..9e872780c59 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 1f113499bb9..38061bd9e4f 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 7350e5020f31d8ef58cf85f11854fcb5018a286a Mon Sep 17 00:00:00 2001 From: Bob Eckhoff Date: Wed, 8 Jan 2025 16:37:06 -0500 Subject: [PATCH 12/12] 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 e334a558cc7..6f5e4e86e74 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 da850155468..4dd9aa2b3a5 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 73e6de5d69a..7581c619b7f 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 1d10e038c0a..4d524e4a9a3 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 0866797994e..b8283a5993b 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 b14b0495416..3c72a16e4be 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 57d8a8bff1d..655e70608f1 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 c1456a76796..9f1b6692d6f 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 95a169c32da..c2bcbb21893 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 d98d47f918c..888086eea4c 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 2de5d0460b8..36f4c0a43ef 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 7cfe567dd18..bae4ab89544 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 e01f5bb811d..956af511263 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 a38a4781701..c1c907b5373 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 e70094f7162..00df03e3ebb 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 366dd36196f..a6e85da5f39 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 9bb0e211a8e..c796b8b1273 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 e389a8504e6..843ddcea309 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 6cc514beeba..5ef9ad4a01e 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 42e3db763f6..873f165082b 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 7132b63ef4e..964e4662ca5 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 eb9951d4277..d448bd478e6 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 4b5a524c9c3..e1d136233cc 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 73663dadbec..f746a913124 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 afd54a93663..c113676e478 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 81190692b8b..b70e2e600cd 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 f018cd6603e..97ebe323d26 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 60d5e2dac4f..5d9d98be771 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 9e872780c59..8e687ce0b37 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 38061bd9e4f..61880766f1f 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