[mideaac] Add Midea Dehumidifier to Midea AC binding (#20665)

* Add Midea Dehumidifier to Midea AC binding

Signed-off-by: Bob Eckhoff <katmandodo@yahoo.com>
This commit is contained in:
Robert Eckhoff
2026-05-13 22:57:52 +02:00
committed by GitHub
parent 1bd4da1e03
commit cff335eed3
50 changed files with 3470 additions and 1075 deletions
+67 -45
View File
@@ -1,8 +1,8 @@
# Midea AC Binding
This binding integrates Air Conditioners that use the Midea protocol. Midea is an OEM for many brands.
This binding integrates Air Conditioners (type AC) and Dehumidifiers 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.
A Midea device is likely supported if it uses one of the following Android apps or it's iOS equivalent.
| Cloud Provider | Comment | Options | Default |
|----------------------------------------------|------------------------------------------|--------------|---------|
@@ -10,16 +10,16 @@ An AC device is likely supported if it uses one of the following Android apps or
| NetHome Plus (com.midea.aircondition) | Full Support of key and token updates | NetHome Plus | Yes |
| SmartHome/MSmartHome (com.midea.ai.overseas) | Note: Reports that this cloud is offline | MSmartHome | |
Note: The Air Conditioner must already be set-up on your WiFi network with a fixed IP Address to be discovered.
Note: The Midea device must already be set-up on your WiFi network with a fixed IP Address to be discovered.
## Supported Things
This binding supports one Thing type `ac`.
This binding supports Thing types (Air Conditioner) `ac` and (Dehumidifier) `a1`
## Discovery
Once the Air Conditioner is on your network activating the Inbox scan with this binding will send an IP broadcast message.
Every responding unit gets added to the Inbox. When adding each Thing, the required parameters will be populated with either
Once the thing is on your network, activating the Inbox scan with this binding will send an IP broadcast message.
Every responding unit gets added to the Inbox. When adding each thing, the required parameters will be populated with either
discovered values or the default settings. For a V.3 device, in the unlikely event the defaults did not get the token and key,
enter your cloud provider, email and password.
@@ -46,67 +46,89 @@ No binding configuration is required.
| promptTone | Yes | "Ding" tone when command is received and executed. | false | |
| version | Yes | Version 3 has token, key and cloud requirements. | 3 | Yes |
| energyDecode | Yes | Binary Coded Decimal (BCD) = true. Big-endian = false. | true | Yes |
| deviceType | Yes | (Air Conditioner) `ac` and (Dehumidifier) `a1` | ac | Yes |
## Channels
Following channels are available:
Note: After discovery, the Thing properties dropdown on the Thing UI page will show what channels and modes your device supports.
Note: After discovery, the thing properties dropdown on the Thing UI page will show what channels and modes your device supports.
| Channel | Type | Description | Read only | Advanced |
|---------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------|
| power | Switch | Turn the AC on or off. | | |
| target-temperature | Number:Temperature | Target temperature. | | |
| operational-mode | String | Operational modes: OFF, AUTO, COOL, DRY, HEAT, FAN ONLY | | |
| fan-speed | String | Fan speeds: OFF (turns off), SILENT, LOW, MEDIUM, HIGH, AUTO. Not all modes supported by all units. | | |
| swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | |
| eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | |
| turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT mode only. | | |
| sleep-function | Switch | Sleep function ("Moon with a star" icon on IR Remote Controller). | | |
| indoor-temperature | Number:Temperature | Indoor temperature measured in the room, where internal unit is installed. | Yes | |
| outdoor-temperature | Number:Temperature | Outdoor temperature by external unit. Some units do not report reading when off. | Yes | |
| temperature-unit | Switch | Sets the LED display on the evaporator to Fahrenheit (true) or Celsius (false). | | Yes |
| on-timer | String | Sets the future time to turn on the AC. | | Yes |
| off-timer | String | Sets the future time to turn off the AC. | | Yes |
| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes |
| maximum-humidity | Number | If device supports, allows setting the maximum humidity in DRY mode | | Yes |
| humidity | Number | If device supports, the indoor room humidity. | Yes | Yes |
| energy-consumption | Number | If device supports, cumulative Kilowatt-Hours usage | Yes | Yes |
| current-draw | Number | If device supports, instantaneous amperage usage | Yes | Yes |
| power-consumption | Number | If device supports, instantaneous wattage reading | Yes | Yes |
| appliance-error | Switch | If device supports, appliance error notification | Yes | Yes |
| filter-status | Switch | If device supports, notification that filter needs cleaning | Yes | Yes |
| auxiliary-heat | Switch | If device supports, auxiliary heat (On or Off) | Yes | Yes |
| Channel | Type | Description | Read only | Advanced | AC | DH |
|---------------------|--------------------|--------------------------------------------------------------------------------------------------------|-----------|----------|----|----|
| power | Switch | Turn the Thing on or off. | | | X | X |
| target-temperature | Number:Temperature | Target temperature for AC. | | | X | |
| operational-mode | String | AC Operational modes: AUTO, COOL, DRY, HEAT, FAN ONLY | | | X | |
| dehumidifier-mode | String | Dehumidifier Operational modes: AUTO, MANUAL, CONTINUOUS, CLOTHES DRY, SHOE DRY | | | | X |
| fan-speed | String | Fan speeds: SILENT, LOW, MEDIUM, HIGH, FULL, AUTO. Not all modes supported by all units. | | | X | |
| dh-fan-speed | String | Fan speeds: OFF (turns off), LOWEST, LOW, MEDIUM, HIGH, AUTO. | | | | X |
| swing-mode | String | Swing mode: OFF, VERTICAL, HORIZONTAL, BOTH. Not all modes supported by all units. | | | X | |
| dehumidifier-swing | Switch | Turns Dehumidifier Swing mode On or Off | | | | X |
| dh-tank-setpoint | Number | Dehumidifier Tank Setpoint 25%, 50%, 75%, 100% | | | | X |
| dehumidifier-tank | Number | Dehumidifier Tank Level | Yes | | | X |
| eco-mode | Switch | Eco mode - Cool only (Temperature is set to 24 C (75 F) and fan on AUTO) | | | X | |
| turbo-mode | Switch | Turbo mode, "Boost" in Midea Air app, long press "+" on IR Remote Controller. COOL and HEAT only. | | | X | |
| sleep-function | Switch | Sleep function ("Moon with a star" icon on IR Remote Controller). | | | X | |
| indoor-temperature | Number:Temperature | Indoor temperature measured in the room, where internal unit is installed. | Yes | | X | X |
| outdoor-temperature | Number:Temperature | Outdoor temperature by external unit. Some units do not report reading when off. | Yes | | X | |
| temperature-unit | Switch | Sets the LED display on the evaporator to Fahrenheit (true) or Celsius (false). | | Yes | X | |
| on-timer | String | Sets the future time to turn on the Device. | | Yes | X | X |
| off-timer | String | Sets the future time to turn off the Device. | | Yes | X | X |
| screen-display | Switch | If device supports across LAN, turns off the LED display. | | Yes | X | |
| maximum-humidity | Number | Dehumidifier control point, AC If device supports in DRY mode | | | X | X |
| humidity | Number | If device supports, the indoor room humidity. | Yes | Yes | X | X |
| energy-consumption | Number | If device supports, cumulative Kilowatt-Hours usage | Yes | Yes | X | |
| current-draw | Number | If device supports, instantaneous amperage usage | Yes | Yes | X | |
| power-consumption | Number | If device supports, instantaneous wattage reading | Yes | Yes | X | |
| appliance-error | Switch | If device supports, appliance error notification | Yes | Yes | X | |
| filter-status | Switch | If device supports, notification that filter needs cleaning | Yes | Yes | X | |
| auxiliary-heat | Switch | If device supports, auxiliary heat (On or Off) | Yes | Yes | X | |
| child-lock | Switch | If device supports, Child Lock (On or Off) | Yes | Yes | | X |
| anion | Switch | If device supports, Anion (On or Off) | Yes | Yes | | X |
## Examples
### `demo.things` Examples
```java
Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, energyPoll = 0, timeout=4, promptTone="false", version="3", energyDecode="true"]
Thing mideaac:ac:mideaac "myAC" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, energyPoll = 0, timeout=4, promptTone="false", version="3", energyDecode="true", deviceType="ac"]
Thing mideaac:a1:mideaac "myDehumidifier" @ "Room" [ ipAddress="192.168.1.200", ipPort=6444, deviceId="deviceId", cloud="your cloud (e.g NetHome Plus)", email="yourclouduser@email.com", password="yourcloudpassword", token="token", key ="key", pollingTime = 60, keyTokenUpdate = 0, energyPoll = 0, timeout=4, promptTone="false", version="3", deviceType="a1"]
```
Minimal IP Address Option to use the built-in defaults.
```java
Thing mideaac:ac:mideaac "myAC" @ "myRoom" [ ipAddress="192.168.0.200"]
Thing mideaac:ac:air_conditioner "myAC" @ "myRoom" [ ipAddress="192.168.0.200"] or
Thing mideaac:a1:dehumidifier "myDehumidifier" @ "myRoom" [ ipAddress="192.168.0.200"]
```
### `demo.items` Examples
### `demo.items` AC 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" }
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" }
Switch power "Power" { channel="mideaac:ac:air_conditioner:power" }
Number:Temperature target_temperature "Target Temperature [%.1f °F]" { channel="mideaac:ac:air_conditioner:target-temperature" }
String operational_mode "Operational Mode" { channel="mideaac:ac:air_conditioner:operational-mode" }
String fan_speed "Fan Speed" { channel="mideaac:ac:air_conditioner:fan-speed" }
String swing_mode "Swing Mode" { channel="mideaac:ac:air_conditioner:swing-mode" }
Number:Temperature indoor_temperature "Indoor Temperature [%.1f °F]" { channel="mideaac:ac:air_conditioner:indoor-temperature" }
Switch eco_mode "Eco Mode" { channel="mideaac:ac:air_conditioner:eco-mode" }
Switch turbo_mode "Turbo Mode" { channel="mideaac:ac:air_conditioner:turbo-mode" }
Switch sleep_function "Sleep function" { channel="mideaac:ac:air_conditioner:sleep-function" }
Switch temperature_unit "Fahrenheit or Celsius" { channel="mideaac:ac:air_conditioner:temperature-unit" }
```
### `demo.sitemap` Examples
### `demo.items` Dehumidifier Example
```java
Switch power "Power" { channel="mideaac:a1:dehumidifier:power" }
Number maximum_humidity "Maximum Humidity" { channel="mideaac:a1:dehumidifier:maximum-humidity" }
String dehumidifier_mode "Dehumidifier Mode" { channel="mideaac:a1:dehumidifier:dehumidifier-mode" }
String dh_fan_speed "Dehumidifier Fan Speed" { channel="mideaac:a1:dehumidifier:dh-fan-speed" }
Switch dehumidifier_swing "Dehumidifier Swing Mode" { channel="mideaac:a1:dehumidifier:dehumidifier-swing" }
Number:Temperature indoor_temperature "Indoor Temperature [%.1f °F]" { channel="mideaac:a1:dehumidifier:indoor-temperature" }
Number humidity "Humidity in Room" { channel="mideaac:a1:dehumidifier:humidity" }
```
### `demo.sitemap` AC Example
```java
sitemap midea label="Midea AC"{
@@ -27,7 +27,7 @@ import org.openhab.core.thing.ThingTypeUID;
* used across the whole binding.
*
* @author Jacek Dobrowolski - Initial contribution
* @author Bob Eckhoff - OH naming conventions and capability properties
* @author Bob Eckhoff - OH naming conventions, capability properties and dehumidifier additions
*/
@NonNullByDefault
public class MideaACBindingConstants {
@@ -35,24 +35,36 @@ public class MideaACBindingConstants {
private static final String BINDING_ID = "mideaac";
/**
* Thing Type
* Thing Types
*/
public static final ThingTypeUID THING_TYPE_MIDEAAC = new ThingTypeUID(BINDING_ID, "ac");
public static final ThingTypeUID THING_TYPE_AC = new ThingTypeUID(BINDING_ID, "ac");
public static final ThingTypeUID THING_TYPE_DEHUMIDIFIER = new ThingTypeUID(BINDING_ID, "a1");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_MIDEAAC);
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(Set.of(THING_TYPE_AC, THING_TYPE_DEHUMIDIFIER));
/**
* List of all channel IDS
* List of all channel IDS for all Devices
*/
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_DEHUMIDIFIER_MODE = "dehumidifier-mode";
public static final String CHANNEL_FAN_SPEED = "fan-speed";
public static final String CHANNEL_DH_FAN_SPEED = "dh-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_SWING_ANGLE_HORIZONTAL = "swing-angle-horizontal";
public static final String CHANNEL_SWING_ANGLE_VERTICAL = "swing-angle-vertical";
public static final String CHANNEL_DEHUMIDIFIER_SWING = "dehumidifier-swing";
public static final String CHANNEL_AUXILIARY_HEAT = "auxiliary-heat";
public static final String CHANNEL_PURIFIER_MODE = "purifier-mode";
public static final String CHANNEL_CHILD_LOCK = "child-lock";
public static final String CHANNEL_ANION = "anion";
public static final String CHANNEL_DEHUMIDIFIER_TANK = "dehumidifier-tank";
public static final String CHANNEL_DEHUMIDIFIER_TANK_SETPOINT = "dh-tank-setpoint";
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";
@@ -75,11 +87,12 @@ public class MideaACBindingConstants {
public static final String CHARSET = "US-ASCII";
/**
* List of all AC thing properties
* List of all 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_DEVICE_TYPE = "deviceType";
public static final String CONFIG_CLOUD = "cloud";
public static final String CONFIG_EMAIL = "email";
public static final String CONFIG_PASSWORD = "password";
@@ -96,15 +109,18 @@ public class MideaACBindingConstants {
// Properties from LAN Discovery
public static final String PROPERTY_SN = "sn";
public static final String PROPERTY_SSID = "ssid";
public static final String PROPERTY_TYPE = "type";
// Capabilities properties discoverable
public static final String PROPERTY_ANION = "anion";
public static final String PROPERTY_AUX_ELECTRIC_HEAT = "auxElectricHeat";
public static final String PROPERTY_AUX_MODE_AUTO = "auxModeAuto";
public static final String PROPERTY_AUX_MODE_ON = "auxModeOn";
public static final String PROPERTY_AUX_MODE_OFF = "auxModeOff";
public static final String PROPERTY_BREEZE_AWAY = "breezeAway";
public static final String PROPERTY_BREEZE_CONTROL = "breezeControl";
public static final String PROPERTY_BREEZELESS = "breezeless";
public static final String PROPERTY_BUZZER = "buzzer";
public static final String PROPERTY_CHILD_LOCK = "childLock";
public static final String PROPERTY_DISPLAY_CONTROL = "displayControl";
public static final String PROPERTY_ENERGY_STATS = "energyStats";
public static final String PROPERTY_ENERGY_SETTING = "energySetting";
@@ -132,8 +148,10 @@ public class MideaACBindingConstants {
public static final String PROPERTY_PRESET_IECO = "ieco";
public static final String PROPERTY_PRESET_TURBO_COOL = "turboCool";
public static final String PROPERTY_PRESET_TURBO_HEAT = "turboHeat";
public static final String PROPERTY_PURIFIER = "purifier";
public static final String PROPERTY_RATE_SELECT = "rateSelect5Level";
public static final String PROPERTY_SELF_CLEAN = "selfClean";
public static final String PROPERTY_SLEEP_MODE = "sleepMode";
public static final String PROPERTY_SMART_EYE = "smartEye";
public static final String PROPERTY_SWING_LR_ANGLE = "swingHorizontalAngle";
public static final String PROPERTY_SWING_UD_ANGLE = "swingVerticalAngle";
@@ -40,6 +40,11 @@ public class MideaACConfiguration {
*/
public String deviceId = "0";
/**
* Device Type. Default Midea AC
*/
public String deviceType = "ac";
/**
* Cloud Account email
*/
@@ -17,6 +17,7 @@ import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.SUPPO
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mideaac.internal.handler.MideaACHandler;
import org.openhab.binding.mideaac.internal.handler.MideaDehumidifierHandler;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
@@ -33,6 +34,7 @@ import org.osgi.service.component.annotations.Reference;
* handlers.
*
* @author Jacek Dobrowolski - Initial contribution
* @author Bob Eckhoff - OH addons changes, added Dehumidifier handler
*/
@NonNullByDefault
@Component(configurationPid = "binding.mideaac", service = ThingHandlerFactory.class)
@@ -40,6 +42,8 @@ public class MideaACHandlerFactory extends BaseThingHandlerFactory {
private final HttpClientFactory httpClientFactory;
private final UnitProvider unitProvider;
private static final ThingTypeUID THING_TYPE_AC = MideaACBindingConstants.THING_TYPE_AC;
private static final ThingTypeUID THING_TYPE_DEHUMIDIFIER = MideaACBindingConstants.THING_TYPE_DEHUMIDIFIER;
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
@@ -60,10 +64,14 @@ public class MideaACHandlerFactory extends BaseThingHandlerFactory {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID)) {
ThingTypeUID typeUID = thing.getThingTypeUID();
if (typeUID.equals(THING_TYPE_AC)) {
return new MideaACHandler(thing, unitProvider, httpClientFactory.getCommonHttpClient());
} else if (typeUID.equals(THING_TYPE_DEHUMIDIFIER)) {
return new MideaDehumidifierHandler(thing, unitProvider, httpClientFactory.getCommonHttpClient());
}
return null;
}
}
@@ -0,0 +1,85 @@
/*
* Copyright (c) 2010-2026 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.callbacks;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.a1.A1Response;
import org.openhab.binding.mideaac.internal.devices.ac.EnergyResponse;
import org.openhab.binding.mideaac.internal.devices.ac.HumidityResponse;
import org.openhab.binding.mideaac.internal.devices.ac.Response;
import org.openhab.binding.mideaac.internal.devices.ac.TemperatureResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
/**
* The {@link Response} performs the polling byte data stream decoding
* The {@link CapabilitiesResponse} performs the capability byte data stream decoding
* The {@link EnergyResponse} performs the energy byte stream data decoding
* The {@link HumidityResponse} performs decoding of unsolicited message 0xA0
* The {@link TemperatureResponse} performs decoding of unsolicited message 0xA1
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public interface ACCallback extends Callback {
/**
* Updates channels with a standard response (0xC0).
*
* @param response The standard response from the device used to update channels.
*/
@Override
void updateChannels(Response response);
/**
* Updates channels with a capabilities response (0xB5).
*
* @param capabilitiesResponse The capabilities response from the device used to update properties.
*/
@Override
void updateChannels(CapabilitiesResponse capabilitiesResponse);
/**
* Updates channels with a Energy response (0xC1 - 0x44).
*
* @param energyResponse The Energy response from the device used to update energy.
*/
@Override
void updateChannels(EnergyResponse energyResponse);
/**
* Updates humidity with a Energy response (0xC1 - 0x45).
*
* @param energyResponse The Energy response from a humidity Poll used to update humidity.
*/
@Override
void updateHumidityFromEnergy(EnergyResponse energyResponse);
/**
* Updates channels with an unsolicted Humidity Response (0xA0).
*
* @param humidityResponse The unsolicited (0xA0) response from the device used to update properties.
*/
@Override
void updateChannels(HumidityResponse humidityResponse);
/**
* Updates channels with an unsolicited Temperature response (0xA1).
*
* @param temperatureResponse The unsolicited (0xA1) response from the device used to update properties.
*/
@Override
void updateChannels(TemperatureResponse temperatureResponse);
default void updateChannels(A1Response a1Response) {
// No implementation needed for AC
}
}
@@ -10,10 +10,15 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.callbacks;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.devices.a1.A1Response;
import org.openhab.binding.mideaac.internal.devices.ac.EnergyResponse;
import org.openhab.binding.mideaac.internal.devices.ac.HumidityResponse;
import org.openhab.binding.mideaac.internal.devices.ac.Response;
import org.openhab.binding.mideaac.internal.devices.ac.TemperatureResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
/**
* The {@link Response} performs the polling byte data stream decoding
@@ -21,6 +26,7 @@ import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesRes
* The {@link EnergyResponse} performs the energy byte stream data decoding
* The {@link HumidityResponse} performs decoding of unsolicited message 0xA0
* The {@link TemperatureResponse} performs decoding of unsolicited message 0xA1
* The {@link A1Response} performs decoding of standard humidifier message 0xC8
*
* @author Leo Siepel - Initial contribution
* @author Bob Eckhoff - added additional Callbacks after Response
@@ -68,4 +74,11 @@ public interface Callback {
* @param temperatureResponse The unsolicited (0xA1) response from the device used to update properties.
*/
void updateChannels(TemperatureResponse temperatureResponse);
/**
* Updates channels with standard humidifier (0xC8) response.
*
* @param response The standard humidifier (0xC8) response from the device used to update properties.
*/
void updateChannels(A1Response a1Response);
}
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2010-2026 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.callbacks;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.a1.A1Response;
import org.openhab.binding.mideaac.internal.devices.ac.EnergyResponse;
import org.openhab.binding.mideaac.internal.devices.ac.HumidityResponse;
import org.openhab.binding.mideaac.internal.devices.ac.Response;
import org.openhab.binding.mideaac.internal.devices.ac.TemperatureResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
/**
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public interface HumidifierCallback extends Callback {
/**
* Updates dehumidifier channels with (0xC8) response.
*
* @param a1Response The humidifier (0xC8) response from the device used to update properties.
*/
void updateChannels(A1Response a1Response);
default void updateChannels(Response response) {
// No implementation needed for humidifier
}
/**
* Updates dehumidifier channels with a capabilities response (0xB5).
*
*/
void updateChannels(CapabilitiesResponse capabilitiesResponse);
default void updateChannels(EnergyResponse energyResponse) {
// No implementation needed for humidifier
}
default void updateHumidityFromEnergy(EnergyResponse energyResponse) {
// No implementation needed for humidifier
}
default void updateChannels(HumidityResponse humidityResponse) {
// No implementation needed for humidifier
}
default void updateChannels(TemperatureResponse temperatureResponse) {
// No implementation needed for humidifier
}
}
@@ -13,8 +13,10 @@
package org.openhab.binding.mideaac.internal.cloud;
import java.nio.ByteOrder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
@@ -102,7 +104,8 @@ public class Cloud {
data.addProperty("clientType", CLIENT_TYPE);
data.addProperty("language", LANGUAGE);
data.addProperty("src", cloudProvider.appid());
data.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
DateFormat fmt = new SimpleDateFormat("yyyyMMddHHmmss", Locale.ROOT);
data.addProperty("stamp", fmt.format(new Date()));
}
// For the getLoginId() this adds the email account
@@ -262,7 +265,8 @@ public class Cloud {
iotData.addProperty("pushToken", Utils.tokenUrlsafe(120));
iotData.addProperty("reqId", StringUtils.getRandomHex(16));
iotData.addProperty("src", cloudProvider.appid());
iotData.addProperty("stamp", new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
DateFormat fmt = new SimpleDateFormat("yyyyMMddHHmmss", Locale.ROOT);
iotData.addProperty("stamp", fmt.format(new Date()));
newData.add("iotData", iotData);
@Nullable
@@ -20,25 +20,29 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
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.callbacks.Callback;
import org.openhab.binding.mideaac.internal.cloud.CloudProvider;
import org.openhab.binding.mideaac.internal.connection.exception.MideaAuthenticationException;
import org.openhab.binding.mideaac.internal.connection.exception.MideaConnectionException;
import org.openhab.binding.mideaac.internal.connection.exception.MideaException;
import org.openhab.binding.mideaac.internal.handler.Callback;
import org.openhab.binding.mideaac.internal.handler.CommandBase;
import org.openhab.binding.mideaac.internal.handler.CommandSet;
import org.openhab.binding.mideaac.internal.handler.EnergyResponse;
import org.openhab.binding.mideaac.internal.handler.HumidityResponse;
import org.openhab.binding.mideaac.internal.handler.Packet;
import org.openhab.binding.mideaac.internal.handler.Response;
import org.openhab.binding.mideaac.internal.handler.TemperatureResponse;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.devices.CommandBase;
import org.openhab.binding.mideaac.internal.devices.Packet;
import org.openhab.binding.mideaac.internal.devices.a1.A1CommandBase;
import org.openhab.binding.mideaac.internal.devices.a1.A1CommandSet;
import org.openhab.binding.mideaac.internal.devices.a1.A1Response;
import org.openhab.binding.mideaac.internal.devices.ac.ACCommandSet;
import org.openhab.binding.mideaac.internal.devices.ac.EnergyResponse;
import org.openhab.binding.mideaac.internal.devices.ac.HumidityResponse;
import org.openhab.binding.mideaac.internal.devices.ac.Response;
import org.openhab.binding.mideaac.internal.devices.ac.TemperatureResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.security.Decryption8370Result;
import org.openhab.binding.mideaac.internal.security.Security;
import org.openhab.binding.mideaac.internal.security.Security.MsgType;
@@ -68,7 +72,9 @@ public class ConnectionManager {
private String token;
private final String cloud;
private final String deviceId;
private String deviceType;
private Response lastResponse;
private A1Response lastA1Response;
private CloudProvider cloudProvider;
private Security security;
private final int version;
@@ -95,9 +101,10 @@ public class ConnectionManager {
* @param deviceId Device ID
* @param version Device version
* @param promptTone Tone after command true or false
* @param deviceType Device type a1 or ac
*/
public ConnectionManager(String ipAddress, int ipPort, int timeout, String key, String token, String cloud,
String email, String password, String deviceId, int version, boolean promptTone) {
String email, String password, String deviceId, int version, boolean promptTone, String deviceType) {
this.deviceIsConnected = false;
this.ipAddress = ipAddress;
this.ipPort = ipPort;
@@ -106,10 +113,13 @@ public class ConnectionManager {
this.token = token;
this.cloud = cloud;
this.deviceId = deviceId;
this.deviceType = deviceType;
this.version = version;
this.promptTone = promptTone;
this.lastResponse = new Response(HexFormat.of().parseHex("C00042667F7F003C0000046066000000000000000000F9ECDB"),
version);
this.lastA1Response = new A1Response(
HexFormat.of().parseHex("C80104507F7F003700000000000000001E64000000003A67C2"));
this.cloudProvider = CloudProvider.getCloudProvider(cloud);
this.security = new Security(cloudProvider);
}
@@ -118,15 +128,6 @@ public class ConnectionManager {
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;
}
/**
* The socket is established with the writer and inputStream (for reading responses)
* V2 devices will proceed to send the poll or the set command.
@@ -270,7 +271,7 @@ public class ConnectionManager {
} else {
throw new MideaAuthenticationException("Invalid Key. Correct Key in configuration.");
}
} else if (Arrays.equals(new String("ERROR").getBytes(), response)) {
} else if (Arrays.equals(new String("ERROR").getBytes(StandardCharsets.US_ASCII), response)) {
throw new MideaAuthenticationException("Authentication failed!");
} else {
logger.debug("Authentication reponse unexpected data length ({} instead of 72)!", response.length);
@@ -284,7 +285,7 @@ public class ConnectionManager {
/**
* Sends the routine polling command from the DoPoll
* in the MideaACHandler
* in the Abstract Midea Handler that is passed to subclasses
*
* @param callback
* @throws MideaConnectionException
@@ -293,7 +294,12 @@ public class ConnectionManager {
*/
public void getStatus(Callback callback)
throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException {
CommandBase requestStatusCommand = new CommandBase();
CommandBase requestStatusCommand;
if ("a1".equals(deviceType)) {
requestStatusCommand = new A1CommandBase();
} else {
requestStatusCommand = new CommandBase();
}
sendCommand(requestStatusCommand, callback);
}
@@ -302,6 +308,10 @@ public class ConnectionManager {
connect();
}
void setDeviceType(String deviceType) {
this.deviceType = deviceType;
}
/**
* Pulls the packet byte array together. There is a check to
* make sure to make sure the input stream is empty before sending
@@ -322,9 +332,14 @@ public class ConnectionManager {
throws MideaConnectionException, MideaAuthenticationException, MideaException, IOException {
ensureConnected();
if (command instanceof CommandSet cmdSet) {
if (command instanceof ACCommandSet cmdSet) {
cmdSet.setPromptTone(promptTone);
}
if (command instanceof A1CommandSet cmdSet) {
cmdSet.setPromptTone(promptTone);
}
Packet packet = new Packet(command, deviceId, security);
packet.compose();
@@ -436,7 +451,7 @@ public class ConnectionManager {
Utils.bytesToBinary(data));
// Validate the proper length
if (data.length < 21) {
if (data.length < 21 && bodyType != (byte) 0x1E && bodyType != (byte) 0xB5) {
logger.warn("Response data is {} long, minimum is 21!", data.length);
return;
}
@@ -446,7 +461,7 @@ public class ConnectionManager {
logger.warn("Error response 0x1E received {} from IP Address:{}", bodyType, ipAddress);
return;
case (byte) 0xB5:
case (byte) 0xB5: // Possible same for 0xB1 - future?
try {
logger.debug("Capabilities response detected with bodyType 0xB5.");
CapabilitiesResponse capabilitiesResponse = new CapabilitiesResponse(data);
@@ -478,18 +493,35 @@ public class ConnectionManager {
return;
case (byte) 0xA0:
try {
logger.debug("Response detected with bodyType 0xA0. Data length: {}", data.length);
HumidityResponse humidityResponse = new HumidityResponse(data);
if (callback != null) {
callback.updateChannels(humidityResponse);
} else {
logger.debug("Callback is null for unsolicited 0xA0 humidity response, channels not updated.");
if ("ac".equals(deviceType)) {
try {
logger.debug("Response detected with bodyType 0xA0. Data length: {}", data.length);
HumidityResponse humidityResponse = new HumidityResponse(data);
if (callback != null) {
callback.updateChannels(humidityResponse);
} else {
logger.debug(
"Callback is null for unsolicited 0xA0 humidity response, channels not updated.");
}
} catch (Exception ex) {
logger.debug("Unsolicited 0xA0 response exception: {}", ex.getMessage());
throw new MideaException(ex);
}
} catch (Exception ex) {
logger.debug("Unsolicited 0xA0 response exception: {}", ex.getMessage());
throw new MideaException(ex);
return;
} else if ("a1".equals(deviceType)) {
lastA1Response = new A1Response(data);
try {
logger.trace("Data length is {}, dehumidifier, IP address is {}", data.length, ipAddress);
if (callback != null) {
callback.updateChannels(lastA1Response);
}
} catch (Exception ex) {
logger.debug("Dehumidifier Poll response exception: {}", ex.getMessage());
throw new MideaException(ex);
}
return;
}
logger.debug("0xA0 response received for unknown device type: {}", deviceType);
return;
case (byte) 0xA1:
@@ -526,6 +558,19 @@ public class ConnectionManager {
}
return;
case (byte) 0xC8:
lastA1Response = new A1Response(data);
try {
logger.trace("Data length is {}, dehumidifier, IP address is {}", data.length, ipAddress);
if (callback != null) {
callback.updateChannels(lastA1Response);
}
} catch (Exception ex) {
logger.debug("Dehumidifier Poll response exception: {}", ex.getMessage());
throw new MideaException(ex);
}
return;
default:
logger.warn("Unexpected response bodyType {}", bodyType);
}
@@ -546,7 +591,7 @@ public class ConnectionManager {
inputStream.close();
socket.close();
} catch (IOException e) {
logger.warn("IOException closing connection to device at {}: {}", ipAddress, e.getMessage(), e);
logger.warn("IOException closing connection to device at {}", ipAddress, e);
}
socket = null;
inputStream = null;
@@ -593,6 +638,14 @@ public class ConnectionManager {
}
}
public Response getLastResponse() {
return lastResponse;
}
public A1Response getLastA1Response() {
return lastA1Response;
}
/**
* Disconnects from the AC device
*
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2010-2026 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.devices;
import java.time.LocalDateTime;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.security.Crc8;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CommandBase} has the discover command, the routine poll command
* and the enums for Operational Modes, Swing Modes, and Fan Speeds.
*
* @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 = new byte[1];
/**
* Returns the command data for testing.
*
* @return command data
*/
public byte[] getData() {
return data;
}
/**
* 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 - This is the message length
(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 };
applyTimestamp();
}
protected void applyTimestamp() {
LocalDateTime now = LocalDateTime.now();
data[data.length - 1] = (byte) now.getSecond();
}
/**
* Pulls the elements of the Base command together
*/
public void compose() {
logger.debug("Base Bytes before crypt {}", HexUtils.bytesToHex(data));
byte crc8 = (byte) Crc8.calculate(Arrays.copyOfRange(data, 10, data.length));
byte[] newData1 = new byte[data.length + 1];
System.arraycopy(data, 0, newData1, 0, data.length);
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);
}
return (byte) ((255 - (sum % 256)) + 1);
}
}
@@ -10,10 +10,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices;
import java.math.BigInteger;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.Utils;
@@ -60,10 +62,13 @@ public class Packet {
// 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() };
Instant now = Instant.now();
LocalDateTime ldt = LocalDateTime.ofInstant(now, ZoneId.systemDefault());
long millis = now.toEpochMilli();
byte[] datetimeBytes = { (byte) (ldt.getYear() / 100), (byte) (ldt.getYear() % 100), (byte) ldt.getMonthValue(),
(byte) ldt.getDayOfMonth(), (byte) ldt.getHour(), (byte) ldt.getMinute(), (byte) ldt.getSecond(),
(byte) millis };
System.arraycopy(datetimeBytes, 0, packet, 12, 8);
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.CommandBase;
/**
* The {@link A1CommandBase} builds the base command frame for A1-style devices.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class A1CommandBase extends CommandBase {
/**
* Base class for A1-style commands
*/
public A1CommandBase() {
super(); // build the AC-style base frame then modify for A1
data[2] = (byte) 0xa1; // override device type byte
data[14] = (byte) 0x00; // override 0x03 byte for poll
data[15] = (byte) 0x00; // override 0xff byte for poll
data[17] = (byte) 0x00; // override 0x02 byte for poll
}
}
@@ -0,0 +1,316 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.Timer;
import org.openhab.binding.mideaac.internal.devices.Timer.TimeParser;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1FanSpeed;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1OperationalMode;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link A1CommandHelper} is a static class that is able to translate {@link Command} to {@link A1CommandSet}
* for Midea Dehumidifier devices.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class A1CommandHelper {
private static Logger logger = LoggerFactory.getLogger(A1CommandHelper.class);
private static final StringType HUMIDIFIER_MODE_MANUAL = new StringType("MANUAL");
private static final StringType HUMIDIFIER_MODE_CONTINUOUS = new StringType("CONTINUOUS");
private static final StringType HUMIDIFIER_MODE_AUTO = new StringType("AUTO");
private static final StringType HUMIDIFIER_MODE_CLOTHES_DRY = new StringType("CLOTHES_DRY");
private static final StringType HUMIDIFIER_MODE_SHOES_DRY = new StringType("SHOES_DRY");
private static final StringType FAN_SPEED_OFF = new StringType("OFF");
private static final StringType FAN_SPEED_LOWEST = new StringType("LOWEST");
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_AUTO = new StringType("AUTO");
/**
* Device Power ON OFF for A1 Dehumidifier
*
* @param command On or Off
*/
public static A1CommandSet handlePower(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.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;
}
/**
* Fan Speeds for A1 Dehumidifier
* This command can also turn the power 7F
*
* @param command Fan Speed Auto, Low, High, etc.
*/
public static A1CommandSet handleA1FanSpeed(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command instanceof StringType) {
commandSet.setPowerState(true);
if (command.equals(FAN_SPEED_OFF)) {
commandSet.setPowerState(false);
} else if (command.equals(FAN_SPEED_LOWEST)) {
commandSet.setA1FanSpeed(A1FanSpeed.LOWEST);
} else if (command.equals(FAN_SPEED_LOW)) {
commandSet.setA1FanSpeed(A1FanSpeed.LOW);
} else if (command.equals(FAN_SPEED_MEDIUM)) {
commandSet.setA1FanSpeed(A1FanSpeed.MEDIUM);
} else if (command.equals(FAN_SPEED_HIGH)) {
commandSet.setA1FanSpeed(A1FanSpeed.HIGH);
} else if (command.equals(FAN_SPEED_AUTO)) {
commandSet.setA1FanSpeed(A1FanSpeed.AUTO);
} else {
throw new UnsupportedOperationException(String.format("Unknown fan speed command: {}", command));
}
}
return commandSet;
}
/**
* Supported Midea Humidifier modes
*
* @param command Modes MANUAL, CONTINUOUS, AUTO, CLOTHES_DRY, SHOES_DRY
*/
public static A1CommandSet handleA1OperationalMode(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command instanceof StringType) {
if (command.equals(HUMIDIFIER_MODE_MANUAL)) {
commandSet.setA1OperationalMode(A1OperationalMode.MANUAL);
} else if (command.equals(HUMIDIFIER_MODE_CONTINUOUS)) {
commandSet.setA1OperationalMode(A1OperationalMode.CONTINUOUS);
} else if (command.equals(HUMIDIFIER_MODE_AUTO)) {
commandSet.setA1OperationalMode(A1OperationalMode.AUTO);
} else if (command.equals(HUMIDIFIER_MODE_CLOTHES_DRY)) {
commandSet.setA1OperationalMode(A1OperationalMode.CLOTHES_DRY);
} else if (command.equals(HUMIDIFIER_MODE_SHOES_DRY)) {
commandSet.setA1OperationalMode(A1OperationalMode.SHOES_DRY);
} else {
throw new UnsupportedOperationException(String.format("Unknown operational mode command: {}", command));
}
}
return commandSet;
}
/**
* Handle A1 Dehumidifier Swing Mode
*
* @param command Swing Mode
*/
public static A1CommandSet handleA1Swing(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setA1SwingMode(false);
} else if (command.equals(OnOffType.ON)) {
commandSet.setA1SwingMode(true);
} else {
throw new UnsupportedOperationException(String.format("Unknown Swing mode command: {}", command));
}
return commandSet;
}
/**
* Handle A1 Dehumidifier Child Lock
*
* @param command Child Lock
*/
public static A1CommandSet handleA1ChildLock(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setA1ChildLock(false);
} else if (command.equals(OnOffType.ON)) {
commandSet.setA1ChildLock(true);
} else {
throw new UnsupportedOperationException(String.format("Unknown Child Lock command: {}", command));
}
return commandSet;
}
/**
* Handle A1 Dehumidifier Anion
*
* @param command Anion
*/
public static A1CommandSet handleA1Anion(Command command, A1Response lastResponse)
throws UnsupportedOperationException {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setA1Anion(false);
} else if (command.equals(OnOffType.ON)) {
commandSet.setA1Anion(true);
} else {
throw new UnsupportedOperationException(String.format("Unknown Anion 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 A1CommandSet handleOnTimer(Command command, A1Response lastResponse) {
A1CommandSet commandSet = A1CommandSet.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 A1CommandSet handleOffTimer(Command command, A1Response lastResponse) {
A1CommandSet commandSet = A1CommandSet.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;
}
/**
* Sets the Target Humidity for Dehumidifier
*
* @param command Target Humidity
*/
public static A1CommandSet handleA1MaximumHumidity(Command command, A1Response lastResponse) {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command instanceof DecimalType decimalCommand) {
int humidity = decimalCommand.intValue();
commandSet.setA1MaximumHumidity(limitA1HumidityToRange(humidity));
} else {
logger.debug("Unknown target humidity command: {}", command);
}
return commandSet;
}
// Limit Humidity to range
private static int limitA1HumidityToRange(int humidity) {
if (humidity < 35) {
return 35;
}
if (humidity > 85) {
return 85;
}
return humidity;
}
/**
* Sets Tank setpoint for the Dehumidifier
*
* @param command Tank Setpoint
*/
public static A1CommandSet handleA1TankSetpoint(Command command, A1Response lastResponse) {
A1CommandSet commandSet = A1CommandSet.fromResponse(lastResponse);
if (command instanceof DecimalType decimalCommand) {
int setpoint = decimalCommand.intValue();
commandSet.setTankSetpoint(setpoint);
} else {
logger.debug("Unknown target humidity command: {}", command);
}
return commandSet;
}
}
@@ -0,0 +1,384 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.Timer.TimerData;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1FanSpeed;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1OperationalMode;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This {@link A1CommandSet} class handles the allowed changes originating from
* the items linked to the Midea dehumidifier channels. Not all devices
* support all commands. The general process is to clear the
* bit(s) the set them to the command value and change message type to command.
*
* @author Bob Eckhoff - - Initial contribution
*/
@NonNullByDefault
public class A1CommandSet extends A1CommandBase {
private final Logger logger = LoggerFactory.getLogger(A1CommandSet.class);
/**
* Byte array structure for Command set
*/
public A1CommandSet() {
super();
data[0x01] = (byte) 0x23;
data[0x09] = (byte) 0x02; // Setting Mode
data[0x0a] = (byte) 0x48; // Command Message for Dehumidifier
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;
// Zap the old timestamp
int oldTimestampIndex = data.length - 4;
data[oldTimestampIndex] = 0x00;
// Add the new timestamp
applyTimestamp();
}
// Controllable items for Dehumidifier
public static A1CommandSet fromResponse(A1Response dh) {
A1CommandSet commandSet = new A1CommandSet();
// Common fields
commandSet.setPowerState(dh.getPowerState());
commandSet.setOnTimer(dh.getOnTimerData());
commandSet.setOffTimer(dh.getOffTimerData());
commandSet.setPromptTone(dh.getPromptTone());
// DH-specific fields
commandSet.setA1FanSpeed(dh.getA1FanSpeed());
commandSet.setA1OperationalMode(dh.getA1OperationalMode());
commandSet.setA1MaximumHumidity(dh.getMaximumHumidity());
commandSet.setA1ChildLock(dh.getA1ChildLock());
commandSet.setA1SwingMode(dh.getA1SwingMode());
commandSet.setA1Anion(dh.getA1Anion());
commandSet.setTankSetpoint(dh.getTankSetpoint());
return commandSet;
}
/**
* Causes Midea device to beep when Set command received
*
* @param feedbackEnabled Beep On or Off
*/
public void setPromptTone(boolean feedbackEnabled) {
if (!feedbackEnabled) {
data[0x0b] &= ~(byte) 0x40; // Clear
} else {
data[0x0b] |= (byte) 0x40; // Set
}
}
/**
* Turns device On or Off (Use whole byte?)
*
* @param state on or off
*/
public void setPowerState(boolean state) {
data[0x0b] &= ~0x80;
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;
}
/**
* Manual, Continuous, Auto, etc. See Command Base class
*
* @param mode Manual, Continuous, Auto, etc.
*/
public void setA1OperationalMode(A1OperationalMode mode) {
data[0x0c] &= ~(byte) 0x0f;
data[0x0c] |= (byte) mode.getId();
}
/**
* For Testing assertion get result
*
* @return operational mode
*/
public int getA1OperationalMode() {
return data[0x0c] & (byte) 0x0f;
}
/**
* Low, Medium, High, Auto etc. See Command Base class
*
* @param speed Set fan speed
*/
public void setA1FanSpeed(A1FanSpeed speed) {
data[0x0d] = (byte) (speed.getId());
}
/**
* For Testing assertion get Fan Speed results
*
* @return fan speed as a number
*/
public int getA1FanSpeed() {
return data[0x0d];
}
/**
* Set the ON timer for Dehumidifier 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 Humidifier 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;
}
/**
* Sets the Target Humidity for dehumidifier
*
* @param humidity
*/
public void setA1MaximumHumidity(int humidity) {
data[0x11] &= ~(byte) 0xff;
data[0x11] |= humidity;
}
/**
* Child Lock setter for Dehumidifier
*
* @param childLockEnabled sets child lock true or false
*/
public void setA1ChildLock(boolean childLockEnabled) {
if (!childLockEnabled) {
data[0x12] &= ~0x80;
} else {
data[0x12] |= 0x80;
}
}
/**
* Anion setter for Dehumidifier
*
* @param anionEnabled sets anion true or false
*/
public void setA1Anion(boolean anionEnabled) {
if (!anionEnabled) {
data[0x13] &= ~0x40;
} else {
data[0x13] |= 0x40;
}
}
/**
* Swing mode setter for Dehumidifier
*
* @param mode sets swing mode
*/
public void setA1SwingMode(boolean swingModeEnabled) {
if (!swingModeEnabled) {
data[0x14] &= ~0x20;
} else {
data[0x14] |= 0x20;
}
}
/**
* For Testing assertion get Dehumidifier Swing result
*
* @return swing mode
*/
public boolean getA1SwingMode() {
return (data[0x14] & 0x20) > 0;
}
/**
* Sets the Humidifier Water tank level setpoint
*
* @param humidity Water tank level setpoint
*/
public void setTankSetpoint(int tankSetpoint) {
data[0x17] &= ~(byte) 0xff;
data[0x17] |= tankSetpoint;
}
/**
* Creates the Initial Get Capability message
*
* @return Capability message
*/
public void getCapabilities() {
modifyBytesForCapabilities();
removeExtraCapabilityBytes();
logger.debug("Set Capability Bytes before encrypt {}", HexUtils.bytesToHex(data));
}
private void modifyBytesForCapabilities() {
data[0x01] = (byte) 0x0E;
data[0x09] = (byte) 0x03;
data[0x0a] = (byte) 0xB5;
data[0x0b] = (byte) 0x01;
data[0x0c] = (byte) 0x00;
}
private void removeExtraCapabilityBytes() {
byte[] newData = new byte[data.length - 21];
System.arraycopy(data, 0, newData, 0, newData.length);
data = newData;
}
/**
* Creates the Additional Get capability message
*
* @return Additional Capability message
*/
public void getAdditionalCapabilities() {
modifyBytesForAdditionalCapabilities();
removeExtraAdditionalCapabilityBytes();
logger.trace("Set Additional Capability Bytes before encrypt {}", HexUtils.bytesToHex(data));
}
private void modifyBytesForAdditionalCapabilities() {
data[0x01] = (byte) 0x0F;
data[0x09] = (byte) 0x03;
data[0x0a] = (byte) 0xB5;
data[0x0b] = (byte) 0x01;
data[0x0c] = (byte) 0x01;
data[0x0d] = (byte) 0x01;
}
private void removeExtraAdditionalCapabilityBytes() {
byte[] newData = new byte[data.length - 20];
System.arraycopy(data, 0, newData, 0, newData.length);
data = newData;
}
}
@@ -0,0 +1,235 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.devices.Timer;
import org.openhab.binding.mideaac.internal.devices.Timer.TimerData;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1FanSpeed;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1OperationalMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link A1Response} 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/a1/message.py#L418
*
* @author Gaël L'hopital - Initial contribution
* @author Bob Eckhoff - OH addons changes, logging, TimerData addition
*/
@NonNullByDefault
public class A1Response {
private Logger logger = LoggerFactory.getLogger(A1Response.class);
private byte[] data;
/**
* Response class Parameters
*
* @param data byte array from device
*/
public A1Response(byte[] data) {
this.data = data;
if (logger.isDebugEnabled()) {
logger.debug("Power State: {}", getPowerState());
logger.debug("Maximum Humidity: {}", getMaximumHumidity());
logger.debug("Operational Mode: {}", getA1OperationalMode());
logger.debug("Fan Speed: {}", getA1FanSpeed());
logger.debug("Child Lock: {}", getA1ChildLock());
logger.debug("Anion: {}", getA1Anion());
logger.debug("Filter Status: {}", getA1FilterStatus());
logger.debug("Tank: {}", getTank());
logger.debug("Water Level Set: {}", getTankSetpoint());
logger.debug("Swing: {}", getA1SwingMode());
logger.debug("Current Humidity: {}", getCurrentHumidity());
logger.debug("Prompt Tone: {}", getPromptTone());
logger.debug("Current Temperature: {}", getIndoorTemperature());
}
}
/**
* 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;
}
/**
* Manual, Continuous, Auto, etc. See Command Base class
*
* @return Manual, Continuous, Auto, etc.
*/
public A1OperationalMode getA1OperationalMode() {
return A1OperationalMode.fromId(data[0x02] & 0x0f);
}
/**
* Lowest, Low, Medium, High, Auto etc. See Command Base class
*
* @return Lowest, Low, Medium, High, Auto etc.
*/
public A1FanSpeed getA1FanSpeed() {
return A1FanSpeed.fromId(data[0x03] & 0x7f);
}
/**
* 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);
}
/**
* Setpoint for Humidity
*
* @return current setpoint in %
*/
public int getMaximumHumidity() {
return data[0x07] < 35 ? 35 : data[0x07];
}
/**
* Child Lock
*
* @return child lock true or false
*/
public boolean getA1ChildLock() {
return (data[0x08] & 0x80) > 0;
}
/**
* Anion Don't know what this is
*
* @return anion true or false
*/
public boolean getA1Anion() {
return (data[0x09] & 0x40) > 0;
}
/**
* Filter Status
*
* @return filter status true or false
*/
public boolean getA1FilterStatus() {
return (data[0x09] & 0x80) > 0;
}
/**
* Tank Status, likely percentage full
*
* @return Tank
*/
public int getTank() {
return data[10] & 0x7F;
}
/**
* Water Level Setpoint
*
* @return Water level setpoint
*/
public int getTankSetpoint() {
return data[15];
}
/**
* Current Humidity in Room
*
* @return Current humidity
*/
public int getCurrentHumidity() {
return data[16];
}
/**
* Device Indoor Temperature
*
* @return Indoor temperature
*/
public float getIndoorTemperature() {
if (((Byte.toUnsignedInt(data[17]) - 50) / 2.0) < -19) {
return -19;
}
if (((Byte.toUnsignedInt(data[17]) - 50) / 2.0) > 50) {
return 50;
}
return (Byte.toUnsignedInt(data[17]) - 50f) / 2.0f;
}
/**
* Swing for Dehumidifier is boolean
*
* @return swing true or false
*/
public boolean getA1SwingMode() {
return (data[19] & 0x20) > 0;
}
}
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link A1StringCommands} handles the String Commands for dehumidifiers
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class A1StringCommands {
/**
* Operational Modes for Dehumidifier
*/
public enum A1OperationalMode {
MANUAL(1),
CONTINUOUS(2),
AUTO(3),
CLOTHES_DRY(4),
SHOES_DRY(5),
UNKNOWN(0);
private final int value;
private A1OperationalMode(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 A1OperationalMode fromId(int id) {
for (A1OperationalMode type : values()) {
if (type.getId() == id) {
return type;
}
}
return UNKNOWN;
}
}
/**
* Converts byte value to the Fan Speed for Dehumidifier
*
*/
public enum A1FanSpeed {
AUTO(102),
HIGH(80),
MEDIUM(60),
LOW(40),
LOWEST(1),
OFF(127),
UNKNOWN(0);
private final int value;
private A1FanSpeed(int value) {
this.value = value;
}
/**
* Gets Fan Speed value
*
* @return value
*/
public int getId() {
return value;
}
/**
* Returns Fan Speed high, medium, low, etc
*
* @param id integer from byte response
* @return type
*/
public static A1FanSpeed fromId(int id) {
for (A1FanSpeed type : values()) {
if (type.getId() == id) {
return type;
}
}
return UNKNOWN;
}
}
}
@@ -10,18 +10,16 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.connection;
package org.openhab.binding.mideaac.internal.devices.ac;
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.binding.mideaac.internal.devices.Timer;
import org.openhab.binding.mideaac.internal.devices.Timer.TimeParser;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
@@ -33,23 +31,21 @@ 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}
* The {@link ACCommandHelper} is a static class that is able to translate {@link Command} to {@link ACCommandSet}
*
* @author Leo Siepel - Initial contribution
*/
@NonNullByDefault
public class CommandHelper {
private static Logger logger = LoggerFactory.getLogger(CommandHelper.class);
public class ACCommandHelper {
private static Logger logger = LoggerFactory.getLogger(ACCommandHelper.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");
@@ -63,12 +59,13 @@ public class CommandHelper {
private static final StringType SWING_MODE_BOTH = new StringType("BOTH");
/**
* Device Power ON OFF
* Device Power ON OFF for AC units
*
* @param command On or Off
*/
public static CommandSet handlePower(Command command, Response lastResponse) throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
public static ACCommandSet handlePower(Command command, Response lastResponse)
throws UnsupportedOperationException {
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setPowerState(false);
@@ -83,16 +80,14 @@ public class CommandHelper {
/**
* Supported AC - Heat Pump modes
*
* @param command Operational Mode Cool, Heat, etc.
* @param command Operational Modes AUTO, COOL, DRY, HEAT, FAN_ONLY
*/
public static CommandSet handleOperationalMode(Command command, Response lastResponse)
public static ACCommandSet handleOperationalMode(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command instanceof StringType) {
if (command.equals(OPERATIONAL_MODE_OFF)) {
commandSet.setPowerState(false);
} else if (command.equals(OPERATIONAL_MODE_AUTO)) {
if (command.equals(OPERATIONAL_MODE_AUTO)) {
commandSet.setOperationalMode(OperationalMode.AUTO);
} else if (command.equals(OPERATIONAL_MODE_COOL)) {
commandSet.setOperationalMode(OperationalMode.COOL);
@@ -110,6 +105,7 @@ public class CommandHelper {
}
// Some devices might support 16.0 degrees C
// Could try override if capabilities show it (future).
private static float limitTargetTemperatureToRange(float temperatureInCelsius) {
if (temperatureInCelsius < 17.0f) {
return 17.0f;
@@ -129,9 +125,9 @@ public class CommandHelper {
*
* @param command Target Temperature
*/
public static CommandSet handleTargetTemperature(Command command, Response lastResponse)
public static ACCommandSet handleTargetTemperature(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command instanceof DecimalType decimalCommand) {
logger.debug("Handle Target Temperature as DecimalType in degrees C");
@@ -149,19 +145,15 @@ public class CommandHelper {
/**
* Fan Speeds vary by V2 or V3 and device - See capabilities.
* This command also turns the power ON
*
* @param command Fan Speed Auto, Low, High, etc.
*/
public static CommandSet handleFanSpeed(Command command, Response lastResponse, int version)
public static ACCommandSet handleFanSpeed(Command command, Response lastResponse, int version)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.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 (command.equals(FAN_SPEED_SILENT)) {
if (version == 2) {
commandSet.setFanSpeed(FanSpeed.SILENT2);
} else if (version == 3) {
@@ -205,37 +197,14 @@ public class CommandHelper {
}
/**
* 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;
}
/**
* Swing Mode for V2 and V3 AC units.
* Modes supported depends on the device - See capabilities
* Power is turned on when swing mode is changed
*
* @param command Swing Mode
*/
public static CommandSet handleSwingMode(Command command, Response lastResponse, int version)
public static ACCommandSet handleSwingMode(Command command, Response lastResponse, int version)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
commandSet.setPowerState(true);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command instanceof StringType) {
if (command.equals(SWING_MODE_OFF)) {
@@ -270,15 +239,39 @@ public class CommandHelper {
return commandSet;
}
/**
* For Eco Mode on the AC Power turned ON
* 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 ACCommandSet handleEcoMode(Command command, Response lastResponse)
throws UnsupportedOperationException {
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
commandSet.setPowerState(true);
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;
}
/**
* 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)
public static ACCommandSet handleTurboMode(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
commandSet.setPowerState(true);
@@ -298,9 +291,9 @@ public class CommandHelper {
*
* @param command Screen Display Toggle to ON or Off - One command
*/
public static CommandSet handleScreenDisplay(Command command, Response lastResponse)
public static ACCommandSet handleScreenDisplay(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setScreenDisplay(true);
@@ -318,9 +311,9 @@ public class CommandHelper {
*
* @param command Temp unit on the indoor evaporator
*/
public static CommandSet handleTempUnit(Command command, Response lastResponse)
public static ACCommandSet handleTempUnit(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command.equals(OnOffType.OFF)) {
commandSet.setFahrenheit(false);
@@ -339,9 +332,9 @@ public class CommandHelper {
*
* @param command Sleep function
*/
public static CommandSet handleSleepFunction(Command command, Response lastResponse)
public static ACCommandSet handleSleepFunction(Command command, Response lastResponse)
throws UnsupportedOperationException {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
commandSet.setPowerState(true);
@@ -361,8 +354,8 @@ public class CommandHelper {
*
* @param command Sets On Timer
*/
public static CommandSet handleOnTimer(Command command, Response lastResponse) {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
public static ACCommandSet handleOnTimer(Command command, Response lastResponse) {
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
int hours = 0;
int minutes = 0;
Timer timer = new Timer(true, hours, minutes);
@@ -402,8 +395,8 @@ public class CommandHelper {
*
* @param command Sets Off Timer
*/
public static CommandSet handleOffTimer(Command command, Response lastResponse) {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
public static ACCommandSet handleOffTimer(Command command, Response lastResponse) {
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
int hours = 0;
int minutes = 0;
Timer timer = new Timer(true, hours, minutes);
@@ -438,7 +431,25 @@ public class CommandHelper {
return commandSet;
}
// Limit Humidity to range
/**
* Sets the Target Humidity for Dry Mode (if supported)
*
* @param command Target Humidity
*/
public static ACCommandSet handleMaximumHumidity(Command command, Response lastResponse) {
ACCommandSet commandSet = ACCommandSet.fromResponse(lastResponse);
if (command instanceof DecimalType decimalCommand) {
int humidity = decimalCommand.intValue();
commandSet.setMaximumHumidity(limitHumidityToRange(humidity));
} else {
logger.debug("Unknown target humidity command: {}", command);
}
return commandSet;
}
// Limit AC Humidity to range
private static int limitHumidityToRange(int humidity) {
if (humidity < 32) {
return 32;
@@ -449,22 +460,4 @@ public class CommandHelper {
return humidity;
}
/**
* Sets the Maximum Humidity for Dry Mode
*
* @param command Maximum Humidity
*/
public static CommandSet handleMaximumHumidity(Command command, Response lastResponse) {
CommandSet commandSet = CommandSet.fromResponse(lastResponse);
if (command instanceof DecimalType decimalCommand) {
int humidity = decimalCommand.intValue();
commandSet.setMaximumHumidity(limitHumidityToRange(humidity));
} else {
logger.debug("Unknown maximum humidity command: {}", command);
}
return commandSet;
}
}
@@ -10,35 +10,39 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.handler.Timer.TimerData;
import org.openhab.binding.mideaac.internal.devices.CommandBase;
import org.openhab.binding.mideaac.internal.devices.Timer.TimerData;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
import org.openhab.core.util.HexUtils;
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
* This {@link ACCommandSet} class handles the allowed changes originating from
* the items linked to the Midea device channels. Not all devices
* support all commands. The general process is to clear the
* bit(s) the set them to the commanded value.
* bit(s) the set them to the command value and change message type to command.
*
* @author Jacek Dobrowolski - Initial contribution
* @author Bob Eckhoff - Add Java Docs, Timer Display LED and capabilities
* @author Bob Eckhoff - Add Java Docs, Timer, Display LED, Capabilities, Energy Poll
*/
@NonNullByDefault
public class CommandSet extends CommandBase {
private final Logger logger = LoggerFactory.getLogger(CommandSet.class);
public class ACCommandSet extends CommandBase {
private final Logger logger = LoggerFactory.getLogger(ACCommandSet.class);
/**
* Byte array structure for Command set
*/
public CommandSet() {
public ACCommandSet() {
super();
data[0x01] = (byte) 0x23;
data[0x09] = (byte) 0x02;
// Set up Mode
data[0x0a] = (byte) 0x40;
data[0x09] = (byte) 0x02; // Setting Mode
data[0x0a] = (byte) 0x40; // Command Message
byte[] extra = { 0x00, 0x00, 0x00 };
byte[] newData = new byte[data.length + 3];
@@ -47,6 +51,13 @@ public class CommandSet extends CommandBase {
newData[data.length + 1] = extra[1];
newData[data.length + 2] = extra[2];
data = newData;
// Zap the old timestamp
int oldTimestampIndex = data.length - 4;
data[oldTimestampIndex] = 0x00;
// Add the new timestamp
applyTimestamp();
}
/**
@@ -56,28 +67,34 @@ public class CommandSet extends CommandBase {
* @param response response from last poll or set command
* @return commandSet
*/
public static CommandSet fromResponse(Response response) {
CommandSet commandSet = new CommandSet();
// Controllable items for AC
public static ACCommandSet fromResponse(Response ac) {
ACCommandSet commandSet = new ACCommandSet();
// Common fields
commandSet.setPowerState(ac.getPowerState());
commandSet.setFanSpeed(ac.getFanSpeed());
commandSet.setOnTimer(ac.getOnTimerData());
commandSet.setOffTimer(ac.getOffTimerData());
commandSet.setPromptTone(ac.getPromptTone());
// AC-specific fields
commandSet.setTargetTemperature(ac.getTargetTemperature());
commandSet.setOperationalMode(ac.getOperationalMode());
commandSet.setMaximumHumidity(ac.getMaximumHumidity());
commandSet.setFahrenheit(ac.getFahrenheit());
commandSet.setTurboMode(ac.getTurboMode());
commandSet.setSwingMode(ac.getSwingMode());
commandSet.setEcoMode(ac.getEcoMode());
commandSet.setSleepMode(ac.getSleepFunction());
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());
commandSet.setMaximumHumidity(response.getMaximumHumidity());
return commandSet;
}
/**
* Causes indoor evaporator to beep when Set command received
* Causes Midea device to beep when Set command received
*
* @param feedbackEnabled will indoor unit beep
* @param feedbackEnabled Beep On or Off
*/
public void setPromptTone(boolean feedbackEnabled) {
if (!feedbackEnabled) {
@@ -130,7 +147,7 @@ public class CommandSet extends CommandBase {
/**
* Clear, then set the temperature bits, including the 0.5 bit
* This is all degrees C
* This is all in degrees C
*
* @param temperature target temperature
*/
@@ -191,7 +208,7 @@ public class CommandSet extends CommandBase {
}
/**
* For Testing assertion get Swing result
* For Testing assertion get AC Swing result
*
* @return swing mode
*/
@@ -256,13 +273,13 @@ public class CommandSet extends CommandBase {
data[0x01] = (byte) 0x20;
data[0x09] = (byte) 0x03;
data[0x0a] = (byte) 0x41;
data[0x0b] = (byte) 0x61; // Includes beep 0x40
data[0x0b] = (byte) 0x61; // Includes beep 0x40 0x81 ZH? or 0x02-local
data[0x0c] = (byte) 0x00;
data[0x0d] = (byte) 0xff;
data[0x0e] = (byte) 0x02;
data[0x0f] = (byte) 0x00;
data[0x0f] = (byte) 0x00; // 0xff ZH -OK with local
data[0x10] = (byte) 0x02;
data[0x11] = (byte) 0x00;
data[0x11] = (byte) 0x00; // 0x02 ZH -OK with local
data[0x12] = (byte) 0x00;
data[0x13] = (byte) 0x00;
data[0x14] = (byte) 0x00;
@@ -272,6 +289,7 @@ public class CommandSet extends CommandBase {
byte[] newData = new byte[data.length - 3];
System.arraycopy(data, 0, newData, 0, newData.length);
data = newData;
applyTimestamp();
}
/**
@@ -467,7 +485,7 @@ public class CommandSet extends CommandBase {
data[0x0c] = (byte) 0x01;
data[0x0d] = (byte) 0x44;
data[0x0e] = (byte) 0x00;
data[0x0f] = (byte) 0x00;
data[0x0f] = (byte) 0x00; // maybe 0x01 for total energy ZH
data[0x10] = (byte) 0x00;
data[0x11] = (byte) 0x00;
data[0x12] = (byte) 0x00;
@@ -479,6 +497,7 @@ public class CommandSet extends CommandBase {
byte[] newData = new byte[data.length - 3];
System.arraycopy(data, 0, newData, 0, newData.length);
data = newData;
applyTimestamp();
}
/**
@@ -500,7 +519,7 @@ public class CommandSet extends CommandBase {
data[0x0c] = (byte) 0x01;
data[0x0d] = (byte) 0x45;
data[0x0e] = (byte) 0x00;
data[0x0f] = (byte) 0x00;
data[0x0f] = (byte) 0x00; // maybe 0x01 for humidity ZH
data[0x10] = (byte) 0x00;
data[0x11] = (byte) 0x00;
data[0x12] = (byte) 0x00;
@@ -512,10 +531,12 @@ public class CommandSet extends CommandBase {
byte[] newData = new byte[data.length - 3];
System.arraycopy(data, 0, newData, 0, newData.length);
data = newData;
applyTimestamp();
}
/**
* Sets the Maximum Humidity for Dry Mode
* Sets the Target Humidity for AC Dry Mode
* if supported
*
* @param humidity
*/
@@ -10,42 +10,20 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
import java.time.LocalDateTime;
import java.util.Arrays;
package org.openhab.binding.mideaac.internal.devices.ac;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.security.Crc8;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link CommandBase} has the discover command and the routine poll command
* The {@link ACStringCommands} handles the String Commands for ACs
*
* @author Jacek Dobrowolski - Initial contribution
* @author Bob Eckhoff - Add Java Docs, minor fixes
* @author Bob Eckhoff - Initial contribution
*/
@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;
public class ACStringCommands {
/**
* Operational Modes
* Operational Modes for AC Device
*/
public enum OperationalMode {
AUTO(1),
@@ -88,6 +66,7 @@ public class CommandBase {
/**
* Converts byte value to the Swing Mode label by version
* for AC devices.
* Two versions of V3, Supported Swing or Non-Supported (4)
* V2 set without leading 3, but reports with it (1)
*/
@@ -232,83 +211,4 @@ public class CommandBase {
return super.toString().replace("2", "").replace("3", "").replace("4", "");
}
}
/**
* 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 - This is the message length
(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 {}", HexUtils.bytesToHex(data));
byte crc8 = (byte) Crc8.calculate(Arrays.copyOfRange(data, 10, data.length));
byte[] newData1 = new byte[data.length + 1];
System.arraycopy(data, 0, newData1, 0, data.length);
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);
}
return (byte) ((255 - (sum % 256)) + 1);
}
}
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import java.nio.ByteBuffer;
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
/**
* The {@link EnergyResponse} handles the energy messages
* from the device
* from the AC device
*
* @author Bob Eckhoff - Initial contribution
*/
@@ -10,18 +10,18 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
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.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HumidityResponse} handles the unsolicited 0xA0 humidity report messages
* from the device
* from the AC device
*
* @author Bob Eckhoff - Initial contribution
*/
@@ -10,13 +10,14 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
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.openhab.binding.mideaac.internal.devices.Timer;
import org.openhab.binding.mideaac.internal.devices.Timer.TimerData;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -245,7 +246,7 @@ public class Response {
*
* @return Indoor temperature
*/
public Float getIndoorTemperature() {
public float getIndoorTemperature() {
double indoorTempInteger;
double indoorTempDecimal;
@@ -309,10 +310,10 @@ public class Response {
}
/**
* This returns the maximum humidity for Dry mode, if supported
* This returns the target humidity for Dry mode, if supported
* Possibly an add-on sensor is required in some cases
*
* @return Maximum Humidity in Dry Mode
* @return Target Humidity in Dry Mode
*/
public int getMaximumHumidity() {
return (data[19] & (byte) 0x7f);
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.slf4j.Logger;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler.capabilities;
package org.openhab.binding.mideaac.internal.devices.capabilities;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler.capabilities;
package org.openhab.binding.mideaac.internal.devices.capabilities;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -62,10 +62,8 @@ public class CapabilityParser {
int rawId = ByteBuffer.wrap(payload, offset, 2).order(ByteOrder.LITTLE_ENDIAN).getShort() & 0xFFFF;
// Map the ID to a CapabilityId enum
CapabilityId capabilityId;
try {
capabilityId = CapabilityId.fromId(rawId);
} catch (IllegalArgumentException e) {
CapabilityId capabilityId = CapabilityId.fromId(rawId);
if (capabilityId == CapabilityId.UNMAPPED) {
logger.debug("Unknown capability ID: {}, Size: {}", rawId, size);
offset += 3 + size; // Skip unknown capability
continue;
@@ -149,6 +147,7 @@ public class CapabilityParser {
ENERGY(0x0216), // AKA electricity
FILTER_REMIND(0x0217),
AUX_ELECTRIC_HEAT(0x0219), // AKA PTC
CHILD_LOCK(0x0220), // Possible Child Lock
PRESET_TURBO(0x021A),
FILTER_CHECK(0x0221),
ANION(0x021E),
@@ -161,7 +160,8 @@ public class CapabilityParser {
SUP_HORIZONTAL_GUIDE_STRIP(0x0231), // ??
TWINS_MACHINE(0x0232), // ??
GUIDE_STRIP_TYPE(0x0233), // ??
BODY_CHECK(0x0234); // ??
BODY_CHECK(0x0234), // ??
UNMAPPED(-1);
private final int id;
@@ -190,7 +190,7 @@ public class CapabilityParser {
return capability;
}
}
throw new IllegalArgumentException("Unknown Capability ID: " + id);
return UNMAPPED;
}
}
}
@@ -10,19 +10,17 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler.capabilities;
package org.openhab.binding.mideaac.internal.devices.capabilities;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser.CapabilityId;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilityParser.CapabilityId;
/**
* The {@link CapabilityReaders} reads the raw capability message and
@@ -35,16 +33,14 @@ public class CapabilityReaders {
private static final Map<CapabilityId, List<Reader>> READERS = new HashMap<>();
static {
// Helper to simplify creation of READERS
Function<Integer, Predicate<Integer>> getValue = (expected) -> (value) -> Objects.equals(value, expected);
// Add READERS for each capability - Not all are supported by all AC devices
READERS.put(CapabilityId.ANION, List.of(new Reader("anion", getValue.apply(1))));
READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("auxElectricHeat", getValue.apply(1))));
READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breezeAway", getValue.apply(1))));
READERS.put(CapabilityId.BREEZE_CONTROL, List.of(new Reader("breezeControl", getValue.apply(1))));
READERS.put(CapabilityId.BREEZELESS, List.of(new Reader("breezeless", getValue.apply(1))));
READERS.put(CapabilityId.BUZZER, List.of(new Reader("buzzer", getValue.apply(1))));
READERS.put(CapabilityId.ANION, List.of(new Reader("anion", getValue(1))));
READERS.put(CapabilityId.AUX_ELECTRIC_HEAT, List.of(new Reader("auxElectricHeat", getValue(1))));
READERS.put(CapabilityId.BREEZE_AWAY, List.of(new Reader("breezeAway", getValue(1))));
READERS.put(CapabilityId.BREEZE_CONTROL, List.of(new Reader("breezeControl", getValue(1))));
READERS.put(CapabilityId.BREEZELESS, List.of(new Reader("breezeless", getValue(1))));
READERS.put(CapabilityId.BUZZER, List.of(new Reader("buzzer", getValue(1))));
READERS.put(CapabilityId.CHILD_LOCK, List.of(new Reader("childLock", getValue(1))));
READERS.put(CapabilityId.DISPLAY_CONTROL,
List.of(new Reader("displayControl", (value) -> List.of(1, 2, 100).contains(value))));
@@ -54,15 +50,15 @@ public class CapabilityReaders {
new Reader("energySetting", (value) -> List.of(3, 5).contains(value)),
new Reader("energyBCD", (value) -> List.of(2, 3).contains(value))));
READERS.put(CapabilityId.FAHRENHEIT, List.of(new Reader("fahrenheit", getValue.apply(0))));
READERS.put(CapabilityId.FAHRENHEIT, List.of(new Reader("fahrenheit", getValue(0))));
READERS.put(CapabilityId.FAN_SPEED_CONTROL,
List.of(new Reader("fanSilent", getValue.apply(6)),
List.of(new Reader("fanSilent", getValue(6)),
new Reader("fanLow", (value) -> List.of(3, 4, 5, 6, 7).contains(value)),
new Reader("fanMedium", (value) -> List.of(5, 6, 7).contains(value)),
new Reader("fanHigh", (value) -> List.of(3, 4, 5, 6, 7).contains(value)),
new Reader("fanAuto", (value) -> List.of(4, 5, 6).contains(value)),
new Reader("fanCustom", getValue.apply(1))));
new Reader("fanCustom", getValue(1))));
READERS.put(CapabilityId.FILTER_REMIND,
List.of(new Reader("filterNotice", (value) -> List.of(1, 2, 4).contains(value)),
@@ -77,31 +73,35 @@ public class CapabilityReaders {
new Reader("modeCool", (value) -> !List.of(2, 10, 12).contains(value)),
new Reader("modeDry", (value) -> List.of(0, 1, 5, 6, 9, 11, 13).contains(value)),
new Reader("modeAuto", (value) -> List.of(0, 1, 2, 7, 8, 9, 13).contains(value)),
new Reader("modeAuxHeat", getValue.apply(9)),
new Reader("modeAuxHeat", getValue(9)),
new Reader("modeAux", (value) -> List.of(9, 10, 11, 13).contains(value))));
READERS.put(CapabilityId.PRESET_ECO, List.of(new Reader("ecoCool", (value) -> List.of(1, 2).contains(value))));
READERS.put(CapabilityId.PRESET_FREEZE_PROTECTION, List.of(new Reader("freezeProtection", getValue.apply(1))));
READERS.put(CapabilityId.PRESET_IECO, List.of(new Reader("ieco", getValue.apply(1))));
READERS.put(CapabilityId.PRESET_FREEZE_PROTECTION, List.of(new Reader("freezeProtection", getValue(1))));
READERS.put(CapabilityId.PRESET_IECO, List.of(new Reader("ieco", getValue(1))));
READERS.put(CapabilityId.PRESET_TURBO,
List.of(new Reader("turboHeat", (value) -> List.of(1, 3).contains(value)),
new Reader("turboCool", (value) -> value < 2)));
READERS.put(CapabilityId.RATE_SELECT, List.of(new Reader("rate_select_2_level", getValue.apply(1)),
READERS.put(CapabilityId.RATE_SELECT, List.of(new Reader("rate_select_2_level", getValue(1)),
new Reader("rateSelect5Level", (value) -> List.of(2, 3).contains(value))));
READERS.put(CapabilityId.SELF_CLEAN, List.of(new Reader("selfClean", getValue.apply(1))));
READERS.put(CapabilityId.SMART_EYE, List.of(new Reader("smartEye", getValue.apply(1))));
READERS.put(CapabilityId.SWING_LR_ANGLE, List.of(new Reader("swingHorizontalAngle", getValue.apply(1))));
READERS.put(CapabilityId.SWING_UD_ANGLE, List.of(new Reader("swingVerticalAngle", getValue.apply(1))));
READERS.put(CapabilityId.SELF_CLEAN, List.of(new Reader("selfClean", getValue(1))));
READERS.put(CapabilityId.SMART_EYE, List.of(new Reader("smartEye", getValue(1))));
READERS.put(CapabilityId.SWING_LR_ANGLE, List.of(new Reader("swingHorizontalAngle", getValue(1))));
READERS.put(CapabilityId.SWING_UD_ANGLE, List.of(new Reader("swingVerticalAngle", getValue(1))));
READERS.put(CapabilityId.SWING_MODES,
List.of(new Reader("swingHorizontal", (value) -> List.of(1, 3).contains(value)),
new Reader("swingVertical", (value) -> value < 2)));
READERS.put(CapabilityId.WIND_OFF_ME, List.of(new Reader("windOffMe", getValue.apply(1))));
READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("windOnMe", getValue.apply(1))));
READERS.put(CapabilityId.WIND_OFF_ME, List.of(new Reader("windOffMe", getValue(1))));
READERS.put(CapabilityId.WIND_ON_ME, List.of(new Reader("windOnMe", getValue(1))));
}
private static Predicate<Integer> getValue(int expected) {
return value -> value == expected;
}
/**
@@ -10,7 +10,8 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler.capabilities;
package org.openhab.binding.mideaac.internal.devices.capabilities;
import java.util.function.Predicate;
@@ -25,7 +25,7 @@ public interface DiscoveryHandler {
/**
* Discovery result
*
* @param discoveryResult AC device
* @param discoveryResult Midea device
*/
public void discovered(DiscoveryResult discoveryResult);
}
@@ -24,6 +24,7 @@ import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
@@ -31,8 +32,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.mideaac.internal.Utils;
import org.openhab.binding.mideaac.internal.cloud.CloudProvider;
import org.openhab.binding.mideaac.internal.handler.CommandBase;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser;
import org.openhab.binding.mideaac.internal.devices.CommandBase;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilityParser;
import org.openhab.binding.mideaac.internal.security.Security;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
@@ -57,7 +58,9 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
private static int discoveryTimeoutSeconds = 10;
private final int receiveJobTimeout = 20000;
private final int udpPacketTimeout = receiveJobTimeout - 50;
private final String mideaacNamePrefix = "MideaAC";
private final String acNamePrefix = "Air_Conditioner";
private final String a1NamePrefix = "Dehumidifier";
/**
* UDP port1 to send command.
*/
@@ -93,13 +96,13 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
@Override
protected void startScan() {
logger.debug("Start scan for Midea AC devices.");
logger.debug("Start scan for Midea devices.");
discoverThings();
}
@Override
protected void stopScan() {
logger.debug("Stop scan for Midea AC devices.");
logger.debug("Stop scan for Midea devices.");
closeDiscoverSocket();
super.stopScan();
}
@@ -315,11 +318,21 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
mSmartType = mSmartSSID.split("_")[1];
logger.debug("Type: '{}'", mSmartType);
String thingName = createThingName(packet.getAddress().getAddress(), mSmartId);
ThingUID thingUID = new ThingUID(THING_TYPE_MIDEAAC, thingName.toLowerCase());
String thingName = createThingName(packet.getAddress().getAddress(),
"ac".equalsIgnoreCase(mSmartType) ? acNamePrefix
: "a1".equalsIgnoreCase(mSmartType) ? a1NamePrefix : "Midea_Device");
// choose thing type based on discovered device type/version or default to AC
ThingUID thingUID;
if ("ac".equalsIgnoreCase(mSmartType)) {
thingUID = new ThingUID(THING_TYPE_AC, thingName.toLowerCase(Locale.ROOT));
} else if ("a1".equalsIgnoreCase(mSmartType)) {
thingUID = new ThingUID(THING_TYPE_DEHUMIDIFIER, thingName.toLowerCase(Locale.ROOT));
} else {
thingUID = new ThingUID(THING_TYPE_AC, thingName.toLowerCase(Locale.ROOT));
}
return DiscoveryResultBuilder.create(thingUID).withLabel(thingName)
.withRepresentationProperty(CONFIG_IP_ADDRESS).withThingType(THING_TYPE_MIDEAAC)
.withRepresentationProperty(CONFIG_IP_ADDRESS)
.withProperties(collectProperties(ipAddress, mSmartVersion, mSmartId, mSmartPort, mSmartSN,
mSmartSSID, mSmartType, new TreeMap<>(), // Placeholder for capabilities
new TreeMap<>())) // Placeholder for numericCapabilities
@@ -329,7 +342,7 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
return null;
} else {
logger.debug(
"Midea AC device was detected, but the retrieved data is incomplete or not supported. Device not registered");
"Midea device was detected, but the retrieved data is incomplete or not supported. Device not registered");
return null;
}
}
@@ -339,8 +352,8 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
*
* @return the name for the device
*/
private String createThingName(final byte[] byteIP, String id) {
return mideaacNamePrefix + "-" + Byte.toUnsignedInt(byteIP[3]) + "-" + id;
private String createThingName(final byte[] byteIP, String mideaNamePrefix) {
return mideaNamePrefix + "-" + Byte.toUnsignedInt(byteIP[3]);
}
/**
@@ -353,7 +366,9 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
* @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)
* @param type Type of device (ac) or (a1) humidifier
* @param numericCapabilities Map of numeric capabilities
* @param capabilities Map of boolean capabilities
* @return Map with properties
*/
private Map<String, Object> collectProperties(String ipAddress, String version, String id, String port, String sn,
@@ -365,10 +380,10 @@ public class MideaACDiscoveryService extends AbstractDiscoveryService {
properties.put(CONFIG_IP_ADDRESS, ipAddress);
properties.put(CONFIG_IP_PORT, port);
properties.put(CONFIG_DEVICEID, id);
properties.put(CONFIG_DEVICE_TYPE, type);
properties.put(CONFIG_VERSION, version);
properties.put(PROPERTY_SN, sn);
properties.put(PROPERTY_SSID, ssid);
properties.put(PROPERTY_TYPE, type);
// Default empty maps for boolean and numeric capabilities
if (capabilities != null) {
@@ -0,0 +1,355 @@
/*
* Copyright (c) 2010-2026 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.math.BigDecimal;
import java.util.HashMap;
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 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.cloud.Cloud;
import org.openhab.binding.mideaac.internal.cloud.CloudProvider;
import org.openhab.binding.mideaac.internal.connection.ConnectionManager;
import org.openhab.binding.mideaac.internal.discovery.DiscoveryHandler;
import org.openhab.binding.mideaac.internal.discovery.MideaACDiscoveryService;
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.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.ThingTypeUID;
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 AbstractMideaHandler} is responsible for initializing the devices
* and passing commands to specific device handlers.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public abstract class AbstractMideaHandler extends BaseThingHandler implements DiscoveryHandler {
private final Logger logger = LoggerFactory.getLogger(AbstractMideaHandler.class);
private final HttpClient httpClient;
protected MideaACConfiguration config = new MideaACConfiguration();
protected Map<String, String> properties = new HashMap<>();
// Default parameters are the same as in the MideaACConfiguration class
protected ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false,
"");
protected ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
private @Nullable ScheduledFuture<?> scheduledTask;
private @Nullable ScheduledFuture<?> scheduledKeyTokenUpdate;
public AbstractMideaHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
/**
* Initializes the handler by performing the following steps:
* <ol>
* <li>Retrieves the configuration for the handler.</li>
* <li>Ensures the discovery or configuration is valid. If not, starts the discovery process and exits early.</li>
* <li>Ensures the token and key for V3 devices are available. If not, starts the retrieval process and exits
* early.</li>
* <li>Updates the thing's status to {@link ThingStatus#UNKNOWN}.</li>
* <li>Initializes the connection manager using the configuration.</li>
* <li>Requests device capabilities if they are missing.</li>
* <li>Starts any necessary schedulers for the handler.</li>
* </ol>
*/
@Override
public void initialize() {
config = getConfigAs(MideaACConfiguration.class);
properties = editProperties();
// 1) Ensure discovery/config is valid or start discovery and exit early
if (!ensureConfigOrStartDiscovery()) {
return;
}
// 2) Ensure token/key for V3 devices or start retrieval and exit early
if (!ensureTokenKeyOrStartRetrieval()) {
return;
}
updateStatus(ThingStatus.UNKNOWN);
initConnectionManagerFromConfig();
requestCapabilitiesIfMissing(); // abstract hook, uses protected properties and config
startSchedulers();
}
/**
* Ensure we have all configuration needed to reach the device. If incomplete
* but discoverable,
* trigger discovery asynchronously and return false to stop current
* initialization.
*/
private boolean ensureConfigOrStartDiscovery() {
if (config.isValid()) {
logger.debug("Discovery parameters are valid for {}", thing.getUID());
return true;
}
if (!config.isDiscoveryPossible()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_discovery");
return false;
}
MideaACDiscoveryService discoveryService = new MideaACDiscoveryService();
// Kick off discovery asynchronously and end this initialization thread.
scheduler.execute(() -> {
try {
// Keep thing OFFLINE with message about attempting discovery.
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_pending_discovery");
discoveryService.discoverThing(config.ipAddress, this);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication_error_discovery");
}
});
return false;
}
/**
* Ensure token/key are available for V3 devices. If retrievable from cloud,
* trigger async
* retrieval and return false to stop current initialization.
*/
private boolean ensureTokenKeyOrStartRetrieval() {
if (config.version != 3 || config.isV3ConfigValid()) {
logger.debug("Valid token and key for V.3 device {}", thing.getUID());
return true;
}
if (!config.isTokenKeyObtainable()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_token");
return false;
}
scheduler.execute(() -> {
try {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_pending_token");
CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud);
getTokenKeyCloud(cloudProvider);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication_error_token");
}
});
return false;
}
/** Initialize the connection manager from the current configuration. */
private void initConnectionManagerFromConfig() {
String effectiveDeviceType = getDeviceTypeFromThingTypeUID();
if (!effectiveDeviceType.equals(config.deviceType)) {
logger.warn("Configured deviceType '{}' does not match thing type '{}'; using '{}'", config.deviceType,
thing.getThingTypeUID(), effectiveDeviceType);
}
connectionManager = new ConnectionManager(config.ipAddress, config.ipPort, config.timeout, config.key,
config.token, config.cloud, config.email, config.password, config.deviceId, config.version,
config.promptTone, effectiveDeviceType);
}
private String getDeviceTypeFromThingTypeUID() {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_DEHUMIDIFIER.equals(thingTypeUID)) {
return "a1";
}
return "ac";
}
/** Subclasses decide what to do */
protected abstract void requestCapabilitiesIfMissing();
/**
* Start routine and token refresh schedulers as needed.
*
*/
private void startSchedulers() {
// Routine polling
if (scheduledTask == null) {
scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS);
logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime);
} else {
logger.debug("Scheduler already running");
}
// Token/key update
if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) {
scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay(
() -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate,
config.keyTokenUpdate, TimeUnit.HOURS);
logger.debug("Token Key Update Scheduler started, update interval {} hours", config.keyTokenUpdate);
} else {
logger.debug("Token Key Scheduler already running or disabled");
}
}
private void pollJob() {
try {
refreshDeviceState(); // Delegate to subclass
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
// delegate refresh to subclass
refreshDeviceStateAll();
return;
}
try {
handleDeviceCommand(channelUID, command);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
/** Subclasses implement device command handling */
protected abstract void handleDeviceCommand(ChannelUID channelUID, Command command);
/** Subclasses implement device state refresh */
protected abstract void refreshDeviceState();
/** Subclasses implement full device state refresh for RefreshType */
protected abstract void refreshDeviceStateAll();
/** Utility for updating a single channel state */
protected void updateChannel(String channelId, State state) {
updateState(new ChannelUID(getThing().getUID(), channelId), state);
}
@Override
public void discovered(DiscoveryResult discoveryResult) {
logger.debug("Discovered {}", thing.getUID());
Map<String, Object> 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());
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 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(CONFIG_DEVICE_TYPE));
properties.put(CONFIG_DEVICE_TYPE, propertyType.toString());
updateProperties(properties);
initialize();
}
/**
* Gets the token and key from the Cloud
*
* @param cloudProvider Cloud Provider account
*/
public void getTokenKeyCloud(CloudProvider cloudProvider) {
if (scheduledTask != null) {
stopScheduler();
}
logger.debug("Retrieving Token and/or Key from cloud");
Cloud cloud = new Cloud(config.email, config.password, cloudProvider, httpClient);
if (cloud.login()) {
TokenKey tk = cloud.getToken(config.deviceId);
Configuration configuration = editConfiguration();
configuration.put(CONFIG_TOKEN, tk.token());
configuration.put(CONFIG_KEY, tk.key());
updateConfiguration(configuration);
logger.trace("Token: {}", tk.token());
logger.trace("Key: {}", tk.key());
logger.debug("Token and Key obtained from cloud, saving, back to initialize");
initialize();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_token ");
}
}
private void stopScheduler() {
ScheduledFuture<?> localScheduledTask = this.scheduledTask;
if (localScheduledTask != null && !localScheduledTask.isCancelled()) {
localScheduledTask.cancel(true);
logger.debug("Scheduled task cancelled.");
scheduledTask = null;
}
}
private void stopTokenKeyUpdate() {
ScheduledFuture<?> localScheduledTask = this.scheduledKeyTokenUpdate;
if (localScheduledTask != null && !localScheduledTask.isCancelled()) {
localScheduledTask.cancel(true);
logger.debug("Scheduled Key Token Update cancelled.");
scheduledKeyTokenUpdate = null;
}
}
@Override
public void dispose() {
stopScheduler();
stopTokenKeyUpdate();
connectionManager.dispose(true);
}
}
@@ -15,12 +15,7 @@ package org.openhab.binding.mideaac.internal.handler;
import static org.openhab.binding.mideaac.internal.MideaACBindingConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.HashMap;
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;
@@ -29,21 +24,18 @@ import javax.measure.quantity.Temperature;
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.cloud.Cloud;
import org.openhab.binding.mideaac.internal.cloud.CloudProvider;
import org.openhab.binding.mideaac.internal.connection.CommandHelper;
import org.openhab.binding.mideaac.internal.connection.ConnectionManager;
import org.openhab.binding.mideaac.internal.callbacks.ACCallback;
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.handler.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser;
import org.openhab.binding.mideaac.internal.security.TokenKey;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.binding.mideaac.internal.devices.ac.ACCommandHelper;
import org.openhab.binding.mideaac.internal.devices.ac.ACCommandSet;
import org.openhab.binding.mideaac.internal.devices.ac.EnergyResponse;
import org.openhab.binding.mideaac.internal.devices.ac.HumidityResponse;
import org.openhab.binding.mideaac.internal.devices.ac.Response;
import org.openhab.binding.mideaac.internal.devices.ac.TemperatureResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilityParser;
import org.openhab.core.i18n.UnitProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
@@ -51,15 +43,11 @@ 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;
@@ -71,54 +59,15 @@ import org.slf4j.LoggerFactory;
* @author Justan Oldman - Last Response added
* @author Bob Eckhoff - Longer Polls, OH developer guidelines added other messages
* @author Leo Siepel - Refactored class, improved separation of concerns
* @author Bob Eckhoff - Energy scheduling, humidity via energy poll added, separated AC
* and Dehumidifier handlers
*/
@NonNullByDefault
public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler, Callback {
public class MideaACHandler extends AbstractMideaHandler implements ACCallback {
private final Logger logger = LoggerFactory.getLogger(MideaACHandler.class);
private final boolean imperialUnits;
private final HttpClient httpClient;
private MideaACConfiguration config = new MideaACConfiguration();
private Map<String, String> properties = new HashMap<>();
// Default parameters are the same as in the MideaACConfiguration class
private ConnectionManager connectionManager = new ConnectionManager("", 6444, 4, "", "", "", "", "", "", 0, false);
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);
private @Nullable ScheduledFuture<?> scheduledTask;
private @Nullable ScheduledFuture<?> scheduledKeyTokenUpdate;
private @Nullable ScheduledFuture<?> scheduledEnergyUpdate;
private final Callback callbackLambda = new Callback() {
@Override
public void updateChannels(Response response) {
MideaACHandler.this.updateChannels(response);
}
@Override
public void updateChannels(CapabilitiesResponse capabilitiesResponse) {
MideaACHandler.this.updateChannels(capabilitiesResponse);
}
@Override
public void updateChannels(EnergyResponse energyUpdate) {
MideaACHandler.this.updateChannels(energyUpdate);
}
@Override
public void updateHumidityFromEnergy(EnergyResponse energyUpdate) {
MideaACHandler.this.updateHumidityFromEnergy(energyUpdate);
}
@Override
public void updateChannels(HumidityResponse humidityResponse) {
MideaACHandler.this.updateChannels(humidityResponse);
}
@Override
public void updateChannels(TemperatureResponse temperatureResponse) {
MideaACHandler.this.updateChannels(temperatureResponse);
}
};
/**
* Initial creation of the Midea AC Handler
*
@@ -127,254 +76,21 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
* @param httpClient http Client
*/
public MideaACHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient) {
super(thing);
super(thing, unitProvider, httpClient);
this.thing = thing;
this.imperialUnits = unitProvider.getMeasurementSystem() instanceof ImperialUnits;
this.httpClient = httpClient;
}
/**
* 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.
* For a Refresh both regular and energy polls are triggerred.
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString());
ConnectionManager connectionManager = this.connectionManager;
if (command instanceof RefreshType) {
try {
connectionManager.getStatus(callbackLambda);
// Read only Energy and Humidity channels not updated with routine poll
CommandSet energyUpdate = new CommandSet();
energyUpdate.energyPoll();
connectionManager.sendCommand(energyUpdate, this);
CommandSet humidityUpdate = new CommandSet();
humidityUpdate.humidityPoll();
connectionManager.sendCommand(humidityUpdate, this);
} catch (MideaAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (MideaConnectionException | MideaException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
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);
} else if (channelUID.getId().equals(CHANNEL_MAXIMUM_HUMIDITY)) {
connectionManager.sendCommand(CommandHelper.handleMaximumHumidity(command, lastresponse),
callbackLambda);
}
} catch (MideaConnectionException | MideaAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (MideaException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
/**
* Initializes the handler by performing the following steps:
* <ol>
* <li>Retrieves the configuration for the handler.</li>
* <li>Ensures the discovery or configuration is valid. If not, starts the discovery process and exits early.</li>
* <li>Ensures the token and key for V3 devices are available. If not, starts the retrieval process and exits
* early.</li>
* <li>Updates the thing's status to {@link ThingStatus#UNKNOWN}.</li>
* <li>Initializes the connection manager using the configuration.</li>
* <li>Requests device capabilities if they are missing.</li>
* <li>Starts any necessary schedulers for the handler.</li>
* </ol>
*/
@Override
public void initialize() {
config = getConfigAs(MideaACConfiguration.class);
// 1) Ensure discovery/config is valid or start discovery and exit early
if (!ensureConfigOrStartDiscovery()) {
return;
}
// 2) Ensure token/key for V3 devices or start retrieval and exit early
if (!ensureTokenKeyOrStartRetrieval()) {
return;
}
updateStatus(ThingStatus.UNKNOWN);
initConnectionManagerFromConfig();
requestCapabilitiesIfMissing();
startSchedulers();
}
/**
* Ensure we have all configuration needed to reach the device. If incomplete
* but discoverable,
* trigger discovery asynchronously and return false to stop current
* initialization.
*/
private boolean ensureConfigOrStartDiscovery() {
if (config.isValid()) {
logger.debug("Discovery parameters are valid for {}", thing.getUID());
return true;
}
if (!config.isDiscoveryPossible()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_discovery");
return false;
}
MideaACDiscoveryService discoveryService = new MideaACDiscoveryService();
// Kick off discovery asynchronously and end this initialization thread.
scheduler.execute(() -> {
try {
// Keep thing OFFLINE with message about attempting discovery.
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_pending_discovery");
discoveryService.discoverThing(config.ipAddress, this);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication_error_discovery");
}
});
return false;
}
/**
* Ensure token/key are available for V3 devices. If retrievable from cloud,
* trigger async
* retrieval and return false to stop current initialization.
*/
private boolean ensureTokenKeyOrStartRetrieval() {
if (config.version != 3 || config.isV3ConfigValid()) {
logger.debug("Valid token and key for V.3 device {}", thing.getUID());
return true;
}
if (!config.isTokenKeyObtainable()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_token");
return false;
}
scheduler.execute(() -> {
try {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_pending_token");
CloudProvider cloudProvider = CloudProvider.getCloudProvider(config.cloud);
getTokenKeyCloud(cloudProvider);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication_error_token");
}
});
return false;
}
/** Initialize the connection manager from the current configuration. */
private void initConnectionManagerFromConfig() {
connectionManager = new org.openhab.binding.mideaac.internal.connection.ConnectionManager(config.ipAddress,
config.ipPort, config.timeout, config.key, config.token, config.cloud, config.email, config.password,
config.deviceId, config.version, config.promptTone);
}
/**
* Send capabilities command(s) if we don't yet have them stored in properties.
*/
private void requestCapabilitiesIfMissing() {
if (properties.containsKey("modeFanOnly")) {
return;
}
scheduler.execute(() -> {
try {
CommandSet initializationCommand = new CommandSet();
initializationCommand.getCapabilities();
this.connectionManager.sendCommand(initializationCommand, this);
// Check if additional capabilities are available and fetch them if so
CapabilityParser parser = new CapabilityParser();
logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities());
if (parser.hasAdditionalCapabilities()) {
scheduler.schedule(() -> {
try {
CommandSet additionalCommand = new CommandSet();
additionalCommand.getAdditionalCapabilities();
this.connectionManager.sendCommand(additionalCommand, this);
} catch (Exception e) {
logger.debug("AC additional capabilities not returned {}", e.getMessage());
}
}, 2, TimeUnit.SECONDS);
}
} catch (Exception e) {
// Will not affect AC device readiness, just log the issue
logger.debug("AC capabilities not returned {}", e.getMessage());
}
});
@Override
public void initialize() {
super.initialize(); // common plumbing
startEnergyScheduler(); // AC-specific
}
/**
* Start routine, token refresh and energy schedulers according to
* configuration.
*/
private void startSchedulers() {
// Routine polling
if (scheduledTask == null) {
scheduledTask = scheduler.scheduleWithFixedDelay(this::pollJob, 2, config.pollingTime, TimeUnit.SECONDS);
logger.debug("Scheduled task started, Poll Time {} seconds", config.pollingTime);
} else {
logger.debug("Scheduler already running");
}
// Token/key update
if (config.keyTokenUpdate != 0 && scheduledKeyTokenUpdate == null) {
scheduledKeyTokenUpdate = scheduler.scheduleWithFixedDelay(
() -> getTokenKeyCloud(CloudProvider.getCloudProvider(config.cloud)), config.keyTokenUpdate,
config.keyTokenUpdate, TimeUnit.HOURS);
logger.debug("Token Key Update Scheduler started, update interval {} hours", config.keyTokenUpdate);
} else {
logger.debug("Token Key Scheduler already running or disabled");
}
private void startEnergyScheduler() {
// Energy polling
if (config.energyPoll != 0 && scheduledEnergyUpdate == null) {
scheduledEnergyUpdate = scheduler.scheduleWithFixedDelay(this::energyUpdate, 1, config.energyPoll,
@@ -386,59 +102,133 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
}
private void energyUpdate() {
ConnectionManager connectionManager = this.connectionManager;
try {
CommandSet energyUpdate = new CommandSet();
ACCommandSet energyUpdate = new ACCommandSet();
energyUpdate.energyPoll();
connectionManager.sendCommand(energyUpdate, this);
} catch (MideaAuthenticationException e) {
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (MideaConnectionException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (MideaException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void pollJob() {
ConnectionManager connectionManager = this.connectionManager;
@Override
protected void refreshDeviceState() {
try {
connectionManager.getStatus(callbackLambda);
connectionManager.getStatus(this);
// If we reach here, the device is online.
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) {
} catch (MideaConnectionException | MideaException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void updateChannel(String channelName, State state) {
if (ThingStatus.OFFLINE.equals(getThing().getStatus())) {
@Override
protected void refreshDeviceStateAll() {
try {
connectionManager.getStatus(this);
humidityUpdate();
energyUpdate();
} catch (MideaAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (MideaConnectionException | MideaException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
private void humidityUpdate() {
try {
ACCommandSet humidityUpdate = new ACCommandSet();
humidityUpdate.humidityPoll();
connectionManager.sendCommand(humidityUpdate, this);
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
/**
* 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.
* For a Refresh both regular and energy and humidity polls are triggerred.
*/
@Override
protected void handleDeviceCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString());
try {
Response lastresponse = connectionManager.getLastResponse();
if (channelUID.getId().equals(CHANNEL_POWER)) {
connectionManager.sendCommand(ACCommandHelper.handlePower(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_OPERATIONAL_MODE)) {
connectionManager.sendCommand(ACCommandHelper.handleOperationalMode(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_TARGET_TEMPERATURE)) {
connectionManager.sendCommand(ACCommandHelper.handleTargetTemperature(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_FAN_SPEED)) {
connectionManager.sendCommand(ACCommandHelper.handleFanSpeed(command, lastresponse, config.version),
this);
} else if (channelUID.getId().equals(CHANNEL_ECO_MODE)) {
connectionManager.sendCommand(ACCommandHelper.handleEcoMode(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_TURBO_MODE)) {
connectionManager.sendCommand(ACCommandHelper.handleTurboMode(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_SWING_MODE)) {
connectionManager.sendCommand(ACCommandHelper.handleSwingMode(command, lastresponse, config.version),
this);
} else if (channelUID.getId().equals(CHANNEL_SCREEN_DISPLAY)) {
connectionManager.sendCommand(ACCommandHelper.handleScreenDisplay(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_TEMPERATURE_UNIT)) {
connectionManager.sendCommand(ACCommandHelper.handleTempUnit(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_SLEEP_FUNCTION)) {
connectionManager.sendCommand(ACCommandHelper.handleSleepFunction(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_ON_TIMER)) {
connectionManager.sendCommand(ACCommandHelper.handleOnTimer(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) {
connectionManager.sendCommand(ACCommandHelper.handleOffTimer(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_MAXIMUM_HUMIDITY)) {
connectionManager.sendCommand(ACCommandHelper.handleMaximumHumidity(command, lastresponse), this);
}
} catch (MideaConnectionException | MideaAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (MideaException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
/**
* Send capabilities command(s) if we don't yet have them stored in properties.
*/
@Override
protected void requestCapabilitiesIfMissing() {
if (properties.containsKey("modeFanOnly")) {
return;
}
Channel channel = thing.getChannel(channelName);
if (channel != null && isLinked(channel.getUID())) {
updateState(channel.getUID(), state);
}
scheduler.execute(() -> {
try {
ACCommandSet initializationCommand = new ACCommandSet();
initializationCommand.getCapabilities();
this.connectionManager.sendCommand(initializationCommand, this);
} catch (Exception ex) {
// Will not affect AC device readiness, just log the issue
logger.debug("AC capabilities not returned {}", ex.getMessage());
}
});
}
@Override
public void updateChannels(Response response) {
updateChannel(CHANNEL_POWER, OnOffType.from(response.getPowerState()));
updateChannel(CHANNEL_APPLIANCE_ERROR, OnOffType.from(response.getApplianceError()));
updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString()));
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_APPLIANCE_ERROR, OnOffType.from(response.getApplianceError()));
updateChannel(CHANNEL_OPERATIONAL_MODE, new StringType(response.getOperationalMode().toString()));
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()));
@@ -518,6 +308,20 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
updateProperties(properties);
// Check if additional capabilities are available and fetch them if so
logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities());
if (parser.hasAdditionalCapabilities()) {
scheduler.schedule(() -> {
try {
ACCommandSet additionalCommand = new ACCommandSet();
additionalCommand.getAdditionalCapabilities();
this.connectionManager.sendCommand(additionalCommand, this);
} catch (Exception ex) {
logger.debug("AC additional capabilities not returned {}", ex.getMessage());
}
}, 2, TimeUnit.SECONDS);
}
logger.debug("Capabilities and temperature settings parsed and stored in properties: {}", properties);
}
@@ -567,89 +371,6 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
updateChannel(CHANNEL_OUTDOOR_TEMPERATURE, outdoorTemperature);
}
@Override
public void discovered(DiscoveryResult discoveryResult) {
logger.debug("Discovered {}", thing.getUID());
Map<String, Object> 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());
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 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();
}
/**
* Gets the token and key from the Cloud
*
* @param cloudProvider Cloud Provider account
*/
public void getTokenKeyCloud(CloudProvider cloudProvider) {
if (scheduledTask != null) {
stopScheduler();
}
logger.debug("Retrieving Token and/or Key from cloud");
Cloud cloud = new Cloud(config.email, config.password, cloudProvider, httpClient);
if (cloud.login()) {
TokenKey tk = cloud.getToken(config.deviceId);
Configuration configuration = editConfiguration();
configuration.put(CONFIG_TOKEN, tk.token());
configuration.put(CONFIG_KEY, tk.key());
updateConfiguration(configuration);
logger.trace("Token: {}", tk.token());
logger.trace("Key: {}", tk.key());
logger.debug("Token and Key obtained from cloud, saving, back to initialize");
initialize();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration_error_invalid_token ");
}
}
private void stopScheduler() {
ScheduledFuture<?> localScheduledTask = this.scheduledTask;
if (localScheduledTask != null && !localScheduledTask.isCancelled()) {
localScheduledTask.cancel(true);
logger.debug("Scheduled task cancelled.");
scheduledTask = null;
}
}
private void stopTokenKeyUpdate() {
ScheduledFuture<?> localScheduledTask = this.scheduledKeyTokenUpdate;
if (localScheduledTask != null && !localScheduledTask.isCancelled()) {
localScheduledTask.cancel(true);
logger.debug("Scheduled Key Token Update cancelled.");
scheduledKeyTokenUpdate = null;
}
}
private void stopEnergyUpdate() {
ScheduledFuture<?> localScheduledTask = this.scheduledEnergyUpdate;
@@ -662,9 +383,7 @@ public class MideaACHandler extends BaseThingHandler implements DiscoveryHandler
@Override
public void dispose() {
stopScheduler();
stopTokenKeyUpdate();
stopEnergyUpdate();
connectionManager.dispose(true);
super.dispose(); // stop common schedulers + connection manager
stopEnergyUpdate(); // AC-specific
}
}
@@ -0,0 +1,217 @@
/*
* Copyright (c) 2010-2026 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.IOException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.mideaac.internal.callbacks.HumidifierCallback;
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.devices.a1.A1CommandHelper;
import org.openhab.binding.mideaac.internal.devices.a1.A1CommandSet;
import org.openhab.binding.mideaac.internal.devices.a1.A1Response;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilitiesResponse;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilityParser;
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.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MideaDehumidifierHandler} is responsible for handling commands, and
* updating the dehumidifier thing's status and channels based on responses
* from the device.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class MideaDehumidifierHandler extends AbstractMideaHandler implements HumidifierCallback {
private final Logger logger = LoggerFactory.getLogger(MideaDehumidifierHandler.class);
private final boolean imperialUnits;
public MideaDehumidifierHandler(Thing thing, UnitProvider unitProvider, HttpClient httpClient) {
super(thing, unitProvider, httpClient);
this.imperialUnits = unitProvider.getMeasurementSystem() instanceof ImperialUnits;
}
@Override
protected void requestCapabilitiesIfMissing() {
if (properties.containsKey("humidityAutoSet")) {
return;
}
scheduler.execute(() -> {
try {
A1CommandSet initializationCommand = new A1CommandSet();
initializationCommand.getCapabilities();
this.connectionManager.sendCommand(initializationCommand, this);
} catch (Exception ex) {
// Will not affect dehumidifier readiness, just log the issue
logger.debug("Dehumidifier capabilities not returned {}", ex.getMessage());
}
});
}
@Override
protected void refreshDeviceStateAll() {
refreshDeviceState(); // No extra energy or humidity polls for dehumidifier
}
@Override
protected void refreshDeviceState() {
try {
connectionManager.getStatus(this);
// If we reach here, the device is online.
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());
} catch (IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
protected void handleDeviceCommand(ChannelUID channelUID, Command command) {
logger.debug("Handling channelUID {} with command {}", channelUID.getId(), command.toString());
try {
A1Response lastresponse = connectionManager.getLastA1Response();
if (channelUID.getId().equals(CHANNEL_POWER)) {
connectionManager.sendCommand(A1CommandHelper.handlePower(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_DH_FAN_SPEED)) {
connectionManager.sendCommand(A1CommandHelper.handleA1FanSpeed(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_ON_TIMER)) {
connectionManager.sendCommand(A1CommandHelper.handleOnTimer(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_OFF_TIMER)) {
connectionManager.sendCommand(A1CommandHelper.handleOffTimer(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_MAXIMUM_HUMIDITY)) {
connectionManager.sendCommand(A1CommandHelper.handleA1MaximumHumidity(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_DEHUMIDIFIER_MODE)) {
connectionManager.sendCommand(A1CommandHelper.handleA1OperationalMode(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_DEHUMIDIFIER_SWING)) {
connectionManager.sendCommand(A1CommandHelper.handleA1Swing(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_CHILD_LOCK)) {
connectionManager.sendCommand(A1CommandHelper.handleA1ChildLock(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_DEHUMIDIFIER_TANK_SETPOINT)) {
connectionManager.sendCommand(A1CommandHelper.handleA1TankSetpoint(command, lastresponse), this);
} else if (channelUID.getId().equals(CHANNEL_ANION)) {
connectionManager.sendCommand(A1CommandHelper.handleA1Anion(command, lastresponse), this);
}
} catch (MideaConnectionException | MideaAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} catch (MideaException | IOException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
public void updateChannels(A1Response response) {
updateChannel(CHANNEL_POWER, OnOffType.from(response.getPowerState()));
updateChannel(CHANNEL_DH_FAN_SPEED, new StringType(response.getA1FanSpeed().toString()));
updateChannel(CHANNEL_ON_TIMER, new StringType(response.getOnTimer().toChannel()));
updateChannel(CHANNEL_OFF_TIMER, new StringType(response.getOffTimer().toChannel()));
updateChannel(CHANNEL_DEHUMIDIFIER_MODE, new StringType(response.getA1OperationalMode().toString()));
updateChannel(CHANNEL_MAXIMUM_HUMIDITY, new DecimalType(response.getMaximumHumidity()));
updateChannel(CHANNEL_HUMIDITY, new DecimalType(response.getCurrentHumidity()));
updateChannel(CHANNEL_ANION, OnOffType.from(response.getA1Anion()));
updateChannel(CHANNEL_FILTER_STATUS, OnOffType.from(response.getA1FilterStatus()));
updateChannel(CHANNEL_CHILD_LOCK, OnOffType.from(response.getA1ChildLock()));
updateChannel(CHANNEL_DEHUMIDIFIER_TANK, new DecimalType(response.getTank()));
updateChannel(CHANNEL_DEHUMIDIFIER_TANK_SETPOINT, new DecimalType(response.getTankSetpoint()));
updateChannel(CHANNEL_DEHUMIDIFIER_SWING, OnOffType.from(response.getA1SwingMode()));
QuantityType<Temperature> indoorTemperature = new QuantityType<Temperature>(response.getIndoorTemperature(),
SIUnits.CELSIUS);
if (imperialUnits) {
indoorTemperature = Objects.requireNonNull(indoorTemperature.toUnit(ImperialUnits.FAHRENHEIT));
}
updateChannel(CHANNEL_INDOOR_TEMPERATURE, indoorTemperature);
}
// Handle capabilities responses
@Override
public void updateChannels(CapabilitiesResponse capabilitiesResponse) {
CapabilityParser parser = new CapabilityParser();
parser.parse(capabilitiesResponse.getRawData());
properties = editProperties();
parser.getCapabilities().forEach((capabilityId, capabilityMap) -> {
capabilityMap.forEach((key, value) -> {
properties.put(key, String.valueOf(value));
});
});
parser.getNumericCapabilities().forEach((capabilityId, temperatureMap) -> {
temperatureMap.forEach((key, value) -> {
properties.put(key, String.valueOf(value));
});
});
// Defaults if FAN_SPEED_CONTROL is missing from response
if (!properties.containsKey("fanLow")) {
properties.put("fanLow", "true - default");
}
if (!properties.containsKey("fanMedium")) {
properties.put("fanMedium", "true - default");
}
if (!properties.containsKey("fanHigh")) {
properties.put("fanHigh", "true - default");
}
if (!properties.containsKey("fanAuto")) {
properties.put("fanAuto", "true - default");
}
updateProperties(properties);
// Check if additional capabilities are available and fetch them if so
logger.debug("additional capabilities {}", parser.hasAdditionalCapabilities());
if (parser.hasAdditionalCapabilities()) {
scheduler.schedule(() -> {
try {
A1CommandSet additionalCommand = new A1CommandSet();
additionalCommand.getAdditionalCapabilities();
this.connectionManager.sendCommand(additionalCommand, this);
} catch (Exception ex) {
logger.debug("Humidifier additional capabilities not returned {}", ex.getMessage());
}
}, 2, TimeUnit.SECONDS);
}
logger.debug("Capabilities and temperature settings parsed and stored in properties: {}", properties);
}
}
@@ -23,6 +23,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
import javax.crypto.BadPaddingException;
@@ -506,7 +507,7 @@ public class Security {
String sign = path + query + cloudProvider.appkey();
logger.trace("sign: {}", sign);
return HexUtils.bytesToHex(sha256(sign.getBytes(StandardCharsets.US_ASCII))).toLowerCase();
return HexUtils.bytesToHex(sha256(sign.getBytes(StandardCharsets.US_ASCII))).toLowerCase(Locale.ROOT);
} catch (URISyntaxException e) {
logger.warn("Error parsing URI '{}': {}", url, e.getMessage());
}
@@ -554,10 +555,10 @@ public class Security {
* @return lower case string
*/
public String hmac(String data, String key, String algorithm) throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), algorithm);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), algorithm);
Mac mac = Mac.getInstance(algorithm);
mac.init(secretKeySpec);
return HexUtils.bytesToHex(mac.doFinal(data.getBytes())).toLowerCase();
return HexUtils.bytesToHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8))).toLowerCase(Locale.ROOT);
}
/**
@@ -574,10 +575,11 @@ public class Security {
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 + HexUtils.bytesToHex(m.digest()).toLowerCase() + cloudProvider.appkey();
String loginHash = loginId + HexUtils.bytesToHex(m.digest()).toLowerCase(Locale.ROOT)
+ cloudProvider.appkey();
m = MessageDigest.getInstance("SHA-256");
m.update(loginHash.getBytes(StandardCharsets.US_ASCII));
return HexUtils.bytesToHex(m.digest()).toLowerCase();
return HexUtils.bytesToHex(m.digest()).toLowerCase(Locale.ROOT);
} catch (NoSuchAlgorithmException e) {
logger.warn("encryptPassword error: NoSuchAlgorithmException: {}", e.getMessage());
}
@@ -597,12 +599,14 @@ public class Security {
md.update(password.getBytes(StandardCharsets.US_ASCII));
MessageDigest mdSecond = MessageDigest.getInstance("MD5");
mdSecond.update((HexUtils.bytesToHex(md.digest()).toLowerCase()).getBytes(StandardCharsets.US_ASCII));
mdSecond.update(
(HexUtils.bytesToHex(md.digest()).toLowerCase(Locale.ROOT)).getBytes(StandardCharsets.US_ASCII));
String loginHash = loginId + HexUtils.bytesToHex(mdSecond.digest()).toLowerCase() + cloudProvider.appkey();
return HexUtils.bytesToHex(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))).toLowerCase();
String loginHash = loginId + HexUtils.bytesToHex(mdSecond.digest()).toLowerCase(Locale.ROOT)
+ cloudProvider.appkey();
return HexUtils.bytesToHex(sha256(loginHash.getBytes(StandardCharsets.US_ASCII))).toLowerCase(Locale.ROOT);
} catch (NoSuchAlgorithmException e) {
logger.warn("encryptIamPasswordt error: NoSuchAlgorithmException: {}", e.getMessage());
logger.warn("encryptIamPassword error: NoSuchAlgorithmException: {}", e.getMessage());
}
return null;
}
@@ -624,6 +628,6 @@ public class Security {
b3[i] = (byte) (b1[i] ^ b2[i]);
i++;
}
return HexUtils.bytesToHex(b3).toLowerCase();
return HexUtils.bytesToHex(b3).toLowerCase(Locale.ROOT);
}
}
@@ -5,7 +5,7 @@
<type>binding</type>
<name>MideaAC Binding</name>
<description>This is the binding for MideaAC.</description>
<description>This is the binding for a Midea AC (type AC) and Dehumidifier.</description>
<connection>local</connection>
<discovery-methods>
<discovery-method>
@@ -1,15 +1,50 @@
# add-on
addon.mideaac.name = MideaAC Binding
addon.mideaac.description = This is the binding for MideaAC.
addon.mideaac.description = This is the binding for a Midea AC (type AC) and Dehumidifier.
# thing types
thing-type.mideaac.a1.label = Midea Dehumidifier
thing-type.mideaac.a1.description = Midea Dehumidifier with USB WIFI stick.
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.a1.cloud.label = Cloud Provider
thing-type.config.mideaac.a1.cloud.description = Cloud Provider name for email and password.
thing-type.config.mideaac.a1.cloud.option.MSmartHome = MSmartHome
thing-type.config.mideaac.a1.cloud.option.Midea\ Air = Midea Air
thing-type.config.mideaac.a1.cloud.option.NetHome\ Plus = NetHome Plus
thing-type.config.mideaac.a1.deviceId.label = Device ID
thing-type.config.mideaac.a1.deviceId.description = ID of the device. Leave 0 to do ID discovery.
thing-type.config.mideaac.a1.deviceType.label = Device Type
thing-type.config.mideaac.a1.deviceType.description = ac or a1(dehumidifier) discoverable
thing-type.config.mideaac.a1.email.label = Email
thing-type.config.mideaac.a1.email.description = Email for cloud account chosen in Cloud Provider.
thing-type.config.mideaac.a1.energyPoll.label = Energy Poll
thing-type.config.mideaac.a1.energyPoll.description = Energy polling in minutes. default 0 ; (in case not supported).
thing-type.config.mideaac.a1.ipAddress.label = IP Address
thing-type.config.mideaac.a1.ipAddress.description = IP Address of the device.
thing-type.config.mideaac.a1.ipPort.label = IP Port
thing-type.config.mideaac.a1.ipPort.description = IP port of the device.
thing-type.config.mideaac.a1.key.label = Key
thing-type.config.mideaac.a1.key.description = Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it).
thing-type.config.mideaac.a1.keyTokenUpdate.label = Key Token Update
thing-type.config.mideaac.a1.keyTokenUpdate.description = Update the Key and Token from the cloud in hours, min 24h, default 0 to disable.
thing-type.config.mideaac.a1.password.label = Password
thing-type.config.mideaac.a1.password.description = Password for cloud account chosen in Cloud Provider.
thing-type.config.mideaac.a1.pollingTime.label = Poll Frequency
thing-type.config.mideaac.a1.pollingTime.description = Base device poll in seconds. Minimum is 30, default 60.
thing-type.config.mideaac.a1.promptTone.label = Prompt Tone
thing-type.config.mideaac.a1.promptTone.description = After sending a command device will play "ding" tone when command is received and executed.
thing-type.config.mideaac.a1.timeout.label = Socket Timeout
thing-type.config.mideaac.a1.timeout.description = Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default).
thing-type.config.mideaac.a1.token.label = Token
thing-type.config.mideaac.a1.token.description = Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not known, enter email, password for Cloud account to retrieve it).
thing-type.config.mideaac.a1.version.label = AC Version
thing-type.config.mideaac.a1.version.description = Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.
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
@@ -17,6 +52,8 @@ 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.deviceType.label = Device Type
thing-type.config.mideaac.ac.deviceType.description = ac or a1(dehumidifier) discoverable
thing-type.config.mideaac.ac.email.label = Email
thing-type.config.mideaac.ac.email.description = Email for cloud account chosen in Cloud Provider.
thing-type.config.mideaac.ac.energyDecode.label = Energy Response Decode Method
@@ -46,10 +83,36 @@ thing-type.config.mideaac.ac.version.description = Version 3 requires Token, Key
# channel types
channel-type.mideaac.anion.label = Anion
channel-type.mideaac.appliance-error.label = Appliance Error
channel-type.mideaac.auxiliary-heat.label = Auxiliary Heat
channel-type.mideaac.child-lock.label = Child Lock
channel-type.mideaac.child-lock.description = Lock Controls.
channel-type.mideaac.current-draw.label = Amperes
channel-type.mideaac.current-draw.description = Amperes (current) reported by the indoor unit.
channel-type.mideaac.dehumidifier-mode.label = Dehumidifier Mode
channel-type.mideaac.dehumidifier-mode.description = Operational modes: AUTO, MANUAL, CONTINUOUS, CLOTHES DRY, SHOE DRY.
channel-type.mideaac.dehumidifier-mode.state.option.AUTO = AUTO
channel-type.mideaac.dehumidifier-mode.state.option.MANUAL = MANUAL
channel-type.mideaac.dehumidifier-mode.state.option.CONTINUOUS = CONTINUOUS
channel-type.mideaac.dehumidifier-mode.state.option.CLOTHES_DRY = CLOTHES DRY
channel-type.mideaac.dehumidifier-mode.state.option.SHOES_DRY = SHOES DRY
channel-type.mideaac.dehumidifier-swing.label = Dehumidifier Swing
channel-type.mideaac.dehumidifier-swing.description = Louver Movement.
channel-type.mideaac.dehumidifier-tank.label = Tank Water Level
channel-type.mideaac.dh-fan-speed.label = Fan Speed
channel-type.mideaac.dh-fan-speed.description = Fan speeds: OFF, LOWEST, LOW, MEDIUM, HIGH, AUTO.
channel-type.mideaac.dh-fan-speed.state.option.OFF = OFF
channel-type.mideaac.dh-fan-speed.state.option.LOWEST = LOWEST
channel-type.mideaac.dh-fan-speed.state.option.LOW = LOW
channel-type.mideaac.dh-fan-speed.state.option.MEDIUM = MEDIUM
channel-type.mideaac.dh-fan-speed.state.option.HIGH = HIGH
channel-type.mideaac.dh-fan-speed.state.option.AUTO = AUTO
channel-type.mideaac.dh-tank-setpoint.label = Dehumidifier Tank Setpoint
channel-type.mideaac.dh-tank-setpoint.state.option.25% = 25
channel-type.mideaac.dh-tank-setpoint.state.option.50% = 50
channel-type.mideaac.dh-tank-setpoint.state.option.75% = 75
channel-type.mideaac.dh-tank-setpoint.state.option.100% = 100
channel-type.mideaac.eco-mode.label = Eco Mode
channel-type.mideaac.eco-mode.description = Eco mode, Cool only, Temp: min. 24C, Fan: AUTO.
channel-type.mideaac.energy-consumption.label = Kilowatt Hours
@@ -66,9 +129,9 @@ channel-type.mideaac.filter-status.label = Filter Needs Cleaning
channel-type.mideaac.humidity.label = Humidity
channel-type.mideaac.humidity.description = Humidity in room (if supported).
channel-type.mideaac.indoor-temperature.label = Indoor Temperature
channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit. Not frequent when unit is off
channel-type.mideaac.indoor-temperature.description = Indoor temperature measured by the internal unit.
channel-type.mideaac.maximum-humidity.label = Maximum Humidity
channel-type.mideaac.maximum-humidity.description = Set Maximum Humidity level for Dry Mode (if supported).
channel-type.mideaac.maximum-humidity.description = Set Maximum Humidity level.
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
@@ -86,10 +149,35 @@ channel-type.mideaac.power-consumption.label = Watts
channel-type.mideaac.power-consumption.description = Watts reported by the indoor unit.
channel-type.mideaac.power.label = Power
channel-type.mideaac.power.description = Turn the AC on or off.
channel-type.mideaac.purifier-mode.label = Purifier Mode
channel-type.mideaac.purifier-mode.description = Purifier Modes (ON, OFF or AUTO)
channel-type.mideaac.purifier-mode.state.option.OFF = OFF
channel-type.mideaac.purifier-mode.state.option.ON = ON
channel-type.mideaac.purifier-mode.state.option.AUTO = AUTO
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.silent-mode.label = Silent Mode
channel-type.mideaac.silent-mode.description = Silent Mode mode.
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-angle-horizontal.label = Swing Angle Horizontal
channel-type.mideaac.swing-angle-horizontal.description = Swing Angle Horizontal: Closed, Positions 1 - 5 plus Auto.
channel-type.mideaac.swing-angle-horizontal.state.option.CLOSE = CLOSE
channel-type.mideaac.swing-angle-horizontal.state.option.POSITION_1 = POSITION_1
channel-type.mideaac.swing-angle-horizontal.state.option.POSITION_2 = POSITION_2
channel-type.mideaac.swing-angle-horizontal.state.option.POSITION_3 = POSITION_3
channel-type.mideaac.swing-angle-horizontal.state.option.POSITION_4 = POSITION_4
channel-type.mideaac.swing-angle-horizontal.state.option.POSITION_5 = POSITION_5
channel-type.mideaac.swing-angle-horizontal.state.option.AUTO = AUTO
channel-type.mideaac.swing-angle-vertical.label = Swing Angle Vertical
channel-type.mideaac.swing-angle-vertical.description = Swing Angle Vertical: Closed, Positions 1 - 5 plus Auto.
channel-type.mideaac.swing-angle-vertical.state.option.CLOSE = CLOSE
channel-type.mideaac.swing-angle-vertical.state.option.POSITION_1 = POSITION_1
channel-type.mideaac.swing-angle-vertical.state.option.POSITION_2 = POSITION_2
channel-type.mideaac.swing-angle-vertical.state.option.POSITION_3 = POSITION_3
channel-type.mideaac.swing-angle-vertical.state.option.POSITION_4 = POSITION_4
channel-type.mideaac.swing-angle-vertical.state.option.POSITION_5 = POSITION_5
channel-type.mideaac.swing-angle-vertical.state.option.AUTO = AUTO
channel-type.mideaac.swing-mode.label = Swing Mode
channel-type.mideaac.swing-mode.description = Swing modes: OFF, VERTICAL, HORIZONTAL, BOTH. Some V3 versions do not support
channel-type.mideaac.swing-mode.state.option.OFF = OFF
@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mideaac"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Thing Type -->
<thing-type id="ac">
<label>Midea Air Conditioner</label>
<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.
</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="target-temperature" typeId="target-temperature"/>
<channel id="operational-mode" typeId="operational-mode"/>
<channel id="fan-speed" typeId="fan-speed"/>
<channel id="swing-mode" typeId="swing-mode"/>
<channel id="eco-mode" typeId="eco-mode"/>
<channel id="turbo-mode" typeId="turbo-mode"/>
<channel id="indoor-temperature" typeId="indoor-temperature"/>
<channel id="outdoor-temperature" typeId="outdoor-temperature"/>
<channel id="sleep-function" typeId="sleep-function"/>
<channel id="temperature-unit" typeId="temperature-unit"/>
<channel id="on-timer" typeId="on-timer"/>
<channel id="off-timer" typeId="off-timer"/>
<channel id="appliance-error" typeId="appliance-error"/>
<channel id="auxiliary-heat" typeId="auxiliary-heat"/>
<channel id="maximum-humidity" typeId="maximum-humidity"/>
<channel id="humidity" typeId="humidity"/>
<channel id="energy-consumption" typeId="energy-consumption"/>
<channel id="current-draw" typeId="current-draw"/>
<channel id="power-consumption" typeId="power-consumption"/>
<channel id="screen-display" typeId="screen-display"/>
<channel id="filter-status" typeId="filter-status"/>
</channels>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
<description>IP Address of the device.</description>
</parameter>
<parameter name="ipPort" type="decimal" required="true">
<label>IP Port</label>
<description>IP port of the device.</description>
<default>6444</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>ID of the device. Leave 0 to do ID discovery.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="cloud" type="text" required="false">
<label>Cloud Provider</label>
<description>Cloud Provider name for email and password.</description>
<options>
<option value=""></option>
<option value="MSmartHome">MSmartHome</option>
<option value="Midea Air">Midea Air</option>
<option value="NetHome Plus">NetHome Plus</option>
</options>
<limitToOptions>true</limitToOptions>
<default>NetHome Plus</default>
</parameter>
<parameter name="email" type="text" required="false">
<context>email</context>
<label>Email</label>
<description>Email for cloud account chosen in Cloud Provider.</description>
<default>nethome+us@mailinator.com</default>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Password</label>
<description>Password for cloud account chosen in Cloud Provider.</description>
<default>password1</default>
</parameter>
<parameter name="token" type="text" required="false">
<label>Token</label>
<description>Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="key" type="text" required="false">
<label>Key</label>
<description>Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="pollingTime" type="decimal" required="true" min="30" unit="s">
<label>Poll Frequency</label>
<description>Base device poll in seconds. Minimum is 30, default 60.</description>
<default>60</default>
</parameter>
<parameter name="energyPoll" type="decimal" required="true" min="0" unit="m">
<label>Energy Poll</label>
<description>Energy polling in minutes. default 0 ; (in case not supported).</description>
<default>0</default>
</parameter>
<parameter name="keyTokenUpdate" type="decimal" required="false" min="0" unit="h">
<label>Key Token Update</label>
<description>Update the Key and Token from the cloud in hours, min 24h, default 0 to disable.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="decimal" required="true" min="2" max="10" unit="s">
<label>Socket Timeout</label>
<description>Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default).</description>
<default>4</default>
<advanced>true</advanced>
</parameter>
<parameter name="promptTone" type="boolean" required="true">
<label>Prompt Tone</label>
<description>After sending a command device will play "ding" tone when command is received and executed.</description>
<default>false</default>
</parameter>
<parameter name="version" type="decimal" required="true">
<label>AC Version</label>
<description>Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.</description>
<default>3</default>
<advanced>true</advanced>
</parameter>
<parameter name="energyDecode" type="boolean" required="true">
<label>Energy Response Decode Method</label>
<description>Binary-Coded Decimal (BCD) = true. Big-endian = false.</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceType" type="text" required="true">
<label>Device Type</label>
<description>ac or a1(dehumidifier) discoverable</description>
<default>ac</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>
@@ -4,138 +4,6 @@
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Thing Type -->
<thing-type id="ac">
<label>Midea Air Conditioner</label>
<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.
</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="target-temperature" typeId="target-temperature"/>
<channel id="operational-mode" typeId="operational-mode"/>
<channel id="fan-speed" typeId="fan-speed"/>
<channel id="swing-mode" typeId="swing-mode"/>
<channel id="eco-mode" typeId="eco-mode"/>
<channel id="turbo-mode" typeId="turbo-mode"/>
<channel id="indoor-temperature" typeId="indoor-temperature"/>
<channel id="outdoor-temperature" typeId="outdoor-temperature"/>
<channel id="sleep-function" typeId="sleep-function"/>
<channel id="temperature-unit" typeId="temperature-unit"/>
<channel id="on-timer" typeId="on-timer"/>
<channel id="off-timer" typeId="off-timer"/>
<channel id="appliance-error" typeId="appliance-error"/>
<channel id="auxiliary-heat" typeId="auxiliary-heat"/>
<channel id="maximum-humidity" typeId="maximum-humidity"/>
<channel id="humidity" typeId="humidity"/>
<channel id="energy-consumption" typeId="energy-consumption"/>
<channel id="current-draw" typeId="current-draw"/>
<channel id="power-consumption" typeId="power-consumption"/>
<channel id="screen-display" typeId="screen-display"/>
<channel id="filter-status" typeId="filter-status"/>
</channels>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
<description>IP Address of the device.</description>
</parameter>
<parameter name="ipPort" type="decimal" required="true">
<label>IP Port</label>
<description>IP port of the device.</description>
<default>6444</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>ID of the device. Leave 0 to do ID discovery.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="cloud" type="text" required="false">
<label>Cloud Provider</label>
<description>Cloud Provider name for email and password.</description>
<options>
<option value=""></option>
<option value="MSmartHome">MSmartHome</option>
<option value="Midea Air">Midea Air</option>
<option value="NetHome Plus">NetHome Plus</option>
</options>
<limitToOptions>true</limitToOptions>
<default>NetHome Plus</default>
</parameter>
<parameter name="email" type="text" required="false">
<context>email</context>
<label>Email</label>
<description>Email for cloud account chosen in Cloud Provider.</description>
<default>nethome+us@mailinator.com</default>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Password</label>
<description>Password for cloud account chosen in Cloud Provider.</description>
<default>password1</default>
</parameter>
<parameter name="token" type="text" required="false">
<label>Token</label>
<description>Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="key" type="text" required="false">
<label>Key</label>
<description>Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="pollingTime" type="decimal" required="true" min="30" unit="s">
<label>Poll Frequency</label>
<description>Base device poll in seconds. Minimum is 30, default 60.</description>
<default>60</default>
</parameter>
<parameter name="energyPoll" type="decimal" required="true" min="0" unit="m">
<label>Energy Poll</label>
<description>Energy polling in minutes. default 0 ; (in case not supported).</description>
<default>0</default>
</parameter>
<parameter name="keyTokenUpdate" type="decimal" required="false" min="0" unit="h">
<label>Key Token Update</label>
<description>Update the Key and Token from the cloud in hours, min 24h, default 0 to disable.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="decimal" required="true" min="2" max="10" unit="s">
<label>Socket Timeout</label>
<description>Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default).</description>
<default>4</default>
<advanced>true</advanced>
</parameter>
<parameter name="promptTone" type="boolean" required="true">
<label>Prompt Tone</label>
<description>After sending a command device will play "ding" tone when command is received and executed.</description>
<default>false</default>
</parameter>
<parameter name="version" type="decimal" required="true">
<label>AC Version</label>
<description>Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.</description>
<default>3</default>
<advanced>true</advanced>
</parameter>
<parameter name="energyDecode" type="boolean" required="true">
<label>Energy Response Decode Method</label>
<description>Binary-Coded Decimal (BCD) = true. Big-endian = false.</description>
<default>true</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="power">
<item-type>Switch</item-type>
<label>Power</label>
@@ -193,6 +61,65 @@
</options>
</state>
</channel-type>
<channel-type id="dh-fan-speed">
<item-type>String</item-type>
<label>Fan Speed</label>
<description>Fan speeds: OFF, LOWEST, LOW, MEDIUM, HIGH, AUTO.</description>
<tags>
<tag>Control</tag>
<tag>Airflow</tag>
</tags>
<state>
<options>
<option value="OFF">OFF</option>
<option value="LOWEST">LOWEST</option>
<option value="LOW">LOW</option>
<option value="MEDIUM">MEDIUM</option>
<option value="HIGH">HIGH</option>
<option value="AUTO">AUTO</option>
</options>
</state>
</channel-type>
<channel-type id="swing-angle-horizontal">
<item-type>String</item-type>
<label>Swing Angle Horizontal</label>
<description>Swing Angle Horizontal: Closed, Positions 1 - 5 plus Auto.</description>
<tags>
<tag>Control</tag>
<tag>Airflow</tag>
</tags>
<state>
<options>
<option value="CLOSE">CLOSE</option>
<option value="POSITION_1">POSITION_1</option>
<option value="POSITION_2">POSITION_2</option>
<option value="POSITION_3">POSITION_3</option>
<option value="POSITION_4">POSITION_4</option>
<option value="POSITION_5">POSITION_5</option>
<option value="AUTO">AUTO</option>
</options>
</state>
</channel-type>
<channel-type id="swing-angle-vertical">
<item-type>String</item-type>
<label>Swing Angle Vertical</label>
<description>Swing Angle Vertical: Closed, Positions 1 - 5 plus Auto.</description>
<tags>
<tag>Control</tag>
<tag>Airflow</tag>
</tags>
<state>
<options>
<option value="CLOSE">CLOSE</option>
<option value="POSITION_1">POSITION_1</option>
<option value="POSITION_2">POSITION_2</option>
<option value="POSITION_3">POSITION_3</option>
<option value="POSITION_4">POSITION_4</option>
<option value="POSITION_5">POSITION_5</option>
<option value="AUTO">AUTO</option>
</options>
</state>
</channel-type>
<channel-type id="swing-mode">
<item-type>String</item-type>
<label>Swing Mode</label>
@@ -231,10 +158,21 @@
<tag>Airconditioning</tag>
</tags>
</channel-type>
<channel-type id="silent-mode">
<item-type>Switch</item-type>
<label>Silent Mode</label>
<description>Silent Mode
mode.</description>
<category>Switch</category>
<tags>
<tag>Switch</tag>
<tag>Airconditioning</tag>
</tags>
</channel-type>
<channel-type id="indoor-temperature">
<item-type>Number:Temperature</item-type>
<label>Indoor Temperature</label>
<description>Indoor temperature measured by the internal unit. Not frequent when unit is off</description>
<description>Indoor temperature measured by the internal unit.</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
@@ -332,27 +270,21 @@
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="maximum-humidity" advanced="true">
<item-type>Number</item-type>
<label>Maximum Humidity</label>
<description>Set Maximum Humidity level for Dry Mode (if supported).</description>
<category>Humidity</category>
<channel-type id="purifier-mode" advanced="true">
<item-type>String</item-type>
<label>Purifier Mode</label>
<description>Purifier Modes (ON, OFF or AUTO)</description>
<tags>
<tag>Control</tag>
<tag>Humidity</tag>
<tag>Heating</tag>
</tags>
<state pattern="%d%%"/>
</channel-type>
<channel-type id="humidity" advanced="true">
<item-type>Number</item-type>
<label>Humidity</label>
<description>Humidity in room (if supported).</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Humidity</tag>
</tags>
<state readOnly="true" pattern="%d%%"/>
<state>
<options>
<option value="OFF">OFF</option>
<option value="ON">ON</option>
<option value="AUTO">AUTO</option>
</options>
</state>
</channel-type>
<channel-type id="energy-consumption" advanced="true">
<item-type>Number</item-type>
@@ -387,4 +319,99 @@
</tags>
<state readOnly="true" pattern="%.1f"/>
</channel-type>
<channel-type id="dehumidifier-mode" advanced="true">
<item-type>String</item-type>
<label>Dehumidifier Mode</label>
<description>Operational modes: AUTO, MANUAL, CONTINUOUS, CLOTHES DRY, SHOE DRY.</description>
<tags>
<tag>Control</tag>
<tag>Mode</tag>
</tags>
<state>
<options>
<option value="AUTO">AUTO</option>
<option value="MANUAL">MANUAL</option>
<option value="CONTINUOUS">CONTINUOUS</option>
<option value="CLOTHES_DRY">CLOTHES DRY</option>
<option value="SHOES_DRY">SHOES DRY</option>
</options>
</state>
</channel-type>
<channel-type id="dehumidifier-tank">
<item-type>Number</item-type>
<label>Tank Water Level</label>
<tags>
<tag>Measurement</tag>
<tag>Level</tag>
</tags>
<state readOnly="true" pattern="%d%%"/>
</channel-type>
<channel-type id="dh-tank-setpoint">
<item-type>Number</item-type>
<label>Dehumidifier Tank Setpoint</label>
<tags>
<tag>Setpoint</tag>
<tag>Humidity</tag>
</tags>
<state>
<options>
<option value="25%">25</option>
<option value="50%">50</option>
<option value="75%">75</option>
<option value="100%">100</option>
</options>
</state>
</channel-type>
<channel-type id="dehumidifier-swing">
<item-type>Switch</item-type>
<label>Dehumidifier Swing</label>
<description>Louver Movement.</description>
<category>Switch</category>
<tags>
<tag>Switch</tag>
<tag>Mode</tag>
</tags>
</channel-type>
<channel-type id="humidity">
<item-type>Number</item-type>
<label>Humidity</label>
<description>Humidity in room (if supported).</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Humidity</tag>
</tags>
<state readOnly="true" pattern="%d%%"/>
</channel-type>
<channel-type id="child-lock" advanced="true">
<item-type>Switch</item-type>
<label>Child Lock</label>
<description>Lock Controls. </description>
<category>Switch</category>
<tags>
<tag>Switch</tag>
<tag>LockState</tag>
</tags>
</channel-type>
<channel-type id="anion" advanced="true">
<item-type>Switch</item-type>
<label>Anion</label>
<category>Switch</category>
<tags>
<tag>Switch</tag>
<tag>Enabled</tag>
</tags>
</channel-type>
<channel-type id="maximum-humidity">
<item-type>Number</item-type>
<label>Maximum Humidity</label>
<description>Set Maximum Humidity level.</description>
<category>Humidity</category>
<tags>
<tag>Setpoint</tag>
<tag>Humidity</tag>
</tags>
<state pattern="%d%%"/>
</channel-type>
</thing:thing-descriptions>
@@ -0,0 +1,130 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mideaac"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Thing Type -->
<thing-type id="a1">
<label>Midea Dehumidifier</label>
<description>Midea Dehumidifier with USB WIFI stick.
</description>
<channels>
<channel id="power" typeId="power"/>
<channel id="dehumidifier-mode" typeId="dehumidifier-mode"/>
<channel id="maximum-humidity" typeId="maximum-humidity"/>
<channel id="humidity" typeId="humidity"/>
<channel id="dh-fan-speed" typeId="dh-fan-speed"/>
<channel id="dehumidifier-swing" typeId="dehumidifier-swing"/>
<channel id="indoor-temperature" typeId="indoor-temperature"/>
<channel id="dehumidifier-tank" typeId="dehumidifier-tank"/>
<channel id="dh-tank-setpoint" typeId="dh-tank-setpoint"/>
<channel id="on-timer" typeId="on-timer"/>
<channel id="off-timer" typeId="off-timer"/>
<channel id="anion" typeId="anion"/>
<channel id="filter-status" typeId="filter-status"/>
<channel id="child-lock" typeId="child-lock"/>
</channels>
<representation-property>ipAddress</representation-property>
<config-description>
<parameter name="ipAddress" type="text" required="true">
<context>network-address</context>
<label>IP Address</label>
<description>IP Address of the device.</description>
</parameter>
<parameter name="ipPort" type="decimal" required="true">
<label>IP Port</label>
<description>IP port of the device.</description>
<default>6444</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceId" type="text" required="true">
<label>Device ID</label>
<description>ID of the device. Leave 0 to do ID discovery.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="cloud" type="text" required="false">
<label>Cloud Provider</label>
<description>Cloud Provider name for email and password.</description>
<options>
<option value=""></option>
<option value="MSmartHome">MSmartHome</option>
<option value="Midea Air">Midea Air</option>
<option value="NetHome Plus">NetHome Plus</option>
</options>
<limitToOptions>true</limitToOptions>
<default>NetHome Plus</default>
</parameter>
<parameter name="email" type="text" required="false">
<context>email</context>
<label>Email</label>
<description>Email for cloud account chosen in Cloud Provider.</description>
<default>nethome+us@mailinator.com</default>
</parameter>
<parameter name="password" type="text" required="false">
<context>password</context>
<label>Password</label>
<description>Password for cloud account chosen in Cloud Provider.</description>
<default>password1</default>
</parameter>
<parameter name="token" type="text" required="false">
<label>Token</label>
<description>Secret Token (length 128 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="key" type="text" required="false">
<label>Key</label>
<description>Secret Key (length 64 HEX) used for secure connection authentication used with devices v3 (if not
known, enter email, password for Cloud account to retrieve it).</description>
<advanced>true</advanced>
</parameter>
<parameter name="pollingTime" type="decimal" required="true" min="30" unit="s">
<label>Poll Frequency</label>
<description>Base device poll in seconds. Minimum is 30, default 60.</description>
<default>60</default>
</parameter>
<parameter name="energyPoll" type="decimal" required="true" min="0" unit="m">
<label>Energy Poll</label>
<description>Energy polling in minutes. default 0 ; (in case not supported).</description>
<default>0</default>
</parameter>
<parameter name="keyTokenUpdate" type="decimal" required="false" min="0" unit="h">
<label>Key Token Update</label>
<description>Update the Key and Token from the cloud in hours, min 24h, default 0 to disable.</description>
<default>0</default>
<advanced>true</advanced>
</parameter>
<parameter name="timeout" type="decimal" required="true" min="2" max="10" unit="s">
<label>Socket Timeout</label>
<description>Connecting socket timeout (seconds). Minimum is 2, maximum is 10 (4 is default).</description>
<default>4</default>
<advanced>true</advanced>
</parameter>
<parameter name="promptTone" type="boolean" required="true">
<label>Prompt Tone</label>
<description>After sending a command device will play "ding" tone when command is received and executed.</description>
<default>false</default>
</parameter>
<parameter name="version" type="decimal" required="true">
<label>AC Version</label>
<description>Version 3 requires Token, Key and Cloud provider. Version 2 doesn't.</description>
<default>3</default>
<advanced>true</advanced>
</parameter>
<parameter name="deviceType" type="text" required="true">
<label>Device Type</label>
<description>ac or a1(dehumidifier) discoverable</description>
<default>a1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>
@@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import java.lang.reflect.Field;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -39,13 +40,17 @@ import org.junit.jupiter.api.Test;
@NonNullByDefault
public class CloudTest {
private static <T> T typedMock(Class<T> clazz) {
return Objects.requireNonNull(mock(clazz));
}
@Test
public void testLogin() throws Exception {
// Mock HttpClient and ContentResponse
HttpClient mockHttpClient = mock(HttpClient.class);
Request mockRequest = mock(Request.class);
ContentResponse mockResponse = mock(ContentResponse.class);
HttpFields mockHeaders = mock(HttpFields.class);
HttpClient mockHttpClient = typedMock(HttpClient.class);
Request mockRequest = typedMock(Request.class);
ContentResponse mockResponse = typedMock(ContentResponse.class);
HttpFields mockHeaders = typedMock(HttpFields.class);
// Define behavior of HttpFields
when(mockHeaders.toString()).thenReturn("Mocked Headers");
@@ -95,10 +100,10 @@ public class CloudTest {
@Test
public void testLoginproxy() throws Exception {
// Mock HttpClient and ContentResponse
HttpClient mockHttpClient = mock(HttpClient.class);
Request mockRequest = mock(Request.class);
ContentResponse mockResponse = mock(ContentResponse.class);
HttpFields mockHeaders = mock(HttpFields.class);
HttpClient mockHttpClient = typedMock(HttpClient.class);
Request mockRequest = typedMock(Request.class);
ContentResponse mockResponse = typedMock(ContentResponse.class);
HttpFields mockHeaders = typedMock(HttpFields.class);
// Define behavior of HttpFields
when(mockHeaders.toString()).thenReturn("Mocked Headers");
@@ -148,7 +153,7 @@ public class CloudTest {
"https://mapp.appsmb.com", "xhdiwjnchekd4d512chdjx5d8e4c394D2D7S", "", "", "");
// Create the Cloud class
HttpClient mockHttpClient = mock(HttpClient.class);
HttpClient mockHttpClient = typedMock(HttpClient.class);
Cloud cloud = new Cloud("email", "password", provider, mockHttpClient);
// Set loginId using reflection so that the getLoginId() check doesn't trigger
@@ -171,10 +176,10 @@ public class CloudTest {
@Test
public void testGetLoginId() throws Exception {
// Mock HttpClient and dependent objects
HttpClient mockHttpClient = mock(HttpClient.class);
Request mockRequest = mock(Request.class);
ContentResponse mockResponse = mock(ContentResponse.class);
HttpFields mockHeaders = mock(HttpFields.class);
HttpClient mockHttpClient = typedMock(HttpClient.class);
Request mockRequest = typedMock(Request.class);
ContentResponse mockResponse = typedMock(ContentResponse.class);
HttpFields mockHeaders = typedMock(HttpFields.class);
// Define behavior for HttpFields
when(mockHeaders.toString()).thenReturn("Mocked Headers");
@@ -0,0 +1,60 @@
/*
* Copyright (c) 2010-2026 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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.binding.mideaac.internal.callbacks.Callback;
import org.openhab.binding.mideaac.internal.devices.CommandBase;
import org.openhab.binding.mideaac.internal.devices.a1.A1CommandBase;
/**
* The {@link ConnectionManagerTest} tests the methods in the ConnectionManager
* class with mock responses.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class ConnectionManagerTest {
@Test
public void testGetStatusUsesA1CommandBase() throws Exception {
// Arrange: construct a real instance with dummy values
ConnectionManager real = new ConnectionManager("127.0.0.1", 6444, 4, "", "", "", "", "", "000000000000", 3,
false, "ac");
ConnectionManager manager = spy(real);
// Stub sendCommand so no real network call is made
doNothing().when(manager).sendCommand(any(), any());
manager.setDeviceType("a1");
Callback callback = Objects.requireNonNull(mock(Callback.class));
// Act
manager.getStatus(callback);
// Assert
ArgumentCaptor<CommandBase> captor = ArgumentCaptor.forClass(CommandBase.class);
verify(manager).sendCommand(captor.capture(), eq(callback));
assertInstanceOf(A1CommandBase.class, captor.getValue());
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
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.devices.a1.A1StringCommands.A1FanSpeed;
import org.openhab.binding.mideaac.internal.devices.a1.A1StringCommands.A1OperationalMode;
/**
* The {@link A1CommandSetTest} tests the methods in the A1CommandSet class
* for correctness.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class A1CommandSetTest {
/**
* Dehumidifier Swing Mode test
*/
@Test
public void testHandleA1SwingMode() {
A1CommandSet commandSet = new A1CommandSet();
commandSet.setA1SwingMode(true);
byte[] frame = commandSet.getData();
// Device type byte
assertEquals((byte) 0xA1, frame[0x02]);
// Swing mode bit should be ON (bit 5 = 0x20)
assertEquals((byte) 0x20, frame[0x14] & 0x20);
}
/**
* Dehumidifier Power test
*/
@Test
public void testHandleA1Power() {
A1CommandSet commandSet = new A1CommandSet();
commandSet.setPowerState(true);
byte[] frame = commandSet.getData();
// Device type byte
assertEquals((byte) 0x01, frame[0x0b]);
// Power bit should be ON (bit 1 = 0x01)
assertEquals((byte) 0x01, frame[0x0b] & 0x01);
}
/**
* Dehumidifier Operational mode test
*/
@Test
public void testHandleA1OperationalMode() {
A1CommandSet commandSet = new A1CommandSet();
commandSet.setA1OperationalMode(A1OperationalMode.AUTO);
byte[] frame = commandSet.getData();
// Device type byte
assertEquals((byte) 0xA1, frame[0x02]);
// Low nibble should equal mode ID
assertEquals(3, frame[0x0C] & 0x0F);
// Getter should return the same value
assertEquals(3, commandSet.getA1OperationalMode());
}
/**
* Dehumidifier Fan Speed test
*/
@Test
public void testHandleA1FanSpeed() {
A1CommandSet commandSet = new A1CommandSet();
commandSet.setA1FanSpeed(A1FanSpeed.HIGH);
byte[] frame = commandSet.getData();
// Device type byte
assertEquals((byte) 0xA1, frame[0x02]);
// Getter should return the same value
assertEquals(80, frame[0x0d]);
}
@Test
public void testA1Capabilities() {
A1CommandSet commandSet = new A1CommandSet();
commandSet.getCapabilities();
byte[] frame = commandSet.getData();
// Device type byte
assertEquals((byte) 0xA1, frame[0x02]);
// Check the length of the data array
assertEquals(13, frame.length);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2010-2026 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.devices.a1;
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 A1ResponseTest} tests the methods in the A1Response class
* against an example A1response string.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class A1ResponseTest {
byte[] data = HexFormat.of().parseHex("C80104507F7F003700000000000000001E64000000003A67C2");
A1Response response = new A1Response(data);
/**
* Power State Test
*/
@Test
public void testGetPowerState() {
boolean actualPowerState = response.getPowerState();
assertEquals(true, actualPowerState);
}
/**
* Operational Mode Test
*/
@Test
public void testGetA1OperationalMode() {
A1StringCommands.A1OperationalMode mode = response.getA1OperationalMode();
assertEquals(A1StringCommands.A1OperationalMode.CLOTHES_DRY, mode);
}
/**
* Fan Speed Test
*/
@Test
public void testGetFanSpeed() {
A1StringCommands.A1FanSpeed fanSpeed = response.getA1FanSpeed();
assertEquals(A1StringCommands.A1FanSpeed.HIGH, fanSpeed);
}
/**
* Target Humidity Test
*/
@Test
public void testGetTargetHumidity() {
assertEquals(55, response.getMaximumHumidity());
}
/**
* Indoor Temperature Test
*/
@Test
public void testGetIndoorTemperature() {
assertEquals(25, response.getIndoorTemperature());
}
/**
* Room Humidity Test (actual humidity)
*/
@Test
public void testGetHumidity() {
assertEquals(30, response.getCurrentHumidity());
}
}
@@ -10,24 +10,24 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
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;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
/**
* The {@link CommandSetTest} tests the methods in the CommandSet class
* The {@link ACCommandSetTest} tests the methods in the ACCommandSet class
* for correctness.
*
* @author Bob Eckhoff - Initial contribution
*/
@NonNullByDefault
public class CommandSetTest {
public class ACCommandSetTest {
/**
* Power State Test
@@ -36,7 +36,7 @@ public class CommandSetTest {
public void setPowerStateTest() {
boolean status = true;
boolean status1 = true;
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.setPowerState(status);
assertEquals(status1, commandSet.getPowerState());
}
@@ -46,7 +46,7 @@ public class CommandSetTest {
*/
@Test
public void testsetTargetTemperature() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
// Device is limited to 0.5 degree C increments. Check rounding too
// Test case 1
@@ -78,13 +78,13 @@ public class CommandSetTest {
}
/**
* Swing Mode test
* AC Swing Mode test
*/
@Test
public void testHandleSwingMode() {
SwingMode mode = SwingMode.VERTICAL3;
int mode1 = 60;
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.setSwingMode(mode);
assertEquals(mode1, commandSet.getSwingMode());
}
@@ -96,19 +96,19 @@ public class CommandSetTest {
public void testHandleFanSpeedCommand() {
FanSpeed speed = FanSpeed.AUTO3;
int speed1 = 102;
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.setFanSpeed(speed);
assertEquals(speed1, commandSet.getFanSpeed());
}
/**
* Operational mode test
* AC Operational mode test
*/
@Test
public void testHandleOperationalMode() {
OperationalMode mode = OperationalMode.COOL;
int mode1 = 64;
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.setOperationalMode(mode);
assertEquals(mode1, commandSet.getOperationalMode());
}
@@ -118,7 +118,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOnTimer() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = true;
int hours = 3;
int minutes = 59;
@@ -135,7 +135,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOnTimer2() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = false;
int hours = 3;
int minutes = 60;
@@ -151,7 +151,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOnTimer3() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = true;
int hours = 0;
int minutes = 14;
@@ -167,7 +167,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOffTimer() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = true;
int hours = 3;
int minutes = 59;
@@ -184,7 +184,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOffTimer2() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = false;
int hours = 3;
int minutes = 60;
@@ -200,7 +200,7 @@ public class CommandSetTest {
*/
@Test
public void testHandleOffTimer3() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
boolean on = true;
int hours = 0;
int minutes = 14;
@@ -216,26 +216,27 @@ public class CommandSetTest {
*/
@Test
public void testSetScreenDisplayOff() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.setScreenDisplay(true);
byte[] frame = commandSet.getData();
// Check the modified bytes
assertEquals((byte) 0x20, commandSet.data[0x01]);
assertEquals((byte) 0x03, commandSet.data[0x09]);
assertEquals((byte) 0x41, commandSet.data[0x0a]);
assertEquals((byte) 0x61, commandSet.data[0x0b]);
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]);
assertEquals((byte) 0x20, frame[0x01]);
assertEquals((byte) 0x03, frame[0x09]);
assertEquals((byte) 0x41, frame[0x0a]);
assertEquals((byte) 0x61, frame[0x0b]);
assertEquals((byte) 0x00, frame[0x0c]);
assertEquals((byte) 0xff, frame[0x0d]);
assertEquals((byte) 0x02, frame[0x0e]);
assertEquals((byte) 0x00, frame[0x0f]);
assertEquals((byte) 0x02, frame[0x10]);
assertEquals((byte) 0x00, frame[0x11]);
assertEquals((byte) 0x00, frame[0x12]);
assertEquals((byte) 0x00, frame[0x13]);
assertEquals((byte) 0x00, frame[0x14]);
// Check the length of the data array
assertEquals(31, commandSet.data.length);
assertEquals(31, frame.length);
}
/**
@@ -244,26 +245,27 @@ public class CommandSetTest {
*/
@Test
public void testEnergyPoll() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.energyPoll();
byte[] frame = commandSet.getData();
// Check the modified bytes
assertEquals((byte) 0x20, commandSet.data[0x01]);
assertEquals((byte) 0x03, commandSet.data[0x09]);
assertEquals((byte) 0x41, commandSet.data[0x0a]);
assertEquals((byte) 0x21, commandSet.data[0x0b]);
assertEquals((byte) 0x01, commandSet.data[0x0c]);
assertEquals((byte) 0x44, commandSet.data[0x0d]);
assertEquals((byte) 0x00, commandSet.data[0x0e]);
assertEquals((byte) 0x00, commandSet.data[0x0f]);
assertEquals((byte) 0x00, commandSet.data[0x10]);
assertEquals((byte) 0x00, commandSet.data[0x11]);
assertEquals((byte) 0x00, commandSet.data[0x12]);
assertEquals((byte) 0x00, commandSet.data[0x13]);
assertEquals((byte) 0x00, commandSet.data[0x14]);
assertEquals((byte) 0x20, frame[0x01]);
assertEquals((byte) 0x03, frame[0x09]);
assertEquals((byte) 0x41, frame[0x0a]);
assertEquals((byte) 0x21, frame[0x0b]);
assertEquals((byte) 0x01, frame[0x0c]);
assertEquals((byte) 0x44, frame[0x0d]);
assertEquals((byte) 0x00, frame[0x0e]);
assertEquals((byte) 0x00, frame[0x0f]);
assertEquals((byte) 0x00, frame[0x10]);
assertEquals((byte) 0x00, frame[0x11]);
assertEquals((byte) 0x00, frame[0x12]);
assertEquals((byte) 0x00, frame[0x13]);
assertEquals((byte) 0x00, frame[0x14]);
// Check the length of the data array
assertEquals(31, commandSet.data.length);
assertEquals(31, frame.length);
}
/**
@@ -272,18 +274,19 @@ public class CommandSetTest {
*/
@Test
public void testCapabilities() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.getCapabilities();
byte[] frame = commandSet.getData();
// Check the modified bytes
assertEquals((byte) 0x0e, commandSet.data[0x01]);
assertEquals((byte) 0x03, commandSet.data[0x09]);
assertEquals((byte) 0xB5, commandSet.data[0x0a]);
assertEquals((byte) 0x01, commandSet.data[0x0b]);
assertEquals((byte) 0x00, commandSet.data[0x0c]);
assertEquals((byte) 0x0e, frame[0x01]);
assertEquals((byte) 0x03, frame[0x09]);
assertEquals((byte) 0xB5, frame[0x0a]);
assertEquals((byte) 0x01, frame[0x0b]);
assertEquals((byte) 0x00, frame[0x0c]);
// Check the length of the data array
assertEquals(13, commandSet.data.length);
assertEquals(13, frame.length);
}
/**
@@ -292,18 +295,19 @@ public class CommandSetTest {
*/
@Test
public void testAdditionalCapabilities() {
CommandSet commandSet = new CommandSet();
ACCommandSet commandSet = new ACCommandSet();
commandSet.getAdditionalCapabilities();
byte[] frame = commandSet.getData();
// Check the modified bytes
assertEquals((byte) 0x0f, commandSet.data[0x01]);
assertEquals((byte) 0x03, commandSet.data[0x09]);
assertEquals((byte) 0xB5, commandSet.data[0x0a]);
assertEquals((byte) 0x01, commandSet.data[0x0b]);
assertEquals((byte) 0x01, commandSet.data[0x0c]);
assertEquals((byte) 0x01, commandSet.data[0x0d]);
assertEquals((byte) 0x0f, frame[0x01]);
assertEquals((byte) 0x03, frame[0x09]);
assertEquals((byte) 0xB5, frame[0x0a]);
assertEquals((byte) 0x01, frame[0x0b]);
assertEquals((byte) 0x01, frame[0x0c]);
assertEquals((byte) 0x01, frame[0x0d]);
// Check the length of the data array
assertEquals(14, commandSet.data.length);
assertEquals(14, frame.length);
}
}
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
*/
@NonNullByDefault
public class HumidityResponseTest {
@org.jupnp.registry.event.Before
// From OH forum; Midea topic July 2025
byte[] data = HexFormat.of().parseHex("A01240640000003C00000000003600000000000000004234EA");
@@ -62,7 +61,7 @@ public class HumidityResponseTest {
*/
@Test
public void testOperationalMode() {
assertEquals(CommandBase.OperationalMode.COOL, response.getOperationalMode());
assertEquals(ACStringCommands.OperationalMode.COOL, response.getOperationalMode());
}
/**
@@ -70,7 +69,7 @@ public class HumidityResponseTest {
*/
@Test
public void testFanSpeed() {
assertEquals("HIGH", response.getFanSpeed().toString());
assertEquals(ACStringCommands.FanSpeed.HIGH4, response.getFanSpeed());
}
/**
@@ -78,6 +77,6 @@ public class HumidityResponseTest {
*/
@Test
public void testSwingMode() {
assertEquals(CommandBase.SwingMode.VERTICAL3, response.getSwingMode());
assertEquals(ACStringCommands.SwingMode.VERTICAL3, response.getSwingMode());
}
}
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -18,6 +18,10 @@ import java.util.HexFormat;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mideaac.internal.devices.Timer;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.FanSpeed;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.OperationalMode;
import org.openhab.binding.mideaac.internal.devices.ac.ACStringCommands.SwingMode;
/**
* The {@link ResponseTest} tests the methods in the Response class
@@ -27,7 +31,6 @@ import org.junit.jupiter.api.Test;
*/
@NonNullByDefault
public class ResponseTest {
@org.jupnp.registry.event.Before
byte[] data = HexFormat.of().parseHex("C00042668387123C00000460FF0C7000000000320000F9ECDB");
private int version = 3;
@@ -71,8 +74,8 @@ public class ResponseTest {
*/
@Test
public void testGetOperationalMode() {
CommandBase.OperationalMode mode = response.getOperationalMode();
assertEquals(CommandBase.OperationalMode.COOL, mode);
OperationalMode mode = response.getOperationalMode();
assertEquals(ACStringCommands.OperationalMode.COOL, mode);
}
/**
@@ -80,8 +83,8 @@ public class ResponseTest {
*/
@Test
public void testGetFanSpeed() {
CommandBase.FanSpeed fanSpeed = response.getFanSpeed();
assertEquals(CommandBase.FanSpeed.AUTO3, fanSpeed);
FanSpeed fanSpeed = response.getFanSpeed();
assertEquals(ACStringCommands.FanSpeed.AUTO3, fanSpeed);
}
/**
@@ -109,8 +112,8 @@ public class ResponseTest {
*/
@Test
public void testGetSwingMode() {
CommandBase.SwingMode swing = response.getSwingMode();
assertEquals(CommandBase.SwingMode.VERTICAL3, swing);
SwingMode swing = response.getSwingMode();
assertEquals(ACStringCommands.SwingMode.VERTICAL3, swing);
}
/**
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler;
package org.openhab.binding.mideaac.internal.devices.ac;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
*/
@NonNullByDefault
public class TemperatureResponseTest {
@org.jupnp.registry.event.Before
// From OH forum; Midea topic July 2025
byte[] data = HexFormat.of().parseHex("A10000000000000000000A0A0A64FF000031050000000000000000000000003CD5DB");
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mideaac.internal.handler.capabilities;
package org.openhab.binding.mideaac.internal.devices.capabilities;
import static org.junit.jupiter.api.Assertions.*;
@@ -19,7 +19,7 @@ import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.mideaac.internal.handler.capabilities.CapabilityParser.CapabilityId;
import org.openhab.binding.mideaac.internal.devices.capabilities.CapabilityParser.CapabilityId;
/**
* The {@link CapabilityParserTest} tests the methods in the
@@ -78,10 +78,10 @@ public class CapabilityParserTest {
}
@Test
void testParseWithUnknownCapability() {
// Arrange: Create a payload with an unknown capability
void testParseWithChildLockCapability() {
// Arrange: Create a payload with new Child Lock
byte[] payload = new byte[] { (byte) 0xB5, 0x01, // Header and count (1 capability)
0x50, 0x50, 0x01, 0x01 // Unknown capability (0x5050)
0x20, 0x02, 0x01, 0x01 // Unknown capability (0x0220)
};
CapabilityParser parser = new CapabilityParser();
@@ -89,9 +89,30 @@ public class CapabilityParserTest {
// Act: Parse the payload
parser.parse(payload);
// Assert: Ensure unknown capability is ignored
Map<CapabilityId, Map<String, Boolean>> capabilities = parser.getCapabilities();
assertTrue(capabilities.isEmpty());
// Assert Capability is marked as Child_Lock
assertEquals(CapabilityId.CHILD_LOCK, CapabilityId.fromId(0x0220));
}
@Test
void testFromIdDistinguishesUnknownValues() {
assertEquals(CapabilityId._UNKNOWN, CapabilityId.fromId(0x0040));
assertEquals(CapabilityId.UNMAPPED, CapabilityId.fromId(0xFFFF));
}
@Test
void testParseSkipsUnmappedCapability() {
byte[] payload = new byte[] { (byte) 0xB5, 0x02, // Header and count (2 capabilities)
0x34, 0x12, 0x01, 0x01, // Unmapped capability (0x1234)
0x20, 0x02, 0x01, 0x01, // Capability 2 (0x0220 = CHILD_LOCK, value = 1)
(byte) 0xDE, (byte) 0xDF // CRC Check (trailing bytes)
};
CapabilityParser parser = new CapabilityParser();
parser.parse(payload);
assertFalse(parser.getCapabilities().containsKey(CapabilityId.UNMAPPED));
assertTrue(parser.getCapabilities().containsKey(CapabilityId.CHILD_LOCK));
}
@Test
@@ -36,6 +36,7 @@ public class MideaACDiscoveryServiceTest {
byte[] data = HexFormat.of().parseHex(
"837000C8200F00005A5A0111B8007A80000000006B0925121D071814C0110800008A0000000000000000018000000000AF55C8897BEA338348DA7FC0B3EF1F1C889CD57C06462D83069558B66AF14A2D66353F52BAECA68AEB4C3948517F276F72D8A3AD4652EFA55466D58975AEB8D948842E20FBDCA6339558C848ECE09211F62B1D8BB9E5C25DBA7BF8E0CC4C77944BDFB3E16E33D88768CC4C3D0658937D0BB19369BF0317B24D3A4DE9E6A13106AFFBBE80328AEA7426CD6BA2AD8439F72B4EE2436CC634040CB976A92A53BCD5");
// Example reply from AC after discovery request; replace 63 with 31 after 6E65745F for a1
byte[] reply = HexFormat.of().parseHex(
"E600A8C02C19000030303030303050303030303030305131423838433239353634334243303030300B6E65745F61635F343342430000870002000000000000000000AC00ACAC00000000B88C295643BC150023082122000300000000000000000000000000000000000000000000000000000000000000000000");
String mSmartId = "", mSmartVersion = "", mSmartip = "", mSmartPort = "", mSmartSN = "", mSmartSSID = "",