Skip to content

Commit

Permalink
Merge branch 'main' into feature/b8-devicetype
Browse files Browse the repository at this point in the history
  • Loading branch information
rokam authored Jul 24, 2024
2 parents da41b0e + fbbc1c7 commit b77cf23
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 102 deletions.
31 changes: 31 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@
"--device-sn",
"${input:device-sn}"
]
},
{
"name": "Python Debugger: Set device attribute",
"type": "debugpy",
"request": "launch",
"module": "midealocal.cli",
"args": [
"setattr",
"-d",
"${input:host}",
"${input:attribute}",
"${input:value}",
"--attr-type",
"${input:attr-type}"
]
}
],
"inputs": [
Expand Down Expand Up @@ -99,6 +114,22 @@
"type": "promptString",
"password": true,
"description": "Enter cloud password."
},
{
"id": "attribute",
"type": "promptString",
"description": "Enter attribute name."
},
{
"id": "value",
"type": "promptString",
"description": "Enter attribute value."
},
{
"id": "attr-type",
"type": "pickString",
"options": ["bool", "int", "str"],
"description": "Choose attribute type."
}
]
}
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## [2.4.0](https://github.com/rokam/midea-local/compare/v2.3.0...v2.4.0) (2024-07-24)


### Features

* **cli:** set attribute from device ([#241](https://github.com/rokam/midea-local/issues/241)) ([6f0a109](https://github.com/rokam/midea-local/commit/6f0a10942c0b5defd3622c14bcd2795b34ec01a8))
* segregate connect/auth/refresh/enable device duties ([#233](https://github.com/rokam/midea-local/issues/233)) ([681bd79](https://github.com/rokam/midea-local/commit/681bd79f078deafe82cc4708f47c53c808dca064))

## [2.3.0](https://github.com/rokam/midea-local/compare/v2.2.0...v2.3.0) (2024-07-23)


### Features

* **message:** body parsers ([#235](https://github.com/rokam/midea-local/issues/235)) ([c636eee](https://github.com/rokam/midea-local/commit/c636eeef5128504c079704d023e2215896e3c770))

## [2.2.0](https://github.com/rokam/midea-local/compare/v2.1.1...v2.2.0) (2024-07-20)


Expand Down
65 changes: 61 additions & 4 deletions midealocal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from colorlog import ColoredFormatter

from midealocal.cloud import SUPPORTED_CLOUDS, MideaCloud, get_midea_cloud
from midealocal.device import ProtocolVersion
from midealocal.device import MideaDevice, ProtocolVersion
from midealocal.devices import device_selector
from midealocal.discover import discover
from midealocal.exceptions import ElementMissing
Expand Down Expand Up @@ -59,13 +59,13 @@ async def _get_keys(self, device_id: int) -> dict[int, dict[str, Any]]:
default_keys = await cloud.get_default_keys()
return {**cloud_keys, **default_keys}

async def discover(self) -> None:
async def discover(self) -> MideaDevice | None:
"""Discover device information."""
devices = discover(ip_address=self.namespace.host)

if len(devices) == 0:
_LOGGER.error("No devices found.")
return
return None

# Dump only basic device info from the base class
_LOGGER.info("Found %d devices.", len(devices))
Expand Down Expand Up @@ -93,9 +93,10 @@ async def discover(self) -> None:
_LOGGER.debug("Trying to connect with key: %s", key)
if dev.connect():
_LOGGER.info("Found device:\n%s", dev.attributes)
break
return dev

_LOGGER.debug("Unable to connect with key: %s", key)
return None

def message(self) -> None:
"""Load message into device."""
Expand Down Expand Up @@ -159,6 +160,33 @@ async def download(self) -> None:
lua = await cloud.download_lua(str(Path()), device_type, device_sn, model)
_LOGGER.info("Downloaded lua file: %s", lua)

async def set_attribute(self) -> None:
"""Set attribute for device."""
device = await self.discover()
if device is None:
return

_LOGGER.info(
"Setting attribute %s for %s [%s]",
self.namespace.attribute,
device.device_id,
device.device_type,
)
device.set_attribute(
self.namespace.attribute,
self._cast_attr_value(),
)
await asyncio.sleep(2)
device.refresh_status(True)
_LOGGER.info("New device status:\n%s", device.attributes)

def _cast_attr_value(self) -> int | bool | str:
if self.namespace.attr_type == "bool":
return self.namespace.value not in ["false", "False", "0", ""]
if self.namespace.attr_type == "int":
return int(self.namespace.value)
return str(self.namespace.value)

def run(self, namespace: Namespace) -> None:
"""Do setup logging, validate args and execute the desired function."""
self.namespace = namespace
Expand Down Expand Up @@ -306,6 +334,35 @@ def main() -> NoReturn:
)
download_parser.set_defaults(func=cli.download)

attribute_parser = subparsers.add_parser(
"setattr",
description="Set device attribute after discover.",
parents=[common_parser],
)
attribute_parser.add_argument(
"host",
help="Hostname or IP address of a single device.",
default=None,
)
attribute_parser.add_argument(
"attribute",
help="Attribute name.",
default=None,
)
attribute_parser.add_argument(
"value",
help="Attribute value.",
default=None,
)
attribute_parser.add_argument(
"--attr-type",
help="Attribute type.",
type=str,
default="int",
choices=["bool", "int", "str"],
)
attribute_parser.set_defaults(func=cli.set_attribute)

config = get_config_file_path()
namespace = parser.parse_args()
if config.exists():
Expand Down
106 changes: 53 additions & 53 deletions midealocal/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from enum import IntEnum, StrEnum
from typing import Any

from .exceptions import SocketException
from .exceptions import CannotConnect, SocketException
from .message import (
MessageApplianceResponse,
MessageQueryAppliance,
Expand Down Expand Up @@ -140,7 +140,7 @@ def __init__(
self._updates: list[Callable[[dict[str, Any]], None]] = []
self._unsupported_protocol: list[str] = []
self._is_run = False
self._available = True
self._available = False
self._appliance_query = True
self._refresh_interval = 30
self._heartbeat_interval = 10
Expand Down Expand Up @@ -190,67 +190,66 @@ def fetch_v2_message(msg: bytes) -> tuple[list, bytes]:
break
return result, msg

def connect(
self,
refresh_status: bool = True,
get_capabilities: bool = True,
) -> bool:
def _authenticate_refresh_capabilities(self) -> None:
if self._protocol == ProtocolVersion.V3:
self.authenticate()
self.refresh_status(wait_response=True)
self.get_capabilities()

def connect(self) -> bool:
"""Connect to device."""
connected = False
try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10)
_LOGGER.debug(
"[%s] Connecting to %s:%s",
self._device_id,
self._ip_address,
self._port,
)
self._socket.connect((self._ip_address, self._port))
_LOGGER.debug("[%s] Connected", self._device_id)
if self._protocol == ProtocolVersion.V3:
self.authenticate()
_LOGGER.debug("[%s] Authentication success", self._device_id)
if refresh_status:
self.refresh_status(wait_response=True)
if get_capabilities:
self.get_capabilities()
connected = True
except TimeoutError:
_LOGGER.debug("[%s] Connection timed out", self._device_id)
except OSError:
_LOGGER.debug("[%s] Connection error", self._device_id)
except AuthException:
_LOGGER.debug("[%s] Authentication failed", self._device_id)
except RefreshFailed:
_LOGGER.debug("[%s] Refresh status is timed out", self._device_id)
except Exception as e:
file = None
lineno = None
if e.__traceback__:
file = e.__traceback__.tb_frame.f_globals["__file__"] # pylint: disable=E1101
lineno = e.__traceback__.tb_lineno
_LOGGER.exception(
"[%s] Unknown error : %s, %s",
self._device_id,
file,
lineno,
)
for _ in range(3):
try:
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._socket.settimeout(10)
_LOGGER.debug(
"[%s] Connecting to %s:%s",
self._device_id,
self._ip_address,
self._port,
)
self._socket.connect((self._ip_address, self._port))
_LOGGER.debug("[%s] Connected", self._device_id)
connected = True
except TimeoutError:
_LOGGER.debug("[%s] Connection timed out", self._device_id)
except OSError:
_LOGGER.debug("[%s] Connection error", self._device_id)
except AuthException:
_LOGGER.debug("[%s] Authentication failed", self._device_id)
except RefreshFailed:
_LOGGER.debug("[%s] Refresh status is timed out", self._device_id)
except Exception as e:
file = None
lineno = None
if e.__traceback__:
file = e.__traceback__.tb_frame.f_globals["__file__"] # pylint: disable=E1101
lineno = e.__traceback__.tb_lineno
_LOGGER.exception(
"[%s] Unknown error : %s, %s",
self._device_id,
file,
lineno,
)
self.enable_device(connected)
return connected

def authenticate(self) -> None:
"""Authenticate to device. V3 only."""
request = self._security.encode_8370(self._token, MSGTYPE_HANDSHAKE_REQUEST)
_LOGGER.debug("[%s] Handshaking", self._device_id)
_LOGGER.debug("[%s] Authentication handshaking", self._device_id)
if not self._socket:
self.enable_device(False)
raise SocketException
self._socket.send(request)
response = self._socket.recv(512)
if len(response) < MIN_AUTH_RESPONSE:
self.enable_device(False)
raise AuthException
response = response[8:72]
self._security.tcp_key(response, self._key)
_LOGGER.debug("[%s] Authentication success", self._device_id)

def send_message(self, data: bytes) -> None:
"""Send message."""
Expand Down Expand Up @@ -462,6 +461,7 @@ def update_all(self, status: dict[str, Any]) -> None:

def enable_device(self, available: bool = True) -> None:
"""Enable device."""
_LOGGER.debug("[%s] Enabling device", self._device_id)
self._available = available
status = {"available": available}
self.update_all(status)
Expand Down Expand Up @@ -510,14 +510,14 @@ def _check_heartbeat(self, now: float) -> None:
def run(self) -> None:
"""Run loop."""
while self._is_run:
while self._socket is None:
if self.connect(refresh_status=True) is False:
self.close_socket()
time.sleep(5)
if not self.connect():
raise CannotConnect
if not self._socket:
raise SocketException
self._authenticate_refresh_capabilities()
timeout_counter = 0
start = time.time()
self._previous_refresh = start
self._previous_heartbeat = start
self._previous_refresh = self._previous_heartbeat = start
self._socket.settimeout(1)
while True:
try:
Expand Down
4 changes: 4 additions & 0 deletions midealocal/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ class CannotAuthenticate(MideaLocalError):
"""Exception raised when credentials are incorrect."""


class CannotConnect(MideaLocalError):
"""Exception raised when connection fails."""


class DataUnexpectedLength(MideaLocalError):
"""Exception raised when data length is less or more than expected."""

Expand Down
2 changes: 1 addition & 1 deletion midealocal/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Midea Local Version."""

__version__ = "2.2.0"
__version__ = "2.4.0"
Loading

0 comments on commit b77cf23

Please sign in to comment.