Skip to content

Commit

Permalink
Introduce auto configure device (#33)
Browse files Browse the repository at this point in the history
* Use HA Consts for platforms and change to dict

* Refresh the token if expired, when request made

* Clean up and remove get_access_token from config_flow.py

* revert obtain  access token if config_flow installtion.

* Add icon support and entity names now follow device_name.

* add notice for entity friendly name

* Apply device class for all entites.

* pushed wrong common on last commit.

* Remove OFF STATE relay only on STATE_ON.

* use true as default on state always.

* configure config_entry on migrating

* Scenes and Select Values now separate by comma

* adjust expectaion msgs only shows when device debug true

* adjust name entity

* Sensor: Now Support STATE_CLASS

* add context  infos for sensors

* Move CONF_STATE TO Const.py

* config flow reconnect and passive to optional

* Climate now MIN AND MAX temp manually setted

* update climate.py

* add device_class for [switch, cover, numbers]

* restore icon if entity has one [auto_feature]

* add note in climate

* ADD Siren platform

* ADD auto configure support and tuya data files, new folder

* Label cover position values

* auto configure: sort entites by id.

* adjust parse function for enum

* parse enum before export template

* Optimize the token refresh method.

* refresh token `30 secs before` it expires

* just preparing for numbers to support native units

* Adjust tuya_device data by adding localtuya to define configs

* add select function to tuya_data selects

* remove native comments for now

* events states_update -> device_updated

* add RELAY_STATUS posssibilty to "kg"

* Revert event to states_update

* Fix: when auto detect fails always return same msg

* Auto Detect: Add more codes, entity.

* update en.json

* en.json typo

* Auto Detect: Add function for sensors for localtuya configs.

* Add unit of measurement for numbers.

* add more codes

* Add some stuff for future

* update en.json

* typo ms -> mc

* rename configure device flow title

* small adjustments

* Handle tuple if codes isn't in detected dps

* Dimmer support up to 4 LED

* Auto detect: Fix tuple check and lights brightness typo

* Auto Configure: Remove duplicated category and fix typo

* Revert climate to use min and max dp.

* Auto Configure: Add Code: Battery Sensors

* Fix: ZigBee `3.4` devices updates affect other nodes on same GW

* Fix `3.3` Sub devices control.

* typo.
  • Loading branch information
xZetsubou authored Oct 16, 2023
1 parent 4dc8e65 commit b8d1a95
Show file tree
Hide file tree
Showing 35 changed files with 5,355 additions and 249 deletions.
25 changes: 12 additions & 13 deletions custom_components/localtuya/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def _shutdown(event):
return True


async def async_migrate_entry(hass, config_entry: ConfigEntry):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry):
"""Migrate old entries merging all of them in one."""
new_version = ENTRIES_VERSION
stored_entries = hass.config_entries.async_entries(DOMAIN)
Expand All @@ -183,18 +183,17 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
if config_entry.version == 2:
# Switch config flow to selectors convert DP IDs from int to str require HA 2022.4.
_LOGGER.debug("Migrating config entry from version %s", config_entry.version)
if config_entry.entry_id == stored_entries[0].entry_id:
new_data = stored_entries[0].data.copy()
for device in new_data[CONF_DEVICES]:
i = 0
for _ent in new_data[CONF_DEVICES][device][CONF_ENTITIES]:
ent_items = {}
for k, v in _ent.items():
ent_items[k] = str(v) if type(v) is int else v
new_data[CONF_DEVICES][device][CONF_ENTITIES][i].update(ent_items)
i = i + 1
config_entry.version = new_version
hass.config_entries.async_update_entry(config_entry, data=new_data)
new_data = config_entry.data.copy()
for device in new_data[CONF_DEVICES]:
i = 0
for _ent in new_data[CONF_DEVICES][device][CONF_ENTITIES]:
ent_items = {}
for k, v in _ent.items():
ent_items[k] = str(v) if type(v) is int else v
new_data[CONF_DEVICES][device][CONF_ENTITIES][i].update(ent_items)
i = i + 1
config_entry.version = new_version
hass.config_entries.async_update_entry(config_entry, data=new_data)

_LOGGER.info(
"Entry %s successfully migrated to version %s.",
Expand Down
17 changes: 4 additions & 13 deletions custom_components/localtuya/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@
from homeassistant.const import CONF_DEVICE_CLASS

from .common import LocalTuyaEntity, async_setup_entry
from .const import CONF_STATE_ON

_LOGGER = logging.getLogger(__name__)

CONF_STATE_ON = "state_on"
CONF_STATE_OFF = "state_off"


def flow_schema(dps):
"""Return schema used in config flow."""
return {
vol.Required(CONF_STATE_ON, default="True"): str,
vol.Required(CONF_STATE_OFF, default="False"): str,
# vol.Required(CONF_STATE_OFF, default="False"): str,
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
}

Expand All @@ -46,24 +46,15 @@ def is_on(self):
"""Return sensor state."""
return self._is_on

@property
def device_class(self):
"""Return the class of this device."""
return self._config.get(CONF_DEVICE_CLASS)

def status_updated(self):
"""Device status was updated."""
super().status_updated()

state = str(self.dps(self._dp_id)).lower()
if state == self._config[CONF_STATE_ON].lower():
if state == self._config[CONF_STATE_ON].lower() or state == "true":
self._is_on = True
elif state == self._config[CONF_STATE_OFF].lower():
self._is_on = False
else:
self.warning(
"State for entity %s did not match state patterns", self.entity_id
)
self._is_on = False

# No need to restore state for a sensor
async def restore_state_when_connected(self):
Expand Down
116 changes: 73 additions & 43 deletions custom_components/localtuya/climate.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Platform to locally control Tuya-based climate devices."""
"""Platform to locally control Tuya-based climate devices.
# PRESETS and HVAC_MODE Needs to be handle in better way.
"""
import asyncio
import logging
from functools import partial
Expand All @@ -12,11 +14,8 @@
ClimateEntity,
)
from homeassistant.components.climate.const import (
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
HVAC_MODE_AUTO,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
HVACMode,
HVACAction,
PRESET_AWAY,
PRESET_ECO,
PRESET_HOME,
Expand Down Expand Up @@ -45,59 +44,79 @@
CONF_HVAC_ACTION_SET,
CONF_HVAC_MODE_DP,
CONF_HVAC_MODE_SET,
CONF_MAX_TEMP_DP,
CONF_MIN_TEMP_DP,
CONF_PRECISION,
CONF_PRESET_DP,
CONF_PRESET_SET,
CONF_TARGET_PRECISION,
CONF_TARGET_TEMPERATURE_DP,
CONF_TEMPERATURE_STEP,
CONF_MIN_TEMP_DP,
CONF_MAX_TEMP_DP,
)

_LOGGER = logging.getLogger(__name__)

HVAC_MODE_SETS = {
"manual/auto": {
HVAC_MODE_HEAT: "manual",
HVAC_MODE_AUTO: "auto",
HVACMode.HEAT: "manual",
HVACMode.AUTO: "auto",
},
"Manual/Auto": {
HVAC_MODE_HEAT: "Manual",
HVAC_MODE_AUTO: "Auto",
HVACMode.HEAT: "Manual",
HVACMode.AUTO: "Auto",
},
"Manual/Program": {
HVAC_MODE_HEAT: "Manual",
HVAC_MODE_AUTO: "Program",
HVACMode.HEAT: "Manual",
HVACMode.AUTO: "Program",
},
"auto/cold/hot/wet": {
HVACMode.AUTO: "auto",
HVACMode.COOL: "cold",
HVACMode.HEAT: "hot",
HVACMode.DRY: "wet",
},
"m/p": {
HVAC_MODE_HEAT: "m",
HVAC_MODE_AUTO: "p",
HVACMode.HEAT: "m",
HVACMode.AUTO: "p",
},
"True/False": {
HVAC_MODE_HEAT: True,
HVACMode.HEAT: True,
},
"1/0": {
HVAC_MODE_HEAT: "1",
HVAC_MODE_AUTO: "0",
HVACMode.HEAT: "1",
HVACMode.AUTO: "0",
},
"smart/auto": {
HVACMode.HEAT_COOL: "1",
HVACMode.AUTO: "auto",
},
}
HVAC_ACTION_SETS = {
"True/False": {
CURRENT_HVAC_HEAT: True,
CURRENT_HVAC_IDLE: False,
HVACAction.HEATING: True,
HVACAction.IDLE: False,
},
"open/close": {
CURRENT_HVAC_HEAT: "open",
CURRENT_HVAC_IDLE: "close",
HVACAction.HEATING: "open",
HVACAction.IDLE: "close",
},
"heating/no_heating": {
CURRENT_HVAC_HEAT: "heating",
CURRENT_HVAC_IDLE: "no_heating",
HVACAction.HEATING: "heating",
HVACAction.IDLE: "no_heating",
},
"heating/cooling": {
HVACAction.HEATING: "heating",
HVACAction.COOLING: "cooling",
HVACAction.IDLE: "ventilation",
HVACAction.OFF: "off",
},
"Heat/Warming": {
CURRENT_HVAC_HEAT: "Heat",
CURRENT_HVAC_IDLE: "Warming",
HVACAction.HEATING: "Heat",
HVACAction.IDLE: "Warming",
},
"heating/warming": {
HVACAction.HEATING: "heating",
HVACAction.IDLE: "warming",
},
}
PRESET_SETS = {
Expand All @@ -106,6 +125,17 @@
PRESET_HOME: "Program",
PRESET_NONE: "Manual",
},
"auto/smart": {
"auto": "Auto",
"smart": "Smart",
},
"auto/manual/smart/comfortable/eco": {
"auto": "Auto",
"manual": "Manual",
"smart": "Smart",
"comfortable": "Comfort",
"eco": "ECO",
},
}

TEMPERATURE_CELSIUS = "celsius"
Expand All @@ -125,8 +155,8 @@ def flow_schema(dps):
vol.Optional(CONF_TEMPERATURE_STEP): _col_to_select(
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
),
vol.Optional(CONF_MAX_TEMP_DP): _col_to_select(dps, is_dps=True),
vol.Optional(CONF_MIN_TEMP_DP): _col_to_select(dps, is_dps=True),
vol.Optional(CONF_MAX_TEMP_DP): _col_to_select(dps, is_dps=True),
vol.Optional(CONF_PRECISION): _col_to_select(
[PRECISION_WHOLE, PRECISION_HALVES, PRECISION_TENTHS]
),
Expand Down Expand Up @@ -169,9 +199,9 @@ def __init__(
self._preset_mode = None
self._hvac_action = None
self._precision = float(self._config.get(CONF_PRECISION, DEFAULT_PRECISION))
self._target_precision = float(self._config.get(
CONF_TARGET_PRECISION, self._precision
))
self._target_precision = float(
self._config.get(CONF_TARGET_PRECISION, self._precision)
)
self._conf_hvac_mode_dp = self._config.get(CONF_HVAC_MODE_DP)
self._conf_hvac_mode_set = HVAC_MODE_SETS.get(
self._config.get(CONF_HVAC_MODE_SET), {}
Expand Down Expand Up @@ -231,7 +261,7 @@ def hvac_modes(self):
"""Return the list of available operation modes."""
if not self.has_config(CONF_HVAC_MODE_DP):
return None
return list(self._conf_hvac_mode_set) + [HVAC_MODE_OFF]
return list(self._conf_hvac_mode_set) + [HVACMode.OFF]

@property
def hvac_action(self):
Expand All @@ -240,22 +270,22 @@ def hvac_action(self):
Need to be one of CURRENT_HVAC_*.
"""
if self._config.get(CONF_HEURISTIC_ACTION, False):
if self._hvac_mode == HVAC_MODE_HEAT:
if self._hvac_mode == HVACMode.HEAT:
if self._current_temperature < (
self._target_temperature - self._precision
):
self._hvac_action = CURRENT_HVAC_HEAT
self._hvac_action = HVACMode.HEAT
if self._current_temperature == (
self._target_temperature - self._precision
):
if self._hvac_action == CURRENT_HVAC_HEAT:
self._hvac_action = CURRENT_HVAC_HEAT
if self._hvac_action == CURRENT_HVAC_IDLE:
self._hvac_action = CURRENT_HVAC_IDLE
if self._hvac_action == HVACMode.HEAT:
self._hvac_action = HVACMode.HEAT
if self._hvac_action == HVACAction.IDLE:
self._hvac_action = HVACAction.IDLE
if (
self._current_temperature + self._precision
) > self._target_temperature:
self._hvac_action = CURRENT_HVAC_IDLE
self._hvac_action = HVACAction.IDLE
return self._hvac_action
return self._hvac_action

Expand Down Expand Up @@ -313,7 +343,7 @@ def set_fan_mode(self, fan_mode):

async def async_set_hvac_mode(self, hvac_mode):
"""Set new target operation mode."""
if hvac_mode == HVAC_MODE_OFF:
if hvac_mode == HVACMode.OFF:
await self._device.set_dp(False, self._dp_id)
return
if not self._state and self._conf_hvac_mode_dp != self._dp_id:
Expand Down Expand Up @@ -344,7 +374,7 @@ async def async_set_preset_mode(self, preset_mode):
@property
def min_temp(self):
"""Return the minimum temperature."""
if self.has_config(CONF_MIN_TEMP_DP):
if _min_temp := self._config.get(CONF_MIN_TEMP_DP):
return self.dps_conf(CONF_MIN_TEMP_DP)
# DEFAULT_MIN_TEMP is in C
if self.temperature_unit == TEMP_FAHRENHEIT:
Expand Down Expand Up @@ -394,15 +424,15 @@ def status_updated(self):
# Update the HVAC status
if self.has_config(CONF_HVAC_MODE_DP):
if not self._state:
self._hvac_mode = HVAC_MODE_OFF
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:
self._hvac_mode = mode
break
else:
# in case hvac mode and preset share the same dp
self._hvac_mode = HVAC_MODE_AUTO
self._hvac_mode = HVACMode.AUTO

# Update the current action
for action, value in self._conf_hvac_action_set.items():
Expand Down
20 changes: 20 additions & 0 deletions custom_components/localtuya/cloud_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def __init__(self, hass, region_code, client_id, secret, user_id):
self._secret = secret
self._user_id = user_id
self._access_token = ""
self._token_expire_time: int = -1

self.device_list = {}

def generate_payload(self, method, timestamp, url, headers, body=None):
Expand All @@ -63,6 +65,10 @@ def generate_payload(self, method, timestamp, url, headers, body=None):

async def async_make_request(self, method, url, body=None, headers={}):
"""Perform requests."""
# obtain new token if expired.
if not self.token_validate and self._token_expire_time != -1:
await self.async_get_access_token()

timestamp = str(int(time.time() * 1000))
payload = self.generate_payload(method, timestamp, url, headers, body)
default_par = {
Expand Down Expand Up @@ -101,6 +107,10 @@ async def async_make_request(self, method, url, body=None, headers={}):

async def async_get_access_token(self):
"""Obtain a valid access token."""
# Reset access token
self._token_expire_time = -1
self._access_token = ""

try:
resp = await self.async_make_request("GET", "/v1.0/token?grant_type=1")
except requests.exceptions.ConnectionError:
Expand All @@ -113,6 +123,8 @@ async def async_get_access_token(self):
if not r_json["success"]:
return f"Error {r_json['code']}: {r_json['msg']}"

req_results = r_json["result"]
self._token_expire_time = int(time.time()) + int(req_results.get("expire_time"))
self._access_token = resp.json()["result"]["access_token"]
return "ok"

Expand Down Expand Up @@ -167,3 +179,11 @@ async def async_get_device_query_properties(self, device_id):
return {}, f"Error {r_json['code']}: {r_json['msg']}"

return r_json["result"], "ok"

@property
def token_validate(self):
"""Return whether token is expired or not"""
cur_time = int(time.time())
expire_time = self._token_expire_time - 30

return expire_time >= cur_time
Loading

0 comments on commit b8d1a95

Please sign in to comment.