Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.2.0] Rework SubDevices Connection and more #38

Merged
merged 50 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
99ddac0
Rework SubDevices Connection
xZetsubou Oct 16, 2023
7e1b4e3
Fix some stuff in payload_dict for sub devices `3.3`
xZetsubou Oct 16, 2023
d8e25e6
Don't send warning disconnect dc is intended
xZetsubou Oct 16, 2023
6170b69
Hotfix for `3.3` devices `detect_available_dps`
xZetsubou Oct 16, 2023
c1c382c
Add sensors for code `wsdcg`
xZetsubou Oct 16, 2023
2fe0aa2
Attempt Fix: some dps aren't detected.
xZetsubou Oct 16, 2023
f692983
Adjust `detect_available_dps` Function when gateway existed
xZetsubou Oct 16, 2023
23a34f4
int `t` since sub_devices payload `t` is int
xZetsubou Oct 17, 2023
bc95346
revert last commit force int `"t"` always, need for some cmds
xZetsubou Oct 17, 2023
80c5458
test: disable removing the [ gwID, devId and uid ]
xZetsubou Oct 17, 2023
6243fa9
Disconnect the sub devices if gateway dc'd
xZetsubou Oct 19, 2023
e0d93f6
ensure that the gateway is ready before setup subdevices
xZetsubou Oct 20, 2023
483f8c2
Fix: Block status_updated if gateway is sub_device
xZetsubou Oct 21, 2023
789d4e2
Add gateway_gwId to discovered sub devices. and prevent non local sub…
xZetsubou Oct 21, 2023
7810668
Store Gateway ID If found on mergeDevicesList
xZetsubou Oct 21, 2023
17dc8f9
minor changes of connect on initialization
xZetsubou Oct 21, 2023
8c6889f
config_entry_by_device_id search for gateways
xZetsubou Oct 21, 2023
2d0589a
Minor fix: Rename climates.py in tuya devices data
xZetsubou Oct 21, 2023
f02edec
Tuya Devices Data rename binary_sensor
xZetsubou Oct 21, 2023
0e3727a
Adjust Auto Configure category `wkf`
xZetsubou Oct 21, 2023
4d557cf
Fix automatic update `ip` and includes sub_devices
xZetsubou Oct 21, 2023
1b9c5ff
Except errors from pytuya.
xZetsubou Oct 22, 2023
f5a1dfe
Except error on config flow and set timeout for connect.
xZetsubou Oct 22, 2023
9ab8246
Fix: Entity_Category and convert get_gateway to async
xZetsubou Oct 23, 2023
fbdc809
Fix Mirgate: Force Int for config_flow values and fix reverse always off
xZetsubou Oct 23, 2023
0a78f89
Fix: Fake Gateway fails due to no DPS found on parent DPS.
xZetsubou Oct 24, 2023
6c30fd0
reformat unload function.
xZetsubou Oct 24, 2023
cfe5003
Fix Reconfigure cloud step
xZetsubou Oct 24, 2023
9ee17b8
Fix error if "Auto Configure" used without cloud.
xZetsubou Oct 24, 2023
644c7dc
Add platform support for humidifiers and Rename DPS, and DPS_CONF Fun…
xZetsubou Oct 25, 2023
848879b
Adjust `en` translation for humidifer
xZetsubou Oct 25, 2023
a60ee81
mark set_humiditiy as optional
xZetsubou Oct 25, 2023
703924b
log the disconnect reason of exists
xZetsubou Oct 25, 2023
46d5bff
Adjust get_gateway function
xZetsubou Oct 26, 2023
b59047c
adjust error msgs in auto configure
xZetsubou Oct 26, 2023
252d23f
Refactor: entities initialization msg and remove dp_value_conf (#49)
xZetsubou Oct 27, 2023
e040909
Improve entry initialization (#50)
xZetsubou Oct 28, 2023
f2b7be4
Improves config flow (#51)
xZetsubou Oct 28, 2023
f290394
Enable manually enter template filename (#52)
xZetsubou Oct 28, 2023
57e37e2
update pytuya version
xZetsubou Oct 28, 2023
23e401e
revert auther name
xZetsubou Oct 28, 2023
3a14855
Sort discovered devices by `ip`
xZetsubou Oct 28, 2023
3442fe5
Handle sorting discovered devices better
xZetsubou Oct 28, 2023
e307c08
refactor: devices list sorting
xZetsubou Oct 28, 2023
cda0c9a
adjust dc reason log
xZetsubou Oct 28, 2023
ad533d8
refactor: mergeDevicesList
xZetsubou Oct 29, 2023
164a051
new helper `get_gateway_by_id` refactor mergeDeivceList
xZetsubou Oct 29, 2023
5f001eb
typo
xZetsubou Oct 29, 2023
a3f63c7
* Refactor codes a little bit to make it easier to maintain. (#53)
xZetsubou Oct 30, 2023
19f7a59
Hide reconifgured device if no devices setuped (#54)
xZetsubou Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 78 additions & 73 deletions custom_components/localtuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import homeassistant.helpers.config_validation as cv
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
Expand All @@ -28,17 +28,17 @@
from homeassistant.helpers.event import async_track_time_interval

from .cloud_api import TuyaCloudApi
from .common import TuyaDevice, async_config_entry_by_device_id
from .common import HassLocalTuyaData, TuyaDevice, async_config_entry_by_device_id
from .config_flow import ENTRIES_VERSION, config_schema
from .const import (
ATTR_UPDATED_AT,
CONF_GATEWAY_ID,
CONF_NODE_ID,
CONF_NO_CLOUD,
CONF_PRODUCT_KEY,
CONF_USER_ID,
DATA_CLOUD,
DATA_DISCOVERY,
DOMAIN,
TUYA_DEVICES,
)
from .discovery import TuyaDiscovery

Expand Down Expand Up @@ -90,46 +90,59 @@ async def _handle_set_dp(event):
if not entry.entry_id:
raise HomeAssistantError("unknown device id")

device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES][dev_id]
device: TuyaDevice = hass.data[DOMAIN][entry.entry_id].tuya_devices[dev_id]
if not device.connected:
raise HomeAssistantError("not connected to device")

await device.set_dp(event.data[CONF_VALUE], event.data[CONF_DP])

def _device_discovered(device: TuyaDevice):
def _device_discovered(device: dict):
"""Update address of device if it has changed."""
device_ip = device["ip"]
device_id = device["gwId"]
product_key = device["productKey"]
# If device is not in cache, check if a config entry exists
entry = async_config_entry_by_device_id(hass, device_id)
entry: ConfigEntry = async_config_entry_by_device_id(hass, device_id)
if entry is None:
return

if device_id not in device_cache:
if device_id not in device_cache or device_id not in device_cache.get(
device_id, {}
):
if entry and device_id in entry.data[CONF_DEVICES]:
# Save address from config entry in cache to trigger
# potential update below
host_ip = entry.data[CONF_DEVICES][device_id][CONF_HOST]
device_cache[device_id] = host_ip
device_cache[device_id] = {device_id: host_ip}

for subdev_id, dev_config in entry.data[CONF_DEVICES].items():
if dev_config.get(CONF_NODE_ID):
if gateway_id := dev_config.get(CONF_GATEWAY_ID):
if entry and device_id == gateway_id:
device_cache[device_id] = device_cache.get(device_id, {})
device_cache[device_id].update(
{subdev_id: dev_config.get(CONF_HOST)}
)

if device_id not in device_cache:
return

dev_entry = entry.data[CONF_DEVICES][device_id]
if not entry.state == ConfigEntryState.LOADED:
return

new_data = entry.data.copy()
updated = False

if device_cache[device_id] != device_ip:
updated = True
new_data[CONF_DEVICES][device_id][CONF_HOST] = device_ip
device_cache[device_id] = device_ip

if dev_entry.get(CONF_PRODUCT_KEY) != product_key:
updated = True
new_data[CONF_DEVICES][device_id][CONF_PRODUCT_KEY] = product_key

for dev_id, host in device_cache[device_id].items():
if dev_id not in entry.data[CONF_DEVICES]:
continue
dev_entry = entry.data[CONF_DEVICES][dev_id]
if host != device_ip:
updated = True
new_data[CONF_DEVICES][dev_id][CONF_HOST] = device_ip
device_cache[device_id][dev_id] = device_ip

if (p_key := dev_entry.get(CONF_PRODUCT_KEY)) and p_key != product_key:
updated = True
new_data[CONF_DEVICES][dev_id][CONF_PRODUCT_KEY] = product_key
# Update settings if something changed, otherwise try to connect. Updating
# settings triggers a reload of the config entry, which tears down the device
# so no need to connect in that case.
Expand All @@ -139,14 +152,6 @@ def _device_discovered(device: TuyaDevice):
)
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
hass.config_entries.async_update_entry(entry, data=new_data)
# No need to do connect task here, when entry updated, it will reconnect. [elif].
# device = hass.data[DOMAIN][TUYA_DEVICES][device_id]
# if not device.connected:
# hass.create_task(device.async_connect())
# elif device_id in hass.data[DOMAIN][TUYA_DEVICES]:
# device = hass.data[DOMAIN][TUYA_DEVICES][device_id]
# if not device.connected:
# hass.create_task(device.async_connect())

def _shutdown(event):
"""Clean up resources when shutting down."""
Expand Down Expand Up @@ -213,8 +218,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry.version,
)
return
hass.data[DOMAIN][entry.entry_id] = {}
hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES] = {}

region = entry.data[CONF_REGION]
client_id = entry.data[CONF_CLIENT_ID]
Expand All @@ -229,47 +232,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
# wait 1 second to make sure possible migration has finished
await asyncio.sleep(1)
else:
res = await tuya_api.async_get_access_token()
if res != "ok":
_LOGGER.error("Cloud API connection failed: %s", res)
else:
_LOGGER.info("Cloud API connection succeeded.")
res = await tuya_api.async_get_devices_list()
hass.data[DOMAIN][entry.entry_id][DATA_CLOUD] = tuya_api

async def setup_entities(device_ids):
entry.async_create_background_task(
hass, tuya_api.async_connect(), "localtuya-cloudAPI"
)

async def setup_entities(entry_devices: dict):
platforms = set()
for dev_id in device_ids:
devices: dict[str, TuyaDevice] = {}
for dev_id, config in entry_devices.items():
host = config.get(CONF_HOST)
entities = entry.data[CONF_DEVICES][dev_id][CONF_ENTITIES]
platforms = platforms.union(
set(entity[CONF_PLATFORM] for entity in entities)
)

hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES][dev_id] = TuyaDevice(
hass, entry, dev_id
)
if node_id := config.get(CONF_NODE_ID):
# Setup sub device as gateway if no gateway not exist.
if host not in devices:
devices[host] = TuyaDevice(hass, entry, dev_id, True)

host = f"{host}_{node_id}"

devices[host] = TuyaDevice(hass, entry, dev_id)

# Unsub listener: callback to unsub
hass_localtuya = HassLocalTuyaData(tuya_api, devices, [])
hass.data[DOMAIN][entry.entry_id] = hass_localtuya

await async_remove_orphan_entities(hass, entry)
await hass.config_entries.async_forward_entry_setups(entry, platforms)

# Connect to tuya devices.
connect_task = [
device.async_connect()
for device in hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES].values()
connect_to_devices = [
device.async_connect() for device in hass_localtuya.tuya_devices.values()
]
try:
await asyncio.wait_for(asyncio.gather(*connect_task), 1)
except:
# If there is device that isn't connected to network it will return failed Initialization.
...
entry_update = entry.add_update_listener(update_listener)
hass_localtuya.unsub_listeners.append(entry_update)

await setup_entities(entry.data[CONF_DEVICES].keys())
# callback back to unsub listener
unsub_listener = entry.add_update_listener(update_listener)
await asyncio.gather(*connect_to_devices)

hass.data[DOMAIN][entry.entry_id].update({UNSUB_LISTENER: unsub_listener})
await setup_entities(entry.data[CONF_DEVICES])

# Add reconnect trigger every 1mins to reconnect if device not connected.
# Add reconnect task.
reconnectTask(hass, entry)
return True

Expand All @@ -278,28 +282,26 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unloading the Tuya platforms."""
# Get used platforms.
platforms = {}
for dev_id, dev_entry in entry.data[CONF_DEVICES].items():
for entity in dev_entry[CONF_ENTITIES]:
disconnect_devices = []
hass_data: HassLocalTuyaData = hass.data[DOMAIN][entry.entry_id]

for dev in hass_data.tuya_devices.values():
disconnect_devices.append(dev.close())
for entity in dev._device_config[CONF_ENTITIES]:
platforms[entity[CONF_PLATFORM]] = True

# Unload the platforms.
await hass.config_entries.async_unload_platforms(entry, platforms)

# Close all connection to the devices.
close_devices = [
device.close()
for device in hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES].values()
if device.connected
]
# Just to prevent the loop get stuck in-case it calls multiples quickly
try:
await asyncio.wait_for(asyncio.gather(*close_devices), 3)
await asyncio.wait_for(asyncio.gather(*disconnect_devices), 3)
except:
pass

# Unsub events.
hass.data[DOMAIN][entry.entry_id][RECONNECT_TASK]()
hass.data[DOMAIN][entry.entry_id][UNSUB_LISTENER]()
[unsub() for unsub in hass_data.unsub_listeners]

hass.data[DOMAIN].pop(entry.entry_id)

Expand Down Expand Up @@ -332,7 +334,8 @@ async def async_remove_config_entry_device(
)
return True

await hass.data[DOMAIN][config_entry.entry_id][TUYA_DEVICES][dev_id].close()
# host = config_entry.data[CONF_DEVICES][dev_id][CONF_HOST]
# await hass.data[DOMAIN][config_entry.entry_id].tuya_devices[host].close()

new_data = config_entry.data.copy()
new_data[CONF_DEVICES].pop(dev_id)
Expand All @@ -349,16 +352,18 @@ async def async_remove_config_entry_device(


def reconnectTask(hass: HomeAssistant, entry: ConfigEntry):
"""Add reconnect task to (every 1mins), If devices is not connected"""
"""Add a task to reconnect to the devices if is not connected [interval: RECONNECT_INTERVAL]"""
hass_localtuya: HassLocalTuyaData = hass.data[DOMAIN][entry.entry_id]

async def _async_reconnect(now):
"""Try connecting to devices not already connected to."""
for devID, dev in hass.data[DOMAIN][entry.entry_id][TUYA_DEVICES].items():
for dev in hass_localtuya.tuya_devices.values():
if not dev.connected:
hass.create_task(dev.async_connect())
hass.async_create_task(dev.async_connect())

hass.data[DOMAIN][entry.entry_id][RECONNECT_TASK] = async_track_time_interval(
hass, _async_reconnect, RECONNECT_INTERVAL
# Add unsub callbeack in unsub_listeners object.
hass_localtuya.unsub_listeners.append(
async_track_time_interval(hass, _async_reconnect, RECONNECT_INTERVAL)
)


Expand Down
2 changes: 1 addition & 1 deletion custom_components/localtuya/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def status_updated(self):
"""Device status was updated."""
super().status_updated()

state = str(self.dps(self._dp_id)).lower()
state = str(self.dp_value(self._dp_id)).lower()
if state == self._config[CONF_STATE_ON].lower() or state == "true":
self._is_on = True
else:
Expand Down
1 change: 0 additions & 1 deletion custom_components/localtuya/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def __init__(
"""Initialize the Tuya button."""
super().__init__(device, config_entry, buttonid, _LOGGER, **kwargs)
self._state = None
_LOGGER.debug("Initialized button [%s]", self.name)

async def async_press(self):
"""Press the button."""
Expand Down
27 changes: 13 additions & 14 deletions custom_components/localtuya/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,6 @@
HVACMode.HEAT: "manual",
HVACMode.AUTO: "auto",
},
"Manual/Auto": {
HVACMode.HEAT: "Manual",
HVACMode.AUTO: "Auto",
},
"Manual/Program": {
HVACMode.HEAT: "Manual",
HVACMode.AUTO: "Program",
Expand Down Expand Up @@ -100,6 +96,10 @@
HVACAction.HEATING: "open",
HVACAction.IDLE: "close",
},
"opened/closed": {
HVACAction.HEATING: "opened",
HVACAction.IDLE: "closed",
},
"heating/no_heating": {
HVACAction.HEATING: "heating",
HVACAction.IDLE: "no_heating",
Expand Down Expand Up @@ -217,7 +217,6 @@ def __init__(
self._has_presets = self.has_config(CONF_ECO_DP) or self.has_config(
CONF_PRESET_DP
)
_LOGGER.debug("Initialized climate [%s]", self.name)

@property
def supported_features(self):
Expand Down Expand Up @@ -375,7 +374,7 @@ async def async_set_preset_mode(self, preset_mode):
def min_temp(self):
"""Return the minimum temperature."""
if _min_temp := self._config.get(CONF_MIN_TEMP_DP):
return self.dps_conf(CONF_MIN_TEMP_DP)
return self.dp_value(CONF_MIN_TEMP_DP)
# DEFAULT_MIN_TEMP is in C
if self.temperature_unit == TEMP_FAHRENHEIT:
return DEFAULT_MIN_TEMP * 1.8 + 32
Expand All @@ -386,7 +385,7 @@ def min_temp(self):
def max_temp(self):
"""Return the maximum temperature."""
if self.has_config(CONF_MAX_TEMP_DP):
return self.dps_conf(CONF_MAX_TEMP_DP)
return self.dp_value(CONF_MAX_TEMP_DP)
# DEFAULT_MAX_TEMP is in C
if self.temperature_unit == TEMP_FAHRENHEIT:
return DEFAULT_MAX_TEMP * 1.8 + 32
Expand All @@ -395,27 +394,27 @@ def max_temp(self):

def status_updated(self):
"""Device status was updated."""
self._state = self.dps(self._dp_id)
self._state = self.dp_value(self._dp_id)

if self.has_config(CONF_TARGET_TEMPERATURE_DP):
self._target_temperature = (
self.dps_conf(CONF_TARGET_TEMPERATURE_DP) * self._target_precision
self.dp_value(CONF_TARGET_TEMPERATURE_DP) * self._target_precision
)

if self.has_config(CONF_CURRENT_TEMPERATURE_DP):
self._current_temperature = (
self.dps_conf(CONF_CURRENT_TEMPERATURE_DP) * self._precision
self.dp_value(CONF_CURRENT_TEMPERATURE_DP) * self._precision
)

if self._has_presets:
if (
self.has_config(CONF_ECO_DP)
and self.dps_conf(CONF_ECO_DP) == self._conf_eco_value
and self.dp_value(CONF_ECO_DP) == self._conf_eco_value
):
self._preset_mode = PRESET_ECO
else:
for preset, value in self._conf_preset_set.items(): # todo remove
if self.dps_conf(CONF_PRESET_DP) == value:
if self.dp_value(CONF_PRESET_DP) == value:
self._preset_mode = preset
break
else:
Expand All @@ -427,7 +426,7 @@ def status_updated(self):
self._hvac_mode = HVACMode.OFF
else:
for mode, value in self._conf_hvac_mode_set.items():
if self.dps_conf(CONF_HVAC_MODE_DP) == value:
if self.dp_value(CONF_HVAC_MODE_DP) == value:
self._hvac_mode = mode
break
else:
Expand All @@ -436,7 +435,7 @@ def status_updated(self):

# Update the current action
for action, value in self._conf_hvac_action_set.items():
if self.dps_conf(CONF_HVAC_ACTION_DP) == value:
if self.dp_value(CONF_HVAC_ACTION_DP) == value:
self._hvac_action = action


Expand Down
Loading
Loading