Modified config flow, introduced device deletion
This commit is contained in:
@@ -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:
|
||||
|
@@ -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()
|
||||
def calc_sign(msg, key):
|
||||
sign = (
|
||||
hmac.new(
|
||||
msg=bytes(msg, "latin-1"),
|
||||
key=bytes(key, "latin-1"),
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
.hexdigest()
|
||||
.upper()
|
||||
)
|
||||
return sign
|
||||
|
||||
|
||||
@@ -20,84 +27,107 @@ 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):
|
||||
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
|
||||
|
||||
async def async_make_request(self, method, url, body=None, headers={}):
|
||||
"""Perform requests."""
|
||||
t = str(int(time.time()*1000))
|
||||
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',
|
||||
default_par = {
|
||||
"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"
|
||||
|
@@ -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."""
|
||||
|
@@ -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):
|
||||
|
@@ -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"
|
||||
|
@@ -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"])
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
Reference in New Issue
Block a user