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.
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
"""
"""The LocalTuya integration. """
import asyncio
import logging
import time
from datetime import timedelta
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceEntry
import homeassistant.helpers.entity_registry as er
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES,
CONF_HOST,
CONF_ID,
@@ -85,14 +20,22 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.event import async_track_time_interval
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 .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
_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):
"""Set up the LocalTuya integration component."""
hass.data.setdefault(DOMAIN, {})
@@ -140,10 +73,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
return
current_entries = hass.config_entries.async_entries(DOMAIN)
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)
# entries_by_id = {entry.data[CONF_DEVICE_ID]: entry for entry in current_entries}
reload_tasks = [
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.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)
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
)
for host_config in config.get(DOMAIN, []):
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=host_config
)
)
return True
async def async_migrate_entry(domain):
"""Migrate old entry."""
print("WHYYYYYYY")
_LOGGER.error("WHHHYYYYYY")
return True
@@ -260,15 +202,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up LocalTuya integration from a config entry."""
unsub_listener = entry.add_update_listener(update_listener)
device = TuyaDevice(hass, entry.data)
hass.data[DOMAIN][entry.entry_id] = {
# 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,
}
async def setup_entities():
platforms = set(entity[CONF_PLATFORM] for entity in entry.data[CONF_ENTITIES])
platforms = set(entity[CONF_PLATFORM] for entity in dev_entry[CONF_ENTITIES])
print("DEV {} platforms: {}".format(dev_id, platforms))
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_setup(entry, platform)
@@ -279,7 +229,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
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
@@ -310,13 +261,60 @@ async def update_listener(hass, config_entry):
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):
"""Remove entities associated with config entry that has been removed."""
ent_reg = await er.async_get_registry(hass)
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)
}
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]:
if entity[CONF_ID] in entities:

View File

@@ -4,13 +4,20 @@ import hashlib
import hmac
import json
import requests
import sys
import time
# Signature algorithm.
def calc_sign(msg, key):
sign = hmac.new(msg=bytes(msg, 'latin-1'),key = bytes(key, 'latin-1'), digestmod = hashlib.sha256).hexdigest().upper()
sign = (
hmac.new(
msg=bytes(msg, "latin-1"),
key=bytes(key, "latin-1"),
digestmod=hashlib.sha256,
)
.hexdigest()
.upper()
)
return sign
@@ -20,23 +27,31 @@ class TuyaCloudApi:
def __init__(self, hass, region_code, client_id, secret, user_id):
"""Initialize the class."""
self._hass = hass
self._base_url = f'https://openapi.tuya{region_code}.com'
self._access_token = ""
self._base_url = f"https://openapi.tuya{region_code}.com"
self._client_id = client_id
self._secret = secret
self._user_id = user_id
self._access_token = ""
self._device_list = {}
def generate_payload(self, method, t, url, headers, body=None):
payload = self._client_id + self._access_token + t
payload += (method + '\n' +
hashlib.sha256(bytes((body or "").encode('utf-8'))).hexdigest() + '\n' + # Content-SHA256
''.join(
['%s:%s\n'%(key, headers[key]) # Headers
payload += method + "\n"
# Content-SHA256
payload += hashlib.sha256(bytes((body or "").encode("utf-8"))).hexdigest()
payload += (
"\n"
+ "".join(
[
"%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 (extracted from 'url')
if key in headers
]
)
+ "\n/"
+ url.split("//", 1)[-1].split("/", 1)[-1] # Url
)
# print("PAYLOAD: {}".format(payload))
return payload
@@ -45,59 +60,74 @@ class TuyaCloudApi:
t = str(int(time.time() * 1000))
payload = self.generate_payload(method, t, url, headers, body)
default_par = {
'client_id':self._client_id,
'access_token':self._access_token,
'sign':calc_sign(payload, self._secret),
't':t,
'sign_method':'HMAC-SHA256',
"client_id": self._client_id,
"access_token": self._access_token,
"sign": calc_sign(payload, self._secret),
"t": t,
"sign_method": "HMAC-SHA256",
}
full_url = self._base_url + url
print("\n" + method + ": [{}]".format(full_url))
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":
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))
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 = json.dumps(r.json(), indent=2, ensure_ascii=False) # Beautify the request result format for easy printing and viewing
# r = json.dumps(r.json(), indent=2, ensure_ascii=False) # Beautify the format
return r
async def async_get_access_token(self):
"""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:
print("Request failed, status {}".format(r.status))
return "Request failed, status " + str(r.status)
r_json = r.json()
if not r_json['success']:
print("Request failed, reply is {}".format(json.dumps(r_json, indent=2, ensure_ascii=False)))
if not r_json["success"]:
return f"Error {r_json['code']}: {r_json['msg']}"
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))
return "ok"
async def async_get_devices_list(self):
"""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:
print("Request failed, status {}".format(r.status))
return None
return "Request failed, status " + str(r.status)
r_json = r.json()
if not r_json['success']:
print("Request failed, reply is {}".format(json.dumps(r_json, indent=2, ensure_ascii=False)))
return None
return r
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']}"
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 (
CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES,
CONF_FRIENDLY_NAME,
CONF_HOST,
CONF_ID,
CONF_MODEL,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
)
@@ -23,9 +25,9 @@ from homeassistant.helpers.restore_state import RestoreEntity
from . import pytuya
from .const import (
CONF_LOCAL_KEY,
CONF_PRODUCT_KEY,
CONF_PROTOCOL_VERSION,
DOMAIN,
DATA_CLOUD,
TUYA_DEVICE,
)
@@ -42,7 +44,7 @@ def prepare_setup_entities(hass, config_entry, platform):
if not entities_to_setup:
return None, None
tuyainterface = hass.data[DOMAIN][config_entry.entry_id][TUYA_DEVICE]
tuyainterface = []
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
entity_class with functools.partial.
"""
tuyainterface, entities_to_setup = prepare_setup_entities(
hass, config_entry, domain
print("ASYNC_SETUP_ENTRY: {} {} {}".format(
config_entry.data,
entity_class,
flow_schema(None).items())
)
if not entities_to_setup:
return
entities = []
for dev_id in config_entry.data[CONF_DEVICES]:
# entities_to_setup = prepare_setup_entities(
# hass, config_entry.data[dev_id], domain
# )
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))
entities = []
for device_config in entities_to_setup:
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 device_config:
tuyainterface.dps_to_request[device_config[dp_conf]] = None
if dp_conf in entity_config:
tuyainterface.dps_to_request[entity_config[dp_conf]] = None
entities.append(
entity_class(
tuyainterface,
config_entry,
device_config[CONF_ID],
dev_entry,
entity_config[CONF_ID],
)
)
print("ADDING {} entities".format(len(entities)))
async_add_entities(entities)
@@ -90,7 +108,7 @@ def get_dps_for_platform(flow_schema):
def get_entity_config(config_entry, dp_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:
return entity
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._disconnect_task = None
self._unsub_interval = None
self._local_key = self._config_entry[CONF_LOCAL_KEY]
self.set_logger(_LOGGER, config_entry[CONF_DEVICE_ID])
# 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._config_entry[CONF_HOST],
self._config_entry[CONF_DEVICE_ID],
self._config_entry[CONF_LOCAL_KEY],
self._local_key,
float(self._config_entry[CONF_PROTOCOL_VERSION]),
self,
)
@@ -180,8 +199,30 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
self._async_refresh,
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.error("BBBB: %s", type(e))
if self._interface is not None:
await self._interface.close()
self._interface = None
@@ -201,6 +242,10 @@ class TuyaDevice(pytuya.TuyaListener, pytuya.ContextualLogger):
await self._interface.close()
if self._disconnect_task is not None:
self._disconnect_task()
self.debug(
"Closed connection with device %s.",
self._config_entry[CONF_FRIENDLY_NAME]
)
async def set_dp(self, state, dp_index):
"""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._dp_id = dp_id
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):
"""Subscribe localtuya events."""
@@ -281,27 +326,28 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
self.status_updated()
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(
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)
@property
def device_info(self):
"""Return device information for the device registry."""
model = self._config_entry.get(CONF_MODEL, "Tuya generic")
return {
"identifiers": {
# 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],
"manufacturer": "Unknown",
"model": self._config_entry.data.get(CONF_PRODUCT_KEY, "Tuya generic"),
"sw_version": self._config_entry.data[CONF_PROTOCOL_VERSION],
"name": self._config_entry[CONF_FRIENDLY_NAME],
"manufacturer": "Tuya",
"model": f"{model} ({self._config_entry[CONF_DEVICE_ID]})",
"sw_version": self._config_entry[CONF_PROTOCOL_VERSION],
}
@property
@@ -317,7 +363,7 @@ class LocalTuyaEntity(RestoreEntity, pytuya.ContextualLogger):
@property
def unique_id(self):
"""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):
"""Return if a config parameter has a valid value."""

View File

@@ -1,30 +1,45 @@
"""Config flow for LocalTuya integration integration."""
import errno
import logging
import time
from importlib import import_module
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DEVICES,
CONF_ENTITIES,
CONF_FRIENDLY_NAME,
CONF_HOST,
CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_REGION,
)
from homeassistant.core import callback
from .cloud_api import TuyaCloudApi
from .common import async_config_entry_by_device_id, pytuya
from .const import CONF_DPS_STRINGS # pylint: disable=unused-import
from .const import (
CONF_ACTION,
CONF_ADD_DEVICE,
CONF_EDIT_DEVICE,
CONF_SETUP_CLOUD,
CONF_LOCAL_KEY,
CONF_PRODUCT_KEY,
CONF_PRODUCT_NAME,
CONF_PROTOCOL_VERSION,
CONF_USER_ID,
CONF_DPS_STRINGS,
ATTR_UPDATED_AT,
DATA_DISCOVERY,
DATA_CLOUD,
DOMAIN,
PLATFORMS,
)
@@ -33,11 +48,32 @@ from .discovery import discover
_LOGGER = logging.getLogger(__name__)
PLATFORM_TO_ADD = "platform_to_add"
NO_ADDITIONAL_PLATFORMS = "no_additional_platforms"
NO_ADDITIONAL_ENTITIES = "no_additional_entities"
DISCOVERED_DEVICE = "discovered_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(
{
vol.Required(CONF_FRIENDLY_NAME): str,
@@ -49,7 +85,6 @@ BASIC_INFO_SCHEMA = vol.Schema(
}
)
DEVICE_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
@@ -62,23 +97,29 @@ DEVICE_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):
"""Create schema for user step."""
devices = {dev_id: dev["ip"] for dev_id, dev in devices.items()}
devices.update(
{
ent.data[CONF_DEVICE_ID]: ent.data[CONF_FRIENDLY_NAME]
for ent in entries
if ent.source != SOURCE_IMPORT
}
)
device_list = [f"{key} ({value})" for key, value in devices.items()]
def devices_schema(discovered_devices, cloud_devices_list):
"""Create schema for devices step."""
devices = {}
for dev_id, dev in discovered_devices.items():
dev_name = dev_id
if dev_id in cloud_devices_list.keys():
dev_name = cloud_devices_list[dev_id][CONF_NAME]
devices[dev_id] = f"{dev_name} ({discovered_devices[dev_id]['ip']})"
devices.update({CUSTOM_DEVICE: CUSTOM_DEVICE})
# devices.update(
# {
# ent.data[CONF_DEVICE_ID]: ent.data[CONF_FRIENDLY_NAME]
# for ent in entries
# }
# )
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)
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):
"""Handle a config flow for LocalTuya integration."""
VERSION = 1
VERSION = 2
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
@staticmethod
@@ -224,29 +291,139 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def __init__(self):
"""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):
"""Handle the initial step."""
errors = {}
placeholders = {}
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:
self.selected_device = user_input[DISCOVERED_DEVICE].split(" ")[0]
self.selected_device = user_input[DISCOVERED_DEVICE]
return await self.async_step_basic_info()
# Use cache if available or fallback to manual discovery
devices = {}
discovered_devices = {}
data = self.hass.data.get(DOMAIN)
if data and DATA_DISCOVERY in data:
devices = data[DATA_DISCOVERY].devices
discovered_devices = data[DATA_DISCOVERY].devices
else:
try:
devices = await discover()
discovered_devices = await discover()
except OSError as ex:
if ex.errno == errno.EADDRINUSE:
errors["base"] = "address_in_use"
@@ -258,29 +435,38 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.devices = {
ip: dev
for ip, dev in devices.items()
if dev["gwId"] not in self._async_current_ids()
for ip, dev in discovered_devices.items()
if dev["gwId"] not in self.config_entry.data[CONF_DEVICES]
}
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,
data_schema=user_schema(self.devices, self._async_current_entries()),
)
async def async_step_basic_info(self, user_input=None):
"""Handle input of basic info."""
errors = {}
dev_id = self.selected_device
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:
self.basic_info = user_input
if self.selected_device is not None:
self.basic_info[CONF_PRODUCT_KEY] = self.devices[
self.selected_device
]["productKey"]
if dev_id is not None:
# self.basic_info[CONF_PRODUCT_KEY] = self.devices[
# self.selected_device
# ]["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)
print("ZIO KEN!! {} ".format(self.dps_strings))
return await self.async_step_pick_entity_type()
except CannotConnect:
errors["base"] = "cannot_connect"
@@ -293,22 +479,27 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
# If selected device exists as a config entry, load config from it
if self.selected_device in self._async_current_ids():
entry = async_config_entry_by_device_id(self.hass, self.selected_device)
await self.async_set_unique_id(entry.data[CONF_DEVICE_ID])
self.basic_info = entry.data.copy()
self.dps_strings = self.basic_info.pop(CONF_DPS_STRINGS).copy()
self.entities = self.basic_info.pop(CONF_ENTITIES).copy()
return await self.async_step_pick_entity_type()
if self.selected_device in self.config_entry.data[CONF_DEVICES]:
print("ALREADY EXISTING!! {}".format(self.selected_device))
# entry = self.config_entry.data[CONF_DEVICES][self.selected_device]
# await self.async_set_unique_id(entry.data[CONF_DEVICE_ID])
# self.basic_info = entry.data.copy()
# self.dps_strings = self.basic_info.pop(CONF_DPS_STRINGS).copy()
# 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.update(user_input or {})
if self.selected_device is not None:
device = self.devices[self.selected_device]
if dev_id is not None:
device = self.devices[dev_id]
defaults[CONF_HOST] = device.get("ip")
defaults[CONF_DEVICE_ID] = device.get("gwId")
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(
step_id="basic_info",
@@ -316,116 +507,12 @@ class LocaltuyaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
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):
"""Manage entity settings."""
errors = {}
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[CONF_ID] = self.current_entity[CONF_ID]
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):
"""Manage YAML imports."""
if user_input is not None:
return self.async_create_entry(title="", data={})
return self.async_show_form(step_id="yaml_import")
_LOGGER.error("Configuration via YAML file is no longer supported by this integration.")
# if user_input is not None:
# return self.async_create_entry(title="", data={})
# return self.async_show_form(step_id="yaml_import")
@property
def current_entity(self):

View File

@@ -1,13 +1,43 @@
"""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_CONSUMPTION = "current_consumption"
ATTR_VOLTAGE = "voltage"
ATTR_UPDATED_AT = "updated_at"
# config flow
CONF_LOCAL_KEY = "local_key"
CONF_PROTOCOL_VERSION = "protocol_version"
CONF_DPS_STRINGS = "dps_strings"
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
CONF_BRIGHTNESS_LOWER = "brightness_lower"
@@ -81,23 +111,3 @@ CONF_FAULT_DP = "fault_dp"
CONF_PAUSED_STATE = "paused_state"
CONF_RETURN_MODE = "return_mode"
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.helpers.device_registry import DeviceEntry
from .const import DOMAIN
from .const import DOMAIN, DATA_CLOUD
async def async_get_config_entry_diagnostics(
@@ -16,6 +16,8 @@ async def async_get_config_entry_diagnostics(
data = {}
data = {**entry.data}
# print("DATA is {}".format(data))
tuya_api = hass.data[DOMAIN][DATA_CLOUD]
data["cloud_devices"] = tuya_api._device_list
# censoring private information
# data["token"] = re.sub(r"[^\-]", "*", data["token"])

View File

@@ -71,7 +71,7 @@ class TuyaDiscovery(asyncio.DatagramProtocol):
def device_found(self, 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
_LOGGER.debug("Discovered device: %s", device)

View File

@@ -12,16 +12,13 @@
},
"step": {
"user": {
"title": "Add Tuya device",
"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.",
"title": "Main Configuration",
"description": "Input the credentials for Tuya Cloud API.",
"data": {
"name": "Name",
"host": "Host",
"device_id": "Device ID",
"local_key": "Local key",
"protocol_version": "Protocol Version",
"scan_interval": "Scan interval (seconds, only when not updating automatically)",
"device_type": "Device type"
"region": "API server region",
"client_id": "Client ID",
"client_secret": "Secret",
"user_id": "User ID"
}
},
"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"
}

View File

@@ -5,7 +5,9 @@
"device_updated": "Device configuration has been updated!"
},
"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.",
@@ -15,15 +17,57 @@
},
"step": {
"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.",
"data": {
"discovered_device": "Discovered Device"
"discovered_device": "Discovered Devices"
}
},
"basic_info": {
"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": {
"friendly_name": "Name",
"host": "Host",
@@ -38,7 +82,7 @@
"description": "Please pick the type of entity you want to add.",
"data": {
"platform_to_add": "Platform",
"no_additional_platforms": "Do not add any more entities"
"no_additional_entities": "Do not add any more entities"
}
},
"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"
}