mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[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:
@@ -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"{
|
||||
|
||||
+25
-7
@@ -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";
|
||||
|
||||
+5
@@ -40,6 +40,11 @@ public class MideaACConfiguration {
|
||||
*/
|
||||
public String deviceId = "0";
|
||||
|
||||
/**
|
||||
* Device Type. Default Midea AC
|
||||
*/
|
||||
public String deviceType = "ac";
|
||||
|
||||
/**
|
||||
* Cloud Account email
|
||||
*/
|
||||
|
||||
+10
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+85
@@ -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
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -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);
|
||||
}
|
||||
+61
@@ -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
|
||||
}
|
||||
}
|
||||
+6
-2
@@ -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
|
||||
|
||||
+89
-36
@@ -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
|
||||
*
|
||||
|
||||
+138
@@ -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
-5
@@ -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);
|
||||
|
||||
+1
-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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
+35
@@ -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
|
||||
}
|
||||
}
|
||||
+316
@@ -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;
|
||||
}
|
||||
}
|
||||
+384
@@ -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;
|
||||
}
|
||||
}
|
||||
+235
@@ -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;
|
||||
}
|
||||
}
|
||||
+111
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
-88
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
-36
@@ -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
|
||||
*/
|
||||
+6
-106
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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
|
||||
*/
|
||||
+5
-5
@@ -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
|
||||
*/
|
||||
+9
-8
@@ -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);
|
||||
+1
-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 org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.slf4j.Logger;
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+7
-7
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-26
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ public interface DiscoveryHandler {
|
||||
/**
|
||||
* Discovery result
|
||||
*
|
||||
* @param discoveryResult AC device
|
||||
* @param discoveryResult Midea device
|
||||
*/
|
||||
public void discovered(DiscoveryResult discoveryResult);
|
||||
}
|
||||
|
||||
+28
-13
@@ -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) {
|
||||
|
||||
+355
@@ -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);
|
||||
}
|
||||
}
|
||||
+133
-414
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+217
@@ -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);
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -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>
|
||||
|
||||
+91
-3
@@ -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
|
||||
|
||||
+145
@@ -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>
|
||||
+178
-151
@@ -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>
|
||||
+18
-13
@@ -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");
|
||||
|
||||
+60
@@ -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());
|
||||
}
|
||||
}
|
||||
+110
@@ -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);
|
||||
}
|
||||
}
|
||||
+84
@@ -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());
|
||||
}
|
||||
}
|
||||
+68
-64
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
+4
-5
@@ -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());
|
||||
}
|
||||
}
|
||||
+11
-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.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);
|
||||
}
|
||||
|
||||
/**
|
||||
+1
-2
@@ -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");
|
||||
+29
-8
@@ -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
|
||||
+1
@@ -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 = "",
|
||||
|
||||
Reference in New Issue
Block a user