Modified config flow, introduced device deletion

This commit is contained in:
rospogrigio
2022-05-17 17:53:33 +02:00
parent b22cb29811
commit d4c20ebfed
9 changed files with 778 additions and 472 deletions

View File

@@ -1,83 +1,18 @@
"""The LocalTuya integration integration. """The LocalTuya integration. """
Sample YAML config with all supported entity types (default values
are pre-filled for optional fields):
localtuya:
- host: 192.168.1.x
device_id: xxxxx
local_key: xxxxx
friendly_name: Tuya Device
protocol_version: "3.3"
entities:
- platform: binary_sensor
friendly_name: Plug Status
id: 1
device_class: power
state_on: "true" # Optional
state_off: "false" # Optional
- platform: cover
friendly_name: Device Cover
id: 2
commands_set: # Optional, default: "on_off_stop"
["on_off_stop","open_close_stop","fz_zz_stop","1_2_3"]
positioning_mode: ["none","position","timed"] # Optional, default: "none"
currpos_dp: 3 # Optional, required only for "position" mode
setpos_dp: 4 # Optional, required only for "position" mode
position_inverted: [True,False] # Optional, default: False
span_time: 25 # Full movement time: Optional, required only for "timed" mode
- platform: fan
friendly_name: Device Fan
id: 3
- platform: light
friendly_name: Device Light
id: 4
brightness: 20
brightness_lower: 29 # Optional
brightness_upper: 1000 # Optional
color_temp: 21
- platform: sensor
friendly_name: Plug Voltage
id: 20
scaling: 0.1 # Optional
device_class: voltage # Optional
unit_of_measurement: "V" # Optional
- platform: switch
friendly_name: Plug
id: 1
current: 18 # Optional
current_consumption: 19 # Optional
voltage: 20 # Optional
- platform: vacuum
friendly_name: Vacuum
id: 28
idle_status_value: "standby,sleep"
returning_status_value: "docking"
docked_status_value: "charging,chargecompleted"
battery_dp: 14
mode_dp: 27
modes: "smart,standby,chargego,wall_follow,spiral,single"
fan_speed_dp: 30
fan_speeds: "low,normal,high"
clean_time_dp: 33
clean_area_dp: 32
"""
import asyncio import asyncio
import logging import logging
import time
from datetime import timedelta from datetime import timedelta
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntry
import homeassistant.helpers.entity_registry as er import homeassistant.helpers.entity_registry as er
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES, CONF_ENTITIES,
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
@@ -85,14 +20,22 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD, SERVICE_RELOAD,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.reload import async_integration_yaml_config from homeassistant.helpers.reload import async_integration_yaml_config
from .cloud_api import TuyaCloudApi
from .common import TuyaDevice, async_config_entry_by_device_id from .common import TuyaDevice, async_config_entry_by_device_id
from .config_flow import config_schema from .config_flow import config_schema
from .const import CONF_PRODUCT_KEY, DATA_DISCOVERY, DOMAIN, TUYA_DEVICE from .const import (
ATTR_UPDATED_AT,
CONF_PRODUCT_KEY,
DATA_CLOUD,
DATA_DISCOVERY,
DOMAIN,
TUYA_DEVICE
)
from .discovery import TuyaDiscovery from .discovery import TuyaDiscovery
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -116,16 +59,6 @@ SERVICE_SET_DP_SCHEMA = vol.Schema(
) )
@callback
def _async_update_config_entry_if_from_yaml(hass, entries_by_id, conf):
"""Update a config entry with the latest yaml."""
device_id = conf[CONF_DEVICE_ID]
if device_id in entries_by_id and entries_by_id[device_id].source == SOURCE_IMPORT:
entry = entries_by_id[device_id]
hass.config_entries.async_update_entry(entry, data=conf.copy())
async def async_setup(hass: HomeAssistant, config: dict): async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the LocalTuya integration component.""" """Set up the LocalTuya integration component."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
@@ -140,10 +73,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return return
current_entries = hass.config_entries.async_entries(DOMAIN) current_entries = hass.config_entries.async_entries(DOMAIN)
entries_by_id = {entry.data[CONF_DEVICE_ID]: entry for entry in current_entries} # entries_by_id = {entry.data[CONF_DEVICE_ID]: entry for entry in current_entries}
for conf in config[DOMAIN]:
_async_update_config_entry_if_from_yaml(hass, entries_by_id, conf)
reload_tasks = [ reload_tasks = [
hass.config_entries.async_reload(entry.entry_id) hass.config_entries.async_reload(entry.entry_id)
@@ -211,6 +141,18 @@ async def async_setup(hass: HomeAssistant, config: dict):
device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE] device = hass.data[DOMAIN][entry.entry_id][TUYA_DEVICE]
device.async_connect() device.async_connect()
client_id = 'xx'
secret = 'xx'
uid = 'xx'
tuya_api = TuyaCloudApi(hass, "eu", client_id, secret, uid)
res = await tuya_api.async_get_access_token()
_LOGGER.debug("ACCESS TOKEN RES: %s", res)
res = await tuya_api.async_get_devices_list()
for dev_id, dev in tuya_api._device_list.items():
print(f"Name: {dev['name']} \t dev_id {dev['id']} \t key {dev['local_key']} ")
hass.data[DOMAIN][DATA_CLOUD] = tuya_api
_LOGGER.debug("\n\nSTARTING DISCOVERY")
discovery = TuyaDiscovery(_device_discovered) discovery = TuyaDiscovery(_device_discovered)
def _shutdown(event): def _shutdown(event):
@@ -246,13 +188,13 @@ async def async_setup(hass: HomeAssistant, config: dict):
DOMAIN, SERVICE_SET_DP, _handle_set_dp, schema=SERVICE_SET_DP_SCHEMA DOMAIN, SERVICE_SET_DP, _handle_set_dp, schema=SERVICE_SET_DP_SCHEMA
) )
for host_config in config.get(DOMAIN, []): return True
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=host_config
)
)
async def async_migrate_entry(domain):
"""Migrate old entry."""
print("WHYYYYYYY")
_LOGGER.error("WHHHYYYYYY")
return True return True
@@ -260,15 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up LocalTuya integration from a config entry.""" """Set up LocalTuya integration from a config entry."""
unsub_listener = entry.add_update_listener(update_listener) unsub_listener = entry.add_update_listener(update_listener)
device = TuyaDevice(hass, entry.data) # device = TuyaDevice(hass, entry.data)
#
# hass.data[DOMAIN][entry.entry_id] = {
# UNSUB_LISTENER: unsub_listener,
# TUYA_DEVICE: device,
# }
#
async def setup_entities(dev_id):
dev_entry = entry.data[CONF_DEVICES][dev_id]
device = TuyaDevice(hass, dev_entry)
hass.data[DOMAIN][dev_id] = {
UNSUB_LISTENER: unsub_listener,
TUYA_DEVICE: device,
}
hass.data[DOMAIN][entry.entry_id] = { platforms = set(entity[CONF_PLATFORM] for entity in dev_entry[CONF_ENTITIES])
UNSUB_LISTENER: unsub_listener, print("DEV {} platforms: {}".format(dev_id, platforms))
TUYA_DEVICE: device,
}
async def setup_entities():
platforms = set(entity[CONF_PLATFORM] for entity in entry.data[CONF_ENTITIES])
await asyncio.gather( await asyncio.gather(
*[ *[
hass.config_entries.async_forward_entry_setup(entry, platform) hass.config_entries.async_forward_entry_setup(entry, platform)
@@ -277,9 +227,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
) )
device.async_connect() device.async_connect()
await async_remove_orphan_entities(hass, entry) await async_remove_orphan_entities(hass, entry)
hass.async_create_task(setup_entities()) for dev_id in entry.data[CONF_DEVICES]:
hass.async_create_task(setup_entities(dev_id))
return True return True
@@ -310,13 +261,60 @@ async def update_listener(hass, config_entry):
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
async def async_remove_config_entry_device(
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
) -> bool:
"""Remove a config entry from a device."""
print("REMOVING {} FROM {}".format(device_entry.identifiers, config_entry.data))
dev_id = list(device_entry.identifiers)[0][1].split("_")[-1]
_LOGGER.debug("Removing %s", dev_id)
if dev_id not in config_entry.data[CONF_DEVICES]:
_LOGGER.debug(
"Device ID %s not found in config entry: finalizing device removal",
dev_id
)
return True
_LOGGER.debug("Closing device connection for %s", dev_id)
await hass.data[DOMAIN][dev_id][TUYA_DEVICE].close()
new_data = config_entry.data.copy()
new_data[CONF_DEVICES].pop(dev_id)
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
hass.config_entries.async_update_entry(
config_entry,
data=new_data,
)
_LOGGER.debug("Config entry updated")
ent_reg = await er.async_get_registry(hass)
entities = {
ent.unique_id: ent.entity_id
for ent in er.async_entries_for_config_entry(ent_reg, config_entry.entry_id)
if dev_id in ent.unique_id
}
for entity_id in entities.values():
ent_reg.async_remove(entity_id)
print("REMOVED {}".format(entity_id))
_LOGGER.debug("Removed %s entities: finalizing device removal", len(entities))
return True
async def async_remove_orphan_entities(hass, entry): async def async_remove_orphan_entities(hass, entry):
"""Remove entities associated with config entry that has been removed.""" """Remove entities associated with config entry that has been removed."""
ent_reg = await er.async_get_registry(hass) ent_reg = await er.async_get_registry(hass)
entities = { entities = {
int(ent.unique_id.split("_")[-1]): ent.entity_id ent.unique_id: ent.entity_id
for ent in er.async_entries_for_config_entry(ent_reg, entry.entry_id) for ent in er.async_entries_for_config_entry(ent_reg, entry.entry_id)
} }
print("ENTITIES ORPHAN {}".format(entities))
return
res = ent_reg.async_remove('switch.aa')
print("RESULT ORPHAN {}".format(res))
# del entities[101]
for entity in entry.data[CONF_ENTITIES]: for entity in entry.data[CONF_ENTITIES]:
if entity[CONF_ID] in entities: if entity[CONF_ID] in entities:

View File

@@ -4,14 +4,21 @@ import hashlib
import hmac import hmac
import json import json
import requests import requests
import sys
import time import time
# Signature algorithm. # Signature algorithm.
def calc_sign(msg,key): def calc_sign(msg, key):
sign = hmac.new(msg=bytes(msg, 'latin-1'),key = bytes(key, 'latin-1'), digestmod = hashlib.sha256).hexdigest().upper() sign = (
return sign hmac.new(
msg=bytes(msg, "latin-1"),
key=bytes(key, "latin-1"),
digestmod=hashlib.sha256,
)
.hexdigest()
.upper()
)
return sign
class TuyaCloudApi: class TuyaCloudApi:
@@ -20,84 +27,107 @@ class TuyaCloudApi:
def __init__(self, hass, region_code, client_id, secret, user_id): def __init__(self, hass, region_code, client_id, secret, user_id):
"""Initialize the class.""" """Initialize the class."""
self._hass = hass self._hass = hass
self._base_url = f'https://openapi.tuya{region_code}.com' self._base_url = f"https://openapi.tuya{region_code}.com"
self._access_token = ""
self._client_id = client_id self._client_id = client_id
self._secret = secret self._secret = secret
self._user_id = user_id self._user_id = user_id
self._access_token = ""
self._device_list = {}
def generate_payload(self, method, t, url, headers, body = None): def generate_payload(self, method, t, url, headers, body=None):
payload = self._client_id + self._access_token + t payload = self._client_id + self._access_token + t
payload += (method + '\n' + payload += method + "\n"
hashlib.sha256(bytes((body or "").encode('utf-8'))).hexdigest() + '\n' + # Content-SHA256 # Content-SHA256
''.join( payload += hashlib.sha256(bytes((body or "").encode("utf-8"))).hexdigest()
['%s:%s\n'%(key, headers[key]) # Headers payload += (
for key in headers.get("Signature-Headers", "").split(":") "\n"
if key in headers] + "".join(
) + [
'\n/' + url.split('//', 1)[-1].split('/', 1)[-1]) # Url (extracted from 'url') "%s:%s\n" % (key, headers[key]) # Headers
for key in headers.get("Signature-Headers", "").split(":")
if key in headers
]
)
+ "\n/"
+ url.split("//", 1)[-1].split("/", 1)[-1] # Url
)
# print("PAYLOAD: {}".format(payload)) # print("PAYLOAD: {}".format(payload))
return payload return payload
async def async_make_request(self, method, url, body=None, headers={}): async def async_make_request(self, method, url, body=None, headers={}):
"""Perform requests.""" """Perform requests."""
t = str(int(time.time()*1000)) t = str(int(time.time() * 1000))
payload = self.generate_payload(method, t, url, headers, body) payload = self.generate_payload(method, t, url, headers, body)
default_par={ default_par = {
'client_id':self._client_id, "client_id": self._client_id,
'access_token':self._access_token, "access_token": self._access_token,
'sign':calc_sign(payload, self._secret), "sign": calc_sign(payload, self._secret),
't':t, "t": t,
'sign_method':'HMAC-SHA256', "sign_method": "HMAC-SHA256",
} }
full_url = self._base_url + url full_url = self._base_url + url
print("\n" + method + ": [{}]".format(full_url)) print("\n" + method + ": [{}]".format(full_url))
if method == "GET": if method == "GET":
func = functools.partial(requests.get, full_url, headers=dict(default_par,**headers)) func = functools.partial(
requests.get, full_url, headers=dict(default_par, **headers)
)
elif method == "POST": elif method == "POST":
func = functools.partial(requests.post, full_url, headers=dict(default_par,**headers), data=json.dumps(body)) func = functools.partial(
requests.post,
full_url,
headers=dict(default_par, **headers),
data=json.dumps(body),
)
print("BODY: [{}]".format(body)) print("BODY: [{}]".format(body))
elif method == "PUT": elif method == "PUT":
func = functools.partial(requests.put, full_url, headers=dict(default_par,**headers), data=json.dumps(body)) func = functools.partial(
requests.put,
full_url,
headers=dict(default_par, **headers),
data=json.dumps(body),
)
r = await self._hass.async_add_executor_job(func) r = await self._hass.async_add_executor_job(func)
# r = json.dumps(r.json(), indent=2, ensure_ascii=False) # Beautify the format
#r = json.dumps(r.json(), indent=2, ensure_ascii=False) # Beautify the request result format for easy printing and viewing
return r return r
async def async_get_access_token(self): async def async_get_access_token(self):
"""Obtain a valid access token.""" """Obtain a valid access token."""
r = await self.async_make_request("GET", f'/v1.0/token?grant_type=1') r = await self.async_make_request("GET", "/v1.0/token?grant_type=1")
if not r.ok: if not r.ok:
print("Request failed, status {}".format(r.status))
return "Request failed, status " + str(r.status) return "Request failed, status " + str(r.status)
r_json = r.json() r_json = r.json()
if not r_json['success']: if not r_json["success"]:
print("Request failed, reply is {}".format(json.dumps(r_json, indent=2, ensure_ascii=False)))
return f"Error {r_json['code']}: {r_json['msg']}" return f"Error {r_json['code']}: {r_json['msg']}"
print(r.json()) print(r.json())
self._access_token = r.json()['result']['access_token'] self._access_token = r.json()["result"]["access_token"]
print("GET_ACCESS_TOKEN: {}".format(self._access_token)) print("GET_ACCESS_TOKEN: {}".format(self._access_token))
return "ok" return "ok"
async def async_get_devices_list(self): async def async_get_devices_list(self):
"""Obtain the list of devices associated to a user.""" """Obtain the list of devices associated to a user."""
r = await self.async_make_request("GET", url=f'/v1.0/users/{self._user_id}/devices') r = await self.async_make_request(
"GET", url=f"/v1.0/users/{self._user_id}/devices"
)
if not r.ok: if not r.ok:
print("Request failed, status {}".format(r.status)) return "Request failed, status " + str(r.status)
return None
r_json = r.json() r_json = r.json()
if not r_json['success']: if not r_json["success"]:
print("Request failed, reply is {}".format(json.dumps(r_json, indent=2, ensure_ascii=False))) print(
return None "Request failed, reply is {}".format(
json.dumps(r_json, indent=2, ensure_ascii=False)
)
)
return f"Error {r_json['code']}: {r_json['msg']}"
return r self._device_list = {dev['id']: dev for dev in r_json["result"]}
# print("DEV__LIST: {}".format(self._device_list))
return "ok"

View File

@@ -5,10 +5,12 @@ from datetime import timedelta
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES, CONF_ENTITIES,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_MODEL,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
) )
@@ -23,9 +25,9 @@ from homeassistant.helpers.restore_state import RestoreEntity
from . import pytuya from . import pytuya
from .const import ( from .const import (
CONF_LOCAL_KEY, CONF_LOCAL_KEY,
CONF_PRODUCT_KEY,
CONF_PROTOCOL_VERSION, CONF_PROTOCOL_VERSION,
DOMAIN, DOMAIN,
DATA_CLOUD,
TUYA_DEVICE, TUYA_DEVICE,
) )
@@ -42,7 +44,7 @@ def prepare_setup_entities(hass, config_entry, platform):
if not entities_to_setup: if not entities_to_setup:
return None, None return None, None
tuyainterface = hass.data[DOMAIN][config_entry.entry_id][TUYA_DEVICE] tuyainterface = []
return tuyainterface, entities_to_setup return tuyainterface, entities_to_setup
@@ -55,28 +57,44 @@ async def async_setup_entry(
This is a generic method and each platform should lock domain and This is a generic method and each platform should lock domain and
entity_class with functools.partial. entity_class with functools.partial.
""" """
tuyainterface, entities_to_setup = prepare_setup_entities( print("ASYNC_SETUP_ENTRY: {} {} {}".format(
hass, config_entry, domain config_entry.data,
entity_class,
flow_schema(None).items())
) )
if not entities_to_setup:
return
dps_config_fields = list(get_dps_for_platform(flow_schema))
entities = [] entities = []
for device_config in entities_to_setup:
# Add DPS used by this platform to the request list
for dp_conf in dps_config_fields:
if dp_conf in device_config:
tuyainterface.dps_to_request[device_config[dp_conf]] = None
entities.append( for dev_id in config_entry.data[CONF_DEVICES]:
entity_class( # entities_to_setup = prepare_setup_entities(
tuyainterface, # hass, config_entry.data[dev_id], domain
config_entry, # )
device_config[CONF_ID], dev_entry = config_entry.data[CONF_DEVICES][dev_id]
) entities_to_setup = [
) entity
for entity in dev_entry[CONF_ENTITIES]
if entity[CONF_PLATFORM] == domain
]
if len(entities_to_setup) > 0:
tuyainterface = hass.data[DOMAIN][dev_id][TUYA_DEVICE]
dps_config_fields = list(get_dps_for_platform(flow_schema))
for entity_config in entities_to_setup:
# Add DPS used by this platform to the request list
for dp_conf in dps_config_fields:
if dp_conf in entity_config:
tuyainterface.dps_to_request[entity_config[dp_conf]] = None
entities.append(
entity_class(
tuyainterface,
dev_entry,
entity_config[CONF_ID],
)
)
print("ADDING {} entities".format(len(entities)))
async_add_entities(entities) async_add_entities(entities)
@@ -90,7 +108,7 @@ def get_dps_for_platform(flow_schema):
def get_entity_config(config_entry, dp_id): def get_entity_config(config_entry, dp_id):
"""Return entity config for a given DPS id.""" """Return entity config for a given DPS id."""
for entity in config_entry.data[CONF_ENTITIES]: for entity in config_entry[CONF_ENTITIES]:
if entity[CONF_ID] == dp_id: if entity[CONF_ID] == dp_id:
return entity return entity
raise Exception(f"missing entity config for id {dp_id}") raise Exception(f"missing entity config for id {dp_id}")
@@ -121,6 +139,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._connect_task = None self._connect_task = None
self._disconnect_task = None self._disconnect_task = None
self._unsub_interval = None self._unsub_interval = None
self._local_key = self._config_entry[CONF_LOCAL_KEY]
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID]) self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
# This has to be done in case the device type is type_0d # This has to be done in case the device type is type_0d
@@ -145,7 +164,7 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._interface = await pytuya.connect( self._interface = await pytuya.connect(
self._config_entry[CONF_HOST], self._config_entry[CONF_HOST],
self._config_entry[CONF_DEVICE_ID], self._config_entry[CONF_DEVICE_ID],
self._config_entry[CONF_LOCAL_KEY], self._local_key,
float(self._config_entry[CONF_PROTOCOL_VERSION]), float(self._config_entry[CONF_PROTOCOL_VERSION]),
self, self,
) )
@@ -180,8 +199,30 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._async_refresh, self._async_refresh,
timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]), timedelta(seconds=self._config_entry[CONF_SCAN_INTERVAL]),
) )
except Exception: # pylint: disable=broad-except except UnicodeDecodeError as e: # pylint: disable=broad-except
dev_id = self._config_entry[CONF_DEVICE_ID]
cloud_devs = self._hass.data[DOMAIN][DATA_CLOUD]._device_list
if dev_id in cloud_devs:
old_key = self._local_key
self._local_key = cloud_devs[dev_id].get(CONF_LOCAL_KEY)
self.error(
"New local key for %s: from %s to %s",
dev_id,
old_key,
self._local_key
)
self.exception(
f"Connect to {self._config_entry[CONF_HOST]} failed: %s",
type(e)
)
if self._interface is not None:
await self._interface.close()
self._interface = None
except Exception as e: # pylint: disable=broad-except
self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed") self.exception(f"Connect to {self._config_entry[CONF_HOST]} failed")
self.error("BBBB: %s", type(e))
if self._interface is not None: if self._interface is not None:
await self._interface.close() await self._interface.close()
self._interface = None self._interface = None
@@ -201,6 +242,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
await self._interface.close() await self._interface.close()
if self._disconnect_task is not None: if self._disconnect_task is not None:
self._disconnect_task() self._disconnect_task()
self.debug(
"Closed connection with device %s.",
self._config_entry[CONF_FRIENDLY_NAME]
)
async def set_dp(self, state, dp_index): async def set_dp(self, state, dp_index):
"""Change value of a DP of the Tuya device.""" """Change value of a DP of the Tuya device."""
@@ -259,7 +304,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
self._config = get_entity_config(config_entry, dp_id) self._config = get_entity_config(config_entry, dp_id)
self._dp_id = dp_id self._dp_id = dp_id
self._status = {} self._status = {}
self.set_logger(logger, self._config_entry.data[CONF_DEVICE_ID]) self.set_logger(logger, self._config_entry[CONF_DEVICE_ID])
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Subscribe localtuya events.""" """Subscribe localtuya events."""
@@ -281,27 +326,28 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
self.status_updated() self.status_updated()
self.schedule_update_ha_state() self.schedule_update_ha_state()
signal = f"localtuya_{self._config_entry.data[CONF_DEVICE_ID]}" signal = f"localtuya_{self._config_entry[CONF_DEVICE_ID]}"
self.async_on_remove( self.async_on_remove(
async_dispatcher_connect(self.hass, signal, _update_handler) async_dispatcher_connect(self.hass, signal, _update_handler)
) )
signal = f"localtuya_entity_{self._config_entry.data[CONF_DEVICE_ID]}" signal = f"localtuya_entity_{self._config_entry[CONF_DEVICE_ID]}"
async_dispatcher_send(self.hass, signal, self.entity_id) async_dispatcher_send(self.hass, signal, self.entity_id)
@property @property
def device_info(self): def device_info(self):
"""Return device information for the device registry.""" """Return device information for the device registry."""
model = self._config_entry.get(CONF_MODEL, "Tuya generic")
return { return {
"identifiers": { "identifiers": {
# Serial numbers are unique identifiers within a specific domain # Serial numbers are unique identifiers within a specific domain
(DOMAIN, f"local_{self._config_entry.data[CONF_DEVICE_ID]}") (DOMAIN, f"local_{self._config_entry[CONF_DEVICE_ID]}")
}, },
"name": self._config_entry.data[CONF_FRIENDLY_NAME], "name": self._config_entry[CONF_FRIENDLY_NAME],
"manufacturer": "Unknown", "manufacturer": "Tuya",
"model": self._config_entry.data.get(CONF_PRODUCT_KEY, "Tuya generic"), "model": f"{model} ({self._config_entry[CONF_DEVICE_ID]})",
"sw_version": self._config_entry.data[CONF_PROTOCOL_VERSION], "sw_version": self._config_entry[CONF_PROTOCOL_VERSION],
} }
@property @property
@@ -317,7 +363,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
@property @property
def unique_id(self): def unique_id(self):
"""Return unique device identifier.""" """Return unique device identifier."""
return f"local_{self._config_entry.data[CONF_DEVICE_ID]}_{self._dp_id}" return f"local_{self._config_entry[CONF_DEVICE_ID]}_{self._dp_id}"
def has_config(self, attr): def has_config(self, attr):
"""Return if a config parameter has a valid value.""" """Return if a config parameter has a valid value."""

View File

@@ -1,30 +1,45 @@
"""Config flow for LocalTuya integration integration.""" """Config flow for LocalTuya integration integration."""
import errno import errno
import logging import logging
import time
from importlib import import_module from importlib import import_module
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES, CONF_ENTITIES,
CONF_FRIENDLY_NAME, CONF_FRIENDLY_NAME,
CONF_HOST, CONF_HOST,
CONF_ID, CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PLATFORM, CONF_PLATFORM,
CONF_SCAN_INTERVAL, CONF_SCAN_INTERVAL,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_REGION,
) )
from homeassistant.core import callback from homeassistant.core import callback
from .cloud_api import TuyaCloudApi
from .common import async_config_entry_by_device_id, pytuya from .common import async_config_entry_by_device_id, pytuya
from .const import CONF_DPS_STRINGS # pylint: disable=unused-import
from .const import ( from .const import (
CONF_ACTION,
CONF_ADD_DEVICE,
CONF_EDIT_DEVICE,
CONF_SETUP_CLOUD,
CONF_LOCAL_KEY, CONF_LOCAL_KEY,
CONF_PRODUCT_KEY, CONF_PRODUCT_NAME,
CONF_PROTOCOL_VERSION, CONF_PROTOCOL_VERSION,
CONF_USER_ID,
CONF_DPS_STRINGS,
ATTR_UPDATED_AT,
DATA_DISCOVERY, DATA_DISCOVERY,
DATA_CLOUD,
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
) )
@@ -33,11 +48,32 @@ from .discovery import discover
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORM_TO_ADD = "platform_to_add" PLATFORM_TO_ADD = "platform_to_add"
NO_ADDITIONAL_PLATFORMS = "no_additional_platforms" NO_ADDITIONAL_ENTITIES = "no_additional_entities"
DISCOVERED_DEVICE = "discovered_device" DISCOVERED_DEVICE = "discovered_device"
CUSTOM_DEVICE = "..." CUSTOM_DEVICE = "..."
CONF_ACTIONS = {
CONF_ADD_DEVICE: "Add a new device",
CONF_EDIT_DEVICE: "Edit a device",
CONF_SETUP_CLOUD: "Reconfigure Cloud API account",
}
CONFIGURE_SCHEMA = vol.Schema(
{
vol.Required(CONF_ACTION, default=CONF_ADD_DEVICE): vol.In(CONF_ACTIONS),
}
)
CLOUD_SETUP_SCHEMA = vol.Schema(
{
vol.Required(CONF_REGION, default="eu"): vol.In(["eu", "us", "cn", "in"]),
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
vol.Required(CONF_USER_ID): cv.string,
}
)
BASIC_INFO_SCHEMA = vol.Schema( BASIC_INFO_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_FRIENDLY_NAME): str, vol.Required(CONF_FRIENDLY_NAME): str,
@@ -49,7 +85,6 @@ BASIC_INFO_SCHEMA = vol.Schema(
} }
) )
DEVICE_SCHEMA = vol.Schema( DEVICE_SCHEMA = vol.Schema(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(CONF_HOST): cv.string,
@@ -62,23 +97,29 @@ DEVICE_SCHEMA = vol.Schema(
) )
PICK_ENTITY_SCHEMA = vol.Schema( PICK_ENTITY_SCHEMA = vol.Schema(
{vol.Required(PLATFORM_TO_ADD, default=PLATFORMS[0]): vol.In(PLATFORMS)} {vol.Required(PLATFORM_TO_ADD, default="switch"): vol.In(PLATFORMS)}
) )
def user_schema(devices, entries): def devices_schema(discovered_devices, cloud_devices_list):
"""Create schema for user step.""" """Create schema for devices step."""
devices = {dev_id: dev["ip"] for dev_id, dev in devices.items()} devices = {}
devices.update( for dev_id, dev in discovered_devices.items():
{ dev_name = dev_id
ent.data[CONF_DEVICE_ID]: ent.data[CONF_FRIENDLY_NAME] if dev_id in cloud_devices_list.keys():
for ent in entries dev_name = cloud_devices_list[dev_id][CONF_NAME]
if ent.source != SOURCE_IMPORT devices[dev_id] = f"{dev_name} ({discovered_devices[dev_id]['ip']})"
}
) devices.update({CUSTOM_DEVICE: CUSTOM_DEVICE})
device_list = [f"{key} ({value})" for key, value in devices.items()]
# devices.update(
# {
# ent.data[CONF_DEVICE_ID]: ent.data[CONF_FRIENDLY_NAME]
# for ent in entries
# }
# )
return vol.Schema( return vol.Schema(
{vol.Required(DISCOVERED_DEVICE): vol.In(device_list + [CUSTOM_DEVICE])} {vol.Required(DISCOVERED_DEVICE): vol.In(devices)}
) )
@@ -210,10 +251,36 @@ async def validate_input(hass: core.HomeAssistant, data):
return dps_string_list(detected_dps) return dps_string_list(detected_dps)
async def attempt_cloud_connection(hass, user_input):
"""Create device."""
tuya_api = TuyaCloudApi(
hass,
user_input.get(CONF_REGION),
user_input.get(CONF_CLIENT_ID),
user_input.get(CONF_CLIENT_SECRET),
user_input.get(CONF_USER_ID)
)
res = await tuya_api.async_get_access_token()
_LOGGER.debug("ACCESS TOKEN RES: %s", res)
if res != "ok":
return {"reason": "authentication_failed", "msg": res}
res = await tuya_api.async_get_devices_list()
_LOGGER.debug("DEV LIST RES: %s", res)
if res != "ok":
return {"reason": "device_list_failed", "msg": res}
for dev_id, dev in tuya_api._device_list.items():
print(f"Name: {dev['name']} \t dev_id {dev['id']} \t key {dev['local_key']} ")
return {}
class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for LocalTuya integration.""" """Handle a config flow for LocalTuya integration."""
VERSION = 1 VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@staticmethod @staticmethod
@@ -224,29 +291,139 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self): def __init__(self):
"""Initialize a new LocaltuyaConfigFlow.""" """Initialize a new LocaltuyaConfigFlow."""
self.basic_info = None
self.dps_strings = []
self.platform = None
self.devices = {}
self.selected_device = None
self.entities = []
async def async_step_user(self, user_input=None): async def async_step_user(self, user_input=None):
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors = {}
placeholders = {}
if user_input is not None: if user_input is not None:
print("ECCOCI")
res = await attempt_cloud_connection(self.hass, user_input)
if len(res) == 0:
return await self._create_entry(user_input)
errors["base"] = res["reason"]
placeholders = {"msg": res["msg"]}
defaults = {}
defaults[CONF_REGION] = 'eu'
defaults[CONF_CLIENT_ID] = 'xx'
defaults[CONF_CLIENT_SECRET] = 'xx'
defaults[CONF_USER_ID] = 'xx'
defaults.update(user_input or {})
return self.async_show_form(
step_id="user",
data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults),
errors=errors,
description_placeholders=placeholders
)
async def _create_entry(self, user_input):
"""Register new entry."""
# if not self.unique_id:
# await self.async_set_unique_id(password)
# self._abort_if_unique_id_configured()
if self._async_current_entries():
return self.async_abort(reason="already_configured")
await self.async_set_unique_id(user_input.get(CONF_USER_ID))
user_input[CONF_DEVICES] = {}
return self.async_create_entry(
title="LocalTuya",
data=user_input,
)
async def async_step_import(self, user_input):
"""Handle import from YAML."""
_LOGGER.error("Configuration via YAML file is no longer supported by this integration.")
# await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
# self._abort_if_unique_id_configured(updates=user_input)
# return self.async_create_entry(
# title=f"{user_input[CONF_FRIENDLY_NAME]} (YAML)", data=user_input
# )
class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for LocalTuya integration."""
def __init__(self, config_entry):
"""Initialize localtuya options flow."""
self.config_entry = config_entry
# self.dps_strings = config_entry.data.get(CONF_DPS_STRINGS, gen_dps_strings())
# self.entities = config_entry.data[CONF_ENTITIES]
self.selected_device = None
self.basic_info = None
self.dps_strings = []
self.selected_platform = None
self.devices = {}
self.entities = []
self.data = None
async def async_step_init(self, user_input=None):
"""Manage basic options."""
# device_id = self.config_entry.data[CONF_DEVICE_ID]
if user_input is not None:
if user_input.get(CONF_ACTION) == CONF_SETUP_CLOUD:
return await self.async_step_cloud_setup()
if user_input.get(CONF_ACTION) == CONF_ADD_DEVICE:
return await self.async_step_add_device()
return self.async_show_form(
step_id="init",
data_schema=CONFIGURE_SCHEMA,
)
async def async_step_cloud_setup(self, user_input=None):
"""Handle the initial step."""
errors = {}
placeholders = {}
if user_input is not None:
res = await attempt_cloud_connection(self.hass, user_input)
if len(res) == 0:
new_data = self.config_entry.data.copy()
new_data.update(user_input)
print("CURR_ENTRY {}".format(self.config_entry))
print("NEW DATA {}".format(new_data))
self.hass.config_entries.async_update_entry(
self.config_entry,
data=new_data,
)
return self.async_create_entry(title="", data={})
errors["base"] = res["reason"]
placeholders = {"msg": res["msg"]}
defaults = self.config_entry.data.copy()
defaults.update(user_input or {})
return self.async_show_form(
step_id="cloud_setup",
data_schema=schema_defaults(CLOUD_SETUP_SCHEMA, **defaults),
errors=errors,
description_placeholders=placeholders
)
async def async_step_add_device(self, user_input=None):
"""Handle adding a new device."""
# Use cache if available or fallback to manual discovery
errors = {}
if user_input is not None:
print("Selected {}".format(user_input))
if user_input[DISCOVERED_DEVICE] != CUSTOM_DEVICE: if user_input[DISCOVERED_DEVICE] != CUSTOM_DEVICE:
self.selected_device = user_input[DISCOVERED_DEVICE].split(" ")[0] self.selected_device = user_input[DISCOVERED_DEVICE]
return await self.async_step_basic_info() return await self.async_step_basic_info()
# Use cache if available or fallback to manual discovery discovered_devices = {}
devices = {}
data = self.hass.data.get(DOMAIN) data = self.hass.data.get(DOMAIN)
if data and DATA_DISCOVERY in data: if data and DATA_DISCOVERY in data:
devices = data[DATA_DISCOVERY].devices discovered_devices = data[DATA_DISCOVERY].devices
else: else:
try: try:
devices = await discover() discovered_devices = await discover()
except OSError as ex: except OSError as ex:
if ex.errno == errno.EADDRINUSE: if ex.errno == errno.EADDRINUSE:
errors["base"] = "address_in_use" errors["base"] = "address_in_use"
@@ -258,29 +435,38 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.devices = { self.devices = {
ip: dev ip: dev
for ip, dev in devices.items() for ip, dev in discovered_devices.items()
if dev["gwId"] not in self._async_current_ids() if dev["gwId"] not in self.config_entry.data[CONF_DEVICES]
} }
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="add_device",
data_schema=devices_schema(
self.devices,
self.hass.data[DOMAIN][DATA_CLOUD]._device_list
),
errors=errors, errors=errors,
data_schema=user_schema(self.devices, self._async_current_entries()),
) )
async def async_step_basic_info(self, user_input=None): async def async_step_basic_info(self, user_input=None):
"""Handle input of basic info.""" """Handle input of basic info."""
errors = {} errors = {}
dev_id = self.selected_device
if user_input is not None: if user_input is not None:
await self.async_set_unique_id(user_input[CONF_DEVICE_ID]) print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID))
try: try:
self.basic_info = user_input self.basic_info = user_input
if self.selected_device is not None: if dev_id is not None:
self.basic_info[CONF_PRODUCT_KEY] = self.devices[ # self.basic_info[CONF_PRODUCT_KEY] = self.devices[
self.selected_device # self.selected_device
]["productKey"] # ]["productKey"]
cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD]._device_list
if dev_id in cloud_devs:
self.basic_info[CONF_MODEL] = cloud_devs[dev_id].get(CONF_PRODUCT_NAME)
self.dps_strings = await validate_input(self.hass, user_input) self.dps_strings = await validate_input(self.hass, user_input)
print("ZIO KEN!! {} ".format(self.dps_strings))
return await self.async_step_pick_entity_type() return await self.async_step_pick_entity_type()
except CannotConnect: except CannotConnect:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
@@ -293,22 +479,27 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
# If selected device exists as a config entry, load config from it # If selected device exists as a config entry, load config from it
if self.selected_device in self._async_current_ids(): if self.selected_device in self.config_entry.data[CONF_DEVICES]:
entry = async_config_entry_by_device_id(self.hass, self.selected_device) print("ALREADY EXISTING!! {}".format(self.selected_device))
await self.async_set_unique_id(entry.data[CONF_DEVICE_ID]) # entry = self.config_entry.data[CONF_DEVICES][self.selected_device]
self.basic_info = entry.data.copy() # await self.async_set_unique_id(entry.data[CONF_DEVICE_ID])
self.dps_strings = self.basic_info.pop(CONF_DPS_STRINGS).copy() # self.basic_info = entry.data.copy()
self.entities = self.basic_info.pop(CONF_ENTITIES).copy() # self.dps_strings = self.basic_info.pop(CONF_DPS_STRINGS).copy()
return await self.async_step_pick_entity_type() # self.entities = self.basic_info.pop(CONF_ENTITIES).copy()
# return await self.async_step_pick_entity_type()
# Insert default values from discovery if present # Insert default values from discovery and cloud if present
defaults = {} defaults = {}
defaults.update(user_input or {}) defaults.update(user_input or {})
if self.selected_device is not None: if dev_id is not None:
device = self.devices[self.selected_device] device = self.devices[dev_id]
defaults[CONF_HOST] = device.get("ip") defaults[CONF_HOST] = device.get("ip")
defaults[CONF_DEVICE_ID] = device.get("gwId") defaults[CONF_DEVICE_ID] = device.get("gwId")
defaults[CONF_PROTOCOL_VERSION] = device.get("version") defaults[CONF_PROTOCOL_VERSION] = device.get("version")
cloud_devs = self.hass.data[DOMAIN][DATA_CLOUD]._device_list
if dev_id in cloud_devs:
defaults[CONF_LOCAL_KEY] = cloud_devs[dev_id].get(CONF_LOCAL_KEY)
defaults[CONF_FRIENDLY_NAME] = cloud_devs[dev_id].get(CONF_NAME)
return self.async_show_form( return self.async_show_form(
step_id="basic_info", step_id="basic_info",
@@ -316,116 +507,12 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
) )
async def async_step_pick_entity_type(self, user_input=None):
"""Handle asking if user wants to add another entity."""
if user_input is not None:
if user_input.get(NO_ADDITIONAL_PLATFORMS):
config = {
**self.basic_info,
CONF_DPS_STRINGS: self.dps_strings,
CONF_ENTITIES: self.entities,
}
entry = async_config_entry_by_device_id(self.hass, self.unique_id)
if entry:
self.hass.config_entries.async_update_entry(entry, data=config)
return self.async_abort(reason="device_updated")
return self.async_create_entry(
title=config[CONF_FRIENDLY_NAME], data=config
)
self.platform = user_input[PLATFORM_TO_ADD]
return await self.async_step_add_entity()
# Add a checkbox that allows bailing out from config flow iff at least one
# entity has been added
schema = PICK_ENTITY_SCHEMA
if self.platform is not None:
schema = schema.extend(
{vol.Required(NO_ADDITIONAL_PLATFORMS, default=True): bool}
)
return self.async_show_form(step_id="pick_entity_type", data_schema=schema)
async def async_step_add_entity(self, user_input=None):
"""Handle adding a new entity."""
errors = {}
if user_input is not None:
already_configured = any(
switch[CONF_ID] == int(user_input[CONF_ID].split(" ")[0])
for switch in self.entities
)
if not already_configured:
user_input[CONF_PLATFORM] = self.platform
self.entities.append(strip_dps_values(user_input, self.dps_strings))
return await self.async_step_pick_entity_type()
errors["base"] = "entity_already_configured"
return self.async_show_form(
step_id="add_entity",
data_schema=platform_schema(self.platform, self.dps_strings),
errors=errors,
description_placeholders={"platform": self.platform},
)
async def async_step_import(self, user_input):
"""Handle import from YAML."""
await self.async_set_unique_id(user_input[CONF_DEVICE_ID])
self._abort_if_unique_id_configured(updates=user_input)
return self.async_create_entry(
title=f"{user_input[CONF_FRIENDLY_NAME]} (YAML)", data=user_input
)
class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for LocalTuya integration."""
def __init__(self, config_entry):
"""Initialize localtuya options flow."""
self.config_entry = config_entry
self.dps_strings = config_entry.data.get(CONF_DPS_STRINGS, gen_dps_strings())
self.entities = config_entry.data[CONF_ENTITIES]
self.data = None
async def async_step_init(self, user_input=None):
"""Manage basic options."""
device_id = self.config_entry.data[CONF_DEVICE_ID]
if user_input is not None:
self.data = user_input.copy()
self.data.update(
{
CONF_DEVICE_ID: device_id,
CONF_DPS_STRINGS: self.dps_strings,
CONF_ENTITIES: [],
}
)
if len(user_input[CONF_ENTITIES]) > 0:
entity_ids = [
int(entity.split(" ")[0]) for entity in user_input[CONF_ENTITIES]
]
self.entities = [
entity
for entity in self.config_entry.data[CONF_ENTITIES]
if entity[CONF_ID] in entity_ids
]
return await self.async_step_entity()
# Not supported for YAML imports
if self.config_entry.source == config_entries.SOURCE_IMPORT:
return await self.async_step_yaml_import()
return self.async_show_form(
step_id="init",
data_schema=schema_defaults(
options_schema(self.entities), **self.config_entry.data
),
description_placeholders={"device_id": device_id},
)
async def async_step_entity(self, user_input=None): async def async_step_entity(self, user_input=None):
"""Manage entity settings.""" """Manage entity settings."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:
print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID))
print("ZIO KEN!! {} ".format(self.dps_strings))
entity = strip_dps_values(user_input, self.dps_strings) entity = strip_dps_values(user_input, self.dps_strings)
entity[CONF_ID] = self.current_entity[CONF_ID] entity[CONF_ID] = self.current_entity[CONF_ID]
entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM] entity[CONF_PLATFORM] = self.current_entity[CONF_PLATFORM]
@@ -454,11 +541,105 @@ class LocalTuyaOptionsFlowHandler(config_entries.OptionsFlow):
}, },
) )
async def async_step_pick_entity_type(self, user_input=None):
"""Handle asking if user wants to add another entity."""
if user_input is not None:
print("INPUT3!! {} {}".format(user_input, self.basic_info))
print("AAAZIO KEN!! {} ".format(self.dps_strings))
print("NAAA!! {} ".format(user_input.get(NO_ADDITIONAL_ENTITIES)))
if user_input.get(NO_ADDITIONAL_ENTITIES):
print("INPUT4!! {}".format(self.dps_strings))
print("INPUT4!! {}".format(self.entities))
config = {
**self.basic_info,
CONF_DPS_STRINGS: self.dps_strings,
CONF_ENTITIES: self.entities,
}
print("NEW CONFIG!! {}".format(config))
# self.config_entry.data[CONF_DEVICES]
entry = async_config_entry_by_device_id(self.hass, self.unique_id)
dev_id = self.basic_info.get(CONF_DEVICE_ID)
if dev_id in self.config_entry.data[CONF_DEVICES]:
print("AGGIORNO !! {}".format(dev_id))
self.hass.config_entries.async_update_entry(self.config_entry, data=config)
return self.async_abort(
reason="device_success",
description_placeholders={
"dev_name": config.get(CONF_FRIENDLY_NAME),
"action": "updated"
}
)
print("CREO NUOVO DEVICE!! {}".format(dev_id))
new_data = self.config_entry.data.copy()
print("PRE: {}".format(new_data))
new_data[ATTR_UPDATED_AT] = str(int(time.time() * 1000))
# new_data[CONF_DEVICES]["AZZ"] = "OK"
new_data[CONF_DEVICES].update({dev_id: config})
print("POST: {}".format(new_data))
self.hass.config_entries.async_update_entry(
self.config_entry,
data=new_data,
)
return self.async_create_entry(title="", data={})
self.async_create_entry(title="", data={})
print("DONE! now message {}".format(new_data))
return self.async_abort(
reason="device_success",
description_placeholders={
"dev_name": config.get(CONF_FRIENDLY_NAME),
"action": "created"
}
)
# return self.async_create_entry(
# title=config[CONF_FRIENDLY_NAME], data=config
# )
print("MA ZZZIO KEN!! {} ".format(self.dps_strings))
self.selected_platform = user_input[PLATFORM_TO_ADD]
return await self.async_step_add_entity()
# Add a checkbox that allows bailing out from config flow if at least one
# entity has been added
schema = PICK_ENTITY_SCHEMA
if self.selected_platform is not None:
schema = schema.extend(
{vol.Required(NO_ADDITIONAL_ENTITIES, default=True): bool}
)
return self.async_show_form(step_id="pick_entity_type", data_schema=schema)
async def async_step_add_entity(self, user_input=None):
"""Handle adding a new entity."""
errors = {}
if user_input is not None:
print("INPUT3!! {} {}".format(user_input, CONF_DEVICE_ID))
already_configured = any(
switch[CONF_ID] == int(user_input[CONF_ID].split(" ")[0])
for switch in self.entities
)
if not already_configured:
user_input[CONF_PLATFORM] = self.selected_platform
self.entities.append(strip_dps_values(user_input, self.dps_strings))
return await self.async_step_pick_entity_type()
errors["base"] = "entity_already_configured"
return self.async_show_form(
step_id="add_entity",
data_schema=platform_schema(self.selected_platform, self.dps_strings),
errors=errors,
description_placeholders={"platform": self.selected_platform},
)
async def async_step_yaml_import(self, user_input=None): async def async_step_yaml_import(self, user_input=None):
"""Manage YAML imports.""" """Manage YAML imports."""
if user_input is not None: _LOGGER.error("Configuration via YAML file is no longer supported by this integration.")
return self.async_create_entry(title="", data={}) # if user_input is not None:
return self.async_show_form(step_id="yaml_import") # return self.async_create_entry(title="", data={})
# return self.async_show_form(step_id="yaml_import")
@property @property
def current_entity(self): def current_entity(self):

View File

@@ -1,13 +1,43 @@
"""Constants for localtuya integration.""" """Constants for localtuya integration."""
DOMAIN = "localtuya"
DATA_DISCOVERY = "discovery"
DATA_CLOUD = "cloud_data"
# Platforms in this list must support config flows
PLATFORMS = [
"binary_sensor",
"climate",
"cover",
"fan",
"light",
"number",
"select",
"sensor",
"switch",
"vacuum",
]
TUYA_DEVICE = "tuya_device"
ATTR_CURRENT = "current" ATTR_CURRENT = "current"
ATTR_CURRENT_CONSUMPTION = "current_consumption" ATTR_CURRENT_CONSUMPTION = "current_consumption"
ATTR_VOLTAGE = "voltage" ATTR_VOLTAGE = "voltage"
ATTR_UPDATED_AT = "updated_at"
# config flow
CONF_LOCAL_KEY = "local_key" CONF_LOCAL_KEY = "local_key"
CONF_PROTOCOL_VERSION = "protocol_version" CONF_PROTOCOL_VERSION = "protocol_version"
CONF_DPS_STRINGS = "dps_strings" CONF_DPS_STRINGS = "dps_strings"
CONF_PRODUCT_KEY = "product_key" CONF_PRODUCT_KEY = "product_key"
CONF_PRODUCT_NAME = "product_name"
CONF_USER_ID = "user_id"
CONF_ACTION = "action"
CONF_ADD_DEVICE = "add_device"
CONF_EDIT_DEVICE = "edit_device"
CONF_SETUP_CLOUD = "setup_cloud"
# light # light
CONF_BRIGHTNESS_LOWER = "brightness_lower" CONF_BRIGHTNESS_LOWER = "brightness_lower"
@@ -81,23 +111,3 @@ CONF_FAULT_DP = "fault_dp"
CONF_PAUSED_STATE = "paused_state" CONF_PAUSED_STATE = "paused_state"
CONF_RETURN_MODE = "return_mode" CONF_RETURN_MODE = "return_mode"
CONF_STOP_STATUS = "stop_status" CONF_STOP_STATUS = "stop_status"
DATA_DISCOVERY = "discovery"
DOMAIN = "localtuya"
# Platforms in this list must support config flows
PLATFORMS = [
"binary_sensor",
"climate",
"cover",
"fan",
"light",
"number",
"select",
"sensor",
"switch",
"vacuum",
]
TUYA_DEVICE = "tuya_device"

View File

@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN from .const import DOMAIN, DATA_CLOUD
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
@@ -16,6 +16,8 @@ async def async_get_config_entry_diagnostics(
data = {} data = {}
data = {**entry.data} data = {**entry.data}
# print("DATA is {}".format(data)) # print("DATA is {}".format(data))
tuya_api = hass.data[DOMAIN][DATA_CLOUD]
data["cloud_devices"] = tuya_api._device_list
# censoring private information # censoring private information
# data["token"] = re.sub(r"[^\-]", "*", data["token"]) # data["token"] = re.sub(r"[^\-]", "*", data["token"])

View File

@@ -71,7 +71,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol):
def device_found(self, device): def device_found(self, device):
"""Discover a new device.""" """Discover a new device."""
if device.get("ip") not in self.devices: if device.get("gwId") not in self.devices:
self.devices[device.get("gwId")] = device self.devices[device.get("gwId")] = device
_LOGGER.debug("Discovered device: %s", device) _LOGGER.debug("Discovered device: %s", device)

View File

@@ -12,16 +12,13 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Add Tuya device", "title": "Main Configuration",
"description": "Fill in the basic details and pick device type. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will name each sub-device in the following steps.", "description": "Input the credentials for Tuya Cloud API.",
"data": { "data": {
"name": "Name", "region": "API server region",
"host": "Host", "client_id": "Client ID",
"device_id": "Device ID", "client_secret": "Secret",
"local_key": "Local key", "user_id": "User ID"
"protocol_version": "Protocol Version",
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
"device_type": "Device type"
} }
}, },
"power_outlet": { "power_outlet": {
@@ -39,5 +36,98 @@
} }
} }
}, },
"options": {
"step": {
"init": {
"title": "LocalTuya Configuration",
"description": "Please select the desired actionSSSS.",
"data": {
"add_device": "Add a new device",
"edit_device": "Edit a device",
"delete_device": "Delete a device",
"setup_cloud": "Reconfigure Cloud API account"
}
},
"entity": {
"title": "Entity Configuration",
"description": "Editing entity with DPS `{id}` and platform `{platform}`.",
"data": {
"id": "ID",
"friendly_name": "Friendly name",
"current": "Current",
"current_consumption": "Current Consumption",
"voltage": "Voltage",
"commands_set": "Open_Close_Stop Commands Set",
"positioning_mode": "Positioning mode",
"current_position_dp": "Current Position (for *position* mode only)",
"set_position_dp": "Set Position (for *position* mode only)",
"position_inverted": "Invert 0-100 position (for *position* mode only)",
"span_time": "Full opening time, in secs. (for *timed* mode only)",
"unit_of_measurement": "Unit of Measurement",
"device_class": "Device Class",
"scaling": "Scaling Factor",
"state_on": "On Value",
"state_off": "Off Value",
"powergo_dp": "Power DP (Usually 25 or 2)",
"idle_status_value": "Idle Status (comma-separated)",
"returning_status_value": "Returning Status",
"docked_status_value": "Docked Status (comma-separated)",
"fault_dp": "Fault DP (Usually 11)",
"battery_dp": "Battery status DP (Usually 14)",
"mode_dp": "Mode DP (Usually 27)",
"modes": "Modes list",
"return_mode": "Return home mode",
"fan_speed_dp": "Fan speeds DP (Usually 30)",
"fan_speeds": "Fan speeds list (comma-separated)",
"clean_time_dp": "Clean Time DP (Usually 33)",
"clean_area_dp": "Clean Area DP (Usually 32)",
"clean_record_dp": "Clean Record DP (Usually 34)",
"locate_dp": "Locate DP (Usually 31)",
"paused_state": "Pause state (pause, paused, etc)",
"stop_status": "Stop status",
"brightness": "Brightness (only for white color)",
"brightness_lower": "Brightness Lower Value",
"brightness_upper": "Brightness Upper Value",
"color_temp": "Color Temperature",
"color_temp_reverse": "Color Temperature Reverse",
"color": "Color",
"color_mode": "Color Mode",
"color_temp_min_kelvin": "Minimum Color Temperature in K",
"color_temp_max_kelvin": "Maximum Color Temperature in K",
"music_mode": "Music mode available",
"scene": "Scene",
"fan_speed_control": "Fan Speed Control dps",
"fan_oscillating_control": "Fan Oscillating Control dps",
"fan_speed_min": "minimum fan speed integer",
"fan_speed_max": "maximum fan speed integer",
"fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)",
"fan_direction":"fan direction dps",
"fan_direction_forward": "forward dps string",
"fan_direction_reverse": "reverse dps string",
"current_temperature_dp": "Current Temperature",
"target_temperature_dp": "Target Temperature",
"temperature_step": "Temperature Step (optional)",
"max_temperature_dp": "Max Temperature (optional)",
"min_temperature_dp": "Min Temperature (optional)",
"precision": "Precision (optional, for DPs values)",
"target_precision": "Target Precision (optional, for DPs values)",
"temperature_unit": "Temperature Unit (optional)",
"hvac_mode_dp": "HVAC Mode DP (optional)",
"hvac_mode_set": "HVAC Mode Set (optional)",
"hvac_action_dp": "HVAC Current Action DP (optional)",
"hvac_action_set": "HVAC Current Action Set (optional)",
"preset_dp": "Presets DP (optional)",
"preset_set": "Presets Set (optional)",
"eco_dp": "Eco DP (optional)",
"eco_value": "Eco value (optional)",
"heuristic_action": "Enable heuristic action (optional)"
}
},
"yaml_import": {
"title": "Not Supported",
"description": "Options cannot be edited when configured via YAML."
}
}
},
"title": "LocalTuya" "title": "LocalTuya"
} }

View File

@@ -5,7 +5,9 @@
"device_updated": "Device configuration has been updated!" "device_updated": "Device configuration has been updated!"
}, },
"error": { "error": {
"authentication_failed": "Failed to authenticate.\n{msg}",
"cannot_connect": "Cannot connect to device. Verify that address is correct and try again.", "cannot_connect": "Cannot connect to device. Verify that address is correct and try again.",
"device_list_failed": "Failed to retrieve device list.\n{msg}",
"invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.", "invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.",
"unknown": "An unknown error occurred. See log for details.", "unknown": "An unknown error occurred. See log for details.",
"entity_already_configured": "Entity with this ID has already been configured.", "entity_already_configured": "Entity with this ID has already been configured.",
@@ -15,15 +17,57 @@
}, },
"step": { "step": {
"user": { "user": {
"title": "Device Discovery", "title": "Cloud API account configuration",
"description": "Input the credentials for Tuya Cloud API.",
"data": {
"region": "API server region",
"client_id": "Client ID",
"client_secret": "Secret",
"user_id": "User ID"
}
}
}
},
"options": {
"abort": {
"already_configured": "Device has already been configured.",
"device_success": "Device {dev_name} successfully {action}."
},
"error": {
"authentication_failed": "Failed to authenticate.\n{msg}",
"cannot_connect": "Cannot connect to device. Verify that address is correct and try again.",
"device_list_failed": "Failed to retrieve device list.\n{msg}",
"invalid_auth": "Failed to authenticate with device. Verify that device id and local key are correct.",
"unknown": "An unknown error occurred. See log for details.",
"entity_already_configured": "Entity with this ID has already been configured.",
"address_in_use": "Address used for discovery is already in use. Make sure no other application is using it (TCP port 6668).",
"discovery_failed": "Something failed when discovering devices. See log for details.",
"empty_dps": "Connection to device succeeded but no datapoints found, please try again. Create a new issue and include debug logs if problem persists."
},
"step": {
"yaml_import": {
"title": "Not Supported",
"description": "Options cannot be edited when configured via YAML."
},
"init": {
"title": "LocalTuya Configuration",
"description": "Please select the desired action.",
"data": {
"add_device": "AAAAA a new device",
"edit_device": "Edit a device",
"setup_cloud": "Reconfigure Cloud API account"
}
},
"add_device": {
"title": "Add a new device",
"description": "Pick one of the automatically discovered devices or `...` to manually to add a device.", "description": "Pick one of the automatically discovered devices or `...` to manually to add a device.",
"data": { "data": {
"discovered_device": "Discovered Device" "discovered_device": "Discovered Devices"
} }
}, },
"basic_info": { "basic_info": {
"title": "Add Tuya device", "title": "Add Tuya device",
"description": "Fill in the basic device details. The name entered here will be used to identify the integration itself (as seen in the `Integrations` page). You will add entities and give them names in the following steps.", "description": "Fill in the basic device details. The name entered here will be used to identify the device itself (as seen in the `Integrations` page). You will add entities and give them names in the following steps.",
"data": { "data": {
"friendly_name": "Name", "friendly_name": "Name",
"host": "Host", "host": "Host",
@@ -38,7 +82,7 @@
"description": "Please pick the type of entity you want to add.", "description": "Please pick the type of entity you want to add.",
"data": { "data": {
"platform_to_add": "Platform", "platform_to_add": "Platform",
"no_additional_platforms": "Do not add any more entities" "no_additional_entities": "Do not add any more entities"
} }
}, },
"add_entity": { "add_entity": {
@@ -118,100 +162,5 @@
} }
} }
}, },
"options": {
"step": {
"init": {
"title": "Configure Tuya Device",
"description": "Basic configuration for device id `{device_id}`.",
"data": {
"friendly_name": "Friendly Name",
"host": "Host",
"local_key": "Local key",
"protocol_version": "Protocol Version",
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
"entities": "Entities (uncheck an entity to remove it)"
}
},
"entity": {
"title": "Entity Configuration",
"description": "Editing entity with DPS `{id}` and platform `{platform}`.",
"data": {
"id": "ID",
"friendly_name": "Friendly name",
"current": "Current",
"current_consumption": "Current Consumption",
"voltage": "Voltage",
"commands_set": "Open_Close_Stop Commands Set",
"positioning_mode": "Positioning mode",
"current_position_dp": "Current Position (for *position* mode only)",
"set_position_dp": "Set Position (for *position* mode only)",
"position_inverted": "Invert 0-100 position (for *position* mode only)",
"span_time": "Full opening time, in secs. (for *timed* mode only)",
"unit_of_measurement": "Unit of Measurement",
"device_class": "Device Class",
"scaling": "Scaling Factor",
"state_on": "On Value",
"state_off": "Off Value",
"powergo_dp": "Power DP (Usually 25 or 2)",
"idle_status_value": "Idle Status (comma-separated)",
"returning_status_value": "Returning Status",
"docked_status_value": "Docked Status (comma-separated)",
"fault_dp": "Fault DP (Usually 11)",
"battery_dp": "Battery status DP (Usually 14)",
"mode_dp": "Mode DP (Usually 27)",
"modes": "Modes list",
"return_mode": "Return home mode",
"fan_speed_dp": "Fan speeds DP (Usually 30)",
"fan_speeds": "Fan speeds list (comma-separated)",
"clean_time_dp": "Clean Time DP (Usually 33)",
"clean_area_dp": "Clean Area DP (Usually 32)",
"clean_record_dp": "Clean Record DP (Usually 34)",
"locate_dp": "Locate DP (Usually 31)",
"paused_state": "Pause state (pause, paused, etc)",
"stop_status": "Stop status",
"brightness": "Brightness (only for white color)",
"brightness_lower": "Brightness Lower Value",
"brightness_upper": "Brightness Upper Value",
"color_temp": "Color Temperature",
"color_temp_reverse": "Color Temperature Reverse",
"color": "Color",
"color_mode": "Color Mode",
"color_temp_min_kelvin": "Minimum Color Temperature in K",
"color_temp_max_kelvin": "Maximum Color Temperature in K",
"music_mode": "Music mode available",
"scene": "Scene",
"fan_speed_control": "Fan Speed Control dps",
"fan_oscillating_control": "Fan Oscillating Control dps",
"fan_speed_min": "minimum fan speed integer",
"fan_speed_max": "maximum fan speed integer",
"fan_speed_ordered_list": "Fan speed modes list (overrides speed min/max)",
"fan_direction":"fan direction dps",
"fan_direction_forward": "forward dps string",
"fan_direction_reverse": "reverse dps string",
"current_temperature_dp": "Current Temperature",
"target_temperature_dp": "Target Temperature",
"temperature_step": "Temperature Step (optional)",
"max_temperature_dp": "Max Temperature (optional)",
"min_temperature_dp": "Min Temperature (optional)",
"precision": "Precision (optional, for DPs values)",
"target_precision": "Target Precision (optional, for DPs values)",
"temperature_unit": "Temperature Unit (optional)",
"hvac_mode_dp": "HVAC Mode DP (optional)",
"hvac_mode_set": "HVAC Mode Set (optional)",
"hvac_action_dp": "HVAC Current Action DP (optional)",
"hvac_action_set": "HVAC Current Action Set (optional)",
"preset_dp": "Presets DP (optional)",
"preset_set": "Presets Set (optional)",
"eco_dp": "Eco DP (optional)",
"eco_value": "Eco value (optional)",
"heuristic_action": "Enable heuristic action (optional)"
}
},
"yaml_import": {
"title": "Not Supported",
"description": "Options cannot be edited when configured via YAML."
}
}
},
"title": "LocalTuya" "title": "LocalTuya"
} }