Skip to content

Commit

Permalink
Merge pull request #37 from mill1000/msmart-ng
Browse files Browse the repository at this point in the history
Merge msmart-ng development.
  • Loading branch information
mill1000 authored Aug 29, 2023
2 parents e1b8b87 + ea74efb commit fd768c0
Show file tree
Hide file tree
Showing 28 changed files with 2,516 additions and 2,305 deletions.
81 changes: 66 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,55 @@
# midea-msmart
# msmart-ng
A Python library for local control of Midea (and associated brands) smart air conditioners.

![Code Quality Badge](https://github.com/mill1000/midea-msmart/actions/workflows/checks.yml/badge.svg)
![PyPI](https://img.shields.io/pypi/v/msmart-ng?logo=PYPI)

## Gratitude
This project is a fork of [mac-zhou/midea-msmart](https://github.com/mac-zhou/midea-msmart), and builds upon the work of
* [dudanov/MideaUART](https://github.com/dudanov/MideaUART)
* [NeoAcheron/midea-ac-py](https://github.com/NeoAcheron/midea-ac-py)
* [andersonshatch/midea-ac-py](https://github.com/andersonshatch/midea-ac-py)
* [yitsushi/midea-air-condition](https://github.com/yitsushi/midea-air-condition)
## Features
#### Async Support
The device, LAN and cloud classes have all been rewritten to support async/await syntax.

## Installing
Use pip.
1. Remove old `msmart` package.
```shell
pip uninstall msmart
```python
from msmart.device import AirConditioner as AC

# Build device
device = AC(ip=DEVICE_IP, port=6444, device_id=int(DEVICE_ID))

# Get capabilities
await device.get_capabilities()

# Get current state
await device.refresh()
```

2. Install this fork `msmart-ng`.
#### Device Discovery
A new discovery module can discover and return ready-to-use device objects from the network. A single device can be discovered by IP or hostname with the `discover_single` method.

__Note: V3 devices are automatically authenticated via the Midea cloud.__

```python
from msmart.discover import Discover

# Discover all devices on the network
devices = await Discover.discover()

# Discover a single device by IP
device = await Discover.discover_single(DEVICE_IP)
```

#### Less Dependencies
Some external dependencies have been replaced with standard Python modules.

#### Code Quality
- The majority of the code is now type annotated.
- Code style and import sorting are enforced by autopep8 and isort via Github Actions.
- Unit tests are implemented and executed by Github Actions.
- A number of unused methods and modules have been removed from the code.
- Naming conventions follow PEP8.

## Installing
Use pip, remove the old `msmart` package if necessary, and install this fork `msmart-ng`.
```shell
pip uninstall msmart
pip install msmart-ng
```

Expand All @@ -29,15 +59,36 @@ Discover all devices on the LAN with the `midea-discover` command.
e.g.
```shell
$ midea-discover
INFO:msmart.cli:msmart version: 2023.8.0 Currently only supports ac devices, only support MSmartHome and 美的美居 APP.
INFO:msmart.cli:*** Found a device: {'name': 'net_ac_F7B4', 'ssid': 'net_ac_F7B4', 'ip': '10.100.1.140', 'port': 6444, 'id': 15393162840672, 'version': 2, 'token': None, 'key': None, 'type': 'ac', 'sn': '000P0000000Q1F0C9D153F7B40000', 'model': '00Q1F', 'support': True, 'run_test': True}
INFO:msmart.cli:msmart version: 2023.8.1
INFO:msmart.cli:Only supports AC devices. Only supports MSmartHome and 美的美居.
...
INFO:msmart.cli:Found 2 devices.
INFO:msmart.cli:Found device:
{'ip': '10.100.1.140', 'port': 6444, 'id': 15393162840672, 'online': True, 'supported': True, 'type': <DeviceType.AIR_CONDITIONER: 172>, 'name': 'net_ac_F7B4', 'sn': '000000P0000000Q1F0C9D153F7B40000', 'key': None, 'token': None}
INFO:msmart.cli:Found device:
{'ip': '10.100.1.239', 'port': 6444, 'id': 147334558165565, 'online': True, 'supported': True, 'type': <DeviceType.AIR_CONDITIONER: 172>, 'name': 'net_ac_63BA', 'sn': '000000P0000000Q1B88C29C963BA0000', 'key': '3a13f53f335042f9ae5fd266a6bd779459ed7ee7e09842f1a0e03c024890fc96', 'token': '56a72747cef14d55e17e69b46cd98deae80607e318a7b55cb86bb98974501034c657e39e4a4032e3c8cc9a3cab00fd3ec0bab4a816a57f68b8038977406b7431'}
```
Check the output to ensure the type is 0xAC and the `supported` property is True.

Save the device ID, IP address, and port. Version 3 devices will also require the `token` and `key` fields to control the device.


#### Note: V1 Device Owners
Users with V1 devices will see the following error:
```
ERROR:msmart.discover:V1 device not supported yet.
```
I don't have any V1 devices to test with so please create an issue with the output of `midea-discover -d`.

### Home Assistant
Use [this fork](https://github.com/mill1000/midea-ac-py) of midea-ac-py to control devices from Home Assistant.

### Python
See the included [example](example.py) for controlling devices from a script.

## Gratitude
This project is a fork of [mac-zhou/midea-msmart](https://github.com/mac-zhou/midea-msmart), and builds upon the work of
* [dudanov/MideaUART](https://github.com/dudanov/MideaUART)
* [NeoAcheron/midea-ac-py](https://github.com/NeoAcheron/midea-ac-py)
* [andersonshatch/midea-ac-py](https://github.com/andersonshatch/midea-ac-py)
* [yitsushi/midea-air-condition](https://github.com/yitsushi/midea-air-condition)
140 changes: 84 additions & 56 deletions example.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,85 @@
import asyncio
import logging
import time

from msmart.device import air_conditioning as ac
from msmart.device.base import device

logging.basicConfig(level=logging.DEBUG)

# first take device's ip and id, port is generally 6444
# pip3 install msmart; midea-discover
device = ac('YOUR_AC_IP', int('YOUR_AC_ID'), 6444)
# If the device is using protocol 3 (aka 8370)
# you must authenticate with device's k1 and token.
# adb logcat | grep doKeyAgree
device.authenticate('YOUR_AC_K1', 'YOUR_AC_TOKEN')
# Refresh the object with the actual state by querying it
device.get_capabilities()
device.refresh()
print({
'id': device.id,
'name': device.ip,
'power_state': device.power_state,
'prompt_tone': device.prompt_tone,
'target_temperature': device.target_temperature,
'operational_mode': device.operational_mode,
'fan_speed': device.fan_speed,
'swing_mode': device.swing_mode,
'eco_mode': device.eco_mode,
'turbo_mode': device.turbo_mode,
'fahrenheit': device.fahrenheit,
'indoor_temperature': device.indoor_temperature,
'outdoor_temperature': device.outdoor_temperature
})

# Set the state of the device and
device.prompt_tone = True
device.power_state = True
device.prompt_tone = False
device.target_temperature = 25
device.operational_mode = ac.operational_mode_enum.cool
time.sleep(1)
# commit the changes with apply()
device.apply()
print({
'id': device.id,
'name': device.ip,
'power_state': device.power_state,
'prompt_tone': device.prompt_tone,
'target_temperature': device.target_temperature,
'operational_mode': device.operational_mode,
'fan_speed': device.fan_speed,
'swing_mode': device.swing_mode,
'eco_mode': device.eco_mode,
'turbo_mode': device.turbo_mode,
'indoor_temperature': device.indoor_temperature,
'outdoor_temperature': device.outdoor_temperature
})

from msmart.device import AirConditioner as AC
from msmart.discover import Discover

logging.basicConfig(level=logging.INFO)

DEVICE_IP = "YOUR_DEVICE_IP"
DEVICE_PORT = 6444
DEVICE_ID = "YOUR_AC_ID"

# For V3 devices
DEVICE_TOKEN = None # "YOUR_DEVICE_TOKEN"
DEVICE_KEY = None # "YOUR_DEVICE_KEY"


async def main():

# There are 2 ways to connect

# Discover.discover_single can automatically construct a device from IP or hostname
# - V3 devices will be automatically authenticated
# - The Midea cloud will be accessed for V3 devices to fetch the token and key
# device = await Discover.discover_single(DEVICE_IP)

# Manually construct the device
# - See midea-discover to read ID, token and key
device = AC(ip=DEVICE_IP, port=6444, device_id=int(DEVICE_ID))
if DEVICE_TOKEN and DEVICE_KEY:
await device.authenticate(DEVICE_TOKEN, DEVICE_KEY)

# Get device capabilities
await device.get_capabilities()

# Refresh the state
await device.refresh()

print({
'id': device.id,
'ip': device.ip,
"online": device.online,
"supported": device.supported,
'power_state': device.power_state,
'beep': device.beep,
'target_temperature': device.target_temperature,
'operational_mode': device.operational_mode,
'fan_speed': device.fan_speed,
'swing_mode': device.swing_mode,
'eco_mode': device.eco_mode,
'turbo_mode': device.turbo_mode,
'fahrenheit': device.fahrenheit,
'indoor_temperature': device.indoor_temperature,
'outdoor_temperature': device.outdoor_temperature
})

await asyncio.sleep(1)

# Change some device properties and apply them
device.power_state = True
device.beep = False
device.target_temperature = 25
device.operational_mode = AC.OperationalMode.COOL
await device.apply()

print({
'id': device.id,
'ip': device.ip,
"online": device.online,
"supported": device.supported,
'power_state': device.power_state,
'beep': device.beep,
'target_temperature': device.target_temperature,
'operational_mode': device.operational_mode,
'fan_speed': device.fan_speed,
'swing_mode': device.swing_mode,
'eco_mode': device.eco_mode,
'turbo_mode': device.turbo_mode,
'fahrenheit': device.fahrenheit,
'indoor_temperature': device.indoor_temperature,
'outdoor_temperature': device.outdoor_temperature
})

if __name__ == "__main__":
asyncio.run(main())
60 changes: 23 additions & 37 deletions msmart/base_command.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import logging
from abc import ABC, abstractmethod
from collections import namedtuple

import msmart.crc8 as crc8
from msmart.const import FRAME_TYPE
from msmart.const import DeviceType, FrameType

_LOGGER = logging.getLogger(__name__)


class command(ABC):
class Command(ABC):
_message_id = 0

def __init__(self, device_type=0xAC, FRAME_TYPE=FRAME_TYPE.Request):
self.device_type = device_type
self.FRAME_TYPE = FRAME_TYPE
self.protocol_version = 0
def __init__(self, device_type: DeviceType, frame_type: FrameType) -> None:
self._device_type = device_type
self._frame_type = frame_type
self._protocol_version = 0

def pack(self):
def tobytes(self) -> bytes:
# Create payload with message id
payload = self.payload + bytes([self.message_id])
payload = self.payload + bytes([self._next_message_id()])

# Create payload with CRC appended
payload_crc = payload + bytes([crc8.calculate(payload)])
Expand All @@ -27,57 +26,44 @@ def pack(self):
length = 10 + len(payload_crc)

# Build frame header
header = bytearray([
header = bytes([
# Start byte
0xAA,
# Length of payload and header
length,
# Device/appliance type
self.device_type,
self._device_type,
# Frame checksum (sync?)
self.device_type ^ length,
self._device_type ^ length,
# Reserved
0x00, 0x00,
# Frame ID
0x00,
# Frame protocol version
0x00,
# Device protocol version
self.protocol_version,
self._protocol_version,
# Frame type
self.FRAME_TYPE
self._frame_type
])

# Build frame from header and payload with CRC
frame = header + payload_crc
frame = bytearray(header + payload_crc)

# Calculate total frame checksum
frame.append(command.checksum(frame[1:]))
frame.append(Command.checksum(frame[1:]))

_LOGGER.debug("Frame data: %s", frame.hex())
return bytes(frame)

return frame

@staticmethod
def checksum(frame):
return (~sum(frame) + 1) & 0xFF

@property
def message_id(self):
command._message_id += 1
return command._message_id & 0xFF
def _next_message_id(self) -> int:
Command._message_id += 1
return Command._message_id & 0xFF

@property
@abstractmethod
def payload(self):
def payload(self) -> bytes:
return bytes()


class set_customize_command(command):
def __init__(self, device_type, FRAME_TYPE, customize_cmd,):
super().__init__(device_type, FRAME_TYPE=FRAME_TYPE.Request)
self.customize_cmd = customize_cmd

@property
def payload(self):
return bytearray.fromhex(self.customize_cmd)
@classmethod
def checksum(cls, frame: bytes) -> int:
return (~sum(frame) + 1) & 0xFF
Loading

0 comments on commit fd768c0

Please sign in to comment.