Skip to content

Commit eb10409

Browse files
authored
Merge pull request #233 from mill1000/feature/cc_device_support
Add support for CC device type
2 parents 95004e7 + 3300be3 commit eb10409

22 files changed

+6254
-66
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ This library supports air conditioners from Midea and several associated brands
1313
* Toshiba AC NA (com.midea.toshiba)
1414
* 美的美居 (com.midea.ai.appliances)
1515

16-
__Note: Only air conditioners (type 0xAC) are supported. See the [usage](#usage) section for how to check compatibility.__
16+
__Note: Only air conditioners (type 0xAC and 0xCC) are supported. See the [usage](#usage) section for how to check compatibility.__
1717

1818
## Note On Cloud Usage
1919
This library (and its Home Assistant integration [midea-ac-py](https://github.com/mill1000/midea-ac-py)) works locally. No internet connection is required to control your device.
@@ -118,6 +118,7 @@ $ msmart-ng query <HOST>
118118
Add `--capabilities` to list available capabilities of the device.
119119

120120
**Note:** Version 3 devices need to specify either the `--auto` argument or the `--token`, `--key` and `--id` arguments to make a connection.
121+
**Note:** For CC devices, either the `--auto` argument or the `--device_type` argument must be specified.
121122

122123
#### Control
123124
Control a device with the `msmart-ng control` subcommand. The command takes key-value pairs of settings to control.
@@ -133,6 +134,7 @@ $ msmart-ng control <HOST> operational_mode=cool target_temperature=20.5 fan_spe
133134
```
134135

135136
**Note:** Version 3 devices need to specify either the `--auto` argument or the `--token`, `--key` and `--id` arguments to make a connection.
137+
**Note:** For CC devices, either the `--auto` argument or the `--device_type` argument must be specified.
136138

137139
### Home Assistant
138140
To control your Midea AC units via Home Assistant, use this [midea-ac-py](https://github.com/mill1000/midea-ac-py) fork.

msmart/base_device.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1+
from __future__ import annotations
2+
13
import logging
24
import time
3-
from typing import Optional
5+
from typing import TYPE_CHECKING, Optional, Union
46

57
from msmart.const import DeviceType
68
from msmart.frame import Frame
79
from msmart.lan import LAN, AuthenticationError, Key, ProtocolError, Token
810

911
_LOGGER = logging.getLogger(__name__)
1012

13+
if TYPE_CHECKING:
14+
# Conditionally import device classes for type hints
15+
from msmart.device import AirConditioner, CommercialAirConditioner
16+
1117

1218
class Device():
1319

@@ -136,3 +142,18 @@ def to_dict(self) -> dict:
136142

137143
def __str__(self) -> str:
138144
return str(self.to_dict())
145+
146+
@classmethod
147+
def construct(cls, *, type: DeviceType, **kwargs) -> Union[AirConditioner, CommercialAirConditioner, Device]:
148+
"""Construct a device object based on the provided device type."""
149+
150+
if type == DeviceType.AIR_CONDITIONER:
151+
from msmart.device import AirConditioner
152+
return AirConditioner(**kwargs)
153+
154+
if type == DeviceType.COMMERCIAL_AC:
155+
from msmart.device import CommercialAirConditioner
156+
return CommercialAirConditioner(**kwargs)
157+
158+
# Unknown type return generic device
159+
return Device(device_type=type, **kwargs)

msmart/cli.py

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
import ast
33
import asyncio
44
import logging
5-
from typing import NoReturn
5+
from typing import NoReturn, Union
66

77
from msmart import __version__
8+
from msmart.base_device import Device
89
from msmart.cloud import CloudError, NetHomePlusCloud, SmartHomeCloud
9-
from msmart.const import DEFAULT_CLOUD_REGION
10+
from msmart.const import DEFAULT_CLOUD_REGION, DeviceType
1011
from msmart.device import AirConditioner as AC
12+
from msmart.device import CommercialAirConditioner as CC
1113
from msmart.discover import Discover
1214
from msmart.lan import AuthenticationError
1315
from msmart.utils import MideaIntEnum
@@ -19,6 +21,11 @@
1921

2022
DEFAULT_CLOUD_ACCOUNT, DEFAULT_CLOUD_PASSWORD = CLOUD_CREDENTIALS[DEFAULT_CLOUD_REGION]
2123

24+
DEVICE_TYPES = {
25+
"AC": DeviceType.AIR_CONDITIONER,
26+
"CC": DeviceType.COMMERCIAL_AC,
27+
}
28+
2229

2330
async def _discover(args) -> None:
2431
"""Discover Midea devices and print configuration information."""
@@ -43,16 +50,18 @@ async def _discover(args) -> None:
4350

4451
if isinstance(device, AC):
4552
device = super(AC, device)
53+
elif isinstance(device, CC):
54+
device = super(CC, device)
4655

4756
_LOGGER.info("Found device:\n%s", device.to_dict())
4857

4958

50-
async def _connect(args) -> AC:
59+
async def _connect(args) -> Union[AC, CC]:
5160
"""Connect to a device directly or via discovery."""
5261

53-
if args.auto and (args.token or args.key or args.device_id):
62+
if args.auto and (args.token or args.key or args.device_id or args.device_type):
5463
_LOGGER.warning(
55-
"--token, --key and --id are ignored with --auto option.")
64+
"--token, --key, --id and --device_type are ignored with --auto option.")
5665

5766
if args.auto:
5867
# Use discovery to automatically connect and authenticate with device
@@ -63,16 +72,22 @@ async def _connect(args) -> AC:
6372
_LOGGER.error("Device not found.")
6473
exit(1)
6574
else:
66-
# Manually create device and authenticate
67-
device = AC(ip=args.host, port=6444, device_id=args.device_id)
75+
device = Device.construct(
76+
type=DEVICE_TYPES.get(
77+
args.device_type, DeviceType.AIR_CONDITIONER),
78+
ip=args.host,
79+
port=6444,
80+
device_id=args.device_id
81+
)
82+
6883
if args.token and args.key:
6984
try:
7085
await device.authenticate(args.token, args.key)
7186
except AuthenticationError as e:
7287
_LOGGER.error("Authentication failed. Error: %s", e)
7388
exit(1)
7489

75-
if not isinstance(device, AC):
90+
if not isinstance(device, (AC, CC)):
7691
_LOGGER.error("Device is not supported.")
7792
exit(1)
7893

@@ -93,24 +108,15 @@ async def _query(args) -> None:
93108
_LOGGER.error("Device is not online.")
94109
exit(1)
95110

96-
# TODO method to get caps in string format
97-
_LOGGER.info("%s", str({
98-
"supported_modes": device.supported_operation_modes,
99-
"supported_swing_modes": device.supported_swing_modes,
100-
"supported_fan_speeds": device.supported_fan_speeds,
101-
"supports_custom_fan_speed": device.supports_custom_fan_speed,
102-
"supports_eco": device.supports_eco,
103-
"supports_turbo": device.supports_turbo,
104-
"supports_freeze_protection": device.supports_freeze_protection,
105-
"supports_display_control": device.supports_display_control,
106-
"supports_filter_reminder": device.supports_filter_reminder,
107-
"max_target_temperature": device.max_target_temperature,
108-
"min_target_temperature": device.min_target_temperature,
109-
}))
111+
_LOGGER.info("%s", device.capabilities_dict())
110112
else:
111113
# Enable energy requests
112114
if args.energy:
113-
device._request_energy_usage = True
115+
if hasattr(device, "enable_energy_usage_requests"):
116+
device.enable_energy_usage_requests = True
117+
else:
118+
_LOGGER.error("Device does not support energy data.")
119+
exit(1)
114120

115121
_LOGGER.info("Querying device state.")
116122
await device.refresh()
@@ -136,22 +142,34 @@ def convert(v, t):
136142
v, t.__qualname__)
137143
exit(1)
138144

145+
# Create a dummy device instance for property validation
146+
device_type = DEVICE_TYPES.get(
147+
args.device_type, DeviceType.AIR_CONDITIONER)
148+
dummy_device = Device.construct(
149+
type=device_type,
150+
ip="0.0.0.0",
151+
device_id=0,
152+
port=6444
153+
)
154+
device_class = type(dummy_device)
155+
139156
# Parse each setting, checking if the property exists and the supplied value is valid
140157
new_properties = {}
141158
for name, value in (s.split("=") for s in args.settings):
142-
# Check if property exists
143-
prop = getattr(AC, name, None)
159+
# Check if property exists on the device class
160+
prop = getattr(device_class, name, None)
144161
if prop is None or not isinstance(prop, property):
145-
_LOGGER.error("'%s' is not a valid device property.", name)
162+
_LOGGER.error(
163+
"'%s' is not a valid property for device type %02X.", name, device_type)
146164
exit(1)
147165

148-
# Check if property has a setter, with special handling for the display
149-
if name != KEY_DISPLAY_ON and prop.fset is None:
166+
# Check if property has a setter, with special handling for the AC display
167+
if prop.fset is None and not (device_class == AC and name == KEY_DISPLAY_ON):
150168
_LOGGER.error("'%s' property is not writable.", name)
151169
exit(1)
152170

153171
# Get the default value of the property and its type
154-
attr_value = getattr(AC("0.0.0.0", 0, 0), name)
172+
attr_value = getattr(dummy_device, name)
155173
attr_type = type(attr_value)
156174

157175
if isinstance(attr_value, MideaIntEnum):
@@ -166,8 +184,9 @@ def convert(v, t):
166184
try:
167185
new_properties[name] = attr_type(value)
168186
except ValueError:
169-
# Allow raw integers for AC.FanSpeed
170-
if attr_type == AC.FanSpeed:
187+
# Allow raw integers for FanSpeed if device supports custom fan speeds
188+
if (attr_type == device_class.FanSpeed and
189+
getattr(dummy_device, 'supports_custom_fan_speed', False)):
171190
new_properties[name] = int(value)
172191
else:
173192
_LOGGER.error("Value '%d' is not a valid %s",
@@ -201,8 +220,9 @@ def convert(v, t):
201220
_LOGGER.info("Querying device capabilities.")
202221
await device.get_capabilities()
203222

204-
# Handle display which is unique
205-
if (display := new_properties.pop(KEY_DISPLAY_ON, None)) is not None:
223+
# Handle display which is unique to AC devices
224+
if ((display := new_properties.pop(KEY_DISPLAY_ON, None)) is not None
225+
and isinstance(device, AC)):
206226
if display != device.display_on:
207227
_LOGGER.info("Setting '%s' to %s.", KEY_DISPLAY_ON, display)
208228
await device.toggle_display()
@@ -233,6 +253,8 @@ async def _download(args) -> None:
233253

234254
if isinstance(device, AC):
235255
device = super(AC, device)
256+
elif isinstance(device, CC):
257+
device = super(CC, device)
236258

237259
_LOGGER.info("Found device:\n%s", device.to_dict())
238260

@@ -338,8 +360,11 @@ def main() -> NoReturn:
338360
query_parser.add_argument("--capabilities",
339361
help="Query device capabilities instead of state.",
340362
action="store_true")
363+
query_parser.add_argument("--device_type",
364+
help="Type of device.",
365+
choices=DEVICE_TYPES.keys())
341366
query_parser.add_argument("--auto",
342-
help="Automatically authenticate V3 devices.",
367+
help="Automatically identify, connect and authenticate with the device.",
343368
action="store_true")
344369
query_parser.add_argument("--id",
345370
help="Device ID for V3 devices.",
@@ -364,8 +389,11 @@ def main() -> NoReturn:
364389
control_parser.add_argument("--capabilities",
365390
help="Query device capabilities before sending commands.",
366391
action="store_true")
392+
control_parser.add_argument("--device_type",
393+
help="Type of device.",
394+
choices=DEVICE_TYPES.keys())
367395
control_parser.add_argument("--auto",
368-
help="Automatically authenticate V3 devices.",
396+
help="Automatically identify, connect and authenticate with the device.",
369397
action="store_true")
370398
control_parser.add_argument("--id",
371399
help="Device ID for V3 devices.",

msmart/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
class DeviceType(IntEnum):
2929
AIR_CONDITIONER = 0xAC
30+
COMMERCIAL_AC = 0xCC
3031

3132

3233
class FrameType(IntEnum):

msmart/device/AC/device.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,35 @@ def to_dict(self) -> dict:
10641064
"error_code": self.error_code,
10651065
}}
10661066

1067+
def capabilities_dict(self) -> dict:
1068+
return {
1069+
"supported_modes": self.supported_operation_modes,
1070+
"supported_swing_modes": self.supported_swing_modes,
1071+
"supports_horizontal_swing_angle": self.supports_horizontal_swing_angle,
1072+
"supports_vertical_swing_angle": self.supports_vertical_swing_angle,
1073+
"supported_fan_speeds": self.supported_fan_speeds,
1074+
"supports_custom_fan_speed": self.supports_custom_fan_speed,
1075+
"min_target_temperature": self.min_target_temperature,
1076+
"max_target_temperature": self.max_target_temperature,
1077+
"supports_humidity": self.supports_humidity,
1078+
"supports_target_humidity": self.supports_target_humidity,
1079+
"supports_eco": self.supports_eco,
1080+
"supports_ieco": self.supports_ieco,
1081+
"supports_turbo": self.supports_turbo,
1082+
"supports_freeze_protection": self.supports_freeze_protection,
1083+
"supports_breeze_away": self.supports_breeze_away,
1084+
"supports_breeze_mild": self.supports_breeze_mild,
1085+
"supports_breezeless": self.supports_breezeless,
1086+
"supports_cascade": self.supports_cascade,
1087+
"supports_flash_cool": self.supports_flash_cool,
1088+
"supports_self_clean": self.supports_self_clean,
1089+
"supports_purifier": self.supports_purifier,
1090+
"supported_aux_modes": self.supported_aux_modes,
1091+
"supported_rate_selects": self.supported_rate_selects,
1092+
"supports_display_control": self.supports_display_control,
1093+
"supports_filter_reminder": self.supports_filter_reminder,
1094+
}
1095+
10671096
# Deprecated methods and properties
10681097
@property
10691098
@deprecated("supports_eco")

msmart/device/CC/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)