Skip to content

Commit

Permalink
Add diagnostics and error checking (#12)
Browse files Browse the repository at this point in the history
* Just a test PR

* Add diagnostics.py
Restructure fan config constants
Fix error handling on unsupported fan

* Update Readme
  • Loading branch information
JeffSteinbok authored Jun 28, 2023
1 parent bd8f0fb commit dec13ef
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 60 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ _I plan to add HACS support in the near future, but for now, this is manually in
Copy the `dreo` directory into your `/config/custom_components` directory, then restart your HomeAssistant Core.

### Debugging
Idealy, use the Diagnostics feature in HomeAssistant to get diagnostics from the integration. Sensitive info should be redacted automatically.

This integration logs to two loggers as shown below. To get verbose logs, change the log level. Please have logs handy if you're reaching out for help.

```
Expand All @@ -48,4 +50,4 @@ This is my first HA plugin and a bit slow going; bunch of stuff left to do:
* Tests
* Config Flow
* Temperature Sensor
* Creating a Device in HA for the fan, inclusive of logo.
* Creating a Device in HA for the fan, inclusive of logo
49 changes: 49 additions & 0 deletions custom_components/dreo/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Diagnostics support for VeSync."""
from __future__ import annotations

from typing import Any

from .pydreo import PyDreo, PyDreoBaseDevice

from homeassistant.components.diagnostics import REDACTED
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry

from .const import *

KEYS_TO_REDACT = {"sn", "_sn", "wifi_ssid", "module_hardware_mac"}


async def async_get_config_entry_diagnostics(hass: HomeAssistant, entry: ConfigEntry) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
manager: PyDreo = hass.data[DOMAIN][DREO_MANAGER]

data = {
DOMAIN: {
"fan_count": len(manager.fans),
"raw_devicelist": _redact_values(manager.raw_response)
},
"devices": {
"fans": [_redact_values(device.__dict__) for device in manager.fans],
},
}

return data

def _redact_values(data: dict) -> dict:
"""Rebuild and redact values of a dictionary, recursively"""

new_data = {}

for key, item in data.items():
if key not in KEYS_TO_REDACT:
if isinstance(item, dict):
new_data[key] = _redact_values(item)
else:
new_data[key] = item
else:
new_data[key] = REDACTED

return new_data
57 changes: 17 additions & 40 deletions custom_components/dreo/pydreo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import threading

"""Dreo API Device Libary."""

import sys
import logging
import time
import json
Expand All @@ -16,7 +16,7 @@
from itertools import chain
from typing import Tuple, Optional

from .pydreobasedevice import PyDreoBaseDevice
from .pydreobasedevice import PyDreoBaseDevice, UnknownModelError
from .pydreofan import PyDreoFan
from .helpers import Helpers

Expand All @@ -31,27 +31,6 @@

API_RATE_LIMIT: int = 30

DEFAULT_ENER_UP_INT: int = 21600

def object_factory(dev_type, config, dreo : "PyDreo") -> Tuple[str, PyDreoBaseDevice]:
"""Get device type and instantiate class."""
def fans(dev_type, config, manager):
fan_cls = fan_mods.fan_modules[dev_type] # noqa: F405
fan_obj = getattr(fan_mods, fan_cls)
return 'fans', fan_obj(config, manager)

if dev_type in fan_mods.fan_modules: # type: ignore # noqa: F405
type_str, dev_obj = fans(dev_type, config, dreo)
else:
_LOGGER.debug('Unknown device named %s model %s',
config.get('deviceName', ''),
config.get('deviceType', '')
)
type_str = 'unknown'
dev_obj = None
return type_str, dev_obj


class PyDreo: # pylint: disable=function-redefined
"""Dreo API functions."""

Expand Down Expand Up @@ -129,10 +108,10 @@ def set_dev_id(devices: list) -> list:
devices) if j not in dev_rem]
return devices

def process_devices(self, dev_list: list) -> bool:
def _process_devices(self, dev_list: list) -> bool:
"""Instantiate Device Objects."""
devices = self.set_dev_id(dev_list)
_LOGGER.debug('pydreo.process_devices')
_LOGGER.debug('pydreo._process_devices')
num_devices = 0
for _, v in self._dev_list.items():
if isinstance(v, list):
Expand All @@ -152,24 +131,16 @@ def process_devices(self, dev_list: list) -> bool:

#detail_keys = ['deviceType', 'deviceName', 'deviceStatus']
for dev in devices:
# For now, let's keep this simple and just support fans...
# Get the state of the device...seperate API call...boo
try:
#device_str, device_obj = object_factory(dev_type, dev, self)
#device_list = getattr(self, device_str)
#device_list.append(device_obj)
print(dev)
# For now, let's keep this simple and just support fans...

# Get the state of the device...seperate API call...boo


deviceFan = PyDreoFan(dev, self)
self.load_device_state(deviceFan)
self.fans.append(deviceFan)
self._deviceListBySn[deviceFan.sn] = deviceFan
except AttributeError as err:
_LOGGER.debug('Error - %s', err)
_LOGGER.debug('%s device not added', dev_type)
continue
except UnknownModelError as ume:
_LOGGER.warning("Unknown fan model: %s", ume)
_LOGGER.debug(dev)

return True

Expand All @@ -181,10 +152,14 @@ def load_devices(self) -> bool:
proc_return = False
response, _ = self.call_dreo_api(DREO_API_DEVICELIST)

# Stash the raw response for use by the diagnostics system, so we don't have to pull
# logs
self.raw_response = response

if response and Helpers.code_check(response):
if DATA_KEY in response and LIST_KEY in response[DATA_KEY]:
device_list = response[DATA_KEY][LIST_KEY]
proc_return = self.process_devices(device_list)
proc_return = self._process_devices(device_list)
else:
_LOGGER.error('Device list in response not found')
else:
Expand All @@ -203,6 +178,9 @@ def load_device_state(self, device: PyDreoBaseDevice) -> bool:
proc_return = False
response, _ = self.call_dreo_api(DREO_API_DEVICESTATE, { DEVICESN_KEY: device.sn })

# stash the raw return value from the devicestate api call
device.raw_state = response

if response and Helpers.code_check(response):
if DATA_KEY in response and MIXED_KEY in response[DATA_KEY]:
device_state = response[DATA_KEY][MIXED_KEY]
Expand Down Expand Up @@ -322,7 +300,6 @@ async def _ws_ping_handler(self, ws) :
except websockets.exceptions.ConnectionClosed:
_LOGGER.error('Dreo WebSocket Closed')
break


def _ws_consume_message(self, message) :
messageDeviceSn = message["devicesn"]
Expand Down
39 changes: 24 additions & 15 deletions custom_components/dreo/pydreo/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,28 @@
FAN_MODE_AUTO = "auto"
FAN_MODE_SLEEP = "sleep"

PRESET_MODES = {
"DR-HTF001S": [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
"DR-HTF002S": [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
"DR-HTF004S": [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
"DR-HTF007S": [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
"DR-HTF008S": [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO]
}
PRESET_MODES_KEY = "preset_modes"
SPEED_RANGE_KEY = "speed_range"

SPEED_RANGES = {
# off is not included
"DR-HTF001S": (1, 6),
"DR-HTF002S": (1, 6),
"DR-HTF004S": (1, 12),
"DR-HTF007S": (1, 4),
"DR-HTF008S": (1, 5)
}
SUPPORTED_FANS = {
"DR-HTF001S": {
PRESET_MODES_KEY: [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
SPEED_RANGE_KEY: (1, 6)
},
"DR-HTF002S": {
PRESET_MODES_KEY: [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
SPEED_RANGE_KEY: (1, 6)
},
"DR-HTF004S": {
PRESET_MODES_KEY: [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
SPEED_RANGE_KEY: (1, 12)
},
"DR-HTF007S": {
PRESET_MODES_KEY: [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
SPEED_RANGE_KEY: (1, 4)
},
"DR-HTF008S": {
PRESET_MODES_KEY: [FAN_MODE_NORMAL, FAN_MODE_NATURAL, FAN_MODE_SLEEP, FAN_MODE_AUTO],
SPEED_RANGE_KEY: (1, 5)
},
}
6 changes: 5 additions & 1 deletion custom_components/dreo/pydreo/pydreobasedevice.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

_LOGGER = logging.getLogger(LOGGER_NAME)

class UnknownModelError(Exception):
pass

class PyDreoBaseDevice(object):
"""Base class for all Dreo devices.
Expand All @@ -24,7 +27,8 @@ def __init__(self, details: Dict[str, list], dreo : "PyDreo"):
self._model = details.get("model", None)
self._dreo = dreo
self._is_on = False


self.raw_state = None
self._attr_cbs = []
self._lock = threading.Lock()

Expand Down
9 changes: 6 additions & 3 deletions custom_components/dreo/pydreo/pydreofan.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Dict
from typing import TYPE_CHECKING

from .pydreobasedevice import PyDreoBaseDevice
from .pydreobasedevice import PyDreoBaseDevice, UnknownModelError
from .helpers import Helpers

from .constant import *
Expand All @@ -21,8 +21,11 @@ def __init__(self, details: Dict[str, list], dreo : "PyDreo"):
"""Initialize air devices."""
super().__init__(details, dreo)

self._speed_range = SPEED_RANGES[self.model]
self._preset_modes = PRESET_MODES[self.model]
if (self.model not in SUPPORTED_FANS):
raise UnknownModelError(self.model)

self._speed_range = SUPPORTED_FANS[self.model][SPEED_RANGE_KEY]
self._preset_modes = SUPPORTED_FANS[self.model][PRESET_MODES_KEY]

def __repr__(self):
# Representation string of object.
Expand Down

0 comments on commit dec13ef

Please sign in to comment.