Skip to content

Commit

Permalink
Merge pull request #99 from dbuezas/refactor-code-style
Browse files Browse the repository at this point in the history
feat: redesign library api
  • Loading branch information
EuleMitKeule authored Jan 22, 2024
2 parents 93f0a1e + 732a813 commit b5a12ad
Show file tree
Hide file tree
Showing 40 changed files with 2,327 additions and 1,567 deletions.
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ repos:
- id: trailing-whitespace
- id: check-added-large-files
- id: check-case-conflict
- id: check-executables-have-shebangs
- id: check-json
- id: check-merge-conflict
- id: check-symlinks
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,45 @@ To setup `pre-commit` for automatic issue detection while committing you need to
* `poetry run pre-commit install`
* `poetry run pre-commit install --hook-type commit-msg`
* `poetry run pre-commit install-hooks`

# Protocol

### `ID_GET`
`0x00`

### `INFO_GET`
`0x03`

### `COMFORT_ECO_CONFIGURE`
`0x11 (COMFORT_TEMP * 2) (ECO_TEMP * 2)`

### OFFSET_CONFIGURE
`0x13 TEMP_INDEX`

### WINDOW_OPEN_CONFIGURE
`0x14 (TEMP * 2) (FLOOR(SECONDS / 300))`

### `SCHEDULE_GET`
`0x20 DAY`

### `MODE_SET`
* On: `0x40 (0x40 OR 0x3C)`
* Off: `0x40 (0x40 OR 0x09)`
* Manual: `0x40 (0x40 OR TEMP * 2)`
* Auto: `0x40 0x00`
* Away: `0x40 (0x80 OR TEMP * 2) DAY (YEAR - 2000) (HOUR * 2) (MONTH)`

### `TEMPERATURE_SET`
`0x41 (TEMP * 2)`

### `COMFORT_SET`
`0x43`

### `ECO_SET`
`0x44`

### `BOOST_SET`
`0x45 (0x00 | 0x01)`

### `LOCK_SET`
`0x80 (0x00 | 0x01)`
137 changes: 114 additions & 23 deletions custom_components/eq3btsmart/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
"""Support for EQ3 devices."""
from __future__ import annotations

import logging
from typing import Any

from bleak.backends.device import BLEDevice
from bleak_retry_connector import NO_RSSI_VALUE
from eq3btsmart import Thermostat
from eq3btsmart.thermostat_config import ThermostatConfig
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_MAC, CONF_NAME, CONF_SCAN_INTERVAL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady

from .const import (
CONF_ADAPTER,
CONF_CURRENT_TEMP_SELECTOR,
CONF_DEBUG_MODE,
CONF_EXTERNAL_TEMP_SENSOR,
CONF_STAY_CONNECTED,
CONF_TARGET_TEMP_SELECTOR,
DEFAULT_ADAPTER,
DEFAULT_CURRENT_TEMP_SELECTOR,
DEFAULT_DEBUG_MODE,
DEFAULT_SCAN_INTERVAL,
DEFAULT_STAY_CONNECTED,
DEFAULT_TARGET_TEMP_SELECTOR,
DOMAIN,
Adapter,
)
from .models import Eq3Config, Eq3ConfigEntry

PLATFORMS = [
Platform.CLIMATE,
Expand All @@ -28,45 +43,121 @@

_LOGGER = logging.getLogger(__name__)

# based on https://github.com/home-assistant/example-custom-config/tree/master/custom_components/detailed_hello_world_push


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Hello World from a config entry."""
"""Called when an entry is setup."""

# Store an instance of the "connecting" class that does the work of speaking
# with your actual devices.
mac_address: str = entry.data[CONF_MAC]
name: str = entry.data[CONF_NAME]
adapter: Adapter = entry.options.get(CONF_ADAPTER, DEFAULT_ADAPTER)
stay_connected: bool = entry.options.get(
CONF_STAY_CONNECTED, DEFAULT_STAY_CONNECTED
)
current_temp_selector = entry.options.get(
CONF_CURRENT_TEMP_SELECTOR, DEFAULT_CURRENT_TEMP_SELECTOR
)
target_temp_selector = entry.options.get(
CONF_TARGET_TEMP_SELECTOR, DEFAULT_TARGET_TEMP_SELECTOR
)
external_temp_sensor = entry.options.get(CONF_EXTERNAL_TEMP_SENSOR)
debug_mode = entry.options.get(CONF_DEBUG_MODE, DEFAULT_DEBUG_MODE)
scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)

eq3_config = Eq3Config(
mac_address=mac_address,
name=name,
adapter=adapter,
stay_connected=stay_connected,
current_temp_selector=current_temp_selector,
target_temp_selector=target_temp_selector,
external_temp_sensor=external_temp_sensor,
debug_mode=debug_mode,
scan_interval=scan_interval,
)

thermostat_config = ThermostatConfig(
mac_address=mac_address,
name=name,
adapter=adapter,
stay_connected=stay_connected,
)

try:
device = await async_get_device(hass, eq3_config)
except Exception as e:
raise ConfigEntryNotReady(f"Could not connect to device: {e}")

thermostat = Thermostat(
mac=entry.data["mac"],
name=entry.data["name"],
adapter=entry.options.get(CONF_ADAPTER, DEFAULT_ADAPTER),
stay_connected=entry.options.get(CONF_STAY_CONNECTED, DEFAULT_STAY_CONNECTED),
hass=hass,
thermostat_config=thermostat_config,
ble_device=device,
)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = thermostat

entry.async_on_unload(entry.add_update_listener(update_listener))
try:
await thermostat.async_connect()
except Exception as e:
raise ConfigEntryNotReady(f"Could not connect to device: {e}")

# This creates each HA object for each platform your device requires.
# It's done by calling the `async_setup_entry` function in each platform module.
eq3_config_entry = Eq3ConfigEntry(eq3_config=eq3_config, thermostat=thermostat)

domain_data: dict[str, Any] = hass.data.setdefault(DOMAIN, {})
domain_data[entry.entry_id] = eq3_config_entry

entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
# This is called when an entry/configured device is to be removed. The class
# needs to unload itself, and remove callbacks. See the classes for further
# details
"""Called when an entry is unloaded."""

unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

if unload_ok:
thermostat = hass.data[DOMAIN].pop(entry.entry_id)
thermostat.shutdown()
eq3_config_entry: Eq3ConfigEntry = hass.data[DOMAIN].pop(entry.entry_id)
await eq3_config_entry.thermostat.async_disconnect()

return unload_ok


async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener. Called when integration options are changed"""
"""Called when an entry is updated."""

await hass.config_entries.async_reload(entry.entry_id)


async def async_get_device(hass: HomeAssistant, config: Eq3Config) -> BLEDevice:
device: BLEDevice | None

if config.adapter == Adapter.AUTO:
device = bluetooth.async_ble_device_from_address(
hass, config.mac_address, connectable=True
)
if device is None:
raise Exception("Device not found")
else:
device_advertisement_datas = sorted(
bluetooth.async_scanner_devices_by_address(
hass=hass, address=config.mac_address, connectable=True
),
key=lambda device_advertisement_data: device_advertisement_data.advertisement.rssi
or NO_RSSI_VALUE,
reverse=True,
)
if config.adapter == Adapter.LOCAL:
if len(device_advertisement_datas) == 0:
raise Exception("Device not found")
d_and_a = device_advertisement_datas[0]
else: # adapter is e.g /org/bluez/hci0
list = [
x
for x in device_advertisement_datas
if (d := x.ble_device.details)
and d.get("props", {}).get("Adapter") == config.adapter
]
if len(list) == 0:
raise Exception("Device not found")
d_and_a = list[0]
device = d_and_a.ble_device

return device
Loading

0 comments on commit b5a12ad

Please sign in to comment.