From ced28ad00611e562dab0e8a9b1365c684fcca1f7 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 23 Mar 2019 18:58:25 -0300 Subject: [PATCH 001/643] Better symlink support under Windows (#487) * Better symlink support under Windows * Conditional loading of ctypes wintypes module * Shortening comment line for pylint * Adding plint bypass for Python 3 --- esphome/helpers.py | 13 ---- esphome/symlink_ops.py | 164 +++++++++++++++++++++++++++++++++++++++++ esphome/writer.py | 13 ++-- 3 files changed, 171 insertions(+), 19 deletions(-) create mode 100644 esphome/symlink_ops.py diff --git a/esphome/helpers.py b/esphome/helpers.py index 4def9568f4..01b31111b9 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -140,16 +140,3 @@ def get_bool_env(var, default=False): def is_hassio(): return get_bool_env('ESPHOME_IS_HASSIO') - - -def symlink(src, dst): - if hasattr(os, 'symlink'): - os.symlink(src, dst) - else: - import ctypes - csl = ctypes.windll.kernel32.CreateSymbolicLinkW - csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) - csl.restype = ctypes.c_ubyte - flags = 1 if os.path.isdir(src) else 0 - if csl(dst, src, flags) == 0: - raise ctypes.WinError() diff --git a/esphome/symlink_ops.py b/esphome/symlink_ops.py new file mode 100644 index 0000000000..accfbe5ff7 --- /dev/null +++ b/esphome/symlink_ops.py @@ -0,0 +1,164 @@ +import os + +if hasattr(os, 'symlink'): + def symlink(src, dst): + return os.symlink(src, dst) + + def islink(path): + return os.path.islink(path) + + def readlink(path): + return os.readlink(path) + + def unlink(path): + return os.unlink(path) +else: + import ctypes + from ctypes import wintypes + # Code taken from + # https://stackoverflow.com/questions/27972776/having-trouble-implementing-a-readlink-function + + kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) + + FILE_READ_ATTRIBUTES = 0x0080 + OPEN_EXISTING = 3 + FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 + FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 + FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 + + IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 + IO_REPARSE_TAG_SYMLINK = 0xA000000C + FSCTL_GET_REPARSE_POINT = 0x000900A8 + MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000 + + LPDWORD = ctypes.POINTER(wintypes.DWORD) + LPWIN32_FIND_DATA = ctypes.POINTER(wintypes.WIN32_FIND_DATAW) + INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value + + def IsReparseTagNameSurrogate(tag): + return bool(tag & 0x20000000) + + def _check_invalid_handle(result, func, args): + if result == INVALID_HANDLE_VALUE: + raise ctypes.WinError(ctypes.get_last_error()) + return args + + def _check_bool(result, func, args): + if not result: + raise ctypes.WinError(ctypes.get_last_error()) + return args + + kernel32.FindFirstFileW.errcheck = _check_invalid_handle + kernel32.FindFirstFileW.restype = wintypes.HANDLE + kernel32.FindFirstFileW.argtypes = ( + wintypes.LPCWSTR, # _In_ lpFileName + LPWIN32_FIND_DATA) # _Out_ lpFindFileData + + kernel32.FindClose.argtypes = ( + wintypes.HANDLE,) # _Inout_ hFindFile + + kernel32.CreateFileW.errcheck = _check_invalid_handle + kernel32.CreateFileW.restype = wintypes.HANDLE + kernel32.CreateFileW.argtypes = ( + wintypes.LPCWSTR, # _In_ lpFileName + wintypes.DWORD, # _In_ dwDesiredAccess + wintypes.DWORD, # _In_ dwShareMode + wintypes.LPVOID, # _In_opt_ lpSecurityAttributes + wintypes.DWORD, # _In_ dwCreationDisposition + wintypes.DWORD, # _In_ dwFlagsAndAttributes + wintypes.HANDLE) # _In_opt_ hTemplateFile + + kernel32.CloseHandle.argtypes = ( + wintypes.HANDLE,) # _In_ hObject + + kernel32.DeviceIoControl.errcheck = _check_bool + kernel32.DeviceIoControl.argtypes = ( + wintypes.HANDLE, # _In_ hDevice + wintypes.DWORD, # _In_ dwIoControlCode + wintypes.LPVOID, # _In_opt_ lpInBuffer + wintypes.DWORD, # _In_ nInBufferSize + wintypes.LPVOID, # _Out_opt_ lpOutBuffer + wintypes.DWORD, # _In_ nOutBufferSize + LPDWORD, # _Out_opt_ lpBytesReturned + wintypes.LPVOID) # _Inout_opt_ lpOverlapped + + class REPARSE_DATA_BUFFER(ctypes.Structure): + class ReparseData(ctypes.Union): + class LinkData(ctypes.Structure): + _fields_ = (('SubstituteNameOffset', wintypes.USHORT), + ('SubstituteNameLength', wintypes.USHORT), + ('PrintNameOffset', wintypes.USHORT), + ('PrintNameLength', wintypes.USHORT)) + + @property + def PrintName(self): + dt = wintypes.WCHAR * (self.PrintNameLength // ctypes.sizeof(wintypes.WCHAR)) + name = dt.from_address(ctypes.addressof(self.PathBuffer) + + self.PrintNameOffset).value + if name.startswith(r'\??'): + name = r'\\?' + name[3:] # NT => Windows + return name + + class SymbolicLinkData(LinkData): + _fields_ = (('Flags', wintypes.ULONG), ('PathBuffer', wintypes.BYTE * 0)) + + class MountPointData(LinkData): + _fields_ = (('PathBuffer', wintypes.BYTE * 0),) + + class GenericData(ctypes.Structure): + _fields_ = (('DataBuffer', wintypes.BYTE * 0),) + _fields_ = (('SymbolicLinkReparseBuffer', SymbolicLinkData), + ('MountPointReparseBuffer', MountPointData), + ('GenericReparseBuffer', GenericData)) + _fields_ = (('ReparseTag', wintypes.ULONG), + ('ReparseDataLength', wintypes.USHORT), + ('Reserved', wintypes.USHORT), + ('ReparseData', ReparseData)) + _anonymous_ = ('ReparseData',) + + def symlink(src, dst): + csl = ctypes.windll.kernel32.CreateSymbolicLinkW + csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) + csl.restype = ctypes.c_ubyte + flags = 1 if os.path.isdir(src) else 0 + if csl(dst, src, flags) == 0: + error = ctypes.WinError() + # pylint: disable=no-member + if error.winerror == 1314 and error.errno == 22: + from esphome.core import EsphomeError + raise EsphomeError("Cannot create symlink from '%s' to '%s'. Try running tool \ +with elevated privileges" % (src, dst)) + raise error + + def islink(path): + if not os.path.isdir(path): + return False + data = wintypes.WIN32_FIND_DATAW() + kernel32.FindClose(kernel32.FindFirstFileW(path, ctypes.byref(data))) + if not data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT: + return False + return IsReparseTagNameSurrogate(data.dwReserved0) + + def readlink(path): + n = wintypes.DWORD() + buf = (wintypes.BYTE * MAXIMUM_REPARSE_DATA_BUFFER_SIZE)() + flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS + handle = kernel32.CreateFileW(path, FILE_READ_ATTRIBUTES, 0, None, + OPEN_EXISTING, flags, None) + try: + kernel32.DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, None, 0, + buf, ctypes.sizeof(buf), ctypes.byref(n), None) + finally: + kernel32.CloseHandle(handle) + rb = REPARSE_DATA_BUFFER.from_buffer(buf) + tag = rb.ReparseTag + if tag == IO_REPARSE_TAG_SYMLINK: + return rb.SymbolicLinkReparseBuffer.PrintName + if tag == IO_REPARSE_TAG_MOUNT_POINT: + return rb.MountPointReparseBuffer.PrintName + if not IsReparseTagNameSurrogate(tag): + raise ValueError("not a link") + raise ValueError("unsupported reparse tag: %d" % tag) + + def unlink(path): + return os.rmdir(path) diff --git a/esphome/writer.py b/esphome/writer.py index 0d62372587..450b974592 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -13,7 +13,8 @@ from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP8266_2 CONF_LOCAL, CONF_PLATFORMIO_OPTIONS, CONF_REPOSITORY, CONF_TAG, CONF_USE_CUSTOM_CODE from esphome.core import CORE, EsphomeError from esphome.core_config import GITHUB_ARCHIVE_ZIP, LIBRARY_URI_REPO, VERSION_REGEX -from esphome.helpers import mkdir_p, run_system_command, symlink +from esphome.helpers import mkdir_p, run_system_command +from esphome.symlink_ops import symlink, islink, readlink, unlink from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS from esphome.py_compat import IS_PY3, string_types from esphome.storage_json import StorageJSON, storage_path @@ -220,10 +221,10 @@ def symlink_esphome_core_version(esphome_core_version): if CORE.is_local_esphome_core_copy: src_path = CORE.relative_path(esphome_core_version[CONF_LOCAL]) do_write = True - if os.path.islink(dst_path): - old_path = os.path.join(os.readlink(dst_path), lib_path) + if islink(dst_path): + old_path = os.path.join(readlink(dst_path), lib_path) if old_path != lib_path: - os.unlink(dst_path) + unlink(dst_path) else: do_write = False if do_write: @@ -231,8 +232,8 @@ def symlink_esphome_core_version(esphome_core_version): symlink(src_path, dst_path) else: # Remove symlink when changing back from local version - if os.path.islink(dst_path): - os.unlink(dst_path) + if islink(dst_path): + unlink(dst_path) def format_ini(data): From 356554c08d78c57db9ce29bdb43255c70d6e017a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 24 Mar 2019 18:05:13 +0100 Subject: [PATCH 002/643] ESP8266 SDK 2.3.0 compat (#490) --- esphome/const.py | 1 + esphome/writer.py | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 088750b93b..07d2c4fa9b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -433,3 +433,4 @@ ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ '/stage' ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' +ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' diff --git a/esphome/writer.py b/esphome/writer.py index 450b974592..953680e467 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -10,7 +10,8 @@ import shutil from esphome.config import iter_components from esphome.const import ARDUINO_VERSION_ESP32_1_0_0, ARDUINO_VERSION_ESP8266_2_5_0, \ ARDUINO_VERSION_ESP8266_DEV, CONF_BOARD_FLASH_MODE, CONF_BRANCH, CONF_COMMIT, CONF_ESPHOME, \ - CONF_LOCAL, CONF_PLATFORMIO_OPTIONS, CONF_REPOSITORY, CONF_TAG, CONF_USE_CUSTOM_CODE + CONF_LOCAL, CONF_PLATFORMIO_OPTIONS, CONF_REPOSITORY, CONF_TAG, CONF_USE_CUSTOM_CODE, \ + ARDUINO_VERSION_ESP8266_2_3_0 from esphome.core import CORE, EsphomeError from esphome.core_config import GITHUB_ARCHIVE_ZIP, LIBRARY_URI_REPO, VERSION_REGEX from esphome.helpers import mkdir_p, run_system_command @@ -342,13 +343,14 @@ def gather_build_flags(): '-DUSE_WIFI_SIGNAL_SENSOR', } - if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES: + if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ + CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: flash_size = ESP8266_FLASH_SIZES[CORE.board] ld_scripts = ESP8266_LD_SCRIPTS[flash_size] ld_script = None if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', - 'espressif8266@1.6.0', 'espressif8266@1.5.0'): + 'espressif8266@1.6.0'): ld_script = ld_scripts[0] elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0): ld_script = ld_scripts[1] From 300d3a1f466ffdfbc0085328a929203ec4fbf874 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 30 Mar 2019 15:47:12 +0100 Subject: [PATCH 003/643] Upgrade ESPAsyncTCP to 1.2.0 (#497) --- esphome/components/api.py | 2 +- esphome/writer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/api.py b/esphome/components/api.py index 86e77bb4c6..544d0e418b 100644 --- a/esphome/components/api.py +++ b/esphome/components/api.py @@ -86,7 +86,7 @@ def lib_deps(config): if CORE.is_esp32: return 'AsyncTCP@1.0.3' if CORE.is_esp8266: - return 'ESPAsyncTCP@1.1.3' + return 'ESPAsyncTCP@1.2.0' raise NotImplementedError diff --git a/esphome/writer.py b/esphome/writer.py index 953680e467..9c2bc8fbd8 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -290,7 +290,7 @@ def gather_lib_deps(): lib_deps.add('AsyncTCP@1.0.1') lib_deps.add('ESPmDNS') elif CORE.is_esp8266: - lib_deps.add('ESPAsyncTCP@1.1.3') + lib_deps.add('ESPAsyncTCP@1.2.0') lib_deps.add('ESP8266mDNS') # avoid changing build flags order From e7e785fd60a652acb0099600935817c2df3fb201 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 31 Mar 2019 11:04:41 +0200 Subject: [PATCH 004/643] Fix dashboard wizard unicode (#494) * Fix dashboard wizard unicode Fixes https://github.com/esphome/issues/issues/169 * Fix password md5 --- esphome/dashboard/dashboard.py | 6 +++--- esphome/espota2.py | 2 +- esphome/py_compat.py | 10 ++++++++++ esphome/wizard.py | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index a70fe2add2..6c1d9f2616 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -28,7 +28,7 @@ import tornado.websocket from esphome import const from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2 +from esphome.py_compat import IS_PY2, decode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote @@ -223,8 +223,8 @@ class WizardRequestHandler(BaseHandler): def post(self): from esphome import wizard - kwargs = {k: ''.join(v) for k, v in self.request.arguments.items()} - destination = os.path.join(CONFIG_DIR, kwargs['name'] + '.yaml') + kwargs = {k: u''.join(decode_text(x) for x in v) for k, v in self.request.arguments.items()} + destination = os.path.join(CONFIG_DIR, kwargs['name'] + u'.yaml') wizard.wizard_write(path=destination, **kwargs) self.redirect('/?begin=True') diff --git a/esphome/espota2.py b/esphome/espota2.py index dbe7e94313..ac0dd33ebc 100755 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -195,7 +195,7 @@ def perform_ota(sock, password, file_handle, filename): send_check(sock, cnonce, 'auth cnonce') result_md5 = hashlib.md5() - result_md5.update(password.encode()) + result_md5.update(password.encode('utf-8')) result_md5.update(nonce.encode()) result_md5.update(cnonce.encode()) result = result_md5.hexdigest() diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 16a4d27ebf..4b48aa0f88 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -69,3 +69,13 @@ def indexbytes(buf, i): return buf[i] else: return ord(buf[i]) + + +if IS_PY2: + def decode_text(data, encoding='utf-8', errors='strict'): + # type: (str, str, str) -> unicode + return unicode(data, encoding='utf-8', errors=errors) +else: + def decode_text(data, encoding='utf-8', errors='strict'): + # type: (bytes, str, str) -> str + return data.decode(encoding='utf-8', errors=errors) diff --git a/esphome/wizard.py b/esphome/wizard.py index a2401584a1..abc2270235 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -79,7 +79,7 @@ def wizard_write(path, **kwargs): kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32' platform = kwargs['platform'] - with codecs.open(path, 'w') as f_handle: + with codecs.open(path, 'w', 'utf-8') as f_handle: f_handle.write(wizard_file(**kwargs)) storage = StorageJSON.from_wizard(name, name + '.local', platform, board) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) From 41db8a12646d01577c42f7be4e092ebf4f61afb2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 31 Mar 2019 11:04:57 +0200 Subject: [PATCH 005/643] Fix text sensor MQTT settings (#495) Fixes https://github.com/esphome/issues/issues/170 --- esphome/components/text_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index a4a6caec26..1aff0c1e3b 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -46,7 +46,7 @@ def setup_text_sensor_core_(text_sensor_var, config): trigger = Pvariable(conf[CONF_TRIGGER_ID], rhs) automation.build_automations(trigger, [(std_string, 'x')], conf) - setup_mqtt_component(text_sensor_var.get_mqtt(), config) + setup_mqtt_component(text_sensor_var.Pget_mqtt(), config) def setup_text_sensor(text_sensor_obj, config): From cda9bad233beaf47f4796e4544f3b2deae70e68e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 31 Mar 2019 12:25:44 +0200 Subject: [PATCH 006/643] Upgrade docker base image to 1.4.3 (#499) --- .gitlab-ci.yml | 4 ++-- docker/Dockerfile | 2 +- docker/Dockerfile.hassio | 2 +- docker/hooks/build | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0cc29416c9..f4e2fa40af 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,11 +41,11 @@ stages: - | if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.4.1 + BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.4.3 BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} DOCKERFILE=docker/Dockerfile.hassio else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.4.1 + BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.4.3 if [[ "${BUILD_ARCH}" == "amd64" ]]; then BUILD_TO=esphome/esphome else diff --git a/docker/Dockerfile b/docker/Dockerfile index 2568ef37f3..0416f2496e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:1.4.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:1.4.3 FROM ${BUILD_FROM} COPY . . diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index 95888739bd..eaa7d9d504 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-hassio-base-amd64:1.4.1 +ARG BUILD_FROM=esphome/esphome-hassio-base-amd64:1.4.3 FROM ${BUILD_FROM} # Copy root filesystem diff --git a/docker/hooks/build b/docker/hooks/build index 083c5c50a8..60879aaa88 100755 --- a/docker/hooks/build +++ b/docker/hooks/build @@ -16,11 +16,11 @@ echo "PWD: $PWD" if [[ ${IS_HASSIO} = "YES" ]]; then docker build \ - --build-arg "BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.4.1" \ + --build-arg "BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.4.3" \ --build-arg "BUILD_VERSION=${CACHE_TAG}" \ -t "${IMAGE_NAME}" -f ../docker/Dockerfile.hassio .. else docker build \ - --build-arg "BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.4.1" \ + --build-arg "BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.4.3" \ -t "${IMAGE_NAME}" -f ../docker/Dockerfile .. fi From ac0b095941ff156baf68eab91bfc732195ceb69d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 31 Mar 2019 13:13:12 +0200 Subject: [PATCH 007/643] Bump version to v1.12.2 --- esphome/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 07d2c4fa9b..ba1cdc247d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -2,10 +2,10 @@ MAJOR_VERSION = 1 MINOR_VERSION = 12 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) -ESPHOME_CORE_VERSION = '1.12.1' +ESPHOME_CORE_VERSION = '1.12.2' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' From a676ff23ded4fb533f6ceb0b7e81666efdf6c60a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 27 May 2019 21:50:36 +0200 Subject: [PATCH 008/643] Bump version to v1.13.0b1 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index d285105df4..45f72a19fd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -3,7 +3,7 @@ MAJOR_VERSION = 1 MINOR_VERSION = 13 -PATCH_VERSION = '0-dev' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From 6a17fe375e258ebe62100ead6318586946586ee9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 27 May 2019 21:55:54 +0200 Subject: [PATCH 009/643] Update .gitlab-ci.yml --- .gitlab-ci.yml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94116bcee1..e26eb902bc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,8 +13,7 @@ stages: image: esphome/esphome-base-amd64 stage: lint before_script: - - pip install -e . - - pip install flake8==3.6.0 pylint==1.9.4 pillow + - script/setup tags: - docker @@ -22,7 +21,7 @@ stages: image: esphome/esphome-base-amd64 stage: test before_script: - - pip install -e . + - script/setup tags: - docker variables: @@ -94,15 +93,11 @@ stages: - docker stage: deploy -flake8: +lint-python: <<: *lint script: - - flake8 esphome - -pylint: - <<: *lint - script: - - pylint esphome + - script/ci-custom.py + - script/lint-python test1: <<: *test From 53e8b3ed3e22a0c976b7eb412f505345503ad1e3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 28 May 2019 10:23:15 +0200 Subject: [PATCH 010/643] Update gitlab CI script, add cpp lint --- .gitlab-ci.yml | 177 +++++++++++++++++++++-------------------- docker/Dockerfile.lint | 20 ++++- requirements_test.txt | 1 + script/lint-python | 29 ++++--- 4 files changed, 126 insertions(+), 101 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e26eb902bc..5b247e386f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,8 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ + BASE_VERSION: '1.5.1' + TZ: UTC stages: - lint @@ -10,7 +12,7 @@ stages: - deploy .lint: &lint - image: esphome/esphome-base-amd64 + image: esphome/esphome-lint:latest stage: lint before_script: - script/setup @@ -18,14 +20,12 @@ stages: - docker .test: &test - image: esphome/esphome-base-amd64 + image: esphome/esphome-base-amd64:${BASE_VERSION} stage: test before_script: - script/setup tags: - docker - variables: - TZ: UTC .docker-base: &docker-base image: esphome/esphome-base-builder @@ -40,11 +40,11 @@ stages: - | if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.5.1 + BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} DOCKERFILE=docker/Dockerfile.hassio else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.5.1 + BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} if [[ "${BUILD_ARCH}" == "amd64" ]]; then BUILD_TO=esphome/esphome else @@ -93,12 +93,33 @@ stages: - docker stage: deploy -lint-python: +lint-custom: <<: *lint script: - script/ci-custom.py + +lint-python: + <<: *lint + script: - script/lint-python +lint-tidy: + <<: *lint + script: + - pio init --ide atom + - | + if ! patch -R -p0 -s -f --dry-run - - - {% if begin and len(entries) == 1 %} - - {% end %} - - - - diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html deleted file mode 100644 index 14116484af..0000000000 --- a/esphome/dashboard/templates/login.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - Login - ESPHome - - - - - - - - - - - - -
-
-
-
-
-
-
- - v{{ version }} - Dashboard Login -

- {% if hassio %} - Login by entering your Home Assistant login credentials. - {% else %} - Login by entering your ESPHome login credentials. - {% end %} -

- - {% if error is not None %} -
- Error! - {{ escape(error) }} -
- - - {% end %} - -
- {% if has_username or hassio %} -
-
- person - - -
-
- {% end %} - -
-
- lock - - -
-
-
-
- -
- -
-
-
-
-
- - - -
-
- - - diff --git a/esphome/wizard.py b/esphome/wizard.py index 7d875b7dd2..0d912e4bbf 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -49,17 +49,6 @@ BASE_CONFIG = """esphome: platform: {platform} board: {board} -wifi: - ssid: "{ssid}" - password: "{psk}" - - # Enable fallback hotspot (captive portal) in case wifi connection fails - ap: - ssid: "{fallback_name}" - password: "{fallback_psk}" - -captive_portal: - # Enable logging logger: @@ -83,12 +72,43 @@ def wizard_file(**kwargs): config = BASE_CONFIG.format(**kwargs) - if kwargs["password"]: - config += ' password: "{0}"\n\nota:\n password: "{0}"\n'.format( - kwargs["password"] + # Configure API + if "password" in kwargs: + config += ' password: "{0}"\n'.format(kwargs["password"]) + + # Configure OTA + config += "\nota:\n" + if "ota_password" in kwargs: + config += ' password: "{0}"'.format(kwargs["ota_password"]) + elif "password" in kwargs: + config += ' password: "{0}"'.format(kwargs["password"]) + + # Configuring wifi + config += "\n\nwifi:\n" + + if "ssid" in kwargs: + config += """ ssid: "{ssid}" + password: "{psk}" +""".format( + **kwargs ) else: - config += "\nota:\n" + config += """ # ssid: "My SSID" + # password: "mypassword" + + networks: +""" + + config += """ + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "{fallback_name}" + password: "{fallback_psk}" + +captive_portal: +""".format( + **kwargs + ) return config @@ -97,9 +117,9 @@ def wizard_write(path, **kwargs): name = kwargs["name"] board = kwargs["board"] - kwargs["ssid"] = sanitize_double_quotes(kwargs["ssid"]) - kwargs["psk"] = sanitize_double_quotes(kwargs["psk"]) - kwargs["password"] = sanitize_double_quotes(kwargs["password"]) + for key in ("ssid", "psk", "password", "ota_password"): + if key in kwargs: + kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: kwargs["platform"] = "ESP8266" if board in ESP8266_BOARD_PINS else "ESP32" diff --git a/requirements.txt b/requirements.txt index 5915c2963f..e7f69865da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 +esphome-dashboard==20210611.0 From 0efc1f06f2538cb5caf78ccd666e2171adc97c8d Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 14 Jun 2021 18:01:56 +0200 Subject: [PATCH 493/643] Split files in light component (#1893) --- .../components/light/addressable_light.cpp | 170 ---- esphome/components/light/addressable_light.h | 263 +------ .../components/light/esp_color_correction.cpp | 26 + .../components/light/esp_color_correction.h | 78 ++ esphome/components/light/esp_color_view.h | 110 +++ esphome/components/light/esp_hsv_color.cpp | 74 ++ esphome/components/light/esp_hsv_color.h | 36 + esphome/components/light/esp_range_view.cpp | 96 +++ esphome/components/light/esp_range_view.h | 75 ++ esphome/components/light/light_call.cpp | 522 +++++++++++++ esphome/components/light/light_call.h | 160 ++++ esphome/components/light/light_state.cpp | 737 +++--------------- esphome/components/light/light_state.h | 216 +---- esphome/core/optional.h | 2 + 14 files changed, 1334 insertions(+), 1231 deletions(-) create mode 100644 esphome/components/light/esp_color_correction.cpp create mode 100644 esphome/components/light/esp_color_correction.h create mode 100644 esphome/components/light/esp_color_view.h create mode 100644 esphome/components/light/esp_hsv_color.cpp create mode 100644 esphome/components/light/esp_hsv_color.h create mode 100644 esphome/components/light/esp_range_view.cpp create mode 100644 esphome/components/light/esp_range_view.h create mode 100644 esphome/components/light/light_call.cpp create mode 100644 esphome/components/light/light_call.h diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index ecc48e32b8..e3ab9596d9 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -6,158 +6,6 @@ namespace light { static const char *const TAG = "light.addressable"; -Color ESPHSVColor::to_rgb() const { - // based on FastLED's hsv rainbow to rgb - const uint8_t hue = this->hue; - const uint8_t sat = this->saturation; - const uint8_t val = this->value; - // upper 3 hue bits are for branch selection, lower 5 are for values - const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 - // third of the offset, 255/3 = 85 (actually only up to 82; 164) - const uint8_t third = esp_scale8(offset8, 85); - const uint8_t two_thirds = esp_scale8(offset8, 170); - Color rgb(255, 255, 255, 0); - switch (hue >> 5) { - case 0b000: - rgb.r = 255 - third; - rgb.g = third; - rgb.b = 0; - break; - case 0b001: - rgb.r = 171; - rgb.g = 85 + third; - rgb.b = 0; - break; - case 0b010: - rgb.r = 171 - two_thirds; - rgb.g = 170 + third; - rgb.b = 0; - break; - case 0b011: - rgb.r = 0; - rgb.g = 255 - third; - rgb.b = third; - break; - case 0b100: - rgb.r = 0; - rgb.g = 171 - two_thirds; - rgb.b = 85 + two_thirds; - break; - case 0b101: - rgb.r = third; - rgb.g = 0; - rgb.b = 255 - third; - break; - case 0b110: - rgb.r = 85 + third; - rgb.g = 0; - rgb.b = 171 - third; - break; - case 0b111: - rgb.r = 170 + third; - rgb.g = 0; - rgb.b = 85 - third; - break; - default: - break; - } - // low saturation -> add uniform color to orig. hue - // high saturation -> use hue directly - // scales with square of saturation - // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 - rgb *= sat; - const uint8_t desat = 255 - sat; - rgb += esp_scale8(desat, desat); - // (r,g,b) = (r,g,b) * val - rgb *= val; - return rgb; -} - -void ESPRangeView::set(const Color &color) { - for (int32_t i = this->begin_; i < this->end_; i++) { - (*this->parent_)[i] = color; - } -} -ESPColorView ESPRangeView::operator[](int32_t index) const { - index = interpret_index(index, this->size()) + this->begin_; - return (*this->parent_)[index]; -} -ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } -ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } -void ESPRangeView::set_red(uint8_t red) { - for (auto c : *this) - c.set_red(red); -} -void ESPRangeView::set_green(uint8_t green) { - for (auto c : *this) - c.set_green(green); -} -void ESPRangeView::set_blue(uint8_t blue) { - for (auto c : *this) - c.set_blue(blue); -} -void ESPRangeView::set_white(uint8_t white) { - for (auto c : *this) - c.set_white(white); -} -void ESPRangeView::set_effect_data(uint8_t effect_data) { - for (auto c : *this) - c.set_effect_data(effect_data); -} -void ESPRangeView::fade_to_white(uint8_t amnt) { - for (auto c : *this) - c.fade_to_white(amnt); -} -void ESPRangeView::fade_to_black(uint8_t amnt) { - for (auto c : *this) - c.fade_to_black(amnt); -} -void ESPRangeView::lighten(uint8_t delta) { - for (auto c : *this) - c.lighten(delta); -} -void ESPRangeView::darken(uint8_t delta) { - for (auto c : *this) - c.darken(delta); -} -ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT - // If size doesn't match, error (todo warning) - if (rhs.size() != this->size()) - return *this; - - if (this->parent_ != rhs.parent_) { - for (int32_t i = 0; i < this->size(); i++) - (*this)[i].set(rhs[i].get()); - return *this; - } - - // If both equal, already done - if (rhs.begin_ == this->begin_) - return *this; - - if (rhs.begin_ > this->begin_) { - // Copy from left - for (int32_t i = 0; i < this->size(); i++) { - (*this)[i].set(rhs[i].get()); - } - } else { - // Copy from right - for (int32_t i = this->size() - 1; i >= 0; i--) { - (*this)[i].set(rhs[i].get()); - } - } - - return *this; -} - -ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } - -int32_t HOT interpret_index(int32_t index, int32_t size) { - if (index < 0) - return size + index; - return index; -} - void AddressableLight::call_setup() { this->setup(); @@ -254,23 +102,5 @@ void AddressableLight::write_state(LightState *state) { this->schedule_show(); } -void ESPColorCorrection::calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); - this->gamma_reverse_table_[i] = uncorrected; - } -} - } // namespace light } // namespace esphome diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 39bd905c65..460cb88935 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -3,6 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/color.h" +#include "esp_color_correction.h" +#include "esp_color_view.h" +#include "esp_range_view.h" #include "light_output.h" #include "light_state.h" @@ -15,266 +18,6 @@ namespace light { using ESPColor = Color; -struct ESPHSVColor { - union { - struct { - union { - uint8_t hue; - uint8_t h; - }; - union { - uint8_t saturation; - uint8_t s; - }; - union { - uint8_t value; - uint8_t v; - }; - }; - uint8_t raw[3]; - }; - inline ESPHSVColor() ALWAYS_INLINE : h(0), s(0), v(0) { // NOLINT - } - inline ESPHSVColor(uint8_t hue, uint8_t saturation, uint8_t value) ALWAYS_INLINE : hue(hue), - saturation(saturation), - value(value) {} - Color to_rgb() const; -}; - -class ESPColorCorrection { - public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} - void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } - void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma); - inline Color color_correct(Color color) const ALWAYS_INLINE { - // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma - return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), - this->color_correct_blue(color.blue), this->color_correct_white(color.white)); - } - inline uint8_t color_correct_red(uint8_t red) const ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); - return this->gamma_table_[res]; - } - inline uint8_t color_correct_green(uint8_t green) const ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); - return this->gamma_table_[res]; - } - inline uint8_t color_correct_blue(uint8_t blue) const ALWAYS_INLINE { - uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); - return this->gamma_table_[res]; - } - inline uint8_t color_correct_white(uint8_t white) const ALWAYS_INLINE { - // do not scale white value with brightness - uint8_t res = esp_scale8(white, this->max_brightness_.white); - return this->gamma_table_[res]; - } - inline Color color_uncorrect(Color color) const ALWAYS_INLINE { - // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) - return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green), - this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white)); - } - inline uint8_t color_uncorrect_red(uint8_t red) const ALWAYS_INLINE { - if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; - return res; - } - inline uint8_t color_uncorrect_green(uint8_t green) const ALWAYS_INLINE { - if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; - return res; - } - inline uint8_t color_uncorrect_blue(uint8_t blue) const ALWAYS_INLINE { - if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) - return 0; - uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; - uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; - return res; - } - inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { - if (this->max_brightness_.white == 0) - return 0; - uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = uncorrected / this->max_brightness_.white; - return res; - } - - protected: - uint8_t gamma_table_[256]; - uint8_t gamma_reverse_table_[256]; - Color max_brightness_; - uint8_t local_brightness_{255}; -}; - -class ESPColorSettable { - public: - virtual void set(const Color &color) = 0; - virtual void set_red(uint8_t red) = 0; - virtual void set_green(uint8_t green) = 0; - virtual void set_blue(uint8_t blue) = 0; - virtual void set_white(uint8_t white) = 0; - virtual void set_effect_data(uint8_t effect_data) = 0; - virtual void fade_to_white(uint8_t amnt) = 0; - virtual void fade_to_black(uint8_t amnt) = 0; - virtual void lighten(uint8_t delta) = 0; - virtual void darken(uint8_t delta) = 0; - void set(const ESPHSVColor &color) { this->set_hsv(color); } - void set_hsv(const ESPHSVColor &color) { - Color rgb = color.to_rgb(); - this->set_rgb(rgb.r, rgb.g, rgb.b); - } - void set_rgb(uint8_t red, uint8_t green, uint8_t blue) { - this->set_red(red); - this->set_green(green); - this->set_blue(blue); - } - void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) { - this->set_rgb(red, green, blue); - this->set_white(white); - } -}; - -class ESPColorView : public ESPColorSettable { - public: - ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, - const ESPColorCorrection *color_correction) - : red_(red), - green_(green), - blue_(blue), - white_(white), - effect_data_(effect_data), - color_correction_(color_correction) {} - ESPColorView &operator=(const Color &rhs) { - this->set(rhs); - return *this; - } - ESPColorView &operator=(const ESPHSVColor &rhs) { - this->set_hsv(rhs); - return *this; - } - void set(const Color &color) override { this->set_rgbw(color.r, color.g, color.b, color.w); } - void set_red(uint8_t red) override { *this->red_ = this->color_correction_->color_correct_red(red); } - void set_green(uint8_t green) override { *this->green_ = this->color_correction_->color_correct_green(green); } - void set_blue(uint8_t blue) override { *this->blue_ = this->color_correction_->color_correct_blue(blue); } - void set_white(uint8_t white) override { - if (this->white_ == nullptr) - return; - *this->white_ = this->color_correction_->color_correct_white(white); - } - void set_effect_data(uint8_t effect_data) override { - if (this->effect_data_ == nullptr) - return; - *this->effect_data_ = effect_data; - } - void fade_to_white(uint8_t amnt) override { this->set(this->get().fade_to_white(amnt)); } - void fade_to_black(uint8_t amnt) override { this->set(this->get().fade_to_black(amnt)); } - void lighten(uint8_t delta) override { this->set(this->get().lighten(delta)); } - void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } - Color get() const { return Color(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } - uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } - uint8_t get_red_raw() const { return *this->red_; } - uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } - uint8_t get_green_raw() const { return *this->green_; } - uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } - uint8_t get_blue_raw() const { return *this->blue_; } - uint8_t get_white() const { - if (this->white_ == nullptr) - return 0; - return this->color_correction_->color_uncorrect_white(*this->white_); - } - uint8_t get_white_raw() const { - if (this->white_ == nullptr) - return 0; - return *this->white_; - } - uint8_t get_effect_data() const { - if (this->effect_data_ == nullptr) - return 0; - return *this->effect_data_; - } - void raw_set_color_correction(const ESPColorCorrection *color_correction) { - this->color_correction_ = color_correction; - } - - protected: - uint8_t *const red_; - uint8_t *const green_; - uint8_t *const blue_; - uint8_t *const white_; - uint8_t *const effect_data_; - const ESPColorCorrection *color_correction_; -}; - -class AddressableLight; - -int32_t interpret_index(int32_t index, int32_t size); - -class ESPRangeIterator; - -class ESPRangeView : public ESPColorSettable { - public: - ESPRangeView(AddressableLight *parent, int32_t begin, int32_t an_end) : parent_(parent), begin_(begin), end_(an_end) { - if (this->end_ < this->begin_) { - this->end_ = this->begin_; - } - } - - ESPColorView operator[](int32_t index) const; - ESPRangeIterator begin(); - ESPRangeIterator end(); - - void set(const Color &color) override; - ESPRangeView &operator=(const Color &rhs) { - this->set(rhs); - return *this; - } - ESPRangeView &operator=(const ESPColorView &rhs) { - this->set(rhs.get()); - return *this; - } - ESPRangeView &operator=(const ESPHSVColor &rhs) { - this->set_hsv(rhs); - return *this; - } - ESPRangeView &operator=(const ESPRangeView &rhs); - void set_red(uint8_t red) override; - void set_green(uint8_t green) override; - void set_blue(uint8_t blue) override; - void set_white(uint8_t white) override; - void set_effect_data(uint8_t effect_data) override; - void fade_to_white(uint8_t amnt) override; - void fade_to_black(uint8_t amnt) override; - void lighten(uint8_t delta) override; - void darken(uint8_t delta) override; - int32_t size() const { return this->end_ - this->begin_; } - - protected: - friend ESPRangeIterator; - - AddressableLight *parent_; - int32_t begin_; - int32_t end_; -}; - -class ESPRangeIterator { - public: - ESPRangeIterator(const ESPRangeView &range, int32_t i) : range_(range), i_(i) {} - ESPRangeIterator operator++() { - this->i_++; - return *this; - } - bool operator!=(const ESPRangeIterator &other) const { return this->i_ != other.i_; } - ESPColorView operator*() const; - - protected: - ESPRangeView range_; - int32_t i_; -}; - class AddressableLight : public LightOutput, public Component { public: virtual int32_t size() const = 0; diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp new file mode 100644 index 0000000000..19a2af3da1 --- /dev/null +++ b/esphome/components/light/esp_color_correction.cpp @@ -0,0 +1,26 @@ +#include "esp_color_correction.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +void ESPColorCorrection::calculate_gamma_table(float gamma) { + for (uint16_t i = 0; i < 256; i++) { + // corrected = val ^ gamma + auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + this->gamma_table_[i] = corrected; + } + if (gamma == 0.0f) { + for (uint16_t i = 0; i < 256; i++) + this->gamma_reverse_table_[i] = i; + return; + } + for (uint16_t i = 0; i < 256; i++) { + // val = corrected ^ (1/gamma) + auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + this->gamma_reverse_table_[i] = uncorrected; + } +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h new file mode 100644 index 0000000000..32fee8c8ea --- /dev/null +++ b/esphome/components/light/esp_color_correction.h @@ -0,0 +1,78 @@ +#pragma once + +#include "esphome/core/color.h" + +namespace esphome { +namespace light { + +class ESPColorCorrection { + public: + ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} + void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } + void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } + void calculate_gamma_table(float gamma); + inline Color color_correct(Color color) const ALWAYS_INLINE { + // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma + return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), + this->color_correct_blue(color.blue), this->color_correct_white(color.white)); + } + inline uint8_t color_correct_red(uint8_t red) const ALWAYS_INLINE { + uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); + return this->gamma_table_[res]; + } + inline uint8_t color_correct_green(uint8_t green) const ALWAYS_INLINE { + uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); + return this->gamma_table_[res]; + } + inline uint8_t color_correct_blue(uint8_t blue) const ALWAYS_INLINE { + uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); + return this->gamma_table_[res]; + } + inline uint8_t color_correct_white(uint8_t white) const ALWAYS_INLINE { + // do not scale white value with brightness + uint8_t res = esp_scale8(white, this->max_brightness_.white); + return this->gamma_table_[res]; + } + inline Color color_uncorrect(Color color) const ALWAYS_INLINE { + // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) + return Color(this->color_uncorrect_red(color.red), this->color_uncorrect_green(color.green), + this->color_uncorrect_blue(color.blue), this->color_uncorrect_white(color.white)); + } + inline uint8_t color_uncorrect_red(uint8_t red) const ALWAYS_INLINE { + if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) + return 0; + uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; + uint8_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; + return res; + } + inline uint8_t color_uncorrect_green(uint8_t green) const ALWAYS_INLINE { + if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) + return 0; + uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; + uint8_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; + return res; + } + inline uint8_t color_uncorrect_blue(uint8_t blue) const ALWAYS_INLINE { + if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) + return 0; + uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; + uint8_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; + return res; + } + inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { + if (this->max_brightness_.white == 0) + return 0; + uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; + uint8_t res = uncorrected / this->max_brightness_.white; + return res; + } + + protected: + uint8_t gamma_table_[256]; + uint8_t gamma_reverse_table_[256]; + Color max_brightness_; + uint8_t local_brightness_{255}; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_color_view.h b/esphome/components/light/esp_color_view.h new file mode 100644 index 0000000000..35117e7dd8 --- /dev/null +++ b/esphome/components/light/esp_color_view.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/color.h" +#include "esp_hsv_color.h" +#include "esp_color_correction.h" + +namespace esphome { +namespace light { + +class ESPColorSettable { + public: + virtual void set(const Color &color) = 0; + virtual void set_red(uint8_t red) = 0; + virtual void set_green(uint8_t green) = 0; + virtual void set_blue(uint8_t blue) = 0; + virtual void set_white(uint8_t white) = 0; + virtual void set_effect_data(uint8_t effect_data) = 0; + virtual void fade_to_white(uint8_t amnt) = 0; + virtual void fade_to_black(uint8_t amnt) = 0; + virtual void lighten(uint8_t delta) = 0; + virtual void darken(uint8_t delta) = 0; + void set(const ESPHSVColor &color) { this->set_hsv(color); } + void set_hsv(const ESPHSVColor &color) { + Color rgb = color.to_rgb(); + this->set_rgb(rgb.r, rgb.g, rgb.b); + } + void set_rgb(uint8_t red, uint8_t green, uint8_t blue) { + this->set_red(red); + this->set_green(green); + this->set_blue(blue); + } + void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) { + this->set_rgb(red, green, blue); + this->set_white(white); + } +}; + +class ESPColorView : public ESPColorSettable { + public: + ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, + const ESPColorCorrection *color_correction) + : red_(red), + green_(green), + blue_(blue), + white_(white), + effect_data_(effect_data), + color_correction_(color_correction) {} + ESPColorView &operator=(const Color &rhs) { + this->set(rhs); + return *this; + } + ESPColorView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + void set(const Color &color) override { this->set_rgbw(color.r, color.g, color.b, color.w); } + void set_red(uint8_t red) override { *this->red_ = this->color_correction_->color_correct_red(red); } + void set_green(uint8_t green) override { *this->green_ = this->color_correction_->color_correct_green(green); } + void set_blue(uint8_t blue) override { *this->blue_ = this->color_correction_->color_correct_blue(blue); } + void set_white(uint8_t white) override { + if (this->white_ == nullptr) + return; + *this->white_ = this->color_correction_->color_correct_white(white); + } + void set_effect_data(uint8_t effect_data) override { + if (this->effect_data_ == nullptr) + return; + *this->effect_data_ = effect_data; + } + void fade_to_white(uint8_t amnt) override { this->set(this->get().fade_to_white(amnt)); } + void fade_to_black(uint8_t amnt) override { this->set(this->get().fade_to_black(amnt)); } + void lighten(uint8_t delta) override { this->set(this->get().lighten(delta)); } + void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } + Color get() const { return Color(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } + uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } + uint8_t get_red_raw() const { return *this->red_; } + uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } + uint8_t get_green_raw() const { return *this->green_; } + uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } + uint8_t get_blue_raw() const { return *this->blue_; } + uint8_t get_white() const { + if (this->white_ == nullptr) + return 0; + return this->color_correction_->color_uncorrect_white(*this->white_); + } + uint8_t get_white_raw() const { + if (this->white_ == nullptr) + return 0; + return *this->white_; + } + uint8_t get_effect_data() const { + if (this->effect_data_ == nullptr) + return 0; + return *this->effect_data_; + } + void raw_set_color_correction(const ESPColorCorrection *color_correction) { + this->color_correction_ = color_correction; + } + + protected: + uint8_t *const red_; + uint8_t *const green_; + uint8_t *const blue_; + uint8_t *const white_; + uint8_t *const effect_data_; + const ESPColorCorrection *color_correction_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_hsv_color.cpp b/esphome/components/light/esp_hsv_color.cpp new file mode 100644 index 0000000000..450c2e11ce --- /dev/null +++ b/esphome/components/light/esp_hsv_color.cpp @@ -0,0 +1,74 @@ +#include "esp_hsv_color.h" + +namespace esphome { +namespace light { + +Color ESPHSVColor::to_rgb() const { + // based on FastLED's hsv rainbow to rgb + const uint8_t hue = this->hue; + const uint8_t sat = this->saturation; + const uint8_t val = this->value; + // upper 3 hue bits are for branch selection, lower 5 are for values + const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 + // third of the offset, 255/3 = 85 (actually only up to 82; 164) + const uint8_t third = esp_scale8(offset8, 85); + const uint8_t two_thirds = esp_scale8(offset8, 170); + Color rgb(255, 255, 255, 0); + switch (hue >> 5) { + case 0b000: + rgb.r = 255 - third; + rgb.g = third; + rgb.b = 0; + break; + case 0b001: + rgb.r = 171; + rgb.g = 85 + third; + rgb.b = 0; + break; + case 0b010: + rgb.r = 171 - two_thirds; + rgb.g = 170 + third; + rgb.b = 0; + break; + case 0b011: + rgb.r = 0; + rgb.g = 255 - third; + rgb.b = third; + break; + case 0b100: + rgb.r = 0; + rgb.g = 171 - two_thirds; + rgb.b = 85 + two_thirds; + break; + case 0b101: + rgb.r = third; + rgb.g = 0; + rgb.b = 255 - third; + break; + case 0b110: + rgb.r = 85 + third; + rgb.g = 0; + rgb.b = 171 - third; + break; + case 0b111: + rgb.r = 170 + third; + rgb.g = 0; + rgb.b = 85 - third; + break; + default: + break; + } + // low saturation -> add uniform color to orig. hue + // high saturation -> use hue directly + // scales with square of saturation + // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 + rgb *= sat; + const uint8_t desat = 255 - sat; + rgb += esp_scale8(desat, desat); + // (r,g,b) = (r,g,b) * val + rgb *= val; + return rgb; +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_hsv_color.h b/esphome/components/light/esp_hsv_color.h new file mode 100644 index 0000000000..e0aa388875 --- /dev/null +++ b/esphome/components/light/esp_hsv_color.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/color.h" + +namespace esphome { +namespace light { + +struct ESPHSVColor { + union { + struct { + union { + uint8_t hue; + uint8_t h; + }; + union { + uint8_t saturation; + uint8_t s; + }; + union { + uint8_t value; + uint8_t v; + }; + }; + uint8_t raw[3]; + }; + inline ESPHSVColor() ALWAYS_INLINE : h(0), s(0), v(0) { // NOLINT + } + inline ESPHSVColor(uint8_t hue, uint8_t saturation, uint8_t value) ALWAYS_INLINE : hue(hue), + saturation(saturation), + value(value) {} + Color to_rgb() const; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_range_view.cpp b/esphome/components/light/esp_range_view.cpp new file mode 100644 index 0000000000..e1f0a507bd --- /dev/null +++ b/esphome/components/light/esp_range_view.cpp @@ -0,0 +1,96 @@ +#include "esp_range_view.h" +#include "addressable_light.h" + +namespace esphome { +namespace light { + +int32_t HOT interpret_index(int32_t index, int32_t size) { + if (index < 0) + return size + index; + return index; +} + +ESPColorView ESPRangeView::operator[](int32_t index) const { + index = interpret_index(index, this->size()) + this->begin_; + return (*this->parent_)[index]; +} +ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } +ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } + +void ESPRangeView::set(const Color &color) { + for (int32_t i = this->begin_; i < this->end_; i++) { + (*this->parent_)[i] = color; + } +} + +void ESPRangeView::set_red(uint8_t red) { + for (auto c : *this) + c.set_red(red); +} +void ESPRangeView::set_green(uint8_t green) { + for (auto c : *this) + c.set_green(green); +} +void ESPRangeView::set_blue(uint8_t blue) { + for (auto c : *this) + c.set_blue(blue); +} +void ESPRangeView::set_white(uint8_t white) { + for (auto c : *this) + c.set_white(white); +} +void ESPRangeView::set_effect_data(uint8_t effect_data) { + for (auto c : *this) + c.set_effect_data(effect_data); +} + +void ESPRangeView::fade_to_white(uint8_t amnt) { + for (auto c : *this) + c.fade_to_white(amnt); +} +void ESPRangeView::fade_to_black(uint8_t amnt) { + for (auto c : *this) + c.fade_to_black(amnt); +} +void ESPRangeView::lighten(uint8_t delta) { + for (auto c : *this) + c.lighten(delta); +} +void ESPRangeView::darken(uint8_t delta) { + for (auto c : *this) + c.darken(delta); +} +ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { // NOLINT + // If size doesn't match, error (todo warning) + if (rhs.size() != this->size()) + return *this; + + if (this->parent_ != rhs.parent_) { + for (int32_t i = 0; i < this->size(); i++) + (*this)[i].set(rhs[i].get()); + return *this; + } + + // If both equal, already done + if (rhs.begin_ == this->begin_) + return *this; + + if (rhs.begin_ > this->begin_) { + // Copy from left + for (int32_t i = 0; i < this->size(); i++) { + (*this)[i].set(rhs[i].get()); + } + } else { + // Copy from right + for (int32_t i = this->size() - 1; i >= 0; i--) { + (*this)[i].set(rhs[i].get()); + } + } + + return *this; +} + +ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/esp_range_view.h b/esphome/components/light/esp_range_view.h new file mode 100644 index 0000000000..f2cc347176 --- /dev/null +++ b/esphome/components/light/esp_range_view.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esp_color_view.h" +#include "esp_hsv_color.h" + +namespace esphome { +namespace light { + +int32_t interpret_index(int32_t index, int32_t size); + +class AddressableLight; +class ESPRangeIterator; + +class ESPRangeView : public ESPColorSettable { + public: + ESPRangeView(AddressableLight *parent, int32_t begin, int32_t end) + : parent_(parent), begin_(begin), end_(end < begin ? begin : end) {} + + int32_t size() const { return this->end_ - this->begin_; } + ESPColorView operator[](int32_t index) const; + ESPRangeIterator begin(); + ESPRangeIterator end(); + + void set(const Color &color) override; + void set(const ESPHSVColor &color) { this->set(color.to_rgb()); } + void set_red(uint8_t red) override; + void set_green(uint8_t green) override; + void set_blue(uint8_t blue) override; + void set_white(uint8_t white) override; + void set_effect_data(uint8_t effect_data) override; + + void fade_to_white(uint8_t amnt) override; + void fade_to_black(uint8_t amnt) override; + void lighten(uint8_t delta) override; + void darken(uint8_t delta) override; + + ESPRangeView &operator=(const Color &rhs) { + this->set(rhs); + return *this; + } + ESPRangeView &operator=(const ESPColorView &rhs) { + this->set(rhs.get()); + return *this; + } + ESPRangeView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + ESPRangeView &operator=(const ESPRangeView &rhs); + + protected: + friend ESPRangeIterator; + + AddressableLight *parent_; + int32_t begin_; + int32_t end_; +}; + +class ESPRangeIterator { + public: + ESPRangeIterator(const ESPRangeView &range, int32_t i) : range_(range), i_(i) {} + ESPRangeIterator operator++() { + this->i_++; + return *this; + } + bool operator!=(const ESPRangeIterator &other) const { return this->i_ != other.i_; } + ESPColorView operator*() const; + + protected: + ESPRangeView range_; + int32_t i_; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp new file mode 100644 index 0000000000..359da51de9 --- /dev/null +++ b/esphome/components/light/light_call.cpp @@ -0,0 +1,522 @@ +#include "light_call.h" +#include "light_state.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +static const char *TAG = "light"; + +#ifdef USE_JSON +LightCall &LightCall::parse_color_json(JsonObject &root) { + if (root.containsKey("state")) { + auto val = parse_on_off(root["state"]); + switch (val) { + case PARSE_ON: + this->set_state(true); + break; + case PARSE_OFF: + this->set_state(false); + break; + case PARSE_TOGGLE: + this->set_state(!this->parent_->remote_values.is_on()); + break; + case PARSE_NONE: + break; + } + } + + if (root.containsKey("brightness")) { + this->set_brightness(float(root["brightness"]) / 255.0f); + } + + if (root.containsKey("color")) { + JsonObject &color = root["color"]; + if (color.containsKey("r")) { + this->set_red(float(color["r"]) / 255.0f); + } + if (color.containsKey("g")) { + this->set_green(float(color["g"]) / 255.0f); + } + if (color.containsKey("b")) { + this->set_blue(float(color["b"]) / 255.0f); + } + } + + if (root.containsKey("white_value")) { + this->set_white(float(root["white_value"]) / 255.0f); + } + + if (root.containsKey("color_temp")) { + this->set_color_temperature(float(root["color_temp"])); + } + + return *this; +} +LightCall &LightCall::parse_json(JsonObject &root) { + this->parse_color_json(root); + + if (root.containsKey("flash")) { + auto length = uint32_t(float(root["flash"]) * 1000); + this->set_flash_length(length); + } + + if (root.containsKey("transition")) { + auto length = uint32_t(float(root["transition"]) * 1000); + this->set_transition_length(length); + } + + if (root.containsKey("effect")) { + const char *effect = root["effect"]; + this->set_effect(effect); + } + + return *this; +} +#endif + +void LightCall::perform() { + // use remote values for fallback + const char *name = this->parent_->get_name().c_str(); + if (this->publish_) { + ESP_LOGD(TAG, "'%s' Setting:", name); + } + + LightColorValues v = this->validate_(); + + if (this->publish_) { + // Only print state when it's being changed + bool current_state = this->parent_->remote_values.is_on(); + if (this->state_.value_or(current_state) != current_state) { + ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); + } + + if (this->brightness_.has_value()) { + ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); + } + + if (this->color_temperature_.has_value()) { + ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); + } + + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + v.get_blue() * 100.0f); + } + if (this->white_.has_value()) { + ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); + } + } + + if (this->has_flash_()) { + // FLASH + if (this->publish_) { + ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); + } + + this->parent_->start_flash_(v, *this->flash_length_); + } else if (this->has_transition_()) { + // TRANSITION + if (this->publish_) { + ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); + } + + // Special case: Transition and effect can be set when turning off + if (this->has_effect_()) { + if (this->publish_) { + ESP_LOGD(TAG, " Effect: 'None'"); + } + this->parent_->stop_effect_(); + } + + this->parent_->start_transition_(v, *this->transition_length_); + + } else if (this->has_effect_()) { + // EFFECT + auto effect = this->effect_; + const char *effect_s; + if (effect == 0) + effect_s = "None"; + else + effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); + + if (this->publish_) { + ESP_LOGD(TAG, " Effect: '%s'", effect_s); + } + + this->parent_->start_effect_(*this->effect_); + + // Also set light color values when starting an effect + // For example to turn off the light + this->parent_->set_immediately_(v, true); + } else { + // INSTANT CHANGE + this->parent_->set_immediately_(v, this->publish_); + } + + if (!this->has_transition_()) { + this->parent_->target_state_reached_callback_.call(); + } + if (this->publish_) { + this->parent_->publish_state(); + } + if (this->save_) { + this->parent_->save_remote_values_(); + } +} + +LightColorValues LightCall::validate_() { + // use remote values for fallback + auto *name = this->parent_->get_name().c_str(); + auto traits = this->parent_->get_traits(); + + // Brightness exists check + if (this->brightness_.has_value() && !traits.get_supports_brightness()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); + this->brightness_.reset(); + } + + // Transition length possible check + if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { + ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); + this->transition_length_.reset(); + } + + // RGB exists check + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!traits.get_supports_rgb()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); + this->red_.reset(); + this->green_.reset(); + this->blue_.reset(); + } + } + + // White value exists check + if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); + this->white_.reset(); + } + + // Color temperature exists check + if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); + this->color_temperature_.reset(); + } + + // If white channel is specified, set RGB to white color (when interlock is enabled) + if (this->white_.has_value()) { + if (traits.get_supports_color_interlock()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + // make white values binary aka 0.0f or 1.0f... this allows brightness to do its job + if (*this->white_ > 0.0f) { + this->white_ = optional(1.0f); + } else { + this->white_ = optional(0.0f); + } + } + } + // If only a color channel is specified, set white channel to 100% for white, otherwise 0% (when interlock is enabled) + else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (traits.get_supports_color_interlock()) { + if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { + this->white_ = optional(1.0f); + } else { + this->white_ = optional(0.0f); + } + } + } + // If only a color temperature is specified, change to white light + else if (this->color_temperature_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + + // if setting color temperature from color (i.e. switching to white light), set White to 100% + auto cv = this->parent_->remote_values; + bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; + if (traits.get_supports_color_interlock() || was_color) { + this->white_ = optional(1.0f); + } + } + +#define VALIDATE_RANGE_(name_, upper_name) \ + if (name_##_.has_value()) { \ + auto val = *name_##_; \ + if (val < 0.0f || val > 1.0f) { \ + ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [0.0 - 1.0]!", name, upper_name, val); \ + name_##_ = clamp(val, 0.0f, 1.0f); \ + } \ + } +#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name) + + // Range checks + VALIDATE_RANGE(brightness, "Brightness") + VALIDATE_RANGE(red, "Red") + VALIDATE_RANGE(green, "Green") + VALIDATE_RANGE(blue, "Blue") + VALIDATE_RANGE(white, "White") + + auto v = this->parent_->remote_values; + if (this->state_.has_value()) + v.set_state(*this->state_); + if (this->brightness_.has_value()) + v.set_brightness(*this->brightness_); + + if (this->red_.has_value()) + v.set_red(*this->red_); + if (this->green_.has_value()) + v.set_green(*this->green_); + if (this->blue_.has_value()) + v.set_blue(*this->blue_); + if (this->white_.has_value()) + v.set_white(*this->white_); + + if (this->color_temperature_.has_value()) + v.set_color_temperature(*this->color_temperature_); + + v.normalize_color(traits); + + // Flash length check + if (this->has_flash_() && *this->flash_length_ == 0) { + ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); + this->flash_length_.reset(); + } + + // validate transition length/flash length/effect not used at the same time + bool supports_transition = traits.get_supports_brightness(); + + // If effect is already active, remove effect start + if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { + this->effect_.reset(); + } + + // validate effect index + if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { + ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); + this->effect_.reset(); + } + + if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { + ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); + this->transition_length_.reset(); + this->flash_length_.reset(); + } + + if (this->has_flash_() && this->has_transition_()) { + ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); + this->transition_length_.reset(); + } + + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + supports_transition) { + // nothing specified and light supports transitions, set default transition length + this->transition_length_ = this->parent_->default_transition_length_; + } + + if (this->transition_length_.value_or(0) == 0) { + // 0 transition is interpreted as no transition (instant change) + this->transition_length_.reset(); + } + + if (this->has_transition_() && !supports_transition) { + ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); + this->transition_length_.reset(); + } + + // If not a flash and turning the light off, then disable the light + // Do not use light color values directly, so that effects can set 0% brightness + // Reason: When user turns off the light in frontend, the effect should also stop + if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { + if (this->has_effect_()) { + ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); + this->effect_.reset(); + } else if (this->parent_->active_effect_index_ != 0) { + // Auto turn off effect + this->effect_ = 0; + } + } + + // Disable saving for flashes + if (this->has_flash_()) + this->save_ = false; + + return v; +} +LightCall &LightCall::set_effect(const std::string &effect) { + if (strcasecmp(effect.c_str(), "none") == 0) { + this->set_effect(0); + return *this; + } + + bool found = false; + for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { + LightEffect *e = this->parent_->effects_[i]; + + if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { + this->set_effect(i + 1); + found = true; + break; + } + } + if (!found) { + ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); + } + return *this; +} +LightCall &LightCall::from_light_color_values(const LightColorValues &values) { + this->set_state(values.is_on()); + this->set_brightness_if_supported(values.get_brightness()); + this->set_red_if_supported(values.get_red()); + this->set_green_if_supported(values.get_green()); + this->set_blue_if_supported(values.get_blue()); + this->set_white_if_supported(values.get_white()); + this->set_color_temperature_if_supported(values.get_color_temperature()); + return *this; +} +LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { + if (this->parent_->get_traits().get_supports_brightness()) + this->set_transition_length(transition_length); + return *this; +} +LightCall &LightCall::set_brightness_if_supported(float brightness) { + if (this->parent_->get_traits().get_supports_brightness()) + this->set_brightness(brightness); + return *this; +} +LightCall &LightCall::set_red_if_supported(float red) { + if (this->parent_->get_traits().get_supports_rgb()) + this->set_red(red); + return *this; +} +LightCall &LightCall::set_green_if_supported(float green) { + if (this->parent_->get_traits().get_supports_rgb()) + this->set_green(green); + return *this; +} +LightCall &LightCall::set_blue_if_supported(float blue) { + if (this->parent_->get_traits().get_supports_rgb()) + this->set_blue(blue); + return *this; +} +LightCall &LightCall::set_white_if_supported(float white) { + if (this->parent_->get_traits().get_supports_rgb_white_value()) + this->set_white(white); + return *this; +} +LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { + if (this->parent_->get_traits().get_supports_color_temperature()) + this->set_color_temperature(color_temperature); + return *this; +} +LightCall &LightCall::set_state(optional state) { + this->state_ = state; + return *this; +} +LightCall &LightCall::set_state(bool state) { + this->state_ = state; + return *this; +} +LightCall &LightCall::set_transition_length(optional transition_length) { + this->transition_length_ = transition_length; + return *this; +} +LightCall &LightCall::set_transition_length(uint32_t transition_length) { + this->transition_length_ = transition_length; + return *this; +} +LightCall &LightCall::set_flash_length(optional flash_length) { + this->flash_length_ = flash_length; + return *this; +} +LightCall &LightCall::set_flash_length(uint32_t flash_length) { + this->flash_length_ = flash_length; + return *this; +} +LightCall &LightCall::set_brightness(optional brightness) { + this->brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_brightness(float brightness) { + this->brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_red(optional red) { + this->red_ = red; + return *this; +} +LightCall &LightCall::set_red(float red) { + this->red_ = red; + return *this; +} +LightCall &LightCall::set_green(optional green) { + this->green_ = green; + return *this; +} +LightCall &LightCall::set_green(float green) { + this->green_ = green; + return *this; +} +LightCall &LightCall::set_blue(optional blue) { + this->blue_ = blue; + return *this; +} +LightCall &LightCall::set_blue(float blue) { + this->blue_ = blue; + return *this; +} +LightCall &LightCall::set_white(optional white) { + this->white_ = white; + return *this; +} +LightCall &LightCall::set_white(float white) { + this->white_ = white; + return *this; +} +LightCall &LightCall::set_color_temperature(optional color_temperature) { + this->color_temperature_ = color_temperature; + return *this; +} +LightCall &LightCall::set_color_temperature(float color_temperature) { + this->color_temperature_ = color_temperature; + return *this; +} +LightCall &LightCall::set_effect(optional effect) { + if (effect.has_value()) + this->set_effect(*effect); + return *this; +} +LightCall &LightCall::set_effect(uint32_t effect_number) { + this->effect_ = effect_number; + return *this; +} +LightCall &LightCall::set_effect(optional effect_number) { + this->effect_ = effect_number; + return *this; +} +LightCall &LightCall::set_publish(bool publish) { + this->publish_ = publish; + return *this; +} +LightCall &LightCall::set_save(bool save) { + this->save_ = save; + return *this; +} +LightCall &LightCall::set_rgb(float red, float green, float blue) { + this->set_red(red); + this->set_green(green); + this->set_blue(blue); + return *this; +} +LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) { + this->set_rgb(red, green, blue); + this->set_white(white); + return *this; +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h new file mode 100644 index 0000000000..becea50004 --- /dev/null +++ b/esphome/components/light/light_call.h @@ -0,0 +1,160 @@ +#pragma once + +#include "esphome/core/optional.h" +#include "light_color_values.h" + +namespace esphome { +namespace light { + +class LightState; + +/** This class represents a requested change in a light state. + */ +class LightCall { + public: + explicit LightCall(LightState *parent) : parent_(parent) {} + + /// Set the binary ON/OFF state of the light. + LightCall &set_state(optional state); + /// Set the binary ON/OFF state of the light. + LightCall &set_state(bool state); + /** Set the transition length of this call in milliseconds. + * + * This argument is ignored for starting flashes and effects. + * + * Defaults to the default transition length defined in the light configuration. + */ + LightCall &set_transition_length(optional transition_length); + /** Set the transition length of this call in milliseconds. + * + * This argument is ignored for starting flashes and effects. + * + * Defaults to the default transition length defined in the light configuration. + */ + LightCall &set_transition_length(uint32_t transition_length); + /// Set the transition length property if the light supports transitions. + LightCall &set_transition_length_if_supported(uint32_t transition_length); + /// Start and set the flash length of this call in milliseconds. + LightCall &set_flash_length(optional flash_length); + /// Start and set the flash length of this call in milliseconds. + LightCall &set_flash_length(uint32_t flash_length); + /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) + LightCall &set_brightness(optional brightness); + /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) + LightCall &set_brightness(float brightness); + /// Set the brightness property if the light supports brightness. + LightCall &set_brightness_if_supported(float brightness); + /** Set the red RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_red(optional red); + /** Set the red RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_red(float red); + /// Set the red property if the light supports RGB. + LightCall &set_red_if_supported(float red); + /** Set the green RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_green(optional green); + /** Set the green RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_green(float green); + /// Set the green property if the light supports RGB. + LightCall &set_green_if_supported(float green); + /** Set the blue RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_blue(optional blue); + /** Set the blue RGB value of the light from 0.0 to 1.0. + * + * Note that this only controls the color of the light, not its brightness. + */ + LightCall &set_blue(float blue); + /// Set the blue property if the light supports RGB. + LightCall &set_blue_if_supported(float blue); + /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. + LightCall &set_white(optional white); + /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. + LightCall &set_white(float white); + /// Set the white property if the light supports RGB. + LightCall &set_white_if_supported(float white); + /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. + LightCall &set_color_temperature(optional color_temperature); + /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. + LightCall &set_color_temperature(float color_temperature); + /// Set the color_temperature property if the light supports color temperature. + LightCall &set_color_temperature_if_supported(float color_temperature); + /// Set the effect of the light by its name. + LightCall &set_effect(optional effect); + /// Set the effect of the light by its name. + LightCall &set_effect(const std::string &effect); + /// Set the effect of the light by its internal index number (only for internal use). + LightCall &set_effect(uint32_t effect_number); + LightCall &set_effect(optional effect_number); + /// Set whether this light call should trigger a publish state. + LightCall &set_publish(bool publish); + /// Set whether this light call should trigger a save state to recover them at startup.. + LightCall &set_save(bool save); + + /** Set the RGB color of the light by RGB values. + * + * Please note that this only changes the color of the light, not the brightness. + * + * @param red The red color value from 0.0 to 1.0. + * @param green The green color value from 0.0 to 1.0. + * @param blue The blue color value from 0.0 to 1.0. + * @return The light call for chaining setters. + */ + LightCall &set_rgb(float red, float green, float blue); + /** Set the RGBW color of the light by RGB values. + * + * Please note that this only changes the color of the light, not the brightness. + * + * @param red The red color value from 0.0 to 1.0. + * @param green The green color value from 0.0 to 1.0. + * @param blue The blue color value from 0.0 to 1.0. + * @param white The white color value from 0.0 to 1.0. + * @return The light call for chaining setters. + */ + LightCall &set_rgbw(float red, float green, float blue, float white); +#ifdef USE_JSON + LightCall &parse_color_json(JsonObject &root); + LightCall &parse_json(JsonObject &root); +#endif + LightCall &from_light_color_values(const LightColorValues &values); + + void perform(); + + protected: + /// Validate all properties and return the target light color values. + LightColorValues validate_(); + + bool has_transition_() { return this->transition_length_.has_value(); } + bool has_flash_() { return this->flash_length_.has_value(); } + bool has_effect_() { return this->effect_.has_value(); } + + LightState *parent_; + optional state_; + optional transition_length_; + optional flash_length_; + optional brightness_; + optional red_; + optional green_; + optional blue_; + optional white_; + optional color_temperature_; + optional effect_; + bool publish_{true}; + bool save_{true}; +}; + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 177dbb8d4e..69d147b6f3 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -7,80 +7,13 @@ namespace light { static const char *const TAG = "light"; -void LightState::start_transition_(const LightColorValues &target, uint32_t length) { - this->transformer_ = make_unique(millis(), length, this->current_values, target); - this->remote_values = this->transformer_->get_remote_values(); -} - -void LightState::start_flash_(const LightColorValues &target, uint32_t length) { - LightColorValues end_colors = this->current_values; - // If starting a flash if one is already happening, set end values to end values of current flash - // Hacky but works - if (this->transformer_ != nullptr) - end_colors = this->transformer_->get_end_values(); - this->transformer_ = make_unique(millis(), length, end_colors, target); - this->remote_values = this->transformer_->get_remote_values(); -} - LightState::LightState(const std::string &name, LightOutput *output) : Nameable(name), output_(output) {} -void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { - this->transformer_ = nullptr; - this->current_values = target; - if (set_remote_values) { - this->remote_values = target; - } - this->next_write_ = true; -} - -LightColorValues LightState::get_current_values() { return this->current_values; } - -void LightState::publish_state() { - this->remote_values_callback_.call(); - this->next_write_ = true; -} - -LightColorValues LightState::get_remote_values() { return this->remote_values; } - -std::string LightState::get_effect_name() { - if (this->active_effect_index_ > 0) - return this->effects_[this->active_effect_index_ - 1]->get_name(); - else - return "None"; -} - -void LightState::start_effect_(uint32_t effect_index) { - this->stop_effect_(); - if (effect_index == 0) - return; - - this->active_effect_index_ = effect_index; - auto *effect = this->get_active_effect_(); - effect->start_internal(); -} - -bool LightState::supports_effects() { return !this->effects_.empty(); } -void LightState::set_transformer_(std::unique_ptr transformer) { - this->transformer_ = std::move(transformer); -} -void LightState::stop_effect_() { - auto *effect = this->get_active_effect_(); - if (effect != nullptr) { - effect->stop(); - } - this->active_effect_index_ = 0; -} - -void LightState::set_default_transition_length(uint32_t default_transition_length) { - this->default_transition_length_ = default_transition_length; -} -#ifdef USE_JSON -void LightState::dump_json(JsonObject &root) { - if (this->supports_effects()) - root["effect"] = this->get_effect_name(); - this->remote_values.dump_json(root, this->output_->get_traits()); -} -#endif +LightTraits LightState::get_traits() { return this->output_->get_traits(); } +LightCall LightState::turn_on() { return this->make_call().set_state(true); } +LightCall LightState::turn_off() { return this->make_call().set_state(false); } +LightCall LightState::toggle() { return this->make_call().set_state(!this->remote_values.is_on()); } +LightCall LightState::make_call() { return LightCall(this); } struct LightStateRTCState { bool state{false}; @@ -144,6 +77,17 @@ void LightState::setup() { } call.perform(); } +void LightState::dump_config() { + ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); + if (this->get_traits().get_supports_brightness()) { + ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); + ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); + } + if (this->get_traits().get_supports_color_temperature()) { + ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); + ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); + } +} void LightState::loop() { // Apply effect (if any) auto *effect = this->get_active_effect_(); @@ -171,7 +115,47 @@ void LightState::loop() { this->next_write_ = false; } } -LightTraits LightState::get_traits() { return this->output_->get_traits(); } + +float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } +uint32_t LightState::hash_base() { return 1114400283; } + +LightColorValues LightState::get_current_values() { return this->current_values; } +LightColorValues LightState::get_remote_values() { return this->remote_values; } + +void LightState::publish_state() { + this->remote_values_callback_.call(); + this->next_write_ = true; +} + +LightOutput *LightState::get_output() const { return this->output_; } +std::string LightState::get_effect_name() { + if (this->active_effect_index_ > 0) + return this->effects_[this->active_effect_index_ - 1]->get_name(); + else + return "None"; +} + +void LightState::add_new_remote_values_callback(std::function &&send_callback) { + this->remote_values_callback_.add(std::move(send_callback)); +} +void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { + this->target_state_reached_callback_.add(std::move(send_callback)); +} + +#ifdef USE_JSON +void LightState::dump_json(JsonObject &root) { + if (this->supports_effects()) + root["effect"] = this->get_effect_name(); + this->remote_values.dump_json(root, this->output_->get_traits()); +} +#endif + +void LightState::set_default_transition_length(uint32_t default_transition_length) { + this->default_transition_length_ = default_transition_length; +} +void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } +void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } +bool LightState::supports_effects() { return !this->effects_.empty(); } const std::vector &LightState::get_effects() const { return this->effects_; } void LightState::add_effects(const std::vector &effects) { this->effects_.reserve(this->effects_.size() + effects.size()); @@ -179,551 +163,7 @@ void LightState::add_effects(const std::vector &effects) { this->effects_.push_back(effect); } } -LightCall LightState::turn_on() { return this->make_call().set_state(true); } -LightCall LightState::turn_off() { return this->make_call().set_state(false); } -LightCall LightState::toggle() { return this->make_call().set_state(!this->remote_values.is_on()); } -LightCall LightState::make_call() { return LightCall(this); } -uint32_t LightState::hash_base() { return 1114400283; } -void LightState::dump_config() { - ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().get_supports_brightness()) { - ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); - ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); - } - if (this->get_traits().get_supports_color_temperature()) { - ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); - ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); - } -} -#ifdef USE_MQTT_LIGHT -MQTTJSONLightComponent *LightState::get_mqtt() const { return this->mqtt_; } -void LightState::set_mqtt(MQTTJSONLightComponent *mqtt) { this->mqtt_ = mqtt; } -#endif -#ifdef USE_JSON -LightCall &LightCall::parse_color_json(JsonObject &root) { - if (root.containsKey("state")) { - auto val = parse_on_off(root["state"]); - switch (val) { - case PARSE_ON: - this->set_state(true); - break; - case PARSE_OFF: - this->set_state(false); - break; - case PARSE_TOGGLE: - this->set_state(!this->parent_->remote_values.is_on()); - break; - case PARSE_NONE: - break; - } - } - - if (root.containsKey("brightness")) { - this->set_brightness(float(root["brightness"]) / 255.0f); - } - - if (root.containsKey("color")) { - JsonObject &color = root["color"]; - if (color.containsKey("r")) { - this->set_red(float(color["r"]) / 255.0f); - } - if (color.containsKey("g")) { - this->set_green(float(color["g"]) / 255.0f); - } - if (color.containsKey("b")) { - this->set_blue(float(color["b"]) / 255.0f); - } - } - - if (root.containsKey("white_value")) { - this->set_white(float(root["white_value"]) / 255.0f); - } - - if (root.containsKey("color_temp")) { - this->set_color_temperature(float(root["color_temp"])); - } - - return *this; -} -LightCall &LightCall::parse_json(JsonObject &root) { - this->parse_color_json(root); - - if (root.containsKey("flash")) { - auto length = uint32_t(float(root["flash"]) * 1000); - this->set_flash_length(length); - } - - if (root.containsKey("transition")) { - auto length = uint32_t(float(root["transition"]) * 1000); - this->set_transition_length(length); - } - - if (root.containsKey("effect")) { - const char *effect = root["effect"]; - this->set_effect(effect); - } - - return *this; -} -#endif - -void LightCall::perform() { - // use remote values for fallback - const char *name = this->parent_->get_name().c_str(); - if (this->publish_) { - ESP_LOGD(TAG, "'%s' Setting:", name); - } - - LightColorValues v = this->validate_(); - - if (this->publish_) { - // Only print state when it's being changed - bool current_state = this->parent_->remote_values.is_on(); - if (this->state_.value_or(current_state) != current_state) { - ESP_LOGD(TAG, " State: %s", ONOFF(v.is_on())); - } - - if (this->brightness_.has_value()) { - ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); - } - - if (this->color_temperature_.has_value()) { - ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); - } - - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, - v.get_blue() * 100.0f); - } - if (this->white_.has_value()) { - ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); - } - } - - if (this->has_flash_()) { - // FLASH - if (this->publish_) { - ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); - } - - this->parent_->start_flash_(v, *this->flash_length_); - } else if (this->has_transition_()) { - // TRANSITION - if (this->publish_) { - ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); - } - - // Special case: Transition and effect can be set when turning off - if (this->has_effect_()) { - if (this->publish_) { - ESP_LOGD(TAG, " Effect: 'None'"); - } - this->parent_->stop_effect_(); - } - - this->parent_->start_transition_(v, *this->transition_length_); - - } else if (this->has_effect_()) { - // EFFECT - auto effect = this->effect_; - const char *effect_s; - if (effect == 0) - effect_s = "None"; - else - effect_s = this->parent_->effects_[*this->effect_ - 1]->get_name().c_str(); - - if (this->publish_) { - ESP_LOGD(TAG, " Effect: '%s'", effect_s); - } - - this->parent_->start_effect_(*this->effect_); - - // Also set light color values when starting an effect - // For example to turn off the light - this->parent_->set_immediately_(v, true); - } else { - // INSTANT CHANGE - this->parent_->set_immediately_(v, this->publish_); - } - - if (!this->has_transition_()) { - this->parent_->target_state_reached_callback_.call(); - } - if (this->publish_) { - this->parent_->publish_state(); - } - - if (this->save_) { - LightStateRTCState saved; - saved.state = v.is_on(); - saved.brightness = v.get_brightness(); - saved.red = v.get_red(); - saved.green = v.get_green(); - saved.blue = v.get_blue(); - saved.white = v.get_white(); - saved.color_temp = v.get_color_temperature(); - saved.effect = this->parent_->active_effect_index_; - this->parent_->rtc_.save(&saved); - } -} - -LightColorValues LightCall::validate_() { - // use remote values for fallback - auto *name = this->parent_->get_name().c_str(); - auto traits = this->parent_->get_traits(); - - // Brightness exists check - if (this->brightness_.has_value() && !traits.get_supports_brightness()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); - this->brightness_.reset(); - } - - // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { - ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); - this->transition_length_.reset(); - } - - // RGB exists check - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); - this->red_.reset(); - this->green_.reset(); - this->blue_.reset(); - } - } - - // White value exists check - if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); - this->white_.reset(); - } - - // Color temperature exists check - if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); - this->color_temperature_.reset(); - } - - // If white channel is specified, set RGB to white color (when interlock is enabled) - if (this->white_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - } - // make white values binary aka 0.0f or 1.0f... this allows brightness to do its job - if (*this->white_ > 0.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color channel is specified, set white channel to 100% for white, otherwise 0% (when interlock is enabled) - else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color temperature is specified, change to white light - else if (this->color_temperature_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - - // if setting color temperature from color (i.e. switching to white light), set White to 100% - auto cv = this->parent_->remote_values; - bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; - if (traits.get_supports_color_interlock() || was_color) { - this->white_ = optional(1.0f); - } - } - -#define VALIDATE_RANGE_(name_, upper_name) \ - if (name_##_.has_value()) { \ - auto val = *name_##_; \ - if (val < 0.0f || val > 1.0f) { \ - ESP_LOGW(TAG, "'%s' - %s value %.2f is out of range [0.0 - 1.0]!", name, upper_name, val); \ - name_##_ = clamp(val, 0.0f, 1.0f); \ - } \ - } -#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name) - - // Range checks - VALIDATE_RANGE(brightness, "Brightness") - VALIDATE_RANGE(red, "Red") - VALIDATE_RANGE(green, "Green") - VALIDATE_RANGE(blue, "Blue") - VALIDATE_RANGE(white, "White") - - auto v = this->parent_->remote_values; - if (this->state_.has_value()) - v.set_state(*this->state_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); - - if (this->red_.has_value()) - v.set_red(*this->red_); - if (this->green_.has_value()) - v.set_green(*this->green_); - if (this->blue_.has_value()) - v.set_blue(*this->blue_); - if (this->white_.has_value()) - v.set_white(*this->white_); - - if (this->color_temperature_.has_value()) - v.set_color_temperature(*this->color_temperature_); - - v.normalize_color(traits); - - // Flash length check - if (this->has_flash_() && *this->flash_length_ == 0) { - ESP_LOGW(TAG, "'%s' - Flash length must be greater than zero!", name); - this->flash_length_.reset(); - } - - // validate transition length/flash length/effect not used at the same time - bool supports_transition = traits.get_supports_brightness(); - - // If effect is already active, remove effect start - if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { - this->effect_.reset(); - } - - // validate effect index - if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); - this->effect_.reset(); - } - - if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - ESP_LOGW(TAG, "'%s' - Effect cannot be used together with transition/flash!", name); - this->transition_length_.reset(); - this->flash_length_.reset(); - } - - if (this->has_flash_() && this->has_transition_()) { - ESP_LOGW(TAG, "'%s' - Flash cannot be used together with transition!", name); - this->transition_length_.reset(); - } - - if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && - supports_transition) { - // nothing specified and light supports transitions, set default transition length - this->transition_length_ = this->parent_->default_transition_length_; - } - - if (this->transition_length_.value_or(0) == 0) { - // 0 transition is interpreted as no transition (instant change) - this->transition_length_.reset(); - } - - if (this->has_transition_() && !supports_transition) { - ESP_LOGW(TAG, "'%s' - Light does not support transitions!", name); - this->transition_length_.reset(); - } - - // If not a flash and turning the light off, then disable the light - // Do not use light color values directly, so that effects can set 0% brightness - // Reason: When user turns off the light in frontend, the effect should also stop - if (!this->has_flash_() && !this->state_.value_or(v.is_on())) { - if (this->has_effect_()) { - ESP_LOGW(TAG, "'%s' - Cannot start an effect when turning off!", name); - this->effect_.reset(); - } else if (this->parent_->active_effect_index_ != 0) { - // Auto turn off effect - this->effect_ = 0; - } - } - - // Disable saving for flashes - if (this->has_flash_()) - this->save_ = false; - - return v; -} -LightCall &LightCall::set_effect(const std::string &effect) { - if (strcasecmp(effect.c_str(), "none") == 0) { - this->set_effect(0); - return *this; - } - - bool found = false; - for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { - LightEffect *e = this->parent_->effects_[i]; - - if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { - this->set_effect(i + 1); - found = true; - break; - } - } - if (!found) { - ESP_LOGW(TAG, "'%s' - No such effect '%s'", this->parent_->get_name().c_str(), effect.c_str()); - } - return *this; -} -LightCall &LightCall::from_light_color_values(const LightColorValues &values) { - this->set_state(values.is_on()); - this->set_brightness_if_supported(values.get_brightness()); - this->set_red_if_supported(values.get_red()); - this->set_green_if_supported(values.get_green()); - this->set_blue_if_supported(values.get_blue()); - this->set_white_if_supported(values.get_white()); - this->set_color_temperature_if_supported(values.get_color_temperature()); - return *this; -} -LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { - if (this->parent_->get_traits().get_supports_brightness()) - this->set_transition_length(transition_length); - return *this; -} -LightCall &LightCall::set_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_brightness()) - this->set_brightness(brightness); - return *this; -} -LightCall &LightCall::set_red_if_supported(float red) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_red(red); - return *this; -} -LightCall &LightCall::set_green_if_supported(float green) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_green(green); - return *this; -} -LightCall &LightCall::set_blue_if_supported(float blue) { - if (this->parent_->get_traits().get_supports_rgb()) - this->set_blue(blue); - return *this; -} -LightCall &LightCall::set_white_if_supported(float white) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) - this->set_white(white); - return *this; -} -LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { - if (this->parent_->get_traits().get_supports_color_temperature()) - this->set_color_temperature(color_temperature); - return *this; -} -LightCall &LightCall::set_state(optional state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_state(bool state) { - this->state_ = state; - return *this; -} -LightCall &LightCall::set_transition_length(optional transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_transition_length(uint32_t transition_length) { - this->transition_length_ = transition_length; - return *this; -} -LightCall &LightCall::set_flash_length(optional flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_flash_length(uint32_t flash_length) { - this->flash_length_ = flash_length; - return *this; -} -LightCall &LightCall::set_brightness(optional brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_brightness(float brightness) { - this->brightness_ = brightness; - return *this; -} -LightCall &LightCall::set_red(optional red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_red(float red) { - this->red_ = red; - return *this; -} -LightCall &LightCall::set_green(optional green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_green(float green) { - this->green_ = green; - return *this; -} -LightCall &LightCall::set_blue(optional blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_blue(float blue) { - this->blue_ = blue; - return *this; -} -LightCall &LightCall::set_white(optional white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_white(float white) { - this->white_ = white; - return *this; -} -LightCall &LightCall::set_color_temperature(optional color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_color_temperature(float color_temperature) { - this->color_temperature_ = color_temperature; - return *this; -} -LightCall &LightCall::set_effect(optional effect) { - if (effect.has_value()) - this->set_effect(*effect); - return *this; -} -LightCall &LightCall::set_effect(uint32_t effect_number) { - this->effect_ = effect_number; - return *this; -} -LightCall &LightCall::set_effect(optional effect_number) { - this->effect_ = effect_number; - return *this; -} -LightCall &LightCall::set_publish(bool publish) { - this->publish_ = publish; - return *this; -} -LightCall &LightCall::set_save(bool save) { - this->save_ = save; - return *this; -} -LightCall &LightCall::set_rgb(float red, float green, float blue) { - this->set_red(red); - this->set_green(green); - this->set_blue(blue); - return *this; -} -LightCall &LightCall::set_rgbw(float red, float green, float blue, float white) { - this->set_rgb(red, green, blue); - this->set_white(white); - return *this; -} - -float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -LightOutput *LightState::get_output() const { return this->output_; } -void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness, this->gamma_correct_); @@ -748,19 +188,70 @@ void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bo this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, this->gamma_correct_, constant_brightness); } -void LightState::add_new_remote_values_callback(std::function &&send_callback) { - this->remote_values_callback_.add(std::move(send_callback)); -} -void LightState::add_new_target_state_reached_callback(std::function &&send_callback) { - this->target_state_reached_callback_.add(std::move(send_callback)); -} +void LightState::start_effect_(uint32_t effect_index) { + this->stop_effect_(); + if (effect_index == 0) + return; + + this->active_effect_index_ = effect_index; + auto *effect = this->get_active_effect_(); + effect->start_internal(); +} LightEffect *LightState::get_active_effect_() { if (this->active_effect_index_ == 0) return nullptr; else return this->effects_[this->active_effect_index_ - 1]; } +void LightState::stop_effect_() { + auto *effect = this->get_active_effect_(); + if (effect != nullptr) { + effect->stop(); + } + this->active_effect_index_ = 0; +} + +void LightState::start_transition_(const LightColorValues &target, uint32_t length) { + this->transformer_ = make_unique(millis(), length, this->current_values, target); + this->remote_values = this->transformer_->get_remote_values(); +} + +void LightState::start_flash_(const LightColorValues &target, uint32_t length) { + LightColorValues end_colors = this->current_values; + // If starting a flash if one is already happening, set end values to end values of current flash + // Hacky but works + if (this->transformer_ != nullptr) + end_colors = this->transformer_->get_end_values(); + this->transformer_ = make_unique(millis(), length, end_colors, target); + this->remote_values = this->transformer_->get_remote_values(); +} + +void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { + this->transformer_ = nullptr; + this->current_values = target; + if (set_remote_values) { + this->remote_values = target; + } + this->next_write_ = true; +} + +void LightState::set_transformer_(std::unique_ptr transformer) { + this->transformer_ = std::move(transformer); +} + +void LightState::save_remote_values_() { + LightStateRTCState saved; + saved.state = this->remote_values.is_on(); + saved.brightness = this->remote_values.get_brightness(); + saved.red = this->remote_values.get_red(); + saved.green = this->remote_values.get_green(); + saved.blue = this->remote_values.get_blue(); + saved.white = this->remote_values.get_white(); + saved.color_temp = this->remote_values.get_color_temperature(); + saved.effect = this->active_effect_index_; + this->rtc_.save(&saved); +} } // namespace light } // namespace esphome diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 10bda2d17b..5f3441daf7 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -5,161 +5,15 @@ #include "esphome/core/preferences.h" #include "light_effect.h" #include "light_color_values.h" +#include "light_call.h" #include "light_traits.h" #include "light_transformer.h" namespace esphome { namespace light { -class LightState; class LightOutput; -class LightCall { - public: - explicit LightCall(LightState *parent) : parent_(parent) {} - - /// Set the binary ON/OFF state of the light. - LightCall &set_state(optional state); - /// Set the binary ON/OFF state of the light. - LightCall &set_state(bool state); - /** Set the transition length of this call in milliseconds. - * - * This argument is ignored for starting flashes and effects. - * - * Defaults to the default transition length defined in the light configuration. - */ - LightCall &set_transition_length(optional transition_length); - /** Set the transition length of this call in milliseconds. - * - * This argument is ignored for starting flashes and effects. - * - * Defaults to the default transition length defined in the light configuration. - */ - LightCall &set_transition_length(uint32_t transition_length); - /// Set the transition length property if the light supports transitions. - LightCall &set_transition_length_if_supported(uint32_t transition_length); - /// Start and set the flash length of this call in milliseconds. - LightCall &set_flash_length(optional flash_length); - /// Start and set the flash length of this call in milliseconds. - LightCall &set_flash_length(uint32_t flash_length); - /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) - LightCall &set_brightness(optional brightness); - /// Set the target brightness of the light from 0.0 (fully off) to 1.0 (fully on) - LightCall &set_brightness(float brightness); - /// Set the brightness property if the light supports brightness. - LightCall &set_brightness_if_supported(float brightness); - /** Set the red RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_red(optional red); - /** Set the red RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_red(float red); - /// Set the red property if the light supports RGB. - LightCall &set_red_if_supported(float red); - /** Set the green RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_green(optional green); - /** Set the green RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_green(float green); - /// Set the green property if the light supports RGB. - LightCall &set_green_if_supported(float green); - /** Set the blue RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_blue(optional blue); - /** Set the blue RGB value of the light from 0.0 to 1.0. - * - * Note that this only controls the color of the light, not its brightness. - */ - LightCall &set_blue(float blue); - /// Set the blue property if the light supports RGB. - LightCall &set_blue_if_supported(float blue); - /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. - LightCall &set_white(optional white); - /// Set the white value value of the light from 0.0 to 1.0 for RGBW[W] lights. - LightCall &set_white(float white); - /// Set the white property if the light supports RGB. - LightCall &set_white_if_supported(float white); - /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. - LightCall &set_color_temperature(optional color_temperature); - /// Set the color temperature of the light in mireds for CWWW or RGBWW lights. - LightCall &set_color_temperature(float color_temperature); - /// Set the color_temperature property if the light supports color temperature. - LightCall &set_color_temperature_if_supported(float color_temperature); - /// Set the effect of the light by its name. - LightCall &set_effect(optional effect); - /// Set the effect of the light by its name. - LightCall &set_effect(const std::string &effect); - /// Set the effect of the light by its internal index number (only for internal use). - LightCall &set_effect(uint32_t effect_number); - LightCall &set_effect(optional effect_number); - /// Set whether this light call should trigger a publish state. - LightCall &set_publish(bool publish); - /// Set whether this light call should trigger a save state to recover them at startup.. - LightCall &set_save(bool save); - - /** Set the RGB color of the light by RGB values. - * - * Please note that this only changes the color of the light, not the brightness. - * - * @param red The red color value from 0.0 to 1.0. - * @param green The green color value from 0.0 to 1.0. - * @param blue The blue color value from 0.0 to 1.0. - * @return The light call for chaining setters. - */ - LightCall &set_rgb(float red, float green, float blue); - /** Set the RGBW color of the light by RGB values. - * - * Please note that this only changes the color of the light, not the brightness. - * - * @param red The red color value from 0.0 to 1.0. - * @param green The green color value from 0.0 to 1.0. - * @param blue The blue color value from 0.0 to 1.0. - * @param white The white color value from 0.0 to 1.0. - * @return The light call for chaining setters. - */ - LightCall &set_rgbw(float red, float green, float blue, float white); -#ifdef USE_JSON - LightCall &parse_color_json(JsonObject &root); - LightCall &parse_json(JsonObject &root); -#endif - LightCall &from_light_color_values(const LightColorValues &values); - - void perform(); - - protected: - /// Validate all properties and return the target light color values. - LightColorValues validate_(); - - bool has_transition_() { return this->transition_length_.has_value(); } - bool has_flash_() { return this->flash_length_.has_value(); } - bool has_effect_() { return this->effect_.has_value(); } - - LightState *parent_; - optional state_; - optional transition_length_; - optional flash_length_; - optional brightness_; - optional red_; - optional green_; - optional blue_; - optional white_; - optional color_temperature_; - optional effect_; - bool publish_{true}; - bool save_{true}; -}; - enum LightRestoreMode { LIGHT_RESTORE_DEFAULT_OFF, LIGHT_RESTORE_DEFAULT_ON, @@ -204,14 +58,6 @@ class LightState : public Nameable, public Component { */ LightColorValues current_values; - /// Deprecated method to access current_values. - ESPDEPRECATED("get_current_values() is deprecated, please use .current_values instead.") - LightColorValues get_current_values(); - - /// Deprecated method to access remote_values. - ESPDEPRECATED("get_remote_values() is deprecated, please use .remote_values instead.") - LightColorValues get_remote_values(); - /** The remote color values reported to the frontend. * * These are different from the "current" values: For example transitions will @@ -222,6 +68,14 @@ class LightState : public Nameable, public Component { */ LightColorValues remote_values; + /// Deprecated method to access current_values. + ESPDEPRECATED("get_current_values() is deprecated, please use .current_values instead.") + LightColorValues get_current_values(); + + /// Deprecated method to access remote_values. + ESPDEPRECATED("get_remote_values() is deprecated, please use .remote_values instead.") + LightColorValues get_remote_values(); + /// Publish the currently active state to the frontend. void publish_state(); @@ -231,29 +85,22 @@ class LightState : public Nameable, public Component { /// Return the name of the current effect, or if no effect is active "None". std::string get_effect_name(); - /** This lets front-end components subscribe to light change events. - * - * This is different from add_new_current_values_callback in that it only sends events for start - * and end values. For example, with transitions it will only send a single callback whereas - * the callback passed in add_new_current_values_callback will be called every loop() cycle when - * a transition is active - * - * Note the callback should get the output values through get_remote_values(). + /** + * This lets front-end components subscribe to light change events. This callback is called once + * when the remote color values are changed. * * @param send_callback The callback. */ void add_new_remote_values_callback(std::function &&send_callback); /** - * The callback is called once the state of current_values and remote_values are equal + * The callback is called once the state of current_values and remote_values are equal (when the + * transition is finished). * * @param send_callback */ void add_new_target_state_reached_callback(std::function &&send_callback); - /// Return whether the light has any effects that meet the trait requirements. - bool supports_effects(); - #ifdef USE_JSON /// Dump the state of this light as JSON. void dump_json(JsonObject &root); @@ -265,10 +112,17 @@ class LightState : public Nameable, public Component { /// Set the gamma correction factor void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } - void set_restore_mode(LightRestoreMode restore_mode) { restore_mode_ = restore_mode; } + /// Set the restore mode of this light + void set_restore_mode(LightRestoreMode restore_mode); + + /// Return whether the light has any effects that meet the trait requirements. + bool supports_effects(); + + /// Get all effects for this light state. const std::vector &get_effects() const; + /// Add effects for this light state. void add_effects(const std::vector &effects); void current_values_as_binary(bool *binary); @@ -293,6 +147,8 @@ class LightState : public Nameable, public Component { /// Internal method to start an effect with the given index void start_effect_(uint32_t effect_index); + /// Internal method to get the currently active effect + LightEffect *get_active_effect_(); /// Internal method to stop the current effect (if one is active). void stop_effect_(); /// Internal method to start a transition to the target color with the given length. @@ -307,18 +163,21 @@ class LightState : public Nameable, public Component { /// Internal method to start a transformer. void set_transformer_(std::unique_ptr transformer); - LightEffect *get_active_effect_(); + /// Internal method to save the current remote_values to the preferences + void save_remote_values_(); - /// Object used to store the persisted values of the light. - ESPPreferenceObject rtc_; - /// Restore mode of the light. - LightRestoreMode restore_mode_; - /// Default transition length for all transitions in ms. - uint32_t default_transition_length_{}; + /// Store the output to allow effects to have more access. + LightOutput *output_; /// Value for storing the index of the currently active effect. 0 if no effect is active uint32_t active_effect_index_{}; /// The currently active transformer for this light (transition/flash). std::unique_ptr transformer_{nullptr}; + /// Whether the light value should be written in the next cycle. + bool next_write_{true}; + + /// Object used to store the persisted values of the light. + ESPPreferenceObject rtc_; + /** Callback to call when new values for the frontend are available. * * "Remote values" are light color values that are reported to the frontend and have a lower @@ -333,11 +192,12 @@ class LightState : public Nameable, public Component { */ CallbackManager target_state_reached_callback_{}; - LightOutput *output_; ///< Store the output to allow effects to have more access. - /// Whether the light value should be written in the next cycle. - bool next_write_{true}; + /// Default transition length for all transitions in ms. + uint32_t default_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; + /// Restore mode of the light. + LightRestoreMode restore_mode_; /// List of effects for this light. std::vector effects_; }; diff --git a/esphome/core/optional.h b/esphome/core/optional.h index 7ace7b122a..5b96781e63 100644 --- a/esphome/core/optional.h +++ b/esphome/core/optional.h @@ -16,6 +16,8 @@ // // Modified by Otto Winter on 18.05.18 +#include + namespace esphome { // type for nullopt From 93f8ee7e60cc8b16651dbce4c74fabb1e2686115 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 14 Jun 2021 23:12:06 +0200 Subject: [PATCH 494/643] Fix non-const global (#1907) --- esphome/components/light/light_call.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 359da51de9..3fbe921aa1 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace light { -static const char *TAG = "light"; +static const char *const TAG = "light"; #ifdef USE_JSON LightCall &LightCall::parse_color_json(JsonObject &root) { From c4110436810dfc11da9735b0eddd2097603a5664 Mon Sep 17 00:00:00 2001 From: dentra Date: Tue, 15 Jun 2021 01:45:22 +0300 Subject: [PATCH 495/643] Adds support cpp to vscode (#1828) Co-authored-by: Stefan Agner --- .devcontainer/devcontainer.json | 37 +++++++++++++++++++++++++----- .editorconfig | 10 ++++++-- docker/Dockerfile.dev | 14 +---------- script/bump-docker-base-version.py | 5 ---- script/devcontainer-post-create | 18 +++++++++++++++ 5 files changed, 58 insertions(+), 26 deletions(-) create mode 100755 script/devcontainer-post-create diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8f7d751437..0e6fe19e0e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -2,16 +2,29 @@ "name": "ESPHome Dev", "context": "..", "dockerFile": "../docker/Dockerfile.dev", - "postCreateCommand": "mkdir -p config && pip3 install -e .", - "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], + "postCreateCommand": [ + "script/devcontainer-post-create" + ], + "runArgs": [ + "--privileged", + "-e", + "ESPHOME_DASHBOARD_USE_PING=1" + ], "appPort": 6052, "extensions": [ + // python "ms-python.python", "visualstudioexptteam.vscodeintellicode", - "redhat.vscode-yaml" + // yaml + "redhat.vscode-yaml", + // cpp + "ms-vscode.cpptools", + // editorconfig + "editorconfig.editorconfig", ], "settings": { - "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.pythonPath": "/usr/bin/python3", "python.linting.pylintEnabled": true, "python.linting.enabled": true, "python.formatting.provider": "black", @@ -19,7 +32,7 @@ "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "terminal.integrated.shell.linux": "/bin/bash", + "terminal.integrated.defaultProfile.linux": "/bin/bash", "yaml.customTags": [ "!secret scalar", "!lambda scalar", @@ -27,6 +40,18 @@ "!include_dir_list scalar", "!include_dir_merge_list scalar", "!include_dir_merge_named scalar" - ] + ], + "files.exclude": { + "**/.git": true, + "**/.DS_Store": true, + "**/*.pyc": { + "when": "$(basename).py" + }, + "**/__pycache__": true + }, + "files.associations": { + "**/.vscode/*.json": "jsonc" + }, + "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", } } diff --git a/.editorconfig b/.editorconfig index 29cbb1e32f..8ccf1eeebc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,7 @@ insert_final_newline = true charset = utf-8 # python -[*.{py}] +[*.py] indent_style = space indent_size = 4 @@ -25,4 +25,10 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 -quote_type = single \ No newline at end of file +quote_type = single + +# JSON +[*.json] +indent_style = space +indent_size = 2 + diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index ebcf14d1bc..27eaa18da9 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,13 +1 @@ -FROM esphome/esphome-base-amd64:3.4.0 - -COPY . . - -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - python3-wheel \ - net-tools \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspaces -ENV SHELL /bin/bash +FROM esphome/esphome-lint:1.1 diff --git a/script/bump-docker-base-version.py b/script/bump-docker-base-version.py index 765a330ce4..f5f4399fd2 100755 --- a/script/bump-docker-base-version.py +++ b/script/bump-docker-base-version.py @@ -28,11 +28,6 @@ def write_version(version: str): r"ARG BUILD_FROM=esphome/esphome-base-amd64:.*", f"ARG BUILD_FROM=esphome/esphome-base-amd64:{version}", ) - sub( - "docker/Dockerfile.dev", - r"FROM esphome/esphome-base-amd64:.*", - f"FROM esphome/esphome-base-amd64:{version}", - ) sub( "docker/Dockerfile.lint", r"FROM esphome/esphome-lint-base:.*", diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create new file mode 100755 index 0000000000..f4e5ce0d5a --- /dev/null +++ b/script/devcontainer-post-create @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +# set -x + +mkdir -p config +pip3 install -e . + +cpp_json=.vscode/c_cpp_properties.json +if [ ! -f $cpp_json ]; then + echo "Initializing PlatformIO..." + pio init --ide vscode --silent + sed -i "/\\/workspaces\/esphome\/include/d" $cpp_json +else + echo "Cpp environment already configured. To reconfigure it you could run one the following commands:" + echo " pio init --ide vscode -e livingroom8266" + echo " pio init --ide vscode -e livingroom32" +fi From da7eb9ac907bd5f47ef21c93c8c7c08c79edfac4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Jun 2021 11:05:10 +1200 Subject: [PATCH 496/643] Allow no networks or AP to be set. (#1908) Co-authored-by: Paulus Schoutsen --- esphome/components/wifi/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c45e179bc4..851c5f1b90 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -157,9 +157,7 @@ def _validate(config): config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) if (CONF_NETWORKS not in config) and (CONF_AP not in config): - raise cv.Invalid( - "Please specify at least an SSID or an Access Point " "to create." - ) + config[CONF_NETWORKS] = [] if config.get(CONF_FAST_CONNECT, False): networks = config.get(CONF_NETWORKS, []) From 86710ed4835ea84de714927634b6f78e6e0a88b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 15 Jun 2021 13:16:43 +1200 Subject: [PATCH 497/643] Validate that either networks, ap, or improv is set up (#1910) --- esphome/components/wifi/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 851c5f1b90..a701aa37e5 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -137,6 +137,18 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( ) +def validate(config, item_config): + if ( + (CONF_NETWORKS in item_config) + and (item_config[CONF_NETWORKS] == []) + and (CONF_AP not in item_config) + ): + if "esp32_improv" not in config: + raise ValueError( + "Please specify at least an SSID or an Access Point to create." + ) + + def _validate(config): if CONF_PASSWORD in config and CONF_SSID not in config: raise cv.Invalid("Cannot have WiFi password without SSID!") From 92bbedfa5aef0b44cae8b256460f8a1a28fa0904 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 15 Jun 2021 08:48:18 +0200 Subject: [PATCH 498/643] Fix #1908 mutating input parameter --- esphome/components/wifi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index a701aa37e5..13d698d245 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -169,6 +169,7 @@ def _validate(config): config[CONF_NETWORKS] = cv.ensure_list(WIFI_NETWORK_STA)(network) if (CONF_NETWORKS not in config) and (CONF_AP not in config): + config = config.copy() config[CONF_NETWORKS] = [] if config.get(CONF_FAST_CONNECT, False): From a80f9ed3367a297d709f095ed9324aa47a93617d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 15 Jun 2021 08:50:58 +0200 Subject: [PATCH 499/643] Support ESP8266 Arduino 3.0.0 (#1897) Co-authored-by: Otto Winter --- esphome/components/sensor/filter.cpp | 2 +- esphome/components/wifi/wifi_component_esp8266.cpp | 14 ++++++++++++++ esphome/const.py | 1 + esphome/core/helpers.cpp | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index a2058f7d90..bbe47b43ec 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -237,7 +237,7 @@ optional FilterOutValueFilter::new_value(float value) { return value; } else { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = pow10f(accuracy); + float accuracy_mult = powf(10.0f, accuracy); float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); float rounded_value = roundf(accuracy_mult * value); if (rounded_filter_out == rounded_value) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 0425f58c28..2f6c32aec6 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -10,6 +10,10 @@ #include #endif +#ifdef WIFI_IS_OFF_AT_BOOT // Identifies ESP8266 Arduino 3.0.0 +#define ARDUINO_ESP8266_RELEASE_3 +#endif + extern "C" { #include "lwip/err.h" #include "lwip/dns.h" @@ -18,6 +22,12 @@ extern "C" { #if LWIP_IPV6 #include "lwip/netif.h" // struct netif #endif +#ifdef ARDUINO_ESP8266_RELEASE_3 +#include "LwipDhcpServer.h" +#define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease) +#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) +#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode) +#endif } #include "esphome/core/helpers.h" @@ -649,6 +659,10 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { return false; } +#ifdef ARDUINO_ESP8266_RELEASE_3 + dhcpSoftAP.begin(&info); +#endif + struct dhcps_lease lease {}; IPAddress start_address = info.ip.addr; start_address[3] += 99; diff --git a/esphome/const.py b/esphome/const.py index b50b60a204..bb20c606ce 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -28,6 +28,7 @@ ARDUINO_VERSION_ESP32 = { # See also https://github.com/platformio/platform-espressif8266/releases ARDUINO_VERSION_ESP8266 = { "dev": "https://github.com/platformio/platform-espressif8266.git", + "3.0.0": "platformio/espressif8266@3.0.0", "2.7.4": "platformio/espressif8266@2.6.2", "2.7.3": "platformio/espressif8266@2.6.1", "2.7.2": "platformio/espressif8266@2.6.0", diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f78dcf3183..194ab08af3 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -105,7 +105,7 @@ std::string truncate_string(const std::string &s, size_t length) { } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - auto multiplier = float(pow10(accuracy_decimals)); + auto multiplier = float(powf(10.0f, accuracy_decimals)); float value_rounded = roundf(value * multiplier) / multiplier; char tmp[32]; // should be enough, but we should maybe improve this at some point. dtostrf(value_rounded, 0, uint8_t(std::max(0, int(accuracy_decimals))), tmp); From d781f3a11bed164be3b782216ca0f7a7edc462ca Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Jun 2021 01:18:27 -0700 Subject: [PATCH 500/643] Bump frontend to 20210614.0 (#1912) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e7f69865da..6ae20849da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210611.0 +esphome-dashboard==20210614.1 From 424c34225f831882d313d203da30792d6aab7d31 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 15 Jun 2021 10:19:21 +0200 Subject: [PATCH 501/643] Run script/setup in devcontainer instead of pip install (#1913) --- script/devcontainer-post-create | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/devcontainer-post-create b/script/devcontainer-post-create index f4e5ce0d5a..4b4dc5b6c9 100755 --- a/script/devcontainer-post-create +++ b/script/devcontainer-post-create @@ -4,7 +4,7 @@ set -e # set -x mkdir -p config -pip3 install -e . +script/setup cpp_json=.vscode/c_cpp_properties.json if [ ! -f $cpp_json ]; then From 24ba9eba460658a1e60a0dd6030e91c530e4c28c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 15 Jun 2021 05:20:24 -0300 Subject: [PATCH 502/643] fixes compatibility with esphome cfg vscode (#1911) --- esphome/__main__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 9af08a8f21..c7b2151728 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -504,6 +504,7 @@ def parse_args(argv): "version", "clean", "dashboard", + "vscode", ], ) @@ -686,7 +687,7 @@ def run_esphome(argv): CORE.dashboard = args.dashboard setup_log(args.verbose, args.quiet) - if args.deprecated_argv_suggestion is not None: + if args.deprecated_argv_suggestion is not None and args.command != "vscode": _LOGGER.warning( "Calling ESPHome with the configuration before the command is deprecated " "and will be removed in the future. " From 25b116048c7d7d22b82f30b8234d7efe6b035b1e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Jun 2021 14:04:17 -0700 Subject: [PATCH 503/643] Bump dashboard to 20210615.0 (#1918) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6ae20849da..33436fb1b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210614.1 +esphome-dashboard==20210615.0 From 5591832b5024399a8dbb087655d3e51292dd9b87 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Jun 2021 13:49:06 +1200 Subject: [PATCH 504/643] Shorten the ble name to prevent crash with long device names (#1920) --- esphome/components/esp32_ble/ble.cpp | 11 ++++++++++- esphome/core/application.h | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 756affaead..a54ee48b30 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -91,7 +91,16 @@ bool ESP32BLE::ble_setup_() { } } - err = esp_ble_gap_set_device_name(App.get_name().c_str()); + std::string name = App.get_name(); + if (name.length() > 20) { + if (App.is_name_add_mac_suffix_enabled()) { + name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address + } else { + name = name.substr(0, 20); + } + } + + err = esp_ble_gap_set_device_name(name.c_str()); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_device_name failed: %d", err); return false; diff --git a/esphome/core/application.h b/esphome/core/application.h index c674210ac0..774f6e3aa8 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -38,6 +38,7 @@ namespace esphome { class Application { public: void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { + this->name_add_mac_suffix_ = name_add_mac_suffix; if (name_add_mac_suffix) { this->name_ = name + "-" + get_mac_address().substr(6); } else { @@ -97,6 +98,8 @@ class Application { /// Get the name of this Application set by set_name(). const std::string &get_name() const { return this->name_; } + bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } + const std::string &get_compilation_time() const { return this->compilation_time_; } /** Set the target interval with which to run the loop() calls. @@ -245,6 +248,7 @@ class Application { std::string name_; std::string compilation_time_; + bool name_add_mac_suffix_; uint32_t last_loop_{0}; uint32_t loop_interval_{16}; int dump_config_at_{-1}; From 607c3ae65151dd791a02c92d92eac08ffff2baaa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 16 Jun 2021 19:39:04 +0200 Subject: [PATCH 505/643] Fix update-all from dashboard (#1924) --- esphome/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index c7b2151728..48f8bea083 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -394,7 +394,7 @@ def command_update_all(args): import click success = {} - files = list_yaml_files(args.configuration) + files = list_yaml_files(args.configuration[0]) twidth = 60 def print_bar(middle_text): @@ -408,7 +408,7 @@ def command_update_all(args): print("-" * twidth) print() rc = run_external_process( - "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" + "esphome", "--dashboard", "run", "--no-logs", "--device", "OTA", f ) if rc == 0: print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) From e5929225eb936957317a1a4a43004909031130b3 Mon Sep 17 00:00:00 2001 From: Barry Loong Date: Thu, 17 Jun 2021 17:39:59 +0800 Subject: [PATCH 506/643] Fix typo in test3.yaml (#1928) --- tests/test3.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test3.yaml b/tests/test3.yaml index af5398b604..c709539309 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1097,27 +1097,27 @@ fingerprint_grow: new_password: 0xA65B9840 on_finger_scan_matched: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_finger_scan_matched + event: esphome.${device_name}_fingerprint_grow_finger_scan_matched data: finger_id: !lambda 'return finger_id;' confidence: !lambda 'return confidence;' on_finger_scan_unmatched: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_finger_scan_unmatched + event: esphome.${device_name}_fingerprint_grow_finger_scan_unmatched on_enrollment_scan: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_enrollment_scan + event: esphome.${device_name}_fingerprint_grow_enrollment_scan data: finger_id: !lambda 'return finger_id;' scan_num: !lambda 'return scan_num;' on_enrollment_done: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_node_enrollment_done + event: esphome.${device_name}_fingerprint_grow_node_enrollment_done data: finger_id: !lambda 'return finger_id;' on_enrollment_failed: - homeassistant.event: - event: esphome.${devicename}_fingerprint_grow_enrollment_failed + event: esphome.${device_name}_fingerprint_grow_enrollment_failed data: finger_id: !lambda 'return finger_id;' uart_id: uart6 From ef1e91d838f3222587d8d55fe0e372fc901f878c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Jun 2021 12:35:54 -0700 Subject: [PATCH 507/643] Update dashboard to 20210617.0 (#1930) --- esphome/dashboard/dashboard.py | 28 +++++++++++++--------------- requirements.txt | 2 +- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 90061a3d4e..00b12199c0 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -716,9 +716,6 @@ class LogoutHandler(BaseHandler): self.redirect("./login") -_STATIC_FILE_HASHES = {} - - def get_base_frontend_path(): if ENV_DEV not in os.environ: import esphome_dashboard @@ -741,19 +738,23 @@ def get_static_path(*args): return os.path.join(get_base_frontend_path(), "static", *args) +@functools.lru_cache(maxsize=None) def get_static_file_url(name): + base = f"./static/{name}" + + if ENV_DEV in os.environ: + return base + # Module imports can't deduplicate if stuff added to url if name == "js/esphome/index.js": - return f"./static/{name}" + import esphome_dashboard - if name in _STATIC_FILE_HASHES: - hash_ = _STATIC_FILE_HASHES[name] - else: - path = get_static_path(name) - with open(path, "rb") as f_handle: - hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] - _STATIC_FILE_HASHES[name] = hash_ - return f"./static/{name}?hash={hash_}" + return base.replace("index.js", esphome_dashboard.entrypoint()) + + path = get_static_path(name) + with open(path, "rb") as f_handle: + hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] + return f"{base}?hash={hash_}" def make_app(debug=get_bool_env(ENV_DEV)): @@ -820,9 +821,6 @@ def make_app(debug=get_bool_env(ENV_DEV)): **app_settings, ) - if debug: - _STATIC_FILE_HASHES.clear() - return app diff --git a/requirements.txt b/requirements.txt index 33436fb1b1..f02d65e518 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210615.0 +esphome-dashboard==20210617.1 From c19b3ecd4387dd7b235bcdb1acbbac37f73df387 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Thu, 17 Jun 2021 23:39:13 +0400 Subject: [PATCH 508/643] Fix: midea_ac: fixed query status frame (#1922) --- esphome/components/midea_ac/midea_frame.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp index 3d210d89f0..4041ca7923 100644 --- a/esphome/components/midea_ac/midea_frame.cpp +++ b/esphome/components/midea_ac/midea_frame.cpp @@ -8,9 +8,9 @@ const std::string MIDEA_SILENT_FAN_MODE = "silent"; const std::string MIDEA_TURBO_FAN_MODE = "turbo"; const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; -const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00, - 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68}; +const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x61, + 0x00, 0xFF, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE3, 0xA8}; const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, From 2419bc367820b985d153d7ffdc304949cc242adb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Jun 2021 21:54:14 +0200 Subject: [PATCH 509/643] Improve config final validation (#1917) --- esphome/components/cse7766/sensor.py | 9 +- esphome/components/dfplayer/__init__.py | 9 +- esphome/components/gps/__init__.py | 5 +- esphome/components/http_request/__init__.py | 68 +++++++-------- esphome/components/rc522_spi/__init__.py | 9 +- esphome/components/rtttl/__init__.py | 24 +++-- esphome/components/sim800l/__init__.py | 7 +- esphome/components/spi/__init__.py | 28 ++++-- esphome/components/uart/__init__.py | 97 +++++++++++++-------- esphome/components/wifi/__init__.py | 22 ++--- esphome/config.py | 76 ++++++++++------ esphome/config_validation.py | 8 +- esphome/core/__init__.py | 10 +-- esphome/cpp_helpers.py | 3 +- esphome/final_validate.py | 57 ++++++++++++ esphome/loader.py | 10 ++- esphome/storage_json.py | 5 +- esphome/types.py | 18 ++++ 18 files changed, 303 insertions(+), 162 deletions(-) create mode 100644 esphome/final_validate.py create mode 100644 esphome/types.py diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 98cf4da96d..4ccb346efd 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -45,6 +45,9 @@ CONFIG_SCHEMA = ( .extend(cv.polling_component_schema("60s")) .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "cse7766", baud_rate=4800, require_rx=True +) async def to_code(config): @@ -64,9 +67,3 @@ async def to_code(config): conf = config[CONF_POWER] sens = await sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) - - -def validate(config, item_config): - uart.validate_device( - "cse7766", config, item_config, baud_rate=4800, require_tx=False - ) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 6af83888ab..3cdfc8ab85 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -68,6 +68,9 @@ CONFIG_SCHEMA = cv.All( } ).extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "dfplayer", baud_rate=9600, require_tx=True +) async def to_code(config): @@ -80,12 +83,6 @@ async def to_code(config): await automation.build_automation(trigger, [], conf) -def validate(config, item_config): - uart.validate_device( - "dfplayer", config, item_config, baud_rate=9600, require_rx=False - ) - - @automation.register_action( "dfplayer.play_next", NextAction, diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index de3eae1115..2867aa7325 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -62,6 +62,7 @@ CONFIG_SCHEMA = ( .extend(cv.polling_component_schema("20s")) .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("gps", require_rx=True) async def to_code(config): @@ -95,7 +96,3 @@ async def to_code(config): # https://platformio.org/lib/show/1655/TinyGPSPlus cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict - - -def validate(config, item_config): - uart.validate_device("gps", config, item_config, require_tx=False) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 650602150a..f4475060df 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -2,6 +2,7 @@ import urllib.parse as urlparse import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import automation from esphome.const import ( CONF_ID, @@ -14,7 +15,6 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, Lambda -from esphome.core.config import PLATFORMIO_ESP8266_LUT DEPENDENCIES = ["network"] AUTO_LOAD = ["json"] @@ -36,29 +36,6 @@ CONF_VERIFY_SSL = "verify_ssl" CONF_ON_RESPONSE = "on_response" -def validate_framework(config): - if CORE.is_esp32: - return config - - version = "RECOMMENDED" - if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]: - version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION] - - if version in ["LATEST", "DEV"]: - return config - - framework = ( - PLATFORMIO_ESP8266_LUT[version] - if version in PLATFORMIO_ESP8266_LUT - else version - ) - if framework < ARDUINO_VERSION_ESP8266["2.5.1"]: - raise cv.Invalid( - "This component is not supported on arduino framework version below 2.5.1" - ) - return config - - def validate_url(value): value = cv.string(value) try: @@ -92,19 +69,36 @@ def validate_secure_url(config): return config -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(HttpRequestComponent), - cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string, - cv.Optional( - CONF_TIMEOUT, default="5s" - ): cv.positive_time_period_milliseconds, - } - ) - .add_extra(validate_framework) - .extend(cv.COMPONENT_SCHEMA) -) +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(HttpRequestComponent), + cv.Optional(CONF_USERAGENT, "ESPHome"): cv.string, + cv.Optional(CONF_TIMEOUT, default="5s"): cv.positive_time_period_milliseconds, + } +).extend(cv.COMPONENT_SCHEMA) + + +def validate_framework(config): + if CORE.is_esp32: + return + + # only for ESP8266 + path = [CONF_ESPHOME, CONF_ARDUINO_VERSION] + version: str = fv.full_config.get().get_config_for_path(path) + + reverse_map = {v: k for k, v in ARDUINO_VERSION_ESP8266.items()} + framework_version = reverse_map.get(version) + if framework_version is None or framework_version == "dev": + return + + if framework_version < "2.5.1": + raise cv.Invalid( + "This component is not supported on arduino framework version below 2.5.1", + path=[cv.ROOT_CONFIG_PATH] + path, + ) + + +FINAL_VALIDATE_SCHEMA = cv.Schema(validate_framework) async def to_code(config): diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py index 68b1e64145..77b0a99662 100644 --- a/esphome/components/rc522_spi/__init__.py +++ b/esphome/components/rc522_spi/__init__.py @@ -19,13 +19,12 @@ CONFIG_SCHEMA = cv.All( ).extend(spi.spi_device_schema(cs_pin_required=True)) ) +FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( + "rc522_spi", require_miso=True, require_mosi=True +) + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await rc522.setup_rc522(var, config) await spi.register_spi_device(var, config) - - -def validate(config, item_config): - # validate given SPI hub is suitable for rc522_spi, it needs both miso and mosi - spi.validate_device("rc522_spi", config, item_config, True, True) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index 7f860fe3d7..e9453896ac 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -1,6 +1,7 @@ import logging import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import automation from esphome.components.output import FloatOutput from esphome.const import CONF_ID, CONF_OUTPUT, CONF_PLATFORM, CONF_TRIGGER_ID @@ -36,12 +37,8 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate(config, item_config): - # Not adding this to FloatOutput as this is the only component which needs `update_frequency` - - parent_config = config.get_config_by_id(item_config[CONF_OUTPUT]) - platform = parent_config[CONF_PLATFORM] - +def validate_parent_output_config(value): + platform = value.get(CONF_PLATFORM) PWM_GOOD = ["esp8266_pwm", "ledc"] PWM_BAD = [ "ac_dimmer ", @@ -55,14 +52,25 @@ def validate(config, item_config): ] if platform in PWM_BAD: - raise ValueError(f"Component rtttl cannot use {platform} as output component") + raise cv.Invalid(f"Component rtttl cannot use {platform} as output component") if platform not in PWM_GOOD: _LOGGER.warning( - "Component rtttl is not known to work with the selected output type. Make sure this output supports custom frequency output method." + "Component rtttl is not known to work with the selected output type. " + "Make sure this output supports custom frequency output method." ) +FINAL_VALIDATE_SCHEMA = cv.Schema( + { + cv.Required(CONF_OUTPUT): fv.id_declaration_match_schema( + validate_parent_output_config + ) + }, + extra=cv.ALLOW_EXTRA, +) + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index 40c011a769..0887b8640f 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -40,6 +40,9 @@ CONFIG_SCHEMA = cv.All( .extend(cv.polling_component_schema("5s")) .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "sim800l", baud_rate=9600, require_tx=True, require_rx=True +) async def to_code(config): @@ -54,10 +57,6 @@ async def to_code(config): ) -def validate(config, item_config): - uart.validate_device("sim800l", config, item_config, baud_rate=9600) - - SIM800L_SEND_SMS_SCHEMA = cv.Schema( { cv.GenerateID(): cv.use_id(Sim800LComponent), diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index e6e073c4a4..803a45814c 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import pins from esphome.const import ( CONF_CLK_PIN, @@ -69,9 +70,24 @@ async def register_spi_device(var, config): cg.add(var.set_cs_pin(pin)) -def validate_device(name, config, item_config, require_mosi, require_miso): - spi_config = config.get_config_by_id(item_config[CONF_SPI_ID]) - if require_mosi and CONF_MISO_PIN not in spi_config: - raise ValueError(f"Component {name} requires parent spi to declare miso_pin") - if require_miso and CONF_MOSI_PIN not in spi_config: - raise ValueError(f"Component {name} requires parent spi to declare mosi_pin") +def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso: bool): + hub_schema = {} + if require_miso: + hub_schema[ + cv.Required( + CONF_MISO_PIN, + msg=f"Component {name} requires this spi bus to declare a miso_pin", + ) + ] = cv.valid + if require_mosi: + hub_schema[ + cv.Required( + CONF_MOSI_PIN, + msg=f"Component {name} requires this spi bus to declare a mosi_pin", + ) + ] = cv.valid + + return cv.Schema( + {cv.Required(CONF_SPI_ID): fv.id_declaration_match_schema(hub_schema)}, + extra=cv.ALLOW_EXTRA, + ) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index aaed333e34..d2fcac2cb6 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,5 +1,8 @@ +from typing import Optional + import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import pins, automation from esphome.const import ( CONF_BAUD_RATE, @@ -92,42 +95,6 @@ async def to_code(config): cg.add(var.set_parity(config[CONF_PARITY])) -def validate_device( - name, config, item_config, baud_rate=None, require_tx=True, require_rx=True -): - if not hasattr(config, "uart_devices"): - config.uart_devices = {} - devices = config.uart_devices - - uart_config = config.get_config_by_id(item_config[CONF_UART_ID]) - - uart_id = uart_config[CONF_ID] - device = devices.setdefault(uart_id, {}) - - if require_tx: - if CONF_TX_PIN not in uart_config: - raise ValueError(f"Component {name} requires parent uart to declare tx_pin") - if CONF_TX_PIN in device: - raise ValueError( - f"Component {name} cannot use the same uart.{CONF_TX_PIN} as component {device[CONF_TX_PIN]} is already using it" - ) - device[CONF_TX_PIN] = name - - if require_rx: - if CONF_RX_PIN not in uart_config: - raise ValueError(f"Component {name} requires parent uart to declare rx_pin") - if CONF_RX_PIN in device: - raise ValueError( - f"Component {name} cannot use the same uart.{CONF_RX_PIN} as component {device[CONF_RX_PIN]} is already using it" - ) - device[CONF_RX_PIN] = name - - if baud_rate and uart_config[CONF_BAUD_RATE] != baud_rate: - raise ValueError( - f"Component {name} requires parent uart baud rate be {baud_rate}" - ) - - # A schema to use for all UART devices, all UART integrations must extend this! UART_DEVICE_SCHEMA = cv.Schema( { @@ -135,6 +102,64 @@ UART_DEVICE_SCHEMA = cv.Schema( } ) +KEY_UART_DEVICES = "uart_devices" + + +def final_validate_device_schema( + name: str, + *, + baud_rate: Optional[int] = None, + require_tx: bool = False, + require_rx: bool = False, +): + def validate_baud_rate(value): + if value != baud_rate: + raise cv.Invalid( + f"Component {name} required baud rate {baud_rate} for the uart bus" + ) + return value + + def validate_pin(opt, device): + def validator(value): + if opt in device: + raise cv.Invalid( + f"The uart {opt} is used both by {name} and {device[opt]}, " + f"but can only be used by one. Please create a new uart bus for {name}." + ) + device[opt] = name + return value + + return validator + + def validate_hub(hub_config): + hub_schema = {} + uart_id = hub_config[CONF_ID] + devices = fv.full_config.get().data.setdefault(KEY_UART_DEVICES, {}) + device = devices.setdefault(uart_id, {}) + + if require_tx: + hub_schema[ + cv.Required( + CONF_TX_PIN, + msg=f"Component {name} requires this uart bus to declare a tx_pin", + ) + ] = validate_pin(CONF_TX_PIN, device) + if require_rx: + hub_schema[ + cv.Required( + CONF_RX_PIN, + msg=f"Component {name} requires this uart bus to declare a rx_pin", + ) + ] = validate_pin(CONF_RX_PIN, device) + if baud_rate is not None: + hub_schema[cv.Required(CONF_BAUD_RATE)] = validate_baud_rate + return cv.Schema(hub_schema, extra=cv.ALLOW_EXTRA)(hub_config) + + return cv.Schema( + {cv.Required(CONF_UART_ID): fv.id_declaration_match_schema(validate_hub)}, + extra=cv.ALLOW_EXTRA, + ) + async def register_uart_device(var, config): """Register a UART device, setting up all the internal values. diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 13d698d245..54307388d6 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.final_validate as fv from esphome import automation from esphome.automation import Condition from esphome.components.network import add_mdns_library @@ -137,16 +138,17 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( ) -def validate(config, item_config): - if ( - (CONF_NETWORKS in item_config) - and (item_config[CONF_NETWORKS] == []) - and (CONF_AP not in item_config) - ): - if "esp32_improv" not in config: - raise ValueError( - "Please specify at least an SSID or an Access Point to create." - ) +def final_validate(config): + has_sta = bool(config.get(CONF_NETWORKS, True)) + has_ap = CONF_AP in config + has_improv = "esp32_improv" in fv.full_config.get() + if (not has_sta) and (not has_ap) and (not has_improv): + raise cv.Invalid( + "Please specify at least an SSID or an Access Point to create." + ) + + +FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) def _validate(config): diff --git a/esphome/config.py b/esphome/config.py index fcd2fac90f..93413a009c 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -21,11 +21,13 @@ from esphome.helpers import indent from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union -from esphome.core import ConfigType from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid from esphome.log import color, Fore +import esphome.final_validate as fv +import esphome.config_validation as cv +from esphome.types import ConfigType, ConfigPathType, ConfigFragmentType _LOGGER = logging.getLogger(__name__) @@ -54,7 +56,7 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool return path[: len(other)] == other -class Config(OrderedDict): +class Config(OrderedDict, fv.FinalValidateConfig): def __init__(self): super().__init__() # A list of voluptuous errors @@ -65,6 +67,7 @@ class Config(OrderedDict): self.output_paths = [] # type: List[Tuple[ConfigPath, str]] # A list of components ids with the config path self.declare_ids = [] # type: List[Tuple[core.ID, ConfigPath]] + self._data = {} def add_error(self, error): # type: (vol.Invalid) -> None @@ -72,6 +75,12 @@ class Config(OrderedDict): for err in error.errors: self.add_error(err) return + if cv.ROOT_CONFIG_PATH in error.path: + # Root value means that the path before the root should be ignored + last_root = max( + i for i, v in enumerate(error.path) if v is cv.ROOT_CONFIG_PATH + ) + error.path = error.path[last_root + 1 :] self.errors.append(error) @contextmanager @@ -140,13 +149,16 @@ class Config(OrderedDict): return doc_range - def get_nested_item(self, path): - # type: (ConfigPath) -> ConfigType + def get_nested_item( + self, path: ConfigPathType, raise_error: bool = False + ) -> ConfigFragmentType: data = self for item_index in path: try: data = data[item_index] except (KeyError, IndexError, TypeError): + if raise_error: + raise return {} return data @@ -163,11 +175,20 @@ class Config(OrderedDict): part.append(item_index) return part - def get_config_by_id(self, id): + def get_path_for_id(self, id: core.ID): + """Return the config fragment where the given ID is declared.""" for declared_id, path in self.declare_ids: if declared_id.id == str(id): - return self.get_nested_item(path[:-1]) - return None + return path + raise KeyError(f"ID {id} not found in configuration") + + def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType: + return self.get_nested_item(path, raise_error=True) + + @property + def data(self): + """Return temporary data used by final validation functions.""" + return self._data def iter_ids(config, path=None): @@ -189,23 +210,22 @@ def do_id_pass(result): # type: (Config) -> None from esphome.cpp_generator import MockObjClass from esphome.cpp_types import Component - declare_ids = result.declare_ids # type: List[Tuple[core.ID, ConfigPath]] searching_ids = [] # type: List[Tuple[core.ID, ConfigPath]] for id, path in iter_ids(result): if id.is_declaration: if id.id is not None: # Look for duplicate definitions - match = next((v for v in declare_ids if v[0].id == id.id), None) + match = next((v for v in result.declare_ids if v[0].id == id.id), None) if match is not None: opath = "->".join(str(v) for v in match[1]) result.add_str_error(f"ID {id.id} redefined! Check {opath}", path) continue - declare_ids.append((id, path)) + result.declare_ids.append((id, path)) else: searching_ids.append((id, path)) # Resolve default ids after manual IDs - for id, _ in declare_ids: - id.resolve([v[0].id for v in declare_ids]) + for id, _ in result.declare_ids: + id.resolve([v[0].id for v in result.declare_ids]) if isinstance(id.type, MockObjClass) and id.type.inherits_from(Component): CORE.component_ids.add(id.id) @@ -213,7 +233,7 @@ def do_id_pass(result): # type: (Config) -> None for id, path in searching_ids: if id.id is not None: # manually declared - match = next((v[0] for v in declare_ids if v[0].id == id.id), None) + match = next((v[0] for v in result.declare_ids if v[0].id == id.id), None) if match is None or not match.is_manual: # No declared ID with this name import difflib @@ -224,7 +244,7 @@ def do_id_pass(result): # type: (Config) -> None ) # Find candidates matches = difflib.get_close_matches( - id.id, [v[0].id for v in declare_ids if v[0].is_manual] + id.id, [v[0].id for v in result.declare_ids if v[0].is_manual] ) if matches: matches_s = ", ".join(f'"{x}"' for x in matches) @@ -245,7 +265,7 @@ def do_id_pass(result): # type: (Config) -> None if id.id is None and id.type is not None: matches = [] - for v in declare_ids: + for v in result.declare_ids: if v[0] is None or not isinstance(v[0].type, MockObjClass): continue inherits = v[0].type.inherits_from(id.type) @@ -278,8 +298,6 @@ def do_id_pass(result): # type: (Config) -> None def recursive_check_replaceme(value): - import esphome.config_validation as cv - if isinstance(value, list): return cv.Schema([recursive_check_replaceme])(value) if isinstance(value, dict): @@ -558,14 +576,16 @@ def validate_config(config, command_line_substitutions): # 7. Final validation if not result.errors: # Inter - components validation - for path, conf, comp in validate_queue: - if comp.config_schema is None: + token = fv.full_config.set(result) + + for path, _, comp in validate_queue: + if comp.final_validate_schema is None: continue - if callable(comp.validate): - try: - comp.validate(result, result.get_nested_item(path)) - except ValueError as err: - result.add_str_error(err, path) + conf = result.get_nested_item(path) + with result.catch_error(path): + comp.final_validate_schema(conf) + + fv.full_config.reset(token) return result @@ -621,8 +641,12 @@ def _format_vol_invalid(ex, config): ) elif "extra keys not allowed" in str(ex): message += "[{}] is an invalid option for [{}].".format(ex.path[-1], paren) - elif "required key not provided" in str(ex): - message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) + elif isinstance(ex, vol.RequiredFieldInvalid): + if ex.msg == "required key not provided": + message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) + else: + # Required has set a custom error message + message += ex.msg else: message += humanize_error(config, ex) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 2cdb6b0b76..7292cc3af5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -75,6 +75,9 @@ Inclusive = vol.Inclusive ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid +# this sentinel object can be placed in an 'Invalid' path to say +# the rest of the error path is relative to the root config path +ROOT_CONFIG_PATH = object() RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword @@ -218,8 +221,8 @@ class Required(vol.Required): - *not* the `config.get(CONF_)` syntax. """ - def __init__(self, key): - super().__init__(key) + def __init__(self, key, msg=None): + super().__init__(key, msg=msg) def check_not_templatable(value): @@ -1073,6 +1076,7 @@ def invalid(message): def valid(value): + """A validator that is always valid and returns the value as-is.""" return value diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 1841dfd8be..df98e1b150 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -2,7 +2,7 @@ import logging import math import os import re -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from esphome.const import ( CONF_ARDUINO_VERSION, @@ -23,6 +23,7 @@ from esphome.util import OrderedDict if TYPE_CHECKING: from ..cpp_generator import MockObj, MockObjClass, Statement + from ..types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -462,9 +463,9 @@ class EsphomeCore: # The board that's used (for example nodemcuv2) self.board: Optional[str] = None # The full raw configuration - self.raw_config: Optional[ConfigType] = None + self.raw_config: Optional["ConfigType"] = None # The validated configuration, this is None until the config has been validated - self.config: Optional[ConfigType] = None + self.config: Optional["ConfigType"] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -752,6 +753,3 @@ class EnumValue: CORE = EsphomeCore() - -ConfigType = Dict[str, Any] -CoreType = EsphomeCore diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 1c52f38e50..1d66eabf6c 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -8,7 +8,8 @@ from esphome.const import ( ) # pylint: disable=unused-import -from esphome.core import coroutine, ID, CORE, ConfigType +from esphome.core import coroutine, ID, CORE +from esphome.types import ConfigType from esphome.cpp_generator import RawExpression, add, get_variable from esphome.cpp_types import App, GPIOPin from esphome.util import Registry, RegistryEntry diff --git a/esphome/final_validate.py b/esphome/final_validate.py new file mode 100644 index 0000000000..50fdbaf3f4 --- /dev/null +++ b/esphome/final_validate.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod, abstractproperty +from typing import Dict, Any +import contextvars + +from esphome.types import ConfigFragmentType, ID, ConfigPathType +import esphome.config_validation as cv + + +class FinalValidateConfig(ABC): + @abstractproperty + def data(self) -> Dict[str, Any]: + """A dictionary that can be used by post validation functions to store + global data during the validation phase. Each component should store its + data under a unique key + """ + + @abstractmethod + def get_path_for_id(self, id: ID) -> ConfigPathType: + """Get the config path a given ID has been declared in. + + This is the location under the _validated_ config (for example, with cv.ensure_list applied) + Raises KeyError if the id was not declared in the configuration. + """ + + @abstractmethod + def get_config_for_path(self, path: ConfigPathType) -> ConfigFragmentType: + """Get the config fragment for the given global path. + + Raises KeyError if a key in the path does not exist. + """ + + +FinalValidateConfig.register(dict) + +# Context variable tracking the full config for some final validation functions. +full_config: contextvars.ContextVar[FinalValidateConfig] = contextvars.ContextVar( + "full_config" +) + + +def id_declaration_match_schema(schema): + """A final-validation schema function that applies a schema to the outer config fragment of an + ID declaration. + + This validator must be applied to ID values. + """ + if not isinstance(schema, cv.Schema): + schema = cv.Schema(schema, extra=cv.ALLOW_EXTRA) + + def validator(value): + fconf = full_config.get() + path = fconf.get_path_for_id(value)[:-1] + declaration_config = fconf.get_config_for_path(path) + with cv.prepend_path([cv.ROOT_CONFIG_PATH] + path): + return schema(declaration_config) + + return validator diff --git a/esphome/loader.py b/esphome/loader.py index d9d407d787..f74fc6367d 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -12,6 +12,7 @@ from pathlib import Path from esphome.const import ESP_PLATFORMS, SOURCE_FILE_EXTENSIONS import esphome.core.config from esphome.core import CORE +from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -81,8 +82,13 @@ class ComponentManifest: return getattr(self.module, "CODEOWNERS", []) @property - def validate(self): - return getattr(self.module, "validate", None) + def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]: + """Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called + after the main validation. In that function checks across components can be made. + + Note that the function can't mutate the configuration - no changes are saved + """ + return getattr(self.module, "FINAL_VALIDATE_SCHEMA", None) @property def source_files(self) -> Dict[Path, SourceFile]: diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 6f81e0d96a..c0fbc6edf7 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -4,14 +4,13 @@ from datetime import datetime import json import logging import os +from typing import Any, Optional, List from esphome import const from esphome.core import CORE from esphome.helpers import write_file_if_changed -# pylint: disable=unused-import, wrong-import-order -from esphome.core import CoreType -from typing import Any, Optional, List +from esphome.types import CoreType _LOGGER = logging.getLogger(__name__) diff --git a/esphome/types.py b/esphome/types.py new file mode 100644 index 0000000000..6bbfb00ce6 --- /dev/null +++ b/esphome/types.py @@ -0,0 +1,18 @@ +"""This helper module tracks commonly used types in the esphome python codebase.""" +from typing import Dict, Union, List + +from esphome.core import ID, Lambda, EsphomeCore + +ConfigFragmentType = Union[ + str, + int, + float, + None, + Dict[Union[str, int], "ConfigFragmentType"], + List["ConfigFragmentType"], + ID, + Lambda, +] +ConfigType = Dict[str, ConfigFragmentType] +CoreType = EsphomeCore +ConfigPathType = Union[str, int] From dca1c0f1601edca9ed4d08fc9d787f8003606dd7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Jun 2021 11:48:40 +1200 Subject: [PATCH 510/643] Replace CLIMATE_MODE_AUTO with CLIMATE_MODE_HEAT_COOL in most cases (#1933) --- esphome/components/bang_bang/bang_bang_climate.cpp | 6 +++--- esphome/components/climate/climate.h | 2 +- esphome/components/climate/climate_traits.cpp | 5 +++++ esphome/components/climate/climate_traits.h | 2 ++ esphome/components/climate_ir/climate_ir.cpp | 2 +- esphome/components/climate_ir_lg/climate_ir_lg.cpp | 12 ++++++------ esphome/components/coolix/coolix.cpp | 8 ++++---- esphome/components/daikin/daikin.cpp | 6 +++--- .../components/fujitsu_general/fujitsu_general.cpp | 4 ++-- esphome/components/hitachi_ac344/hitachi_ac344.cpp | 4 ++-- esphome/components/midea_ac/midea_climate.cpp | 2 +- esphome/components/midea_ac/midea_frame.cpp | 4 ++-- esphome/components/mitsubishi/mitsubishi.cpp | 2 +- esphome/components/pid/pid_climate.cpp | 10 +++++----- esphome/components/tcl112/tcl112.cpp | 4 ++-- esphome/components/thermostat/climate.py | 8 ++++---- .../components/thermostat/thermostat_climate.cpp | 13 +++++++++---- esphome/components/thermostat/thermostat_climate.h | 2 ++ esphome/components/toshiba/toshiba.cpp | 4 ++-- esphome/components/tuya/climate/tuya_climate.cpp | 2 +- esphome/components/whirlpool/whirlpool.cpp | 4 ++-- esphome/components/yashima/yashima.cpp | 4 ++-- 22 files changed, 62 insertions(+), 48 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index ca361c7012..e3e833ce2a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -21,7 +21,7 @@ void BangBangClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->change_away_(false); } } @@ -41,7 +41,7 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); + traits.set_supports_heat_cool_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_two_point_target_temperature(true); @@ -50,7 +50,7 @@ climate::ClimateTraits BangBangClimate::traits() { return traits; } void BangBangClimate::compute_state_() { - if (this->mode != climate::CLIMATE_MODE_AUTO) { + if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) { // in non-auto mode, switch directly to appropriate action // - HEAT mode -> HEATING action // - COOL mode -> COOLING action diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 3ac9270341..b688ac8efb 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -159,7 +159,7 @@ struct ClimateDeviceRestoreState { * * The entire state of the climate device is encoded in public properties of the base class (current_temperature, * mode etc). These are read-only for the user and rw for integrations. The reason these are public - * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_AUTO) ...` + * is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_HEAT_COOL) ...` */ class Climate : public Nameable { public: diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index eda4722fcb..774ada785f 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -8,6 +8,8 @@ bool ClimateTraits::supports_mode(ClimateMode mode) const { switch (mode) { case CLIMATE_MODE_OFF: return true; + case CLIMATE_MODE_HEAT_COOL: + return this->supports_heat_cool_mode_; case CLIMATE_MODE_AUTO: return this->supports_auto_mode_; case CLIMATE_MODE_COOL: @@ -31,6 +33,9 @@ void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_ supports_two_point_target_temperature_ = supports_two_point_target_temperature; } void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } +void ClimateTraits::set_supports_heat_cool_mode(bool supports_heat_cool_mode) { + supports_heat_cool_mode_ = supports_heat_cool_mode; +} void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f0a48ca308..f8e6f87306 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -46,6 +46,7 @@ class ClimateTraits { bool get_supports_two_point_target_temperature() const; void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature); void set_supports_auto_mode(bool supports_auto_mode); + void set_supports_heat_cool_mode(bool supports_heat_cool_mode); void set_supports_cool_mode(bool supports_cool_mode); void set_supports_heat_mode(bool supports_heat_mode); void set_supports_fan_only_mode(bool supports_fan_only_mode); @@ -100,6 +101,7 @@ class ClimateTraits { bool supports_current_temperature_{false}; bool supports_two_point_target_temperature_{false}; bool supports_auto_mode_{false}; + bool supports_heat_cool_mode_{false}; bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; bool supports_fan_only_mode_{false}; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 3c9d118736..5b16ed7fae 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -9,7 +9,7 @@ static const char *const TAG = "climate_ir"; climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); + traits.set_supports_heat_cool_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_dry_mode(this->supports_dry_); diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 892560db8d..4c721d5e64 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -39,7 +39,7 @@ void LgIrClimate::transmit_state() { send_swing_cmd_ = false; remote_state |= COMMAND_SWING; } else { - if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) { + if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_HEAT_COOL) { remote_state |= COMMAND_ON_AI; } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { remote_state |= COMMAND_ON; @@ -52,7 +52,7 @@ void LgIrClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state |= COMMAND_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state |= COMMAND_AUTO; break; case climate::CLIMATE_MODE_DRY: @@ -89,7 +89,7 @@ void LgIrClimate::transmit_state() { } } - if (this->mode == climate::CLIMATE_MODE_AUTO) { + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { this->fan_mode = climate::CLIMATE_FAN_AUTO; // remote_state |= FAN_MODE_AUTO_DRY; } @@ -128,7 +128,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { if ((remote_state & COMMAND_MASK) == COMMAND_ON) { this->mode = climate::CLIMATE_MODE_COOL; } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; } if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { @@ -138,7 +138,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) this->mode = climate::CLIMATE_MODE_DRY; else if ((remote_state & COMMAND_MASK) == COMMAND_HEAT) { @@ -152,7 +152,7 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; // Fan Speed - if (this->mode == climate::CLIMATE_MODE_AUTO) { + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) { this->fan_mode = climate::CLIMATE_FAN_AUTO; } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT || this->mode == climate::CLIMATE_MODE_DRY) { diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index b28cee9245..653d8344e0 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -70,7 +70,7 @@ void CoolixClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state |= COOLIX_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state |= COOLIX_AUTO; break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -89,7 +89,7 @@ void CoolixClimate::transmit_state() { } else { remote_state |= COOLIX_FAN_TEMP_CODE; } - if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) { + if (this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_DRY) { this->fan_mode = climate::CLIMATE_FAN_AUTO; remote_state |= COOLIX_FAN_MODE_AUTO_DRY; } else { @@ -197,7 +197,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) this->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) this->mode = climate::CLIMATE_MODE_DRY; @@ -207,7 +207,7 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO || + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_HEAT_COOL || this->mode == climate::CLIMATE_MODE_DRY) this->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index b426b85183..40734203da 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -77,7 +77,7 @@ uint8_t DaikinClimate::operation_mode_() { case climate::CLIMATE_MODE_HEAT: operating_mode |= DAIKIN_MODE_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: operating_mode |= DAIKIN_MODE_AUTO; break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -131,7 +131,7 @@ uint8_t DaikinClimate::temperature_() { switch (this->mode) { case climate::CLIMATE_MODE_FAN_ONLY: return 0x32; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_DRY: return 0xc0; default: @@ -160,7 +160,7 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { this->mode = climate::CLIMATE_MODE_HEAT; break; case DAIKIN_MODE_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; case DAIKIN_MODE_FAN: this->mode = climate::CLIMATE_MODE_FAN_ONLY; diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 892f9cd726..6dd569fa21 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -133,7 +133,7 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_MODE_FAN_ONLY: SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_FAN); break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: default: SET_NIBBLE(remote_state, FUJITSU_GENERAL_MODE_NIBBLE, FUJITSU_GENERAL_MODE_AUTO); break; @@ -344,7 +344,7 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { case FUJITSU_GENERAL_MODE_AUTO: default: // TODO: CLIMATE_MODE_10C is missing from esphome - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp index 35b3d17358..1e5bca1396 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.cpp +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -155,7 +155,7 @@ void HitachiClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: set_mode_(HITACHI_AC344_MODE_HEAT); break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: set_mode_(HITACHI_AC344_MODE_AUTO); break; case climate::CLIMATE_MODE_FAN_ONLY: @@ -251,7 +251,7 @@ bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { this->mode = climate::CLIMATE_MODE_HEAT; break; case HITACHI_AC344_MODE_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; case HITACHI_AC344_MODE_FAN: this->mode = climate::CLIMATE_MODE_FAN_ONLY; diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp index 1efe6c93f8..001d5edb53 100644 --- a/esphome/components/midea_ac/midea_climate.cpp +++ b/esphome/components/midea_ac/midea_climate.cpp @@ -167,7 +167,7 @@ climate::ClimateTraits MideaAC::traits() { traits.set_visual_min_temperature(17); traits.set_visual_max_temperature(30); traits.set_visual_temperature_step(0.5); - traits.set_supports_auto_mode(true); + traits.set_supports_heat_cool_mode(true); traits.set_supports_cool_mode(true); traits.set_supports_dry_mode(true); traits.set_supports_heat_mode(true); diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp index 4041ca7923..098beedf7a 100644 --- a/esphome/components/midea_ac/midea_frame.cpp +++ b/esphome/components/midea_ac/midea_frame.cpp @@ -44,7 +44,7 @@ climate::ClimateMode PropertiesFrame::get_mode() const { return climate::CLIMATE_MODE_OFF; switch (this->pbuf_[12] >> 5) { case MIDEA_MODE_AUTO: - return climate::CLIMATE_MODE_AUTO; + return climate::CLIMATE_MODE_HEAT_COOL; case MIDEA_MODE_COOL: return climate::CLIMATE_MODE_COOL; case MIDEA_MODE_DRY: @@ -61,7 +61,7 @@ climate::ClimateMode PropertiesFrame::get_mode() const { void PropertiesFrame::set_mode(climate::ClimateMode mode) { uint8_t m; switch (mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: m = MIDEA_MODE_AUTO; break; case climate::CLIMATE_MODE_COOL: diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index d0f07e42dc..0b4b9be1eb 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -33,7 +33,7 @@ void MitsubishiClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state[6] = MITSUBISHI_HEAT; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[6] = MITSUBISHI_AUTO; break; case climate::CLIMATE_MODE_OFF: diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 4c32d92576..1601f1a0fe 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -20,7 +20,7 @@ void PIDClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->target_temperature = this->default_target_temperature_; } } @@ -31,7 +31,7 @@ void PIDClimate::control(const climate::ClimateCall &call) { this->target_temperature = *call.get_target_temperature(); // If switching to non-auto mode, set output immediately - if (this->mode != climate::CLIMATE_MODE_AUTO) + if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) this->handle_non_auto_mode_(); this->publish_state(); @@ -39,7 +39,7 @@ void PIDClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits PIDClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); + traits.set_supports_heat_cool_mode(true); traits.set_supports_two_point_target_temperature(false); traits.set_supports_cool_mode(this->supports_cool_()); traits.set_supports_heat_mode(this->supports_heat_()); @@ -121,14 +121,14 @@ void PIDClimate::update_pid_() { // keep autotuner instance so that subsequent dump_configs will print the long result message. } else { value = res.output; - if (mode != climate::CLIMATE_MODE_AUTO) { + if (mode != climate::CLIMATE_MODE_HEAT_COOL) { ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); } } } } - if (this->mode != climate::CLIMATE_MODE_AUTO) { + if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) { this->handle_non_auto_mode_(); } else { this->write_output_(value); diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index da44b3c827..5b938ba0c3 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -47,7 +47,7 @@ void Tcl112Climate::transmit_state() { // Set mode switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[6] &= 0xF0; remote_state[6] |= TCL112_AUTO; break; @@ -204,7 +204,7 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; case TCL112_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 4a371ec165..07a94fd184 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -227,7 +227,7 @@ async def to_code(config): await cg.register_component(var, config) await climate.register_climate(var, config) - auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config ) @@ -258,10 +258,10 @@ async def to_code(config): var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] ) - if auto_mode_available is True: - cg.add(var.set_supports_auto(True)) + if heat_cool_mode_available is True: + cg.add(var.set_supports_heat_cool(True)) else: - cg.add(var.set_supports_auto(False)) + cg.add(var.set_supports_heat_cool(False)) if CONF_COOL_ACTION in config: await automation.build_automation( diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 6154f293dc..3fdb4efa7b 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -21,7 +21,7 @@ void ThermostatClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles temps for us - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->change_away_(false); } // refresh the climate action based on the restored settings @@ -79,6 +79,7 @@ climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_heat_cool_mode(this->supports_heat_cool_); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_dry_mode(this->supports_dry_); traits.set_supports_fan_only_mode(this->supports_fan_only_); @@ -130,7 +131,7 @@ climate::ClimateAction ThermostatClimate::compute_action_() { case climate::CLIMATE_MODE_OFF: target_action = climate::CLIMATE_ACTION_OFF; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_HEAT: if (this->supports_cool_) { @@ -321,7 +322,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { case climate::CLIMATE_MODE_OFF: trig = this->off_mode_trigger_; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: // trig = this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: @@ -339,7 +340,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value - mode = climate::CLIMATE_MODE_AUTO; + mode = climate::CLIMATE_MODE_HEAT_COOL; // trig = this->auto_mode_trigger_; } assert(trig != nullptr); @@ -434,6 +435,9 @@ ThermostatClimate::ThermostatClimate() swing_mode_vertical_trigger_(new Trigger<>()) {} void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { + this->supports_heat_cool_ = supports_heat_cool; +} void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } @@ -521,6 +525,7 @@ void ThermostatClimate::dump_config() { } ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); + ESP_LOGCONFIG(TAG, " Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 86a1007efa..3fd482da53 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -29,6 +29,7 @@ class ThermostatClimate : public climate::Climate, public Component { void set_hysteresis(float hysteresis); void set_sensor(sensor::Sensor *sensor); void set_supports_auto(bool supports_auto); + void set_supports_heat_cool(bool supports_heat_cool); void set_supports_cool(bool supports_cool); void set_supports_dry(bool supports_dry); void set_supports_fan_only(bool supports_fan_only); @@ -113,6 +114,7 @@ class ThermostatClimate : public climate::Climate, public Component { /// A false value for any given attribute means that the controller has no such action /// (for example a thermostat, where only heating and not-heating is possible). bool supports_auto_{false}; + bool supports_heat_cool_{false}; bool supports_cool_{false}; bool supports_dry_{false}; bool supports_fan_only_{false}; diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 4f5b7d8537..c08ae898b5 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -80,7 +80,7 @@ void ToshibaClimate::transmit_state() { mode = TOSHIBA_MODE_COOL; break; - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: default: mode = TOSHIBA_MODE_AUTO; } @@ -190,7 +190,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { case TOSHIBA_MODE_AUTO: default: /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; } /* Get the target temperature */ diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 66fdcbb472..2b6ce833de 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -13,7 +13,7 @@ void TuyaClimate::setup() { this->mode = climate::CLIMATE_MODE_OFF; if (datapoint.value_bool) { if (this->supports_heat_ && this->supports_cool_) { - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; } else if (this->supports_heat_) { this->mode = climate::CLIMATE_MODE_HEAT; } else if (this->supports_cool_) { diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index 07296b6fa5..d705b42a8c 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -48,7 +48,7 @@ void WhirlpoolClimate::transmit_state() { this->powered_on_assumed = powered_on; } switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: // set fan auto // set temp auto temp // set sleep false @@ -239,7 +239,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; case WHIRLPOOL_AUTO: - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; } } diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index 2615d7a353..bda27b62fb 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -82,7 +82,7 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; climate::ClimateTraits YashimaClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); + traits.set_supports_heat_cool_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_two_point_target_temperature(false); @@ -139,7 +139,7 @@ void YashimaClimate::transmit_state_() { // Set mode switch (this->mode) { - case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_HEAT_COOL: remote_state[0] |= YASHIMA_MODE_AUTO_BYTE0; remote_state[5] |= YASHIMA_MODE_AUTO_BYTE5; break; From f9a31c1abb406190d88dfc908e4778fdd3615a88 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 18 Jun 2021 03:49:25 +0200 Subject: [PATCH 511/643] Fix error print in script/helpers.py (#1935) --- script/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/helpers.py b/script/helpers.py index 5b1b7ba918..1a4402aa1d 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -40,7 +40,7 @@ def build_all_include(): def build_compile_commands(): gcc_flags_json = os.path.join(root_path, ".gcc-flags.json") if not os.path.isfile(gcc_flags_json): - print("Could not find {} file which is required for clang-tidy.") + print("Could not find {} file which is required for clang-tidy.".format(gcc_flags_json)) print( 'Please run "pio init --ide atom" in the root esphome folder to generate that file.' ) From 04d926af3957ac418a8a252ef9a205c2a7ec345c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 17 Jun 2021 20:54:46 -0500 Subject: [PATCH 512/643] Add variable bit width for Samsung protocol (#1927) --- esphome/codegen.py | 1 + esphome/components/remote_base/__init__.py | 8 +++++-- .../remote_base/samsung_protocol.cpp | 23 +++++++++++-------- .../components/remote_base/samsung_protocol.h | 9 +++++--- esphome/config_validation.py | 2 ++ esphome/cpp_types.py | 1 + 6 files changed, 30 insertions(+), 14 deletions(-) diff --git a/esphome/codegen.py b/esphome/codegen.py index 8361faeb81..c05cc5efca 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -60,6 +60,7 @@ from esphome.cpp_types import ( # noqa uint8, uint16, uint32, + uint64, int32, const_char_ptr, NAN, diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 76e4b51bde..9416c2220d 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -866,7 +866,8 @@ def rc_switch_dumper(var, config): ) = declare_protocol("Samsung") SAMSUNG_SCHEMA = cv.Schema( { - cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Required(CONF_DATA): cv.hex_uint64_t, + cv.Optional(CONF_NBITS, default=32): cv.int_range(32, 64), } ) @@ -878,6 +879,7 @@ def samsung_binary_sensor(var, config): cg.StructInitializer( SamsungData, ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), ) ) ) @@ -895,8 +897,10 @@ def samsung_dumper(var, config): @register_action("samsung", SamsungAction, SAMSUNG_SCHEMA) async def samsung_action(var, config, args): - template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint64) cg.add(var.set_data(template_)) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint8) + cg.add(var.set_nbits(template_)) # Samsung36 diff --git a/esphome/components/remote_base/samsung_protocol.cpp b/esphome/components/remote_base/samsung_protocol.cpp index 5062c46126..0f2605d865 100644 --- a/esphome/components/remote_base/samsung_protocol.cpp +++ b/esphome/components/remote_base/samsung_protocol.cpp @@ -6,7 +6,6 @@ namespace remote_base { static const char *const TAG = "remote.samsung"; -static const uint8_t NBITS = 32; static const uint32_t HEADER_HIGH_US = 4500; static const uint32_t HEADER_LOW_US = 4500; static const uint32_t BIT_HIGH_US = 560; @@ -17,12 +16,12 @@ static const uint32_t FOOTER_LOW_US = 560; void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { dst->set_carrier_frequency(38000); - dst->reserve(4 + NBITS * 2u); + dst->reserve(4 + data.nbits * 2u); dst->item(HEADER_HIGH_US, HEADER_LOW_US); - for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { - if (data.data & mask) + for (uint8_t bit = data.nbits; bit > 0; bit--) { + if ((data.data >> (bit - 1)) & 1) dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); else dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); @@ -33,16 +32,20 @@ void SamsungProtocol::encode(RemoteTransmitData *dst, const SamsungData &data) { optional SamsungProtocol::decode(RemoteReceiveData src) { SamsungData out{ .data = 0, + .nbits = 0, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) return {}; - for (uint8_t i = 0; i < NBITS; i++) { - out.data <<= 1UL; + for (out.nbits = 0; out.nbits < 64; out.nbits++) { if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { - out.data |= 1UL; + out.data = (out.data << 1) | 1; } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { - out.data |= 0UL; + out.data = (out.data << 1) | 0; + } else if (out.nbits >= 31) { + if (!src.expect_mark(FOOTER_HIGH_US)) + return {}; + return out; } else { return {}; } @@ -52,7 +55,9 @@ optional SamsungProtocol::decode(RemoteReceiveData src) { return {}; return out; } -void SamsungProtocol::dump(const SamsungData &data) { ESP_LOGD(TAG, "Received Samsung: data=0x%08X", data.data); } +void SamsungProtocol::dump(const SamsungData &data) { + ESP_LOGD(TAG, "Received Samsung: data=0x%" PRIX64 ", nbits=%d", data.data, data.nbits); +} } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index f7a54788e5..41434f2889 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -7,9 +7,10 @@ namespace esphome { namespace remote_base { struct SamsungData { - uint32_t data; + uint64_t data; + uint8_t nbits; - bool operator==(const SamsungData &rhs) const { return data == rhs.data; } + bool operator==(const SamsungData &rhs) const { return data == rhs.data && nbits == rhs.nbits; } }; class SamsungProtocol : public RemoteProtocol { @@ -23,11 +24,13 @@ DECLARE_REMOTE_PROTOCOL(Samsung) template class SamsungAction : public RemoteTransmitterActionBase { public: - TEMPLATABLE_VALUE(uint32_t, data) + TEMPLATABLE_VALUE(uint64_t, data) + TEMPLATABLE_VALUE(uint8_t, nbits) void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); + data.nbits = this->nbits_.value(x...); SamsungProtocol().encode(dst, data); } }; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 7292cc3af5..aad147dbc9 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1018,9 +1018,11 @@ def requires_component(comp): uint8_t = int_range(min=0, max=255) uint16_t = int_range(min=0, max=65535) uint32_t = int_range(min=0, max=4294967295) +uint64_t = int_range(min=0, max=18446744073709551615) hex_uint8_t = hex_int_range(min=0, max=255) hex_uint16_t = hex_int_range(min=0, max=65535) hex_uint32_t = hex_int_range(min=0, max=4294967295) +hex_uint64_t = hex_int_range(min=0, max=18446744073709551615) i2c_address = hex_uint8_t diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 3036249a03..ee606ec7b3 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -13,6 +13,7 @@ std_vector = std_ns.class_("vector") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") +uint64 = global_ns.namespace("uint64_t") int32 = global_ns.namespace("int32_t") const_char_ptr = global_ns.namespace("const char *") NAN = global_ns.namespace("NAN") From 439566454715198c63f7bed154c75bba12dcce4b Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Thu, 17 Jun 2021 21:58:39 -0500 Subject: [PATCH 513/643] Don't send Tuya commands while currently receiving a message (#1886) Co-authored-by: Chris Nussbaum --- esphome/components/tuya/tuya.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 540a925879..2c06a79ce3 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -333,7 +333,7 @@ void Tuya::send_raw_command_(TuyaCommand command) { void Tuya::process_command_queue_() { uint32_t delay = millis() - this->last_command_timestamp_; // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly - if (delay > COMMAND_DELAY && !command_queue_.empty()) { + if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty()) { this->send_raw_command_(command_queue_.front()); this->command_queue_.erase(command_queue_.begin()); } From 4891cfef56f46a64aff7b9d370040d5f883db6ae Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 18 Jun 2021 15:50:56 +1200 Subject: [PATCH 514/643] Add data sizes to tuya log message (#1938) --- esphome/components/tuya/tuya.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 2c06a79ce3..d9a0a9932a 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -232,7 +232,7 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { const uint8_t *data = buffer + 4; size_t data_len = len - 4; if (data_size != data_len) { - ESP_LOGW(TAG, "Datapoint %u is not expected size", datapoint.id); + ESP_LOGW(TAG, "Datapoint %u is not expected size (%zu != %zu)", datapoint.id, data_size, data_len); return; } datapoint.len = data_len; From c5eba21ff6f94eefe199800534f425e0e7d39b93 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Mon, 21 Jun 2021 00:59:12 +0400 Subject: [PATCH 515/643] Fix midea_ac query frame (#1940) --- esphome/components/midea_ac/midea_frame.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp index 098beedf7a..5f09f4314f 100644 --- a/esphome/components/midea_ac/midea_frame.cpp +++ b/esphome/components/midea_ac/midea_frame.cpp @@ -8,9 +8,9 @@ const std::string MIDEA_SILENT_FAN_MODE = "silent"; const std::string MIDEA_TURBO_FAN_MODE = "turbo"; const std::string MIDEA_FREEZE_PROTECTION_PRESET = "freeze protection"; -const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x61, - 0x00, 0xFF, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE3, 0xA8}; +const uint8_t QueryFrame::INIT[] = {0xAA, 0x21, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x41, 0x81, + 0x00, 0xFF, 0x03, 0xFF, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x37, 0x31}; const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21, 0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, From 40a5005d94b34356bf3bbc45ba397a2c8aceb30e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jun 2021 09:00:16 +1200 Subject: [PATCH 516/643] Allow wifi setup to proceed when there is no sta or ap (#1931) --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5e02307598..0c3a3054f8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -587,7 +587,7 @@ void WiFiComponent::retry_connect() { } bool WiFiComponent::can_proceed() { - if (this->has_ap() && !this->has_sta()) { + if (!this->has_sta()) { return true; } return this->is_connected(); From b6011b93532676600d138483a710aa9f0e35f11f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jun 2021 14:17:38 +1200 Subject: [PATCH 517/643] Fix bad climate control enum (#1942) --- esphome/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 5a4492216e..88991ff795 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -36,7 +36,7 @@ ClimateTraits = climate_ns.class_("ClimateTraits") ClimateMode = climate_ns.enum("ClimateMode") CLIMATE_MODES = { "OFF": ClimateMode.CLIMATE_MODE_OFF, - "HEAT_COOL": ClimateMode.CLIMATE_HEAT_COOL, + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, "COOL": ClimateMode.CLIMATE_MODE_COOL, "HEAT": ClimateMode.CLIMATE_MODE_HEAT, "DRY": ClimateMode.CLIMATE_MODE_DRY, From b8a7741c617dff13da85eba60d71a3fbb0708f5b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 21 Jun 2021 21:27:35 +1200 Subject: [PATCH 518/643] Update generation script to add const (#1945) --- script/api_protobuf/api_protobuf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 97cc95e556..7f9067cd22 100644 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -821,7 +821,7 @@ cpp += """\ namespace esphome { namespace api { -static const char *TAG = "api.service"; +static const char *const TAG = "api.service"; """ From 871c0ee2a5c8d47626c00a23cfdf03938ad144c1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Jun 2021 21:17:01 +0200 Subject: [PATCH 519/643] Rework climate traits (#1941) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/api/api.proto | 16 +- esphome/components/api/api_connection.cpp | 52 ++--- esphome/components/api/api_pb2.cpp | 40 ++-- esphome/components/api/api_pb2.h | 12 +- .../bang_bang/bang_bang_climate.cpp | 23 +- esphome/components/climate/automation.h | 4 +- esphome/components/climate/climate.cpp | 74 +++--- esphome/components/climate/climate.h | 8 +- esphome/components/climate/climate_mode.cpp | 4 +- esphome/components/climate/climate_mode.h | 9 +- esphome/components/climate/climate_traits.cpp | 203 ---------------- esphome/components/climate/climate_traits.h | 219 +++++++++++------- esphome/components/climate_ir/climate_ir.cpp | 65 +----- esphome/components/climate_ir/climate_ir.h | 9 +- esphome/components/daikin/daikin.h | 11 +- .../components/hitachi_ac344/hitachi_ac344.h | 9 +- esphome/components/midea_ac/midea_climate.cpp | 48 ++-- esphome/components/midea_ac/midea_climate.h | 8 +- esphome/components/mqtt/mqtt_climate.cpp | 14 +- esphome/components/pid/pid_climate.cpp | 10 +- .../thermostat/thermostat_climate.cpp | 73 ++++-- .../components/tuya/climate/tuya_climate.cpp | 6 +- esphome/components/yashima/yashima.cpp | 11 +- script/api_protobuf/api_protobuf.py | 1 + 24 files changed, 387 insertions(+), 542 deletions(-) mode change 100644 => 100755 script/api_protobuf/api_protobuf.py diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index bdb94b3d9b..87a7cf4749 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -710,11 +710,11 @@ enum ClimateAction { CLIMATE_ACTION_FAN = 6; } enum ClimatePreset { - CLIMATE_PRESET_ECO = 0; + CLIMATE_PRESET_HOME = 0; CLIMATE_PRESET_AWAY = 1; CLIMATE_PRESET_BOOST = 2; CLIMATE_PRESET_COMFORT = 3; - CLIMATE_PRESET_HOME = 4; + CLIMATE_PRESET_ECO = 4; CLIMATE_PRESET_SLEEP = 5; CLIMATE_PRESET_ACTIVITY = 6; } @@ -734,7 +734,9 @@ message ListEntitiesClimateResponse { float visual_min_temperature = 8; float visual_max_temperature = 9; float visual_temperature_step = 10; - bool supports_away = 11; + // for older peer versions - in new system this + // is if CLIMATE_PRESET_AWAY exists is supported_presets + bool legacy_supports_away = 11; bool supports_action = 12; repeated ClimateFanMode supported_fan_modes = 13; repeated ClimateSwingMode supported_swing_modes = 14; @@ -754,7 +756,8 @@ message ClimateStateResponse { float target_temperature = 4; float target_temperature_low = 5; float target_temperature_high = 6; - bool away = 7; + // For older peers, equal to preset == CLIMATE_PRESET_AWAY + bool legacy_away = 7; ClimateAction action = 8; ClimateFanMode fan_mode = 9; ClimateSwingMode swing_mode = 10; @@ -777,8 +780,9 @@ message ClimateCommandRequest { float target_temperature_low = 7; bool has_target_temperature_high = 8; float target_temperature_high = 9; - bool has_away = 10; - bool away = 11; + // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset + bool has_legacy_away = 10; + bool legacy_away = 11; bool has_fan_mode = 12; ClimateFanMode fan_mode = 13; bool has_swing_mode = 14; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 5576ace33b..89c0dde24a 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -475,14 +475,14 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { } else { resp.target_temperature = climate->target_temperature; } - if (traits.get_supports_away()) - resp.away = climate->away; if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) resp.fan_mode = static_cast(climate->fan_mode.value()); if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) resp.custom_fan_mode = climate->custom_fan_mode.value(); - if (traits.get_supports_presets() && climate->preset.has_value()) + if (traits.get_supports_presets() && climate->preset.has_value()) { resp.preset = static_cast(climate->preset.value()); + resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY; + } if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) resp.custom_preset = climate->custom_preset.value(); if (traits.get_supports_swing_modes()) @@ -498,40 +498,26 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.unique_id = get_default_unique_id("climate", climate); msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); - for (auto mode : - {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, - climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_HEAT_COOL}) { - if (traits.supports_mode(mode)) - msg.supported_modes.push_back(static_cast(mode)); - } + + for (auto mode : traits.get_supported_modes()) + msg.supported_modes.push_back(static_cast(mode)); + msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_temperature_step = traits.get_visual_temperature_step(); - msg.supports_away = traits.get_supports_away(); + msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); msg.supports_action = traits.get_supports_action(); - for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, - climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, - climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { - if (traits.supports_fan_mode(fan_mode)) - msg.supported_fan_modes.push_back(static_cast(fan_mode)); - } - for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) { + + for (auto fan_mode : traits.get_supported_fan_modes()) + msg.supported_fan_modes.push_back(static_cast(fan_mode)); + for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) msg.supported_custom_fan_modes.push_back(custom_fan_mode); - } - for (auto preset : {climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_BOOST, - climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_SLEEP, - climate::CLIMATE_PRESET_ACTIVITY}) { - if (traits.supports_preset(preset)) - msg.supported_presets.push_back(static_cast(preset)); - } - for (auto const &custom_preset : traits.get_supported_custom_presets()) { + for (auto preset : traits.get_supported_presets()) + msg.supported_presets.push_back(static_cast(preset)); + for (auto const &custom_preset : traits.get_supported_custom_presets()) msg.supported_custom_presets.push_back(custom_preset); - } - for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL}) { - if (traits.supports_swing_mode(swing_mode)) - msg.supported_swing_modes.push_back(static_cast(swing_mode)); - } + for (auto swing_mode : traits.get_supported_swing_modes()) + msg.supported_swing_modes.push_back(static_cast(swing_mode)); return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { @@ -548,8 +534,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_target_temperature_high) call.set_target_temperature_high(msg.target_temperature_high); - if (msg.has_away) - call.set_away(msg.away); + if (msg.has_legacy_away) + call.set_preset(msg.legacy_away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME); if (msg.has_fan_mode) call.set_fan_mode(static_cast(msg.fan_mode)); if (msg.has_custom_fan_mode) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index a9e9d64bc1..1e023f3988 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -192,16 +192,16 @@ template<> const char *proto_enum_to_string(enums::Climate } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { - case enums::CLIMATE_PRESET_ECO: - return "CLIMATE_PRESET_ECO"; + case enums::CLIMATE_PRESET_HOME: + return "CLIMATE_PRESET_HOME"; case enums::CLIMATE_PRESET_AWAY: return "CLIMATE_PRESET_AWAY"; case enums::CLIMATE_PRESET_BOOST: return "CLIMATE_PRESET_BOOST"; case enums::CLIMATE_PRESET_COMFORT: return "CLIMATE_PRESET_COMFORT"; - case enums::CLIMATE_PRESET_HOME: - return "CLIMATE_PRESET_HOME"; + case enums::CLIMATE_PRESET_ECO: + return "CLIMATE_PRESET_ECO"; case enums::CLIMATE_PRESET_SLEEP: return "CLIMATE_PRESET_SLEEP"; case enums::CLIMATE_PRESET_ACTIVITY: @@ -2672,7 +2672,7 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v return true; } case 11: { - this->supports_away = value.as_bool(); + this->legacy_supports_away = value.as_bool(); return true; } case 12: { @@ -2756,7 +2756,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(8, this->visual_min_temperature); buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_temperature_step); - buffer.encode_bool(11, this->supports_away); + buffer.encode_bool(11, this->legacy_supports_away); buffer.encode_bool(12, this->supports_action); for (auto &it : this->supported_fan_modes) { buffer.encode_enum(13, it, true); @@ -2823,8 +2823,8 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" supports_away: "); - out.append(YESNO(this->supports_away)); + out.append(" legacy_supports_away: "); + out.append(YESNO(this->legacy_supports_away)); out.append("\n"); out.append(" supports_action: "); @@ -2869,7 +2869,7 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { return true; } case 7: { - this->away = value.as_bool(); + this->legacy_away = value.as_bool(); return true; } case 8: { @@ -2939,7 +2939,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(4, this->target_temperature); buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); - buffer.encode_bool(7, this->away); + buffer.encode_bool(7, this->legacy_away); buffer.encode_enum(8, this->action); buffer.encode_enum(9, this->fan_mode); buffer.encode_enum(10, this->swing_mode); @@ -2979,8 +2979,8 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" away: "); - out.append(YESNO(this->away)); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); out.append("\n"); out.append(" action: "); @@ -3031,11 +3031,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) return true; } case 10: { - this->has_away = value.as_bool(); + this->has_legacy_away = value.as_bool(); return true; } case 11: { - this->away = value.as_bool(); + this->legacy_away = value.as_bool(); return true; } case 12: { @@ -3120,8 +3120,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(7, this->target_temperature_low); buffer.encode_bool(8, this->has_target_temperature_high); buffer.encode_float(9, this->target_temperature_high); - buffer.encode_bool(10, this->has_away); - buffer.encode_bool(11, this->away); + buffer.encode_bool(10, this->has_legacy_away); + buffer.encode_bool(11, this->legacy_away); buffer.encode_bool(12, this->has_fan_mode); buffer.encode_enum(13, this->fan_mode); buffer.encode_bool(14, this->has_swing_mode); @@ -3176,12 +3176,12 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); - out.append(" has_away: "); - out.append(YESNO(this->has_away)); + out.append(" has_legacy_away: "); + out.append(YESNO(this->has_legacy_away)); out.append("\n"); - out.append(" away: "); - out.append(YESNO(this->away)); + out.append(" legacy_away: "); + out.append(YESNO(this->legacy_away)); out.append("\n"); out.append(" has_fan_mode: "); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 04d5834572..7f37c0b94b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -90,11 +90,11 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_FAN = 6, }; enum ClimatePreset : uint32_t { - CLIMATE_PRESET_ECO = 0, + CLIMATE_PRESET_HOME = 0, CLIMATE_PRESET_AWAY = 1, CLIMATE_PRESET_BOOST = 2, CLIMATE_PRESET_COMFORT = 3, - CLIMATE_PRESET_HOME = 4, + CLIMATE_PRESET_ECO = 4, CLIMATE_PRESET_SLEEP = 5, CLIMATE_PRESET_ACTIVITY = 6, }; @@ -709,7 +709,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { float visual_min_temperature{0.0f}; float visual_max_temperature{0.0f}; float visual_temperature_step{0.0f}; - bool supports_away{false}; + bool legacy_supports_away{false}; bool supports_action{false}; std::vector supported_fan_modes{}; std::vector supported_swing_modes{}; @@ -732,7 +732,7 @@ class ClimateStateResponse : public ProtoMessage { float target_temperature{0.0f}; float target_temperature_low{0.0f}; float target_temperature_high{0.0f}; - bool away{false}; + bool legacy_away{false}; enums::ClimateAction action{}; enums::ClimateFanMode fan_mode{}; enums::ClimateSwingMode swing_mode{}; @@ -758,8 +758,8 @@ class ClimateCommandRequest : public ProtoMessage { float target_temperature_low{0.0f}; bool has_target_temperature_high{false}; float target_temperature_high{0.0f}; - bool has_away{false}; - bool away{false}; + bool has_legacy_away{false}; + bool legacy_away{false}; bool has_fan_mode{false}; enums::ClimateFanMode fan_mode{}; bool has_swing_mode{false}; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index e3e833ce2a..c043f6b7de 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -32,8 +32,8 @@ void BangBangClimate::control(const climate::ClimateCall &call) { this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) - this->change_away_(*call.get_away()); + if (call.get_preset().has_value()) + this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); this->compute_state_(); this->publish_state(); @@ -41,11 +41,20 @@ void BangBangClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_heat_cool_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT_COOL, + }); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); traits.set_supports_two_point_target_temperature(true); - traits.set_supports_away(this->supports_away_); + if (supports_away_) + traits.set_supported_presets({ + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + }); traits.set_supports_action(true); return traits; } @@ -140,7 +149,7 @@ void BangBangClimate::change_away_(bool away) { this->target_temperature_low = this->away_config_.default_temperature_low; this->target_temperature_high = this->away_config_.default_temperature_high; } - this->away = away; + this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index b0b71cb7d7..49a87027f2 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -27,7 +27,9 @@ template class ControlAction : public Action { call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); - call.set_away(this->away_.optional_value(x...)); + if (away_.has_value()) { + call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); + } call.set_fan_mode(this->fan_mode_.optional_value(x...)); call.set_fan_mode(this->custom_fan_mode_.optional_value(x...)); call.set_preset(this->preset_.optional_value(x...)); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 49ed8d922c..07347e4eee 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -43,9 +43,6 @@ void ClimateCall::perform() { if (this->target_temperature_high_.has_value()) { ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_); } - if (this->away_.has_value()) { - ESP_LOGD(TAG, " Away Mode: %s", ONOFF(*this->away_)); - } this->parent_->control(*this); } void ClimateCall::validate_() { @@ -125,12 +122,6 @@ void ClimateCall::validate_() { this->target_temperature_high_.reset(); } } - if (this->away_.has_value()) { - if (!traits.get_supports_away()) { - ESP_LOGW(TAG, " Cannot set away mode for this device!"); - this->away_.reset(); - } - } } ClimateCall &ClimateCall::set_mode(ClimateMode mode) { this->mode_ = mode; @@ -181,8 +172,7 @@ ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { this->set_fan_mode(CLIMATE_FAN_DIFFUSE); } else { - auto custom_fan_modes = this->parent_->get_traits().get_supported_custom_fan_modes(); - if (std::find(custom_fan_modes.begin(), custom_fan_modes.end(), fan_mode) != custom_fan_modes.end()) { + if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { this->custom_fan_mode_ = fan_mode; this->fan_mode_.reset(); } else { @@ -218,8 +208,7 @@ ClimateCall &ClimateCall::set_preset(const std::string &preset) { } else if (str_equals_case_insensitive(preset, "ACTIVITY")) { this->set_preset(CLIMATE_PRESET_ACTIVITY); } else { - auto custom_presets = this->parent_->get_traits().get_supported_custom_presets(); - if (std::find(custom_presets.begin(), custom_presets.end(), preset) != custom_presets.end()) { + if (this->parent_->get_traits().supports_custom_preset(preset)) { this->custom_preset_ = preset; this->preset_.reset(); } else { @@ -269,18 +258,23 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } -const optional &ClimateCall::get_away() const { return this->away_; } +optional ClimateCall::get_away() const { + if (!this->preset_.has_value()) + return {}; + return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY; +} const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } ClimateCall &ClimateCall::set_away(bool away) { - this->away_ = away; + this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; return *this; } ClimateCall &ClimateCall::set_away(optional away) { - this->away_ = away; + if (away.has_value()) + this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; return *this; } ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { @@ -338,20 +332,17 @@ void Climate::save_state_() { } else { state.target_temperature = this->target_temperature; } - if (traits.get_supports_away()) { - state.away = this->away; - } if (traits.get_supports_fan_modes() && fan_mode.has_value()) { state.uses_custom_fan_mode = false; state.fan_mode = this->fan_mode.value(); } if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { state.uses_custom_fan_mode = true; - auto &custom_fan_modes = traits.get_supported_custom_fan_modes(); - auto it = std::find(custom_fan_modes.begin(), custom_fan_modes.end(), this->custom_fan_mode.value()); - // only set custom fan mode if value exists, otherwise leave it as is - if (it != custom_fan_modes.cend()) { - state.custom_fan_mode = std::distance(custom_fan_modes.begin(), it); + const auto &supported = traits.get_supported_custom_fan_modes(); + std::vector vec{supported.begin(), supported.end()}; + auto it = std::find(vec.begin(), vec.end(), custom_fan_mode); + if (it != vec.end()) { + state.custom_fan_mode = std::distance(vec.begin(), it); } } if (traits.get_supports_presets() && preset.has_value()) { @@ -360,11 +351,12 @@ void Climate::save_state_() { } if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { state.uses_custom_preset = true; - auto custom_presets = traits.get_supported_custom_presets(); - auto it = std::find(custom_presets.begin(), custom_presets.end(), this->custom_preset.value()); + const auto &supported = traits.get_supported_custom_presets(); + std::vector vec{supported.begin(), supported.end()}; + auto it = std::find(vec.begin(), vec.end(), custom_preset); // only set custom preset if value exists, otherwise leave it as is - if (it != custom_presets.cend()) { - state.custom_preset = std::distance(custom_presets.begin(), it); + if (it != vec.cend()) { + state.custom_preset = std::distance(vec.begin(), it); } } if (traits.get_supports_swing_modes()) { @@ -405,9 +397,6 @@ void Climate::publish_state() { } else { ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature); } - if (traits.get_supports_away()) { - ESP_LOGD(TAG, " Away: %s", ONOFF(this->away)); - } // Send state to frontend this->state_callback_.call(); @@ -453,9 +442,6 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { } else { call.set_target_temperature(this->target_temperature); } - if (traits.get_supports_away()) { - call.set_away(this->away); - } if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { call.set_fan_mode(this->fan_mode); } @@ -476,23 +462,27 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { } else { climate->target_temperature = this->target_temperature; } - if (traits.get_supports_away()) { - climate->away = this->away; - } if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { climate->fan_mode = this->fan_mode; } if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { - climate->custom_fan_mode = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; + // std::set has consistent order (lexicographic for strings), so this is ok + const auto &modes = traits.get_supported_custom_fan_modes(); + std::vector modes_vec{modes.begin(), modes.end()}; + if (custom_fan_mode < modes_vec.size()) { + climate->custom_fan_mode = modes_vec[this->custom_fan_mode]; + } } if (traits.get_supports_presets() && !this->uses_custom_preset) { climate->preset = this->preset; } - if (!traits.get_supported_custom_presets().empty() && this->uses_custom_preset) { - climate->custom_preset = traits.get_supported_custom_presets()[this->custom_preset]; - } if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { - climate->custom_preset = traits.get_supported_custom_presets()[this->preset]; + // std::set has consistent order (lexicographic for strings), so this is ok + const auto &presets = traits.get_supported_custom_presets(); + std::vector presets_vec{presets.begin(), presets.end()}; + if (custom_preset < presets_vec.size()) { + climate->custom_preset = presets_vec[this->custom_preset]; + } } if (traits.get_supports_swing_modes()) { climate->swing_mode = this->swing_mode; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index b688ac8efb..ed5c5069b7 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -63,7 +63,9 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional target_temperature_high); + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead") ClimateCall &set_away(bool away); + ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead") ClimateCall &set_away(optional away); /// Set the fan mode of the climate device. ClimateCall &set_fan_mode(ClimateFanMode fan_mode); @@ -94,7 +96,8 @@ class ClimateCall { const optional &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; - const optional &get_away() const; + ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead") + optional get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &get_custom_fan_mode() const; @@ -109,7 +112,6 @@ class ClimateCall { optional target_temperature_; optional target_temperature_low_; optional target_temperature_high_; - optional away_; optional fan_mode_; optional swing_mode_; optional custom_fan_mode_; @@ -120,7 +122,6 @@ class ClimateCall { /// Struct used to save the state of the climate device in restore memory. struct ClimateDeviceRestoreState { ClimateMode mode; - bool away; bool uses_custom_fan_mode{false}; union { ClimateFanMode fan_mode; @@ -191,6 +192,7 @@ class Climate : public Nameable { * Away allows climate devices to have two different target temperature configs: * one for normal mode and one for away mode. */ + ESPDEPRECATED("away is deprecated, use preset instead") bool away{false}; /// The active fan mode of the climate device. diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 4540208a3f..099074a887 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -84,6 +84,8 @@ const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { const char *climate_preset_to_string(ClimatePreset preset) { switch (preset) { + case climate::CLIMATE_PRESET_HOME: + return "HOME"; case climate::CLIMATE_PRESET_ECO: return "ECO"; case climate::CLIMATE_PRESET_AWAY: @@ -92,8 +94,6 @@ const char *climate_preset_to_string(ClimatePreset preset) { return "BOOST"; case climate::CLIMATE_PRESET_COMFORT: return "COMFORT"; - case climate::CLIMATE_PRESET_HOME: - return "HOME"; case climate::CLIMATE_PRESET_SLEEP: return "SLEEP"; case climate::CLIMATE_PRESET_ACTIVITY: diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index e129fca91d..7afa2dae55 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -39,7 +39,6 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_FAN = 6, }; -/// Enum for all modes a climate fan can be in enum ClimateFanMode : uint8_t { /// The fan mode is set to On CLIMATE_FAN_ON = 0, @@ -75,16 +74,16 @@ enum ClimateSwingMode : uint8_t { /// Enum for all modes a climate swing can be in enum ClimatePreset : uint8_t { - /// Preset is set to ECO - CLIMATE_PRESET_ECO = 0, + /// Preset is set to HOME + CLIMATE_PRESET_HOME = 0, /// Preset is set to AWAY CLIMATE_PRESET_AWAY = 1, /// Preset is set to BOOST CLIMATE_PRESET_BOOST = 2, /// Preset is set to COMFORT CLIMATE_PRESET_COMFORT = 3, - /// Preset is set to HOME - CLIMATE_PRESET_HOME = 4, + /// Preset is set to ECO + CLIMATE_PRESET_ECO = 4, /// Preset is set to SLEEP CLIMATE_PRESET_SLEEP = 5, /// Preset is set to ACTIVITY diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 774ada785f..c871552360 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -4,55 +4,6 @@ namespace esphome { namespace climate { -bool ClimateTraits::supports_mode(ClimateMode mode) const { - switch (mode) { - case CLIMATE_MODE_OFF: - return true; - case CLIMATE_MODE_HEAT_COOL: - return this->supports_heat_cool_mode_; - case CLIMATE_MODE_AUTO: - return this->supports_auto_mode_; - case CLIMATE_MODE_COOL: - return this->supports_cool_mode_; - case CLIMATE_MODE_HEAT: - return this->supports_heat_mode_; - case CLIMATE_MODE_FAN_ONLY: - return this->supports_fan_only_mode_; - case CLIMATE_MODE_DRY: - return this->supports_dry_mode_; - default: - return false; - } -} -bool ClimateTraits::get_supports_current_temperature() const { return supports_current_temperature_; } -void ClimateTraits::set_supports_current_temperature(bool supports_current_temperature) { - supports_current_temperature_ = supports_current_temperature; -} -bool ClimateTraits::get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } -void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { - supports_two_point_target_temperature_ = supports_two_point_target_temperature; -} -void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } -void ClimateTraits::set_supports_heat_cool_mode(bool supports_heat_cool_mode) { - supports_heat_cool_mode_ = supports_heat_cool_mode; -} -void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } -void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } -void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { - supports_fan_only_mode_ = supports_fan_only_mode; -} -void ClimateTraits::set_supports_dry_mode(bool supports_dry_mode) { supports_dry_mode_ = supports_dry_mode; } -void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } -void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } -float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } -void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { - visual_min_temperature_ = visual_min_temperature; -} -float ClimateTraits::get_visual_max_temperature() const { return visual_max_temperature_; } -void ClimateTraits::set_visual_max_temperature(float visual_max_temperature) { - visual_max_temperature_ = visual_max_temperature; -} -float ClimateTraits::get_visual_temperature_step() const { return visual_temperature_step_; } int8_t ClimateTraits::get_temperature_accuracy_decimals() const { // use printf %g to find number of digits based on temperature step char buf[32]; @@ -64,160 +15,6 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { return str.length() - dot_pos - 1; } -void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } -bool ClimateTraits::get_supports_away() const { return supports_away_; } -bool ClimateTraits::get_supports_action() const { return supports_action_; } -void ClimateTraits::set_supports_fan_mode_on(bool supports_fan_mode_on) { - this->supports_fan_mode_on_ = supports_fan_mode_on; -} -void ClimateTraits::set_supports_fan_mode_off(bool supports_fan_mode_off) { - this->supports_fan_mode_off_ = supports_fan_mode_off; -} -void ClimateTraits::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { - this->supports_fan_mode_auto_ = supports_fan_mode_auto; -} -void ClimateTraits::set_supports_fan_mode_low(bool supports_fan_mode_low) { - this->supports_fan_mode_low_ = supports_fan_mode_low; -} -void ClimateTraits::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { - this->supports_fan_mode_medium_ = supports_fan_mode_medium; -} -void ClimateTraits::set_supports_fan_mode_high(bool supports_fan_mode_high) { - this->supports_fan_mode_high_ = supports_fan_mode_high; -} -void ClimateTraits::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { - this->supports_fan_mode_middle_ = supports_fan_mode_middle; -} -void ClimateTraits::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { - this->supports_fan_mode_focus_ = supports_fan_mode_focus; -} -void ClimateTraits::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { - this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; -} -bool ClimateTraits::supports_fan_mode(ClimateFanMode fan_mode) const { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return this->supports_fan_mode_on_; - case climate::CLIMATE_FAN_OFF: - return this->supports_fan_mode_off_; - case climate::CLIMATE_FAN_AUTO: - return this->supports_fan_mode_auto_; - case climate::CLIMATE_FAN_LOW: - return this->supports_fan_mode_low_; - case climate::CLIMATE_FAN_MEDIUM: - return this->supports_fan_mode_medium_; - case climate::CLIMATE_FAN_HIGH: - return this->supports_fan_mode_high_; - case climate::CLIMATE_FAN_MIDDLE: - return this->supports_fan_mode_middle_; - case climate::CLIMATE_FAN_FOCUS: - return this->supports_fan_mode_focus_; - case climate::CLIMATE_FAN_DIFFUSE: - return this->supports_fan_mode_diffuse_; - default: - return false; - } -} -bool ClimateTraits::get_supports_fan_modes() const { - return this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || - this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || - this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_; -} -void ClimateTraits::set_supported_custom_fan_modes(std::vector &supported_custom_fan_modes) { - this->supported_custom_fan_modes_ = supported_custom_fan_modes; -} -const std::vector ClimateTraits::get_supported_custom_fan_modes() const { - return this->supported_custom_fan_modes_; -} -bool ClimateTraits::supports_custom_fan_mode(std::string &custom_fan_mode) const { - return std::count(this->supported_custom_fan_modes_.begin(), this->supported_custom_fan_modes_.end(), - custom_fan_mode); -} -bool ClimateTraits::supports_preset(ClimatePreset preset) const { - switch (preset) { - case climate::CLIMATE_PRESET_ECO: - return this->supports_preset_eco_; - case climate::CLIMATE_PRESET_AWAY: - return this->supports_preset_away_; - case climate::CLIMATE_PRESET_BOOST: - return this->supports_preset_boost_; - case climate::CLIMATE_PRESET_COMFORT: - return this->supports_preset_comfort_; - case climate::CLIMATE_PRESET_HOME: - return this->supports_preset_home_; - case climate::CLIMATE_PRESET_SLEEP: - return this->supports_preset_sleep_; - case climate::CLIMATE_PRESET_ACTIVITY: - return this->supports_preset_activity_; - default: - return false; - } -} -void ClimateTraits::set_supports_preset_eco(bool supports_preset_eco) { - this->supports_preset_eco_ = supports_preset_eco; -} -void ClimateTraits::set_supports_preset_away(bool supports_preset_away) { - this->supports_preset_away_ = supports_preset_away; -} -void ClimateTraits::set_supports_preset_boost(bool supports_preset_boost) { - this->supports_preset_boost_ = supports_preset_boost; -} -void ClimateTraits::set_supports_preset_comfort(bool supports_preset_comfort) { - this->supports_preset_comfort_ = supports_preset_comfort; -} -void ClimateTraits::set_supports_preset_home(bool supports_preset_home) { - this->supports_preset_home_ = supports_preset_home; -} -void ClimateTraits::set_supports_preset_sleep(bool supports_preset_sleep) { - this->supports_preset_sleep_ = supports_preset_sleep; -} -void ClimateTraits::set_supports_preset_activity(bool supports_preset_activity) { - this->supports_preset_activity_ = supports_preset_activity; -} -bool ClimateTraits::get_supports_presets() const { - return this->supports_preset_eco_ || this->supports_preset_away_ || this->supports_preset_boost_ || - this->supports_preset_comfort_ || this->supports_preset_home_ || this->supports_preset_sleep_ || - this->supports_preset_activity_; -} -void ClimateTraits::set_supported_custom_presets(std::vector &supported_custom_presets) { - this->supported_custom_presets_ = supported_custom_presets; -} -const std::vector ClimateTraits::get_supported_custom_presets() const { - return this->supported_custom_presets_; -} -bool ClimateTraits::supports_custom_preset(std::string &custom_preset) const { - return std::count(this->supported_custom_presets_.begin(), this->supported_custom_presets_.end(), custom_preset); -} -void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) { - this->supports_swing_mode_off_ = supports_swing_mode_off; -} -void ClimateTraits::set_supports_swing_mode_both(bool supports_swing_mode_both) { - this->supports_swing_mode_both_ = supports_swing_mode_both; -} -void ClimateTraits::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { - this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; -} -void ClimateTraits::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { - this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; -} -bool ClimateTraits::supports_swing_mode(ClimateSwingMode swing_mode) const { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return this->supports_swing_mode_off_; - case climate::CLIMATE_SWING_BOTH: - return this->supports_swing_mode_both_; - case climate::CLIMATE_SWING_VERTICAL: - return this->supports_swing_mode_vertical_; - case climate::CLIMATE_SWING_HORIZONTAL: - return this->supports_swing_mode_horizontal_; - default: - return false; - } -} -bool ClimateTraits::get_supports_swing_modes() const { - return this->supports_swing_mode_off_ || this->supports_swing_mode_both_ || supports_swing_mode_vertical_ || - supports_swing_mode_horizontal_; -} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index f8e6f87306..b86a0c7774 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -2,6 +2,7 @@ #include "esphome/core/helpers.h" #include "climate_mode.h" +#include namespace esphome { namespace climate { @@ -24,8 +25,6 @@ namespace climate { * - heat mode (increases current temperature) * - dry mode (removes humidity from air) * - fan mode (only turns on fan) - * - supports away - away mode means that the climate device supports two different - * target temperature settings: one target temp setting for "away" mode and one for non-away mode. * - supports action - if the climate device supports reporting the active * current action of the device with the action property. * - supports fan modes - optionally, if it has a fan which can be configured in different ways: @@ -41,95 +40,147 @@ namespace climate { */ class ClimateTraits { public: - bool get_supports_current_temperature() const; - void set_supports_current_temperature(bool supports_current_temperature); - bool get_supports_two_point_target_temperature() const; - void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature); - void set_supports_auto_mode(bool supports_auto_mode); - void set_supports_heat_cool_mode(bool supports_heat_cool_mode); - void set_supports_cool_mode(bool supports_cool_mode); - void set_supports_heat_mode(bool supports_heat_mode); - void set_supports_fan_only_mode(bool supports_fan_only_mode); - void set_supports_dry_mode(bool supports_dry_mode); - void set_supports_away(bool supports_away); - bool get_supports_away() const; - void set_supports_action(bool supports_action); - bool get_supports_action() const; - bool supports_mode(ClimateMode mode) const; - void set_supports_fan_mode_on(bool supports_fan_mode_on); - void set_supports_fan_mode_off(bool supports_fan_mode_off); - void set_supports_fan_mode_auto(bool supports_fan_mode_auto); - void set_supports_fan_mode_low(bool supports_fan_mode_low); - void set_supports_fan_mode_medium(bool supports_fan_mode_medium); - void set_supports_fan_mode_high(bool supports_fan_mode_high); - void set_supports_fan_mode_middle(bool supports_fan_mode_middle); - void set_supports_fan_mode_focus(bool supports_fan_mode_focus); - void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); - bool supports_fan_mode(ClimateFanMode fan_mode) const; - bool get_supports_fan_modes() const; - void set_supported_custom_fan_modes(std::vector &supported_custom_fan_modes); - const std::vector get_supported_custom_fan_modes() const; - bool supports_custom_fan_mode(std::string &custom_fan_mode) const; - bool supports_preset(ClimatePreset preset) const; - void set_supports_preset_eco(bool supports_preset_eco); - void set_supports_preset_away(bool supports_preset_away); - void set_supports_preset_boost(bool supports_preset_boost); - void set_supports_preset_comfort(bool supports_preset_comfort); - void set_supports_preset_home(bool supports_preset_home); - void set_supports_preset_sleep(bool supports_preset_sleep); - void set_supports_preset_activity(bool supports_preset_activity); - bool get_supports_presets() const; - void set_supported_custom_presets(std::vector &supported_custom_presets); - const std::vector get_supported_custom_presets() const; - bool supports_custom_preset(std::string &custom_preset) const; - void set_supports_swing_mode_off(bool supports_swing_mode_off); - void set_supports_swing_mode_both(bool supports_swing_mode_both); - void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); - void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); - bool supports_swing_mode(ClimateSwingMode swing_mode) const; - bool get_supports_swing_modes() const; + bool get_supports_current_temperature() const { return supports_current_temperature_; } + void set_supports_current_temperature(bool supports_current_temperature) { + supports_current_temperature_ = supports_current_temperature; + } + bool get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; } + void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) { + supports_two_point_target_temperature_ = supports_two_point_target_temperature; + } + void set_supported_modes(std::set modes) { supported_modes_ = std::move(modes); } + void add_supported_mode(ClimateMode mode) { supported_modes_.insert(mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_fan_only_mode(bool supports_fan_only_mode) { + set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode); + } + ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead") + void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); } + bool supports_mode(ClimateMode mode) const { return supported_modes_.count(mode); } + const std::set get_supported_modes() const { return supported_modes_; } - float get_visual_min_temperature() const; - void set_visual_min_temperature(float visual_min_temperature); - float get_visual_max_temperature() const; - void set_visual_max_temperature(float visual_max_temperature); - float get_visual_temperature_step() const; + void set_supports_action(bool supports_action) { supports_action_ = supports_action; } + bool get_supports_action() const { return supports_action_; } + + void set_supported_fan_modes(std::set modes) { supported_fan_modes_ = std::move(modes); } + void add_supported_fan_mode(ClimateFanMode mode) { supported_fan_modes_.insert(mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } + bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } + bool get_supports_fan_modes() const { return !supported_fan_modes_.empty(); } + const std::set get_supported_fan_modes() const { return supported_fan_modes_; } + + void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { + supported_custom_fan_modes_ = std::move(supported_custom_fan_modes); + } + const std::set &get_supported_custom_fan_modes() const { return supported_custom_fan_modes_; } + bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { + return supported_custom_fan_modes_.count(custom_fan_mode); + } + + void set_supported_presets(std::set presets) { supported_presets_ = std::move(presets); } + void add_supported_preset(ClimatePreset preset) { supported_presets_.insert(preset); } + bool supports_preset(ClimatePreset preset) const { return supported_presets_.count(preset); } + bool get_supports_presets() const { return !supported_presets_.empty(); } + const std::set &get_supported_presets() const { return supported_presets_; } + + void set_supported_custom_presets(std::set supported_custom_presets) { + supported_custom_presets_ = std::move(supported_custom_presets); + } + const std::set &get_supported_custom_presets() const { return supported_custom_presets_; } + bool supports_custom_preset(const std::string &custom_preset) const { + return supported_custom_presets_.count(custom_preset); + } + ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead") + void set_supports_away(bool supports) { + if (supports) { + supported_presets_.insert(CLIMATE_PRESET_AWAY); + supported_presets_.insert(CLIMATE_PRESET_HOME); + } + } + ESPDEPRECATED("This method is deprecated, use supports_preset() instead") + bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); } + + void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); } + void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } + ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + void set_supports_swing_mode_horizontal(bool supported) { + set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); + } + bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); } + bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); } + const std::set get_supported_swing_modes() { return supported_swing_modes_; } + + float get_visual_min_temperature() const { return visual_min_temperature_; } + void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; } + float get_visual_max_temperature() const { return visual_max_temperature_; } + void set_visual_max_temperature(float visual_max_temperature) { visual_max_temperature_ = visual_max_temperature; } + float get_visual_temperature_step() const { return visual_temperature_step_; } int8_t get_temperature_accuracy_decimals() const; - void set_visual_temperature_step(float temperature_step); + void set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } protected: + void set_mode_support_(climate::ClimateMode mode, bool supported) { + if (supported) { + supported_modes_.insert(mode); + } else { + supported_modes_.erase(mode); + } + } + void set_fan_mode_support_(climate::ClimateFanMode mode, bool supported) { + if (supported) { + supported_fan_modes_.insert(mode); + } else { + supported_fan_modes_.erase(mode); + } + } + void set_swing_mode_support_(climate::ClimateSwingMode mode, bool supported) { + if (supported) { + supported_swing_modes_.insert(mode); + } else { + supported_swing_modes_.erase(mode); + } + } + bool supports_current_temperature_{false}; bool supports_two_point_target_temperature_{false}; - bool supports_auto_mode_{false}; - bool supports_heat_cool_mode_{false}; - bool supports_cool_mode_{false}; - bool supports_heat_mode_{false}; - bool supports_fan_only_mode_{false}; - bool supports_dry_mode_{false}; - bool supports_away_{false}; + std::set supported_modes_ = {climate::CLIMATE_MODE_OFF}; bool supports_action_{false}; - bool supports_fan_mode_on_{false}; - bool supports_fan_mode_off_{false}; - bool supports_fan_mode_auto_{false}; - bool supports_fan_mode_low_{false}; - bool supports_fan_mode_medium_{false}; - bool supports_fan_mode_high_{false}; - bool supports_fan_mode_middle_{false}; - bool supports_fan_mode_focus_{false}; - bool supports_fan_mode_diffuse_{false}; - bool supports_swing_mode_off_{false}; - bool supports_swing_mode_both_{false}; - bool supports_swing_mode_vertical_{false}; - bool supports_swing_mode_horizontal_{false}; - bool supports_preset_eco_{false}; - bool supports_preset_away_{false}; - bool supports_preset_boost_{false}; - bool supports_preset_comfort_{false}; - bool supports_preset_home_{false}; - bool supports_preset_sleep_{false}; - bool supports_preset_activity_{false}; - std::vector supported_custom_fan_modes_; - std::vector supported_custom_presets_; + std::set supported_fan_modes_; + std::set supported_swing_modes_; + std::set supported_presets_; + std::set supported_custom_fan_modes_; + std::set supported_custom_presets_; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 5b16ed7fae..8d5ae6053d 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -9,63 +9,22 @@ static const char *const TAG = "climate_ir"; climate::ClimateTraits ClimateIR::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_heat_cool_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_dry_mode(this->supports_dry_); - traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_dry_) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (supports_fan_only_) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); + traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); - for (auto fan_mode : this->fan_modes_) { - switch (fan_mode) { - case climate::CLIMATE_FAN_AUTO: - traits.set_supports_fan_mode_auto(true); - break; - case climate::CLIMATE_FAN_DIFFUSE: - traits.set_supports_fan_mode_diffuse(true); - break; - case climate::CLIMATE_FAN_FOCUS: - traits.set_supports_fan_mode_focus(true); - break; - case climate::CLIMATE_FAN_HIGH: - traits.set_supports_fan_mode_high(true); - break; - case climate::CLIMATE_FAN_LOW: - traits.set_supports_fan_mode_low(true); - break; - case climate::CLIMATE_FAN_MEDIUM: - traits.set_supports_fan_mode_medium(true); - break; - case climate::CLIMATE_FAN_MIDDLE: - traits.set_supports_fan_mode_middle(true); - break; - case climate::CLIMATE_FAN_OFF: - traits.set_supports_fan_mode_off(true); - break; - case climate::CLIMATE_FAN_ON: - traits.set_supports_fan_mode_on(true); - break; - } - } - for (auto swing_mode : this->swing_modes_) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - traits.set_supports_swing_mode_off(true); - break; - case climate::CLIMATE_SWING_BOTH: - traits.set_supports_swing_mode_both(true); - break; - case climate::CLIMATE_SWING_VERTICAL: - traits.set_supports_swing_mode_vertical(true); - break; - case climate::CLIMATE_SWING_HORIZONTAL: - traits.set_supports_swing_mode_horizontal(true); - break; - } - } + traits.set_supported_fan_modes(fan_modes_); + traits.set_supported_swing_modes(swing_modes_); return traits; } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 0914f730cf..677021da29 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -21,9 +21,8 @@ namespace climate_ir { class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { public: ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, - bool supports_dry = false, bool supports_fan_only = false, - std::vector fan_modes = {}, - std::vector swing_modes = {}) { + bool supports_dry = false, bool supports_fan_only = false, std::set fan_modes = {}, + std::set swing_modes = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; @@ -60,8 +59,8 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_heat_{true}; bool supports_dry_{false}; bool supports_fan_only_{false}; - std::vector fan_modes_ = {}; - std::vector swing_modes_ = {}; + std::set fan_modes_ = {}; + std::set swing_modes_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index c0a472bce7..b4ac309de9 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -43,12 +43,11 @@ const uint8_t DAIKIN_STATE_FRAME_SIZE = 19; class DaikinClimate : public climate_ir::ClimateIR { public: DaikinClimate() - : climate_ir::ClimateIR( - DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, - std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, - std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, - climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + : climate_ir::ClimateIR(DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} protected: // Transmit via IR the state of this climate controller. diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h index 3126ef0493..c34f033d92 100644 --- a/esphome/components/hitachi_ac344/hitachi_ac344.h +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -79,11 +79,10 @@ const uint16_t HITACHI_AC344_BITS = HITACHI_AC344_STATE_LENGTH * 8; class HitachiClimate : public climate_ir::ClimateIR { public: HitachiClimate() - : climate_ir::ClimateIR( - HITACHI_AC344_TEMP_MIN, HITACHI_AC344_TEMP_MAX, 1.0F, true, true, - std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, - climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, - std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}) {} + : climate_ir::ClimateIR(HITACHI_AC344_TEMP_MIN, HITACHI_AC344_TEMP_MAX, 1.0F, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL}) {} protected: uint8_t remote_state_[HITACHI_AC344_STATE_LENGTH]{0x01, 0x10, 0x00, 0x40, 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00, diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp index 001d5edb53..9fe5df7de3 100644 --- a/esphome/components/midea_ac/midea_climate.cpp +++ b/esphome/components/midea_ac/midea_climate.cpp @@ -167,24 +167,38 @@ climate::ClimateTraits MideaAC::traits() { traits.set_visual_min_temperature(17); traits.set_visual_max_temperature(30); traits.set_visual_temperature_step(0.5); - traits.set_supports_heat_cool_mode(true); - traits.set_supports_cool_mode(true); - traits.set_supports_dry_mode(true); - traits.set_supports_heat_mode(true); - traits.set_supports_fan_only_mode(true); - traits.set_supports_fan_mode_auto(true); - traits.set_supports_fan_mode_low(true); - traits.set_supports_fan_mode_medium(true); - traits.set_supports_fan_mode_high(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT_COOL, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_FAN_ONLY, + }); + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + }); traits.set_supported_custom_fan_modes(this->traits_custom_fan_modes_); - traits.set_supports_swing_mode_off(true); - traits.set_supports_swing_mode_vertical(true); - traits.set_supports_swing_mode_horizontal(this->traits_swing_horizontal_); - traits.set_supports_swing_mode_both(this->traits_swing_both_); - traits.set_supports_preset_home(true); - traits.set_supports_preset_eco(this->traits_preset_eco_); - traits.set_supports_preset_sleep(this->traits_preset_sleep_); - traits.set_supports_preset_boost(this->traits_preset_boost_); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_VERTICAL, + }); + if (traits_swing_horizontal_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + if (traits_swing_both_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + traits.set_supported_presets({ + climate::CLIMATE_PRESET_HOME, + }); + if (traits_preset_eco_) + traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); + if (traits_preset_sleep_) + traits.add_supported_preset(climate::CLIMATE_PRESET_SLEEP); + if (traits_preset_boost_) + traits.add_supported_preset(climate::CLIMATE_PRESET_BOOST); traits.set_supported_custom_presets(this->traits_custom_presets_); traits.set_supports_current_temperature(true); return traits; diff --git a/esphome/components/midea_ac/midea_climate.h b/esphome/components/midea_ac/midea_climate.h index 1d21c4cab2..3cdf42f52e 100644 --- a/esphome/components/midea_ac/midea_climate.h +++ b/esphome/components/midea_ac/midea_climate.h @@ -28,10 +28,10 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu void set_preset_sleep(bool state) { this->traits_preset_sleep_ = state; } void set_preset_boost(bool state) { this->traits_preset_boost_ = state; } bool allow_preset(climate::ClimatePreset preset) const; - void set_custom_fan_modes(std::vector custom_fan_modes) { + void set_custom_fan_modes(std::set custom_fan_modes) { this->traits_custom_fan_modes_ = std::move(custom_fan_modes); } - void set_custom_presets(std::vector custom_presets) { + void set_custom_presets(std::set custom_presets) { this->traits_custom_presets_ = std::move(custom_presets); } bool allow_custom_preset(const std::string &custom_preset) const; @@ -57,8 +57,8 @@ class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, pu bool traits_preset_eco_{false}; bool traits_preset_sleep_{false}; bool traits_preset_boost_{false}; - std::vector traits_custom_fan_modes_{{}}; - std::vector traits_custom_presets_{{}}; + std::set traits_custom_fan_modes_{{}}; + std::set traits_custom_presets_{{}}; }; } // namespace midea_ac diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 164ba16faf..0934922bc6 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -61,7 +61,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC // temp_step root["temp_step"] = traits.get_visual_temperature_step(); - if (traits.get_supports_away()) { + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { // away_mode_command_topic root["away_mode_cmd_t"] = this->get_away_command_topic(); // away_mode_state_topic @@ -164,19 +164,19 @@ void MQTTClimateComponent::setup() { }); } - if (traits.get_supports_away()) { + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { this->subscribe(this->get_away_command_topic(), [this](const std::string &topic, const std::string &payload) { auto onoff = parse_on_off(payload.c_str()); auto call = this->device_->make_call(); switch (onoff) { case PARSE_ON: - call.set_away(true); + call.set_preset(CLIMATE_PRESET_AWAY); break; case PARSE_OFF: - call.set_away(false); + call.set_preset(CLIMATE_PRESET_HOME); break; case PARSE_TOGGLE: - call.set_away(!this->device_->away); + call.set_preset(this->device_->preset == CLIMATE_PRESET_AWAY ? CLIMATE_PRESET_HOME : CLIMATE_PRESET_AWAY); break; case PARSE_NONE: default: @@ -259,8 +259,8 @@ bool MQTTClimateComponent::publish_state_() { success = false; } - if (traits.get_supports_away()) { - std::string payload = ONOFF(this->device_->away); + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { + std::string payload = ONOFF(this->device_->preset == CLIMATE_PRESET_AWAY); if (!this->publish(this->get_away_state_topic(), payload)) success = false; } diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 1601f1a0fe..8a61361fb8 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -39,10 +39,14 @@ void PIDClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits PIDClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_heat_cool_mode(true); traits.set_supports_two_point_target_temperature(false); - traits.set_supports_cool_mode(this->supports_cool_()); - traits.set_supports_heat_mode(this->supports_heat_()); + + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + if (supports_cool_()) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_()) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + traits.set_supports_action(true); return traits; } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 3fdb4efa7b..305db66f16 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -50,12 +50,13 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) { + if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_away_(*call.get_away()); + this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); } else { - this->away = *call.get_away(); + this->preset = *call.get_preset(); + ; } } // set point validation @@ -78,27 +79,51 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(this->supports_auto_); - traits.set_supports_heat_cool_mode(this->supports_heat_cool_); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_dry_mode(this->supports_dry_); - traits.set_supports_fan_only_mode(this->supports_fan_only_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); - traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); - traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); - traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); - traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); - traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); - traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); - traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); - traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); - traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); - traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); - traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); - traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + if (supports_auto_) + traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); + if (supports_heat_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_dry_) + traits.add_supported_mode(climate::CLIMATE_MODE_DRY); + if (supports_fan_only_) + traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + + if (supports_fan_mode_on_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_ON); + if (supports_fan_mode_off_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_OFF); + if (supports_fan_mode_auto_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); + if (supports_fan_mode_low_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); + if (supports_fan_mode_medium_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); + if (supports_fan_mode_high_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); + if (supports_fan_mode_middle_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); + if (supports_fan_mode_focus_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); + if (supports_fan_mode_diffuse_) + traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); + + if (supports_swing_mode_both_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + if (supports_swing_mode_horizontal_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + if (supports_swing_mode_off_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); + if (supports_swing_mode_vertical_) + traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); + + if (supports_away_) + traits.set_supported_presets({climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_AWAY}); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); - traits.set_supports_away(this->supports_away_); traits.set_supports_action(true); return traits; } @@ -399,7 +424,7 @@ void ThermostatClimate::change_away_(bool away) { } else this->target_temperature = this->away_config_.default_temperature; } - this->away = away; + this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 2b6ce833de..5f214d3bfe 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -68,8 +68,10 @@ void TuyaClimate::control(const climate::ClimateCall &call) { climate::ClimateTraits TuyaClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_cool_mode(this->supports_cool_); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); traits.set_supports_action(true); return traits; } diff --git a/esphome/components/yashima/yashima.cpp b/esphome/components/yashima/yashima.cpp index bda27b62fb..8d588127b0 100644 --- a/esphome/components/yashima/yashima.cpp +++ b/esphome/components/yashima/yashima.cpp @@ -82,11 +82,14 @@ const uint32_t YASHIMA_CARRIER_FREQUENCY = 38000; climate::ClimateTraits YashimaClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_heat_cool_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); + + traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + if (supports_cool_) + traits.add_supported_mode(climate::CLIMATE_MODE_COOL); + if (supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); traits.set_visual_min_temperature(YASHIMA_TEMP_MIN); traits.set_visual_max_temperature(YASHIMA_TEMP_MAX); traits.set_visual_temperature_step(1); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py old mode 100644 new mode 100755 index 7f9067cd22..56c3e8ccc8 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 """Python 3 script to automatically generate C++ classes for ESPHome's native API. It's pretty crappy spaghetti code, but it works. From c811141a4f6238333ac42031d6d8fa80c946eb7c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Jun 2021 22:02:18 +0200 Subject: [PATCH 520/643] API raise minor version for climate changes (#1947) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 89c0dde24a..00a8539112 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -615,7 +615,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 4; + resp.api_version_minor = 5; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; From 027e0de48ed356b3ce32654697e6a166aca73668 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Jun 2021 21:17:46 -0700 Subject: [PATCH 521/643] Bump dashboard to 20210621.0 (#1946) --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f02d65e518..0711451955 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210617.1 +esphome-dashboard==20210621.0 + From 3dfff2930a28cc061da8f6c852b46dc931675c96 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 22 Jun 2021 10:07:14 +0200 Subject: [PATCH 522/643] Improve DHT read timings (#1901) Make sure that the initial rising edge is properly detected even if timing is somewhat off. Set MCU start signal to 1ms for AM2302. --- esphome/components/dht/dht.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index fd51a976b7..4a5c418e0a 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -94,11 +94,17 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, delayMicroseconds(40); } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); + } else if (this->model_ == DHT_MODEL_AM2302) { + delayMicroseconds(1000); } else { delayMicroseconds(800); } this->pin_->pin_mode(INPUT_PULLUP); - delayMicroseconds(40); + + // Host pull up 20-40us then DHT response 80us + // Start waiting for initial rising edge at the center when we + // expect the DHT response (30us+40us) + delayMicroseconds(70); uint8_t bit = 7; uint8_t byte = 0; @@ -116,8 +122,6 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, break; } } - if (error_code != 0) - break; start_time = micros(); uint32_t end_time = start_time; @@ -132,8 +136,6 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, break; } } - if (error_code != 0) - break; if (i < 0) continue; From bfca3f242a4bc5535899f23074bfd0e8770d22aa Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 22 Jun 2021 10:53:10 +0200 Subject: [PATCH 523/643] Disallow power_save_mode NONE if used together with BLE (#1950) --- esphome/components/http_request/__init__.py | 19 ++++------- esphome/components/wifi/__init__.py | 36 ++++++++++++++++++++- esphome/final_validate.py | 24 ++++++++++++++ tests/test1.yaml | 2 +- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index f4475060df..7dffdae27f 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -7,10 +7,7 @@ from esphome import automation from esphome.const import ( CONF_ID, CONF_TIMEOUT, - CONF_ESPHOME, CONF_METHOD, - CONF_ARDUINO_VERSION, - ARDUINO_VERSION_ESP8266, CONF_TRIGGER_ID, CONF_URL, ) @@ -78,23 +75,19 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -def validate_framework(config): - if CORE.is_esp32: +def validate_framework(value): + if not CORE.is_esp8266: + # only for ESP8266 return - # only for ESP8266 - path = [CONF_ESPHOME, CONF_ARDUINO_VERSION] - version: str = fv.full_config.get().get_config_for_path(path) - - reverse_map = {v: k for k, v in ARDUINO_VERSION_ESP8266.items()} - framework_version = reverse_map.get(version) + framework_version = fv.get_arduino_framework_version() if framework_version is None or framework_version == "dev": return if framework_version < "2.5.1": raise cv.Invalid( - "This component is not supported on arduino framework version below 2.5.1", - path=[cv.ROOT_CONFIG_PATH] + path, + "This component is not supported on arduino framework version below 2.5.1, ", + "please check esphome->arduino_version", ) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 54307388d6..7a1a01bcc4 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -148,7 +148,41 @@ def final_validate(config): ) -FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) +def final_validate_power_esp32_ble(value): + if not CORE.is_esp32: + return + if value != "NONE": + # WiFi should be in modem sleep (!=NONE) with BLE coexistence + # https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-guides/wifi.html#station-sleep + return + framework_version = fv.get_arduino_framework_version() + if framework_version not in (None, "dev") and framework_version < "1.0.5": + # Only frameworks 1.0.5+ impacted + return + full = fv.full_config.get() + for conflicting in [ + "esp32_ble", + "esp32_ble_beacon", + "esp32_ble_server", + "esp32_ble_tracker", + ]: + if conflicting in full: + raise cv.Invalid( + f"power_save_mode NONE is incompatible with {conflicting}. " + f"Please remove the power save mode. See also " + f"https://github.com/esphome/issues/issues/2141#issuecomment-865688582" + ) + + +FINAL_VALIDATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_POWER_SAVE_MODE): final_validate_power_esp32_ble, + }, + extra=cv.ALLOW_EXTRA, + ), + final_validate, +) def _validate(config): diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 50fdbaf3f4..47071b5391 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -4,6 +4,13 @@ import contextvars from esphome.types import ConfigFragmentType, ID, ConfigPathType import esphome.config_validation as cv +from esphome.const import ( + ARDUINO_VERSION_ESP32, + ARDUINO_VERSION_ESP8266, + CONF_ESPHOME, + CONF_ARDUINO_VERSION, +) +from esphome.core import CORE class FinalValidateConfig(ABC): @@ -55,3 +62,20 @@ def id_declaration_match_schema(schema): return schema(declaration_config) return validator + + +def get_arduino_framework_version(): + path = [CONF_ESPHOME, CONF_ARDUINO_VERSION] + # This is run after core validation, so the property is set even if user didn't + version: str = full_config.get().get_config_for_path(path) + + if CORE.is_esp32: + version_map = ARDUINO_VERSION_ESP32 + elif CORE.is_esp8266: + version_map = ARDUINO_VERSION_ESP8266 + else: + raise ValueError("Platform not supported yet for this validator") + + reverse_map = {v: k for k, v in version_map.items()} + framework_version = reverse_map.get(version) + return framework_version diff --git a/tests/test1.yaml b/tests/test1.yaml index 29df5857d3..08e1d63534 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -80,7 +80,7 @@ wifi: dns2: 1.2.2.1 domain: .local reboot_timeout: 120s - power_save_mode: none + power_save_mode: light http_request: useragent: esphome/device From 32f2da77f8431e7e13ab3e44cc62634a225a13c6 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 22 Jun 2021 16:37:05 +0200 Subject: [PATCH 524/643] More VSCode devcontainer improvements (#1934) --- .devcontainer/devcontainer.json | 2 +- .dockerignore | 4 ++++ .vscode/tasks.json | 35 ++++++++++++++++++++++++++------- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0e6fe19e0e..3904962d7c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -32,7 +32,7 @@ "editor.formatOnSave": true, "editor.formatOnType": true, "files.trimTrailingWhitespace": true, - "terminal.integrated.defaultProfile.linux": "/bin/bash", + "terminal.integrated.defaultProfile.linux": "bash", "yaml.customTags": [ "!secret scalar", "!lambda scalar", diff --git a/.dockerignore b/.dockerignore index e1baed38ca..9f14b98059 100644 --- a/.dockerignore +++ b/.dockerignore @@ -103,6 +103,10 @@ venv.bak/ # mypy .mypy_cache/ +# PlatformIO +.pio/ + +# ESPHome config/ examples/ Dockerfile diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 513228722a..8c55646f28 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,11 +1,32 @@ { - "version": "2.0.0", - "tasks": [ + "version": "2.0.0", + "tasks": [ + { + "label": "run", + "type": "shell", + "command": "python3 -m esphome dashboard config/", + "problemMatcher": [] + }, + { + "label": "clang-tidy", + "type": "shell", + "command": "test -f .gcc-flags.json || pio init --silent --ide atom; ./script/clang-tidy", + "problemMatcher": [ { - "label": "run", - "type": "shell", - "command": "python3 -m esphome dashboard config/", - "problemMatcher": [] + "owner": "clang-tidy", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] } - ] + ] + } + ] } From 61ebc629f6bbd26e6f63253cb2f496e95834a7c4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 23 Jun 2021 17:15:11 +1200 Subject: [PATCH 525/643] Bump esphome-dashboard to 20210622.0 (#1955) --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0711451955..12500d57b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,5 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210621.0 - +esphome-dashboard==20210622.0 From d0859a7d33accdad7fbc54dbcee50be2c7fd2a04 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 23 Jun 2021 20:25:19 +0200 Subject: [PATCH 526/643] Add climate preset NONE again (#1951) --- esphome/components/api/api.proto | 15 ++++++----- esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 15 ++++++----- esphome/components/climate/climate_mode.cpp | 2 ++ esphome/components/climate/climate_mode.h | 30 +++++++++++---------- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 87a7cf4749..a5bd9aec6d 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -710,13 +710,14 @@ enum ClimateAction { CLIMATE_ACTION_FAN = 6; } enum ClimatePreset { - CLIMATE_PRESET_HOME = 0; - CLIMATE_PRESET_AWAY = 1; - CLIMATE_PRESET_BOOST = 2; - CLIMATE_PRESET_COMFORT = 3; - CLIMATE_PRESET_ECO = 4; - CLIMATE_PRESET_SLEEP = 5; - CLIMATE_PRESET_ACTIVITY = 6; + CLIMATE_PRESET_NONE = 0; + CLIMATE_PRESET_HOME = 1; + CLIMATE_PRESET_AWAY = 2; + CLIMATE_PRESET_BOOST = 3; + CLIMATE_PRESET_COMFORT = 4; + CLIMATE_PRESET_ECO = 5; + CLIMATE_PRESET_SLEEP = 6; + CLIMATE_PRESET_ACTIVITY = 7; } message ListEntitiesClimateResponse { option (id) = 46; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 1e023f3988..e53ac2019a 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -192,6 +192,8 @@ template<> const char *proto_enum_to_string(enums::Climate } template<> const char *proto_enum_to_string(enums::ClimatePreset value) { switch (value) { + case enums::CLIMATE_PRESET_NONE: + return "CLIMATE_PRESET_NONE"; case enums::CLIMATE_PRESET_HOME: return "CLIMATE_PRESET_HOME"; case enums::CLIMATE_PRESET_AWAY: diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 7f37c0b94b..956cecdeb9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -90,13 +90,14 @@ enum ClimateAction : uint32_t { CLIMATE_ACTION_FAN = 6, }; enum ClimatePreset : uint32_t { - CLIMATE_PRESET_HOME = 0, - CLIMATE_PRESET_AWAY = 1, - CLIMATE_PRESET_BOOST = 2, - CLIMATE_PRESET_COMFORT = 3, - CLIMATE_PRESET_ECO = 4, - CLIMATE_PRESET_SLEEP = 5, - CLIMATE_PRESET_ACTIVITY = 6, + CLIMATE_PRESET_NONE = 0, + CLIMATE_PRESET_HOME = 1, + CLIMATE_PRESET_AWAY = 2, + CLIMATE_PRESET_BOOST = 3, + CLIMATE_PRESET_COMFORT = 4, + CLIMATE_PRESET_ECO = 5, + CLIMATE_PRESET_SLEEP = 6, + CLIMATE_PRESET_ACTIVITY = 7, }; } // namespace enums diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 099074a887..7a626942eb 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -84,6 +84,8 @@ const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { const char *climate_preset_to_string(ClimatePreset preset) { switch (preset) { + case climate::CLIMATE_PRESET_NONE: + return "NONE"; case climate::CLIMATE_PRESET_HOME: return "HOME"; case climate::CLIMATE_PRESET_ECO: diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 7afa2dae55..07fbf32b26 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -74,20 +74,22 @@ enum ClimateSwingMode : uint8_t { /// Enum for all modes a climate swing can be in enum ClimatePreset : uint8_t { - /// Preset is set to HOME - CLIMATE_PRESET_HOME = 0, - /// Preset is set to AWAY - CLIMATE_PRESET_AWAY = 1, - /// Preset is set to BOOST - CLIMATE_PRESET_BOOST = 2, - /// Preset is set to COMFORT - CLIMATE_PRESET_COMFORT = 3, - /// Preset is set to ECO - CLIMATE_PRESET_ECO = 4, - /// Preset is set to SLEEP - CLIMATE_PRESET_SLEEP = 5, - /// Preset is set to ACTIVITY - CLIMATE_PRESET_ACTIVITY = 6, + /// No preset is active + CLIMATE_PRESET_NONE = 0, + /// Device is in home preset + CLIMATE_PRESET_HOME = 1, + /// Device is in away preset + CLIMATE_PRESET_AWAY = 2, + /// Device is in boost preset + CLIMATE_PRESET_BOOST = 3, + /// Device is in comfort preset + CLIMATE_PRESET_COMFORT = 4, + /// Device is running an energy-saving preset + CLIMATE_PRESET_ECO = 5, + /// Device is prepared for sleep + CLIMATE_PRESET_SLEEP = 6, + /// Device is reacting to activity (e.g., movement sensors) + CLIMATE_PRESET_ACTIVITY = 7, }; /// Convert the given ClimateMode to a human-readable string. From 2cb3015a2814be21ee8f537793d89eb40bb186f5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 23 Jun 2021 20:27:08 +0200 Subject: [PATCH 527/643] Compat argv parsing improvements (#1952) --- esphome/__main__.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 48f8bea083..232652db9f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -514,14 +514,26 @@ def parse_args(argv): compat_parser.error = _raise - try: - result, unparsed = compat_parser.parse_known_args(argv[1:]) - last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) - argv = argv[0:last_option] + [result.command] + result.configuration + unparsed - deprecated_argv_suggestion = argv - except argparse.ArgumentError: - # This is not an old-style command line, so we don't have to do anything. - deprecated_argv_suggestion = None + deprecated_argv_suggestion = None + + if ["dashboard", "config"] == argv[1:3]: + # this is most likely meant in new-style arg format. do not try compat parsing + pass + else: + try: + result, unparsed = compat_parser.parse_known_args(argv[1:]) + last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) + unparsed = [ + "--device" if arg in ("--upload-port", "--serial-port") else arg + for arg in unparsed + ] + argv = ( + argv[0:last_option] + [result.command] + result.configuration + unparsed + ) + deprecated_argv_suggestion = argv + except argparse.ArgumentError: + # This is not an old-style command line, so we don't have to do anything. + pass # And continue on with regular parsing parser = argparse.ArgumentParser( From 7051f897bc86ebea7cfb8eb6c401847b9c6bcbc6 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 24 Jun 2021 00:07:27 +0200 Subject: [PATCH 528/643] Validate color temperature values for RGBWW/CWWW lights (#1957) Check if the color temperature of the cold white channel is colder (less) than the warm white channel. --- esphome/components/cwww/light.py | 21 +++++++++++--------- esphome/components/light/__init__.py | 14 ++++++++++++++ esphome/components/rgbww/light.py | 29 +++++++++++++++------------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 674c48d219..1a027a86b4 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -14,15 +14,18 @@ CWWWLightOutput = cwww_ns.class_("CWWWLightOutput", light.LightOutput) CONF_CONSTANT_BRIGHTNESS = "constant_brightness" -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), - cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, - } +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), + cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + } + ), + light.validate_color_temperature_channels, ) diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 276bf8073d..119ff3703c 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -16,6 +16,8 @@ from esphome.const import ( CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID, + CONF_COLD_WHITE_COLOR_TEMPERATURE, + CONF_WARM_WHITE_COLOR_TEMPERATURE, ) from esphome.core import coroutine_with_priority from .automation import light_control_to_code # noqa @@ -104,6 +106,18 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( ) +def validate_color_temperature_channels(value): + if ( + value[CONF_COLD_WHITE_COLOR_TEMPERATURE] + >= value[CONF_WARM_WHITE_COLOR_TEMPERATURE] + ): + raise cv.Invalid( + "Color temperature of the cold white channel must be colder than that of the warm white channel.", + path=[CONF_COLD_WHITE_COLOR_TEMPERATURE], + ) + return value + + async def setup_light_core_(light_var, output_var, config): cg.add(light_var.set_restore_mode(config[CONF_RESTORE_MODE])) if CONF_INTERNAL in config: diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index d152fbc6db..41cba1f7a1 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -18,19 +18,22 @@ RGBWWLightOutput = rgbww_ns.class_("RGBWWLightOutput", light.LightOutput) CONF_CONSTANT_BRIGHTNESS = "constant_brightness" CONF_COLOR_INTERLOCK = "color_interlock" -CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend( - { - cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), - cv.Required(CONF_RED): cv.use_id(output.FloatOutput), - cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), - cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, - cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, - } +CONFIG_SCHEMA = cv.All( + light.RGB_LIGHT_SCHEMA.extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), + cv.Required(CONF_RED): cv.use_id(output.FloatOutput), + cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), + cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), + cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, + } + ), + light.validate_color_temperature_channels, ) From 5fca4809216554321222c668f85b11b091ee3802 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 24 Jun 2021 12:38:59 +1200 Subject: [PATCH 529/643] Bump dashboard to 20210623.0 (#1958) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 12500d57b7..cc9059f0d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210622.0 +esphome-dashboard==20210623.0 From 3b940b1c04ebabf520d1494a36c719e5ab27caad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 25 Jun 2021 07:09:07 +1200 Subject: [PATCH 530/643] Set is_valid to true straight away when min_length is 0 (#1960) --- esphome/components/binary_sensor/automation.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index a333b33397..ce082aafb3 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -80,6 +80,10 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() { this->cancel_timeout("is_not_valid"); } void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { + if (min_length == 0) { + this->is_valid_ = true; + return; + } this->is_valid_ = false; this->set_timeout("is_valid", min_length, [this]() { ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); From 964ab654974ebb03ebd08949259fe4844537cd0c Mon Sep 17 00:00:00 2001 From: bazuchan Date: Mon, 28 Jun 2021 22:26:30 +0300 Subject: [PATCH 531/643] Climate component for Ballu air conditioners with remote model YKR-K/002E (#1939) --- CODEOWNERS | 1 + esphome/components/ballu/__init__.py | 0 esphome/components/ballu/ballu.cpp | 239 +++++++++++++++++++++++++++ esphome/components/ballu/ballu.h | 31 ++++ esphome/components/ballu/climate.py | 21 +++ 5 files changed, 292 insertions(+) create mode 100644 esphome/components/ballu/__init__.py create mode 100644 esphome/components/ballu/ballu.cpp create mode 100644 esphome/components/ballu/ballu.h create mode 100644 esphome/components/ballu/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index 0594a60ef6..f242cc6d9d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -19,6 +19,7 @@ esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron +esphome/components/ballu/* @bazuchan esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/ble_client/* @buxtronix diff --git a/esphome/components/ballu/__init__.py b/esphome/components/ballu/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ballu/ballu.cpp b/esphome/components/ballu/ballu.cpp new file mode 100644 index 0000000000..e2703a79fb --- /dev/null +++ b/esphome/components/ballu/ballu.cpp @@ -0,0 +1,239 @@ +#include "ballu.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ballu { + +static const char *const TAG = "ballu.climate"; + +const uint16_t BALLU_HEADER_MARK = 9000; +const uint16_t BALLU_HEADER_SPACE = 4500; +const uint16_t BALLU_BIT_MARK = 575; +const uint16_t BALLU_ONE_SPACE = 1675; +const uint16_t BALLU_ZERO_SPACE = 550; + +const uint32_t BALLU_CARRIER_FREQUENCY = 38000; + +const uint8_t BALLU_STATE_LENGTH = 13; + +const uint8_t BALLU_AUTO = 0; +const uint8_t BALLU_COOL = 0x20; +const uint8_t BALLU_DRY = 0x40; +const uint8_t BALLU_HEAT = 0x80; +const uint8_t BALLU_FAN = 0xc0; + +const uint8_t BALLU_FAN_AUTO = 0xa0; +const uint8_t BALLU_FAN_HIGH = 0x20; +const uint8_t BALLU_FAN_MED = 0x40; +const uint8_t BALLU_FAN_LOW = 0x60; + +const uint8_t BALLU_SWING_VER = 0x07; +const uint8_t BALLU_SWING_HOR = 0xe0; +const uint8_t BALLU_POWER = 0x20; + +void BalluClimate::transmit_state() { + uint8_t remote_state[BALLU_STATE_LENGTH] = {0}; + + auto temp = (uint8_t) roundf(clamp(this->target_temperature, YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX)); + auto swing_ver = + ((this->swing_mode == climate::CLIMATE_SWING_VERTICAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)); + auto swing_hor = + ((this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (this->swing_mode == climate::CLIMATE_SWING_BOTH)); + + remote_state[0] = 0xc3; + remote_state[1] = ((temp - 8) << 3) | (swing_ver ? 0 : BALLU_SWING_VER); + remote_state[2] = swing_hor ? 0 : BALLU_SWING_HOR; + remote_state[9] = (this->mode == climate::CLIMATE_MODE_OFF) ? 0 : BALLU_POWER; + remote_state[11] = 0x1e; + + // Fan speed + switch (this->fan_mode.value()) { + case climate::CLIMATE_FAN_HIGH: + remote_state[4] |= BALLU_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[4] |= BALLU_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[4] |= BALLU_FAN_LOW; + break; + case climate::CLIMATE_FAN_AUTO: + remote_state[4] |= BALLU_FAN_AUTO; + break; + default: + break; + } + + // Mode + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + remote_state[6] |= BALLU_AUTO; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[6] |= BALLU_HEAT; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[6] |= BALLU_COOL; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[6] |= BALLU_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[6] |= BALLU_FAN; + break; + case climate::CLIMATE_MODE_OFF: + remote_state[6] |= BALLU_AUTO; + default: + break; + } + + // Checksum + for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++) + remote_state[12] += remote_state[i]; + + ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]); + + // Send code + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(BALLU_HEADER_MARK); + data->space(BALLU_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(BALLU_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? BALLU_ONE_SPACE : BALLU_ZERO_SPACE); + } + } + // Footer + data->mark(BALLU_BIT_MARK); + + transmit.perform(); +} + +bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(BALLU_HEADER_MARK, BALLU_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[BALLU_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < BALLU_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(BALLU_BIT_MARK, BALLU_ONE_SPACE)) + remote_state[i] |= 1 << j; + + else if (!data.expect_item(BALLU_BIT_MARK, BALLU_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + + ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]); + } + // Validate footer + if (!data.expect_mark(BALLU_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // Calculate checksum and compare with signal value. + for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++) + checksum += remote_state[i]; + + if (checksum != remote_state[BALLU_STATE_LENGTH - 1]) { + ESP_LOGVV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]); + + // verify header remote code + if (remote_state[0] != 0xc3) + return false; + + // powr on/off button + ESP_LOGV(TAG, "Power: %02X", (remote_state[9] & BALLU_POWER)); + + if ((remote_state[9] & BALLU_POWER) != BALLU_POWER) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0xe0; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case BALLU_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case BALLU_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case BALLU_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case BALLU_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case BALLU_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + + // Set received temp + int temp = remote_state[1] & 0xf8; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + temp = ((uint8_t) temp >> 3) + 8; + ESP_LOGVV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = remote_state[4] & 0xe0; + ESP_LOGVV(TAG, "Fan: %02X", fan); + switch (fan) { + case BALLU_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case BALLU_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case BALLU_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case BALLU_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + ESP_LOGVV(TAG, "Swing status: %02X %02X", remote_state[1] & BALLU_SWING_VER, remote_state[2] & BALLU_SWING_HOR); + if (((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) && + ((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR)) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else if ((remote_state[1] & BALLU_SWING_VER) != BALLU_SWING_VER) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if ((remote_state[2] & BALLU_SWING_HOR) != BALLU_SWING_HOR) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + + this->publish_state(); + return true; +} + +} // namespace ballu +} // namespace esphome diff --git a/esphome/components/ballu/ballu.h b/esphome/components/ballu/ballu.h new file mode 100644 index 0000000000..80a4699cfb --- /dev/null +++ b/esphome/components/ballu/ballu.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace ballu { + +// Support for Ballu air conditioners with YKR-K/002E remote + +// Temperature +const float YKR_K_002E_TEMP_MIN = 16.0; +const float YKR_K_002E_TEMP_MAX = 32.0; + +class BalluClimate : public climate_ir::ClimateIR { + public: + BalluClimate() + : climate_ir::ClimateIR(YKR_K_002E_TEMP_MIN, YKR_K_002E_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, climate::CLIMATE_SWING_BOTH}) {} + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; +}; + +} // namespace ballu +} // namespace esphome diff --git a/esphome/components/ballu/climate.py b/esphome/components/ballu/climate.py new file mode 100644 index 0000000000..82e9fead1e --- /dev/null +++ b/esphome/components/ballu/climate.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ["climate_ir"] +CODEOWNERS = ["@bazuchan"] + +ballu_ns = cg.esphome_ns.namespace("ballu") +BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BalluClimate), + } +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await climate_ir.register_climate_ir(var, config) From d604321f37d33645fb7801411c6b1987d701da64 Mon Sep 17 00:00:00 2001 From: Frederik Gladhorn Date: Wed, 30 Jun 2021 20:36:48 +0200 Subject: [PATCH 532/643] Simplify initializing glyph_data (#1970) Make it easier to read the initialization with zeros, no loop required. --- esphome/components/font/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 11bbedd80b..16a7d8a612 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -110,7 +110,7 @@ async def to_code(config): _, (offset_x, offset_y) = font.font.getsize(glyph) width, height = mask.size width8 = ((width + 7) // 8) * 8 - glyph_data = [0 for _ in range(height * width8 // 8)] # noqa: F812 + glyph_data = [0] * (height * width8 // 8) for y in range(height): for x in range(width): if not mask.getpixel((x, y)): From 36861595f1aafb920adb05b5df2a8e8a1cc7ff78 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Thu, 1 Jul 2021 16:36:01 +0300 Subject: [PATCH 533/643] Add device_class support for MQTT integration (#1832) --- esphome/components/mqtt/mqtt_sensor.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 07c7fdc00d..c106a95902 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -46,6 +46,9 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } std::string MQTTSensorComponent::friendly_name() const { return this->sensor_->get_name(); } void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->sensor_->get_device_class().empty()) + root["device_class"] = this->sensor_->get_device_class(); + if (!this->sensor_->get_unit_of_measurement().empty()) root["unit_of_measurement"] = this->sensor_->get_unit_of_measurement(); From d5278351da993ad24f9e7523a0d69f75457ea0dd Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 2 Jul 2021 15:42:36 +0200 Subject: [PATCH 534/643] Rename master branch to release (#1976) --- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- .pre-commit-config.yaml | 2 +- README.md | 2 +- esphome/core/config.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index b5f8b7b0e0..91ec88aeb3 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -3,7 +3,7 @@ name: CI for docker images # Only run when docker paths change on: push: - branches: [dev, beta, master] + branches: [dev, beta, release] paths: - 'docker/**' - '.github/workflows/**' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a27cbe67b0..121b3f1339 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,7 @@ on: push: # On dev branch release-dev already performs CI checks # On other branches the `pull_request` trigger will be used - branches: [beta, master] + branches: [beta, release] pull_request: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9523a2164c..383f2878e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -307,4 +307,4 @@ jobs: -X POST \ -H "Accept: application/vnd.github.v3+json" \ https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ - -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}" + -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e56aa17cf..a821c21fa7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,5 +23,5 @@ repos: - id: no-commit-to-branch args: - --branch=dev - - --branch=master + - --branch=release - --branch=beta diff --git a/README.md b/README.md index f21e748d40..bb6fb37d3a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ESPHome [![Build Status](https://travis-ci.org/esphome/esphome.svg?branch=master)](https://travis-ci.org/esphome/esphome) [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) +# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![ESPHome Logo](https://esphome.io/_images/logo-text.png)](https://esphome.io/) diff --git a/esphome/core/config.py b/esphome/core/config.py index fd4b7088cc..f55acc1892 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -77,7 +77,7 @@ PLATFORMIO_ESP8266_LUT = { # recommended version as otherwise a bunch of devices could be bricked # * The docker images need to be updated to ship the new recommended version, in order not # to DDoS platformio servers. - # Update this file: https://github.com/esphome/esphome-docker-base/blob/master/platformio.ini + # Update this file: https://github.com/esphome/esphome-docker-base/blob/main/platformio.ini "RECOMMENDED": ARDUINO_VERSION_ESP8266["2.7.4"], "LATEST": "espressif8266", "DEV": ARDUINO_VERSION_ESP8266["dev"], From 9e400a785700285e837d50c9072b33a80507098a Mon Sep 17 00:00:00 2001 From: Trevor North Date: Sun, 4 Jul 2021 12:47:22 +0100 Subject: [PATCH 535/643] Fix tuya fan speed send (#1978) --- esphome/components/tuya/fan/tuya_fan.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 56efcf2d77..ed92713e52 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -80,7 +80,7 @@ void TuyaFan::write_state() { } if (this->speed_id_.has_value()) { ESP_LOGV(TAG, "Setting speed: %d", this->fan_->speed); - this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed); + this->parent_->set_datapoint_value(*this->speed_id_, this->fan_->speed - 1); } } From 86ac7f3a59b0eca2fa70db6894ad7e38976a7d6b Mon Sep 17 00:00:00 2001 From: Paul Doidge Date: Sun, 4 Jul 2021 21:39:18 +0200 Subject: [PATCH 536/643] Time Based Cover: Fixed apparent race condition on ESP32 chips (#1984) --- esphome/components/time_based/time_based_cover.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 066004f014..60a33aa82a 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -115,13 +115,13 @@ void TimeBasedCover::start_direction_(CoverOperation dir) { this->current_operation = dir; - this->stop_prev_trigger_(); - trig->trigger(); - this->prev_command_trigger_ = trig; - const uint32_t now = millis(); this->start_dir_time_ = now; this->last_recompute_time_ = now; + + this->stop_prev_trigger_(); + trig->trigger(); + this->prev_command_trigger_ = trig; } void TimeBasedCover::recompute_position_() { if (this->current_operation == COVER_OPERATION_IDLE) From 4c4099966ad39d8ffe21e97bc2d24700aa8c8ffd Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 5 Jul 2021 00:04:18 +0200 Subject: [PATCH 537/643] Fix invalid escape sequences in regex (#1814) --- esphome/components/logger/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 2b571b817b..8d79c96f63 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -205,8 +205,7 @@ def maybe_simple_message(schema): def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python - # pylint: disable=anomalous-backslash-in-string - cfmt = """\ + cfmt = r""" ( # start of capture group 1 % # literal "%" (?:[-+0 #]{0,5}) # optional flags From 8ca34f7098f6a54a0c01ead55ca403dee4d3a3be Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 5 Jul 2021 00:10:59 +0200 Subject: [PATCH 538/643] Bump hypothesis from 5.21.0 to 5.49.0 (#1753) --- requirements_test.txt | 2 +- tests/unit_tests/strategies.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 15593a8e12..9f7224a86d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,4 +10,4 @@ pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-asyncio==0.14.0 asyncmock==0.4.2 -hypothesis==5.21.0 +hypothesis==5.49.0 diff --git a/tests/unit_tests/strategies.py b/tests/unit_tests/strategies.py index 4bc0482f5f..30768f9d56 100644 --- a/tests/unit_tests/strategies.py +++ b/tests/unit_tests/strategies.py @@ -4,7 +4,7 @@ import hypothesis.strategies._internal.core as st from hypothesis.strategies._internal.strategies import SearchStrategy -@st.defines_strategy_with_reusable_values +@st.defines_strategy(force_reusable_values=True) def mac_addr_strings(): # type: () -> SearchStrategy[Text] """A strategy for MAC address strings. From 52d19fa43dd5e9ee7e09102aa726aa7bcfb56606 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jul 2021 10:27:13 +1200 Subject: [PATCH 539/643] Bump pytest-mock from 3.5.1 to 3.6.1 (#1754) Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.5.1 to 3.6.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.5.1...v3.6.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9f7224a86d..26755921cd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ pre-commit # Unit tests pytest==6.2.4 pytest-cov==2.11.1 -pytest-mock==3.5.1 +pytest-mock==3.6.1 pytest-asyncio==0.14.0 asyncmock==0.4.2 hypothesis==5.49.0 From d31040f5d8023a81a1011fe2b55e63992724d917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Panella?= Date: Sun, 4 Jul 2021 18:09:09 -0500 Subject: [PATCH 540/643] hlw8012: fix constants for BL0937 (#1973) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/hlw8012/hlw8012.cpp | 34 +++++++++++++++++--------- esphome/components/hlw8012/hlw8012.h | 12 +++++++++ esphome/components/hlw8012/sensor.py | 11 +++++++++ tests/test1.yaml | 1 + 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 356dbd0bf4..79c25a45b0 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -6,15 +6,32 @@ namespace hlw8012 { static const char *const TAG = "hlw8012"; +// valid for HLW8012 and CSE7759 static const uint32_t HLW8012_CLOCK_FREQUENCY = 3579000; -static const float HLW8012_REFERENCE_VOLTAGE = 2.43f; void HLW8012Component::setup() { + float reference_voltage = 0; ESP_LOGCONFIG(TAG, "Setting up HLW8012..."); this->sel_pin_->setup(); this->sel_pin_->digital_write(this->current_mode_); this->cf_store_.pulse_counter_setup(this->cf_pin_); this->cf1_store_.pulse_counter_setup(this->cf1_pin_); + + // Initialize multipliers + if (this->sensor_model_ == HLW8012_SENSOR_MODEL_BL0937) { + reference_voltage = 1.218f; + this->power_multiplier_ = + reference_voltage * reference_voltage * this->voltage_divider_ / this->current_resistor_ / 1721506.0f; + this->current_multiplier_ = reference_voltage / this->current_resistor_ / 94638.0f; + this->voltage_multiplier_ = reference_voltage * this->voltage_divider_ / 15397.0f; + } else { + // HLW8012 and CSE7759 have same reference specs + reference_voltage = 2.43f; + this->power_multiplier_ = reference_voltage * reference_voltage * this->voltage_divider_ / this->current_resistor_ * + 64.0f / 24.0f / HLW8012_CLOCK_FREQUENCY; + this->current_multiplier_ = reference_voltage / this->current_resistor_ * 512.0f / 24.0f / HLW8012_CLOCK_FREQUENCY; + this->voltage_multiplier_ = reference_voltage * this->voltage_divider_ * 256.0f / HLW8012_CLOCK_FREQUENCY; + } } void HLW8012Component::dump_config() { ESP_LOGCONFIG(TAG, "HLW8012:"); @@ -49,25 +66,18 @@ void HLW8012Component::update() { return; } - const float v_ref_squared = HLW8012_REFERENCE_VOLTAGE * HLW8012_REFERENCE_VOLTAGE; - const float power_multiplier_micros = - 64000000.0f * v_ref_squared * this->voltage_divider_ / this->current_resistor_ / 24.0f / HLW8012_CLOCK_FREQUENCY; - float power = cf_hz * power_multiplier_micros / 1000000.0f; + float power = cf_hz * this->power_multiplier_; if (this->change_mode_at_ != 0) { // Only read cf1 after one cycle. Apparently it's quite unstable after being changed. if (this->current_mode_) { - const float current_multiplier_micros = - 512000000.0f * HLW8012_REFERENCE_VOLTAGE / this->current_resistor_ / 24.0f / HLW8012_CLOCK_FREQUENCY; - float current = cf1_hz * current_multiplier_micros / 1000000.0f; + float current = cf1_hz * this->current_multiplier_; ESP_LOGD(TAG, "Got power=%.1fW, current=%.1fA", power, current); if (this->current_sensor_ != nullptr) { this->current_sensor_->publish_state(current); } } else { - const float voltage_multiplier_micros = - 256000000.0f * HLW8012_REFERENCE_VOLTAGE * this->voltage_divider_ / HLW8012_CLOCK_FREQUENCY; - float voltage = cf1_hz * voltage_multiplier_micros / 1000000.0f; + float voltage = cf1_hz * this->voltage_multiplier_; ESP_LOGD(TAG, "Got power=%.1fW, voltage=%.1fV", power, voltage); if (this->voltage_sensor_ != nullptr) { this->voltage_sensor_->publish_state(voltage); @@ -81,7 +91,7 @@ void HLW8012Component::update() { if (this->energy_sensor_ != nullptr) { cf_total_pulses_ += raw_cf; - float energy = cf_total_pulses_ * power_multiplier_micros / 3600 / 1000000.0f; + float energy = cf_total_pulses_ * this->power_multiplier_ / 3600; this->energy_sensor_->publish_state(energy); } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index af1f2e9a8c..52fb03c020 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -10,6 +10,12 @@ namespace hlw8012 { enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE }; +enum HLW8012SensorModels { + HLW8012_SENSOR_MODEL_HLW8012 = 0, + HLW8012_SENSOR_MODEL_CSE7759, + HLW8012_SENSOR_MODEL_BL0937 +}; + class HLW8012Component : public PollingComponent { public: void setup() override; @@ -20,6 +26,7 @@ class HLW8012Component : public PollingComponent { void set_initial_mode(HLW8012InitialMode initial_mode) { current_mode_ = initial_mode == HLW8012_INITIAL_MODE_CURRENT; } + void set_sensor_model(HLW8012SensorModels sensor_model) { sensor_model_ = sensor_model; } void set_change_mode_every(uint32_t change_mode_every) { change_mode_every_ = change_mode_every; } void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; } void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; } @@ -38,6 +45,7 @@ class HLW8012Component : public PollingComponent { uint32_t change_mode_every_{8}; float current_resistor_{0.001}; float voltage_divider_{2351}; + HLW8012SensorModels sensor_model_{HLW8012_SENSOR_MODEL_HLW8012}; uint64_t cf_total_pulses_{0}; GPIOPin *sel_pin_; GPIOPin *cf_pin_; @@ -48,6 +56,10 @@ class HLW8012Component : public PollingComponent { sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *energy_sensor_{nullptr}; + + float voltage_multiplier_{0.0f}; + float current_multiplier_{0.0f}; + float power_multiplier_{0.0f}; }; } // namespace hlw8012 diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 6454a9fcc9..e24e995eba 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_POWER, CONF_ENERGY, CONF_SEL_PIN, + CONF_MODEL, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, DEVICE_CLASS_CURRENT, @@ -31,11 +32,19 @@ AUTO_LOAD = ["pulse_counter"] hlw8012_ns = cg.esphome_ns.namespace("hlw8012") HLW8012Component = hlw8012_ns.class_("HLW8012Component", cg.PollingComponent) HLW8012InitialMode = hlw8012_ns.enum("HLW8012InitialMode") +HLW8012SensorModels = hlw8012_ns.enum("HLW8012SensorModels") + INITIAL_MODES = { CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT, CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE, } +MODELS = { + "HLW8012": HLW8012SensorModels.HLW8012_SENSOR_MODEL_HLW8012, + "CSE7759": HLW8012SensorModels.HLW8012_SENSOR_MODEL_CSE7759, + "BL0937": HLW8012SensorModels.HLW8012_SENSOR_MODEL_BL0937, +} + CONF_CF1_PIN = "cf1_pin" CONF_CF_PIN = "cf_pin" CONFIG_SCHEMA = cv.Schema( @@ -62,6 +71,7 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, + cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True), cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All( cv.uint32_t, cv.Range(min=1) ), @@ -99,3 +109,4 @@ async def to_code(config): cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) + cg.add(var.set_sensor_model(config[CONF_MODEL])) diff --git a/tests/test1.yaml b/tests/test1.yaml index 08e1d63534..78dc40cf8e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -514,6 +514,7 @@ sensor: voltage_divider: 2351 change_mode_every: 16 initial_mode: VOLTAGE + model: hlw8012 - platform: total_daily_energy power_id: hlw8012_power name: 'HLW8012 Total Daily Energy' From ab31117bf329c388cd4e641c0e10ff2b8a63d291 Mon Sep 17 00:00:00 2001 From: buxtronix Date: Mon, 5 Jul 2021 09:59:12 +1000 Subject: [PATCH 541/643] Anova ble component (#1752) Co-authored-by: Ben Buxton Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/anova/__init__.py | 0 esphome/components/anova/anova.cpp | 140 ++++++++++++++++++++++++ esphome/components/anova/anova.h | 50 +++++++++ esphome/components/anova/anova_base.cpp | 119 ++++++++++++++++++++ esphome/components/anova/anova_base.h | 79 +++++++++++++ esphome/components/anova/climate.py | 25 +++++ tests/test1.yaml | 20 ++++ 8 files changed, 434 insertions(+) create mode 100644 esphome/components/anova/__init__.py create mode 100644 esphome/components/anova/anova.cpp create mode 100644 esphome/components/anova/anova.h create mode 100644 esphome/components/anova/anova_base.cpp create mode 100644 esphome/components/anova/anova_base.h create mode 100644 esphome/components/anova/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index f242cc6d9d..a1c7bf9bfd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -15,6 +15,7 @@ esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/addressable_light/* @justfalter esphome/components/animation/* @syndlex +esphome/components/anova/* @buxtronix esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl diff --git a/esphome/components/anova/__init__.py b/esphome/components/anova/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp new file mode 100644 index 0000000000..c4b08ca6b5 --- /dev/null +++ b/esphome/components/anova/anova.cpp @@ -0,0 +1,140 @@ +#include "anova.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace anova { + +static const char *TAG = "anova"; + +using namespace esphome::climate; + +void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); } + +void Anova::setup() { + this->codec_ = new AnovaCodec(); + this->current_request_ = 0; +} + +void Anova::loop() {} + +void Anova::control(const ClimateCall &call) { + if (call.get_mode().has_value()) { + ClimateMode mode = *call.get_mode(); + AnovaPacket *pkt; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + pkt = this->codec_->get_stop_request(); + break; + case climate::CLIMATE_MODE_HEAT: + pkt = this->codec_->get_start_request(); + break; + default: + ESP_LOGW(TAG, "Unsupported mode: %d", mode); + return; + } + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } + if (call.get_target_temperature().has_value()) { + auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + } +} + +void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_DISCONNECT_EVT: { + this->current_temperature = NAN; + this->target_temperature = NAN; + this->publish_state(); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); + if (chr == nullptr) { + ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + break; + } + this->char_handle_ = chr->handle; + + auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); + } + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + this->current_request_ = 0; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle != this->char_handle_) + break; + this->codec_->decode(param->notify.value, param->notify.value_len); + if (this->codec_->has_target_temp()) { + this->target_temperature = this->codec_->target_temp_; + } + if (this->codec_->has_current_temp()) { + this->current_temperature = this->codec_->current_temp_; + } + if (this->codec_->has_running()) { + this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; + } + this->publish_state(); + + if (this->current_request_ > 0) { + AnovaPacket *pkt = nullptr; + switch (this->current_request_++) { + case 1: + pkt = this->codec_->get_read_target_temp_request(); + break; + case 2: + pkt = this->codec_->get_read_current_temp_request(); + break; + default: + this->current_request_ = 0; + break; + } + if (pkt != nullptr) { + auto status = + esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, + pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), + status); + } + } + break; + } + default: + break; + } +} + +void Anova::update() { + if (this->node_state != espbt::ClientState::Established) + return; + + if (this->current_request_ == 0) { + auto pkt = this->codec_->get_read_device_status_request(); + auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, + pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); + this->current_request_++; + } +} + +} // namespace anova +} // namespace esphome + +#endif diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h new file mode 100644 index 0000000000..63d03cb329 --- /dev/null +++ b/esphome/components/anova/anova.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/climate/climate.h" +#include "anova_base.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +namespace esphome { +namespace anova { + +namespace espbt = esphome::esp32_ble_tracker; + +static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0; +static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1; + +class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + climate::ClimateTraits traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_heat_mode(true); + traits.set_visual_min_temperature(25.0); + traits.set_visual_max_temperature(100.0); + traits.set_visual_temperature_step(0.1); + return traits; + } + + protected: + AnovaCodec *codec_; + void control(const climate::ClimateCall &call) override; + uint16_t char_handle_; + uint8_t current_request_; +}; + +} // namespace anova +} // namespace esphome + +#endif diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp new file mode 100644 index 0000000000..8cbc481643 --- /dev/null +++ b/esphome/components/anova/anova_base.cpp @@ -0,0 +1,119 @@ +#include "anova_base.h" + +namespace esphome { +namespace anova { + +AnovaPacket *AnovaCodec::clean_packet_() { + this->packet_.length = strlen((char *) this->packet_.data); + this->packet_.data[this->packet_.length] = '\0'; + ESP_LOGV("anova", "SendPkt: %s\n", this->packet_.data); + return &this->packet_; +} + +AnovaPacket *AnovaCodec::get_read_device_status_request() { + this->current_query_ = READ_DEVICE_STATUS; + sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_target_temp_request() { + this->current_query_ = READ_TARGET_TEMPERATURE; + sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_current_temp_request() { + this->current_query_ = READ_CURRENT_TEMPERATURE; + sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_unit_request() { + this->current_query_ = READ_UNIT; + sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_read_data_request() { + this->current_query_ = READ_DATA; + sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) { + this->current_query_ = SET_TARGET_TEMPERATURE; + sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { + this->current_query_ = SET_UNIT; + sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_start_request() { + this->current_query_ = START; + sprintf((char *) this->packet_.data, CMD_START); + return this->clean_packet_(); +} + +AnovaPacket *AnovaCodec::get_stop_request() { + this->current_query_ = STOP; + sprintf((char *) this->packet_.data, CMD_STOP); + return this->clean_packet_(); +} + +void AnovaCodec::decode(const uint8_t *data, uint16_t length) { + memset(this->buf_, 0, 32); + strncpy(this->buf_, (char *) data, length); + ESP_LOGV("anova", "Received: %s\n", this->buf_); + this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false; + switch (this->current_query_) { + case READ_DEVICE_STATUS: { + if (!strncmp(this->buf_, "stopped", 7)) { + this->has_running_ = true; + this->running_ = false; + } + if (!strncmp(this->buf_, "running", 7)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case START: { + if (!strncmp(this->buf_, "start", 5)) { + this->has_running_ = true; + this->running_ = true; + } + break; + } + case STOP: { + if (!strncmp(this->buf_, "stop", 4)) { + this->has_running_ = true; + this->running_ = false; + } + break; + } + case READ_TARGET_TEMPERATURE: { + this->target_temp_ = strtof(this->buf_, nullptr); + this->has_target_temp_ = true; + break; + } + case SET_TARGET_TEMPERATURE: { + this->target_temp_ = strtof(this->buf_, nullptr); + this->has_target_temp_ = true; + break; + } + case READ_CURRENT_TEMPERATURE: { + this->current_temp_ = strtof(this->buf_, nullptr); + this->has_current_temp_ = true; + break; + } + default: + break; + } +} + +} // namespace anova +} // namespace esphome diff --git a/esphome/components/anova/anova_base.h b/esphome/components/anova/anova_base.h new file mode 100644 index 0000000000..e94fe619a6 --- /dev/null +++ b/esphome/components/anova/anova_base.h @@ -0,0 +1,79 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace anova { + +enum CurrentQuery { + NONE, + READ_DEVICE_STATUS, + READ_TARGET_TEMPERATURE, + READ_CURRENT_TEMPERATURE, + READ_DATA, + READ_UNIT, + SET_TARGET_TEMPERATURE, + SET_UNIT, + START, + STOP, +}; + +struct AnovaPacket { + uint16_t length; + uint8_t data[24]; +}; + +#define CMD_READ_DEVICE_STATUS "status\r" +#define CMD_READ_TARGET_TEMP "read set temp\r" +#define CMD_READ_CURRENT_TEMP "read temp\r" +#define CMD_READ_UNIT "read unit\r" +#define CMD_READ_DATA "read data\r" +#define CMD_SET_TARGET_TEMP "set temp %.1f\r" +#define CMD_SET_TEMP_UNIT "set unit %c\r" + +#define CMD_START "start\r" +#define CMD_STOP "stop\r" + +class AnovaCodec { + public: + AnovaPacket *get_read_device_status_request(); + AnovaPacket *get_read_target_temp_request(); + AnovaPacket *get_read_current_temp_request(); + AnovaPacket *get_read_data_request(); + AnovaPacket *get_read_unit_request(); + + AnovaPacket *get_set_target_temp_request(float temperature); + AnovaPacket *get_set_unit_request(char unit); + + AnovaPacket *get_start_request(); + AnovaPacket *get_stop_request(); + + void decode(const uint8_t *data, uint16_t length); + bool has_target_temp() { return this->has_target_temp_; } + bool has_current_temp() { return this->has_current_temp_; } + bool has_unit() { return this->has_unit_; } + bool has_running() { return this->has_running_; } + + union { + float target_temp_; + float current_temp_; + char unit_; + bool running_; + }; + + protected: + AnovaPacket *clean_packet_(); + AnovaPacket packet_; + + bool has_target_temp_; + bool has_current_temp_; + bool has_unit_; + bool has_running_; + char buf_[32]; + + CurrentQuery current_query_; +}; + +} // namespace anova +} // namespace esphome diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py new file mode 100644 index 0000000000..ab1c9045d8 --- /dev/null +++ b/esphome/components/anova/climate.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, ble_client +from esphome.const import CONF_ID + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["ble_client"] + +anova_ns = cg.esphome_ns.namespace("anova") +Anova = anova_ns.class_( + "Anova", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + climate.CLIMATE_SCHEMA.extend({cv.GenerateID(): cv.declare_id(Anova)}) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("60s")) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/tests/test1.yaml b/tests/test1.yaml index 78dc40cf8e..1f817f0dab 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1526,6 +1526,26 @@ climate: name: Toshiba Climate - platform: hitachi_ac344 name: Hitachi Climate + - platform: midea_ac + visual: + min_temperature: 18 °C + max_temperature: 25 °C + temperature_step: 0.1 °C + name: 'Electrolux EACS' + beeper: true + outdoor_temperature: + name: 'Temp' + power_usage: + name: 'Power' + humidity_setpoint: + name: 'Hum' + - platform: anova + name: Anova cooker + ble_client_id: ble_blah + +midea_dongle: + uart_id: uart0 + strength_icon: true switch: - platform: gpio From 79b9d0579d3a02c88afd57115400ec7fa000288f Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Mon, 5 Jul 2021 11:22:43 +1000 Subject: [PATCH 542/643] Add stepper.set_acceleration and stepper.set_deceleration to stepper component (#1977) --- esphome/components/stepper/__init__.py | 40 +++++++++++++++++++++++++- esphome/components/stepper/stepper.h | 30 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 41b1db7df2..54f6aa4205 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -21,6 +21,8 @@ Stepper = stepper_ns.class_("Stepper") SetTargetAction = stepper_ns.class_("SetTargetAction", automation.Action) ReportPositionAction = stepper_ns.class_("ReportPositionAction", automation.Action) SetSpeedAction = stepper_ns.class_("SetSpeedAction", automation.Action) +SetAccelerationAction = stepper_ns.class_("SetAccelerationAction", automation.Action) +SetDecelerationAction = stepper_ns.class_("SetDecelerationAction", automation.Action) def validate_acceleration(value): @@ -138,11 +140,47 @@ async def stepper_report_position_to_code(config, action_id, template_arg, args) async def stepper_set_speed_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = await cg.templatable(config[CONF_SPEED], args, cg.int32) + template_ = await cg.templatable(config[CONF_SPEED], args, cg.float_) cg.add(var.set_speed(template_)) return var +@automation.register_action( + "stepper.set_acceleration", + SetAccelerationAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_ACCELERATION): cv.templatable(validate_acceleration), + } + ), +) +async def stepper_set_acceleration_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_ACCELERATION], args, cg.float_) + cg.add(var.set_acceleration(template_)) + return var + + +@automation.register_action( + "stepper.set_deceleration", + SetDecelerationAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_DECELERATION): cv.templatable(validate_acceleration), + } + ), +) +async def stepper_set_deceleration_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_DECELERATION], args, cg.float_) + cg.add(var.set_deceleration(template_)) + return var + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(stepper_ns.using) diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 33777dce83..560362e4d0 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -77,5 +77,35 @@ template class SetSpeedAction : public Action { Stepper *parent_; }; +template class SetAccelerationAction : public Action { + public: + explicit SetAccelerationAction(Stepper *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, acceleration); + + void play(Ts... x) override { + float acceleration = this->acceleration_.value(x...); + this->parent_->set_acceleration(acceleration); + } + + protected: + Stepper *parent_; +}; + +template class SetDecelerationAction : public Action { + public: + explicit SetDecelerationAction(Stepper *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, deceleration); + + void play(Ts... x) override { + float deceleration = this->deceleration_.value(x...); + this->parent_->set_deceleration(deceleration); + } + + protected: + Stepper *parent_; +}; + } // namespace stepper } // namespace esphome From 062cedc200e300800a6dbb2fe43380fdf2b3181b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 6 Jul 2021 10:15:29 +1200 Subject: [PATCH 543/643] remote_receiver: use config parent receiver for registering dumpers (#1980) --- esphome/components/remote_base/__init__.py | 6 +++--- esphome/components/remote_receiver/__init__.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 9416c2220d..7c27c1b736 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -176,7 +176,9 @@ validate_binary_sensor = cv.validate_registry_entry( TRIGGER_REGISTRY = SimpleRegistry() DUMPER_REGISTRY = Registry( { - cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), + cv.Optional(CONF_RECEIVER_ID): cv.invalid( + "This has been removed in ESPHome 1.20.0 and the dumper attaches directly to the parent receiver." + ), } ) @@ -228,8 +230,6 @@ async def build_dumpers(config): dumpers = [] for conf in config: dumper = await cg.build_registry_entry(DUMPER_REGISTRY, conf) - receiver = await cg.get_variable(conf[CONF_RECEIVER_ID]) - cg.add(receiver.register_dumper(dumper)) dumpers.append(dumper) return dumpers diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index fed52803cb..7158368ed8 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -54,7 +54,10 @@ async def to_code(config): else: var = cg.new_Pvariable(config[CONF_ID], pin) - await remote_base.build_dumpers(config[CONF_DUMP]) + dumpers = await remote_base.build_dumpers(config[CONF_DUMP]) + for dumper in dumpers: + cg.add(var.register_dumper(dumper)) + await remote_base.build_triggers(config) await cg.register_component(var, config) From fd4b7d4588b38e0ff9d4e16d48545f5719186e7d Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 6 Jul 2021 00:17:36 +0200 Subject: [PATCH 544/643] Don't try compat parsing for "esphome version" (#1966) --- esphome/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 232652db9f..a7a3836b69 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -516,7 +516,7 @@ def parse_args(argv): deprecated_argv_suggestion = None - if ["dashboard", "config"] == argv[1:3]: + if ["dashboard", "config"] == argv[1:3] or ["version"] == argv[1:2]: # this is most likely meant in new-style arg format. do not try compat parsing pass else: From f9797825ad03d2e692290ef4f337a58cad47fd62 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 8 Jul 2021 11:37:47 +0200 Subject: [PATCH 545/643] Change color model to fix white channel issues (#1895) --- esphome/components/api/api.proto | 3 + esphome/components/api/api_connection.cpp | 3 + esphome/components/api/api_pb2.cpp | 29 +++++ esphome/components/api/api_pb2.h | 3 + .../components/light/addressable_light.cpp | 8 +- esphome/components/light/automation.h | 12 +- esphome/components/light/automation.py | 5 + esphome/components/light/effects.py | 4 + .../components/light/esp_color_correction.h | 7 +- esphome/components/light/light_call.cpp | 91 +++++++++++---- esphome/components/light/light_call.h | 7 ++ esphome/components/light/light_color_values.h | 110 ++++++++++-------- esphome/const.py | 1 + 13 files changed, 200 insertions(+), 83 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index a5bd9aec6d..8034c980a4 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -378,6 +378,7 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + float color_brightness = 10; float red = 4; float green = 5; float blue = 6; @@ -396,6 +397,8 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_brightness = 20; + float color_brightness = 21; bool has_rgb = 6; float red = 7; float green = 8; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 00a8539112..c36d36a159 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -308,6 +308,7 @@ bool APIConnection::send_light_state(light::LightState *light) { if (traits.get_supports_brightness()) resp.brightness = values.get_brightness(); if (traits.get_supports_rgb()) { + resp.color_brightness = values.get_color_brightness(); resp.red = values.get_red(); resp.green = values.get_green(); resp.blue = values.get_blue(); @@ -352,6 +353,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_state(msg.state); if (msg.has_brightness) call.set_brightness(msg.brightness); + if (msg.has_color_brightness) + call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { call.set_red(msg.red); call.set_green(msg.green); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index e53ac2019a..83ebdd8b68 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1263,6 +1263,10 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 10: { + this->color_brightness = value.as_float(); + return true; + } case 4: { this->red = value.as_float(); return true; @@ -1291,6 +1295,7 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); + buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); buffer.encode_float(6, this->blue); @@ -1315,6 +1320,11 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" red: "); sprintf(buffer, "%g", this->red); out.append(buffer); @@ -1359,6 +1369,10 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 20: { + this->has_color_brightness = value.as_bool(); + return true; + } case 6: { this->has_rgb = value.as_bool(); return true; @@ -1415,6 +1429,10 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->brightness = value.as_float(); return true; } + case 21: { + this->color_brightness = value.as_float(); + return true; + } case 7: { this->red = value.as_float(); return true; @@ -1445,6 +1463,8 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_brightness); buffer.encode_float(5, this->brightness); + buffer.encode_bool(20, this->has_color_brightness); + buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); buffer.encode_float(7, this->red); buffer.encode_float(8, this->green); @@ -1485,6 +1505,15 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_brightness: "); + out.append(YESNO(this->has_color_brightness)); + out.append("\n"); + + out.append(" color_brightness: "); + sprintf(buffer, "%g", this->color_brightness); + out.append(buffer); + out.append("\n"); + out.append(" has_rgb: "); out.append(YESNO(this->has_rgb)); out.append("\n"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 956cecdeb9..6873e0d54c 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -371,6 +371,7 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; @@ -392,6 +393,8 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_brightness{false}; + float color_brightness{0.0f}; bool has_rgb{false}; float red{0.0f}; float green{0.0f}; diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index e3ab9596d9..ea24736c63 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -25,10 +25,10 @@ void AddressableLight::call_setup() { } Color esp_color_from_light_color_values(LightColorValues val) { - auto r = static_cast(roundf(val.get_red() * 255.0f)); - auto g = static_cast(roundf(val.get_green() * 255.0f)); - auto b = static_cast(roundf(val.get_blue() * 255.0f)); - auto w = static_cast(roundf(val.get_white() * val.get_state() * 255.0f)); + auto r = static_cast(roundf(val.get_color_brightness() * val.get_red() * 255.0f)); + auto g = static_cast(roundf(val.get_color_brightness() * val.get_green() * 255.0f)); + auto b = static_cast(roundf(val.get_color_brightness() * val.get_blue() * 255.0f)); + auto w = static_cast(roundf(val.get_white() * 255.0f)); return Color(r, g, b, w); } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index d1fb2a0bcb..e99d5c58bd 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -31,6 +31,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) TEMPLATABLE_VALUE(float, brightness) + TEMPLATABLE_VALUE(float, color_brightness) TEMPLATABLE_VALUE(float, red) TEMPLATABLE_VALUE(float, green) TEMPLATABLE_VALUE(float, blue) @@ -42,6 +43,7 @@ template class LightControlAction : public Action { auto call = this->parent_->make_call(); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); + call.set_color_brightness(this->color_brightness_.optional_value(x...)); call.set_red(this->red_.optional_value(x...)); call.set_green(this->green_.optional_value(x...)); call.set_blue(this->blue_.optional_value(x...)); @@ -139,6 +141,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(int32_t, range_from) TEMPLATABLE_VALUE(int32_t, range_to) + TEMPLATABLE_VALUE(uint8_t, color_brightness) TEMPLATABLE_VALUE(uint8_t, red) TEMPLATABLE_VALUE(uint8_t, green) TEMPLATABLE_VALUE(uint8_t, blue) @@ -148,13 +151,16 @@ template class AddressableSet : public Action { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = this->range_from_.value_or(x..., 0); int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; + uint8_t remote_color_brightness = + static_cast(roundf(this->parent_->remote_values.get_color_brightness() * 255.0f)); + uint8_t color_brightness = this->color_brightness_.value_or(x..., remote_color_brightness); auto range = out->range(range_from, range_to); if (this->red_.has_value()) - range.set_red(this->red_.value(x...)); + range.set_red(esp_scale8(this->red_.value(x...), color_brightness)); if (this->green_.has_value()) - range.set_green(this->green_.value(x...)); + range.set_green(esp_scale8(this->green_.value(x...), color_brightness)); if (this->blue_.has_value()) - range.set_blue(this->blue_.value(x...)); + range.set_blue(esp_scale8(this->blue_.value(x...), color_brightness)); if (this->white_.has_value()) range.set_white(this->white_.value(x...)); out->schedule_show(); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 3fb3126f14..dd1148131a 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_FLASH_LENGTH, CONF_EFFECT, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -63,6 +64,7 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( ), cv.Exclusive(CONF_EFFECT, "transformer"): cv.templatable(cv.string), cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), @@ -114,6 +116,9 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_BRIGHTNESS in config: template_ = await cg.templatable(config[CONF_BRIGHTNESS], args, float) cg.add(var.set_brightness(template_)) + if CONF_COLOR_BRIGHTNESS in config: + template_ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, float) + cg.add(var.set_color_brightness(template_)) if CONF_RED in config: template_ = await cg.templatable(config[CONF_RED], args, float) cg.add(var.set_red(template_)) diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c213de0ae6..f6ce812c34 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -211,6 +212,7 @@ async def random_effect_to_code(config, effect_id): { cv.Optional(CONF_STATE, default=True): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, cv.Optional(CONF_BLUE, default=1.0): cv.percentage, @@ -223,6 +225,7 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, CONF_BRIGHTNESS, + CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, @@ -245,6 +248,7 @@ async def strobe_effect_to_code(config, effect_id): LightColorValues( color[CONF_STATE], color[CONF_BRIGHTNESS], + color[CONF_COLOR_BRIGHTNESS], color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index 32fee8c8ea..8788246cfc 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -29,8 +29,7 @@ class ESPColorCorrection { return this->gamma_table_[res]; } inline uint8_t color_correct_white(uint8_t white) const ALWAYS_INLINE { - // do not scale white value with brightness - uint8_t res = esp_scale8(white, this->max_brightness_.white); + uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); return this->gamma_table_[res]; } inline Color color_uncorrect(Color color) const ALWAYS_INLINE { @@ -60,10 +59,10 @@ class ESPColorCorrection { return res; } inline uint8_t color_uncorrect_white(uint8_t white) const ALWAYS_INLINE { - if (this->max_brightness_.white == 0) + if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; - uint8_t res = uncorrected / this->max_brightness_.white; + uint8_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; return res; } diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 3fbe921aa1..557d001321 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -43,6 +43,10 @@ LightCall &LightCall::parse_color_json(JsonObject &root) { } } + if (root.containsKey("color_brightness")) { + this->set_color_brightness(float(root["color_brightness"]) / 255.0f); + } + if (root.containsKey("white_value")) { this->set_white(float(root["white_value"]) / 255.0f); } @@ -182,6 +186,12 @@ LightColorValues LightCall::validate_() { this->transition_length_.reset(); } + // Color brightness exists check + if (this->color_brightness_.has_value() && !traits.get_supports_rgb()) { + ESP_LOGW(TAG, "'%s' - This light does not support setting RGB brightness!", name); + this->color_brightness_.reset(); + } + // RGB exists check if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { if (!traits.get_supports_rgb()) { @@ -204,34 +214,48 @@ LightColorValues LightCall::validate_() { this->color_temperature_.reset(); } - // If white channel is specified, set RGB to white color (when interlock is enabled) - if (this->white_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - } - // make white values binary aka 0.0f or 1.0f... this allows brightness to do its job - if (*this->white_ > 0.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } - } - } - // If only a color channel is specified, set white channel to 100% for white, otherwise 0% (when interlock is enabled) - else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (traits.get_supports_color_interlock()) { - if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - this->white_ = optional(1.0f); - } else { - this->white_ = optional(0.0f); - } + // Set color brightness to 100% if currently zero and a color is set. This is both for compatibility with older + // clients that don't know about color brightness, and it's intuitive UX anyway: if I set a color, it should show up. + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) + this->color_brightness_ = optional(1.0f); + } + + // Handle interaction between RGB and white for color interlock + if (traits.get_supports_color_interlock()) { + // Find out which channel (white or color) the user wanted to enable + bool output_white = this->white_.has_value() && *this->white_ > 0.0f; + bool output_color = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + this->red_.has_value() || this->green_.has_value() || this->blue_.has_value(); + + // Interpret setting the color to white as setting the white channel. + if (output_color && *this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { + output_white = true; + output_color = false; + + if (!this->white_.has_value()) + this->white_ = optional(this->color_brightness_.value_or(1.0f)); + } + + // Ensure either the white value or the color brightness is always zero. + if (output_white && output_color) { + ESP_LOGW(TAG, "'%s' - Cannot enable color and white channel simultaneously with interlock!", name); + // For compatibility with historic behaviour, prefer white channel in this case. + this->color_brightness_ = optional(0.0f); + } else if (output_white) { + this->color_brightness_ = optional(0.0f); + } else if (output_color) { + this->white_ = optional(0.0f); } } + // If only a color temperature is specified, change to white light - else if (this->color_temperature_.has_value()) { + if (this->color_temperature_.has_value() && !this->white_.has_value() && !this->red_.has_value() && + !this->green_.has_value() && !this->blue_.has_value()) { + // Disable color LEDs explicitly if not already set + if (traits.get_supports_rgb() && !this->color_brightness_.has_value()) + this->color_brightness_ = optional(0.0f); + this->red_ = optional(1.0f); this->green_ = optional(1.0f); this->blue_ = optional(1.0f); @@ -256,6 +280,7 @@ LightColorValues LightCall::validate_() { // Range checks VALIDATE_RANGE(brightness, "Brightness") + VALIDATE_RANGE(color_brightness, "Color brightness") VALIDATE_RANGE(red, "Red") VALIDATE_RANGE(green, "Green") VALIDATE_RANGE(blue, "Blue") @@ -267,6 +292,8 @@ LightColorValues LightCall::validate_() { if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); + if (this->color_brightness_.has_value()) + v.set_color_brightness(*this->color_brightness_); if (this->red_.has_value()) v.set_red(*this->red_); if (this->green_.has_value()) @@ -371,6 +398,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { LightCall &LightCall::from_light_color_values(const LightColorValues &values) { this->set_state(values.is_on()); this->set_brightness_if_supported(values.get_brightness()); + this->set_color_brightness_if_supported(values.get_color_brightness()); this->set_red_if_supported(values.get_red()); this->set_green_if_supported(values.get_green()); this->set_blue_if_supported(values.get_blue()); @@ -388,6 +416,11 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) { this->set_brightness(brightness); return *this; } +LightCall &LightCall::set_color_brightness_if_supported(float brightness) { + if (this->parent_->get_traits().get_supports_rgb_white_value()) + this->set_brightness(brightness); + return *this; +} LightCall &LightCall::set_red_if_supported(float red) { if (this->parent_->get_traits().get_supports_rgb()) this->set_red(red); @@ -445,6 +478,14 @@ LightCall &LightCall::set_brightness(float brightness) { this->brightness_ = brightness; return *this; } +LightCall &LightCall::set_color_brightness(optional brightness) { + this->color_brightness_ = brightness; + return *this; +} +LightCall &LightCall::set_color_brightness(float brightness) { + this->color_brightness_ = brightness; + return *this; +} LightCall &LightCall::set_red(optional red) { this->red_ = red; return *this; diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index becea50004..c63b63bc54 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -44,6 +44,12 @@ class LightCall { LightCall &set_brightness(float brightness); /// Set the brightness property if the light supports brightness. LightCall &set_brightness_if_supported(float brightness); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(optional brightness); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) + LightCall &set_color_brightness(float brightness); + /// Set the color brightness property if the light supports RGBW. + LightCall &set_color_brightness_if_supported(float brightness); /** Set the red RGB value of the light from 0.0 to 1.0. * * Note that this only controls the color of the light, not its brightness. @@ -146,6 +152,7 @@ class LightCall { optional transition_length_; optional flash_length_; optional brightness_; + optional color_brightness_; optional red_; optional green_; optional blue_; diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index cdd05ae7b7..4b6ca9e576 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -13,17 +13,24 @@ namespace light { /** This class represents the color state for a light object. * - * All values in this class are represented using floats in the range from 0.0 (off) to 1.0 (on). - * Not all values have to be populated though, for example a simple monochromatic light only needs - * to access the state and brightness attributes. + * All values in this class (except color temperature) are represented using floats in the range + * from 0.0 (off) to 1.0 (on). Please note that all values are automatically clamped to this range. * - * Please note all float values are automatically clamped. + * This class has the following properties: + * - state: Whether the light should be on/off. Represented as a float for transitions. Used for + * all lights. + * - brightness: The master brightness of the light, applied to all channels. Used for all lights + * with brightness control. + * - color_brightness: The brightness of the color channels of the light. Used for RGB, RGBW and + * RGBWW lights. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of + * them is always 1.0. + * - white: The brightness of the white channel of the light. Used for RGBW and RGBWW lights. + * - color_temperature: The color temperature of the white channel in mireds. Used for RGBWW and + * CWWW lights. * - * state - Whether the light should be on/off. Represented as a float for transitions. - * brightness - The brightness of the light. - * red, green, blue - RGB values. - * white - The white value for RGBW lights. - * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) + * For lights with a color interlock (RGB lights and white light cannot be on at the same time), a + * valid state has always either color_brightness or white (or both) set to zero. */ class LightColorValues { public: @@ -31,16 +38,18 @@ class LightColorValues { LightColorValues() : state_(0.0f), brightness_(1.0f), + color_brightness_(1.0f), red_(1.0f), green_(1.0f), blue_(1.0f), white_(1.0f), color_temperature_{1.0f} {} - LightColorValues(float state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) { + LightColorValues(float state, float brightness, float color_brightness, float red, float green, float blue, + float white, float color_temperature = 1.0f) { this->set_state(state); this->set_brightness(brightness); + this->set_color_brightness(color_brightness); this->set_red(red); this->set_green(green); this->set_blue(blue); @@ -48,38 +57,46 @@ class LightColorValues { this->set_color_temperature(color_temperature); } - LightColorValues(bool state, float brightness, float red, float green, float blue, float white, - float color_temperature = 1.0f) - : LightColorValues(state ? 1.0f : 0.0f, brightness, red, green, blue, white, color_temperature) {} + LightColorValues(bool state, float brightness, float color_brightness, float red, float green, float blue, + float white, float color_temperature = 1.0f) + : LightColorValues(state ? 1.0f : 0.0f, brightness, color_brightness, red, green, blue, white, + color_temperature) {} /// Create light color values from a binary true/false state. - static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } + static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } /// Create light color values from a monochromatic brightness state. static LightColorValues from_monochromatic(float brightness) { if (brightness == 0.0f) - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; else - return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f}; + return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } /// Create light color values from an RGB state. static LightColorValues from_rgb(float r, float g, float b) { float brightness = std::max(r, std::max(g, b)); if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, 1.0f}; + return {1.0f, brightness, 1.0f, r / brightness, g / brightness, b / brightness, 1.0f}; } } /// Create light color values from an RGBW state. static LightColorValues from_rgbw(float r, float g, float b, float w) { - float brightness = std::max(r, std::max(g, std::max(b, w))); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + float color_brightness = std::max(r, std::max(g, b)); + float master_brightness = std::max(color_brightness, w); + if (master_brightness == 0.0f) { + return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } else { - return {1.0f, brightness, r / brightness, g / brightness, b / brightness, w / brightness}; + return {1.0f, + master_brightness, + color_brightness / master_brightness, + r / color_brightness, + g / color_brightness, + b / color_brightness, + w / master_brightness}; } } @@ -97,6 +114,7 @@ class LightColorValues { LightColorValues v; v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); + v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); v.set_red(esphome::lerp(completion, start.get_red(), end.get_red())); v.set_green(esphome::lerp(completion, start.get_green(), end.get_green())); v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); @@ -121,8 +139,10 @@ class LightColorValues { color["g"] = uint8_t(this->get_green() * 255); color["b"] = uint8_t(this->get_blue() * 255); } - if (traits.get_supports_rgb_white_value()) + if (traits.get_supports_rgb_white_value()) { + root["color_brightness"] = uint8_t(this->get_color_brightness() * 255); root["white_value"] = uint8_t(this->get_white() * 255); + } if (traits.get_supports_color_temperature()) root["color_temp"] = uint32_t(this->get_color_temperature()); } @@ -131,21 +151,15 @@ class LightColorValues { /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. - * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8 + * For example: r=0.3, g=0.5, b=0.4 => r=0.6, g=1.0, b=0.8. + * + * Note that this does NOT retain the brightness information from the color attributes. * * @param traits Used for determining which attributes to consider. */ void normalize_color(const LightTraits &traits) { if (traits.get_supports_rgb()) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); - if (traits.get_supports_rgb_white_value()) { - max_value = fmaxf(max_value, this->get_white()); - if (max_value == 0.0f) { - this->set_white(1.0f); - } else { - this->set_white(this->get_white() / max_value); - } - } if (max_value == 0.0f) { this->set_red(1.0f); this->set_green(1.0f); @@ -158,15 +172,10 @@ class LightColorValues { } if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { - if (traits.get_supports_rgb_white_value()) { - // 0% brightness for RGBW[W] means no RGB channel, but white channel on. - // do nothing - } else { - // 0% brightness means off - this->set_state(false); - // reset brightness to 100% - this->set_brightness(1.0f); - } + // 0% brightness means off + this->set_state(false); + // reset brightness to 100% + this->set_brightness(1.0f); } } @@ -180,9 +189,9 @@ class LightColorValues { /// Convert these light color values to an RGB representation and write them to red, green, blue. void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { - float brightness = this->state_ * this->brightness_; - if (color_interlock) { - brightness = brightness * (1.0f - this->white_); + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + if (color_interlock && this->white_ > 0.0f) { + brightness = 0; } *red = gamma_correct(brightness * this->red_, gamma); *green = gamma_correct(brightness * this->green_, gamma); @@ -232,8 +241,9 @@ class LightColorValues { /// Compare this LightColorValues to rhs, return true if and only if all attributes match. bool operator==(const LightColorValues &rhs) const { - return state_ == rhs.state_ && brightness_ == rhs.brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && - blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_; + return state_ == rhs.state_ && brightness_ == rhs.brightness_ && color_brightness_ == rhs.color_brightness_ && + red_ == rhs.red_ && green_ == rhs.green_ && blue_ == rhs.blue_ && white_ == rhs.white_ && + color_temperature_ == rhs.color_temperature_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } @@ -251,6 +261,11 @@ class LightColorValues { /// Set the brightness property of these light color values. In range 0.0 to 1.0 void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the color brightness property of these light color values. In range 0.0 to 1.0 + float get_color_brightness() const { return this->color_brightness_; } + /// Set the color brightness property of these light color values. In range 0.0 to 1.0 + void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); } + /// Get the red property of these light color values. In range 0.0 to 1.0 float get_red() const { return this->red_; } /// Set the red property of these light color values. In range 0.0 to 1.0 @@ -281,6 +296,7 @@ class LightColorValues { protected: float state_; ///< ON / OFF, float for transition float brightness_; + float color_brightness_; float red_; float green_; float blue_; diff --git a/esphome/const.py b/esphome/const.py index bb20c606ce..9b7490878d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -121,6 +121,7 @@ CONF_CODE = "code" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" +CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" From be61b38a2cb82312c67adec67ee821817555dfa2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 9 Jul 2021 00:39:37 +1200 Subject: [PATCH 546/643] Allow WiFi AP to use device name (#1990) --- esphome/components/wifi/wifi_component.cpp | 19 +++++++++++++++++-- esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0c3a3054f8..e99cd0e1b1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -164,7 +164,7 @@ void WiFiComponent::loop() { WiFiComponent::WiFiComponent() { global_wifi_component = this; } -bool WiFiComponent::has_ap() const { return !this->ap_.get_ssid().empty(); } +bool WiFiComponent::has_ap() const { return this->has_ap_; } bool WiFiComponent::has_sta() const { return !this->sta_.empty(); } void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ = fast_connect; } IPAddress WiFiComponent::get_ip_address() { @@ -187,6 +187,18 @@ void WiFiComponent::setup_ap_config_() { if (this->ap_setup_) return; + if (this->ap_.get_ssid().empty()) { + std::string name = App.get_name(); + if (name.length() > 32) { + if (App.is_name_add_mac_suffix_enabled()) { + name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address + } else { + name = name.substr(0, 32); + } + } + this->ap_.set_ssid(name); + } + ESP_LOGCONFIG(TAG, "Setting up AP..."); ESP_LOGCONFIG(TAG, " AP SSID: '%s'", this->ap_.get_ssid().c_str()); @@ -212,7 +224,10 @@ void WiFiComponent::setup_ap_config_() { float WiFiComponent::get_loop_priority() const { return 10.0f; // before other loop components } -void WiFiComponent::set_ap(const WiFiAP &ap) { this->ap_ = ap; } +void WiFiComponent::set_ap(const WiFiAP &ap) { + this->ap_ = ap; + this->has_ap_ = true; +} void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d690a35420..f698e09d93 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -282,6 +282,7 @@ class WiFiComponent : public Component { WiFiAP selected_ap_; bool fast_connect_{false}; + bool has_ap_{false}; WiFiAP ap_; WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; uint32_t action_started_; From 294ba1fca7a572d0bc2df3a5bc71b32e71d8d960 Mon Sep 17 00:00:00 2001 From: Michael Gorven Date: Fri, 9 Jul 2021 14:25:27 -0700 Subject: [PATCH 547/643] Support custom fan modes in mqtt_climate (#1989) Co-authored-by: Michael Gorven --- esphome/components/climate/climate_traits.h | 2 +- esphome/components/mqtt/mqtt_climate.cpp | 65 +++++++++++---------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index b86a0c7774..8f9e716e1d 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -91,7 +91,7 @@ class ClimateTraits { ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return supported_fan_modes_.count(fan_mode); } - bool get_supports_fan_modes() const { return !supported_fan_modes_.empty(); } + bool get_supports_fan_modes() const { return !supported_fan_modes_.empty() || !supported_custom_fan_modes_.empty(); } const std::set get_supported_fan_modes() const { return supported_fan_modes_; } void set_supported_custom_fan_modes(std::set supported_custom_fan_modes) { diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 0934922bc6..ab8354e66c 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -97,6 +97,8 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC fan_modes.add("focus"); if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) fan_modes.add("diffuse"); + for (const auto &fan_mode : traits.get_supported_custom_fan_modes()) + fan_modes.add(fan_mode); } if (traits.get_supports_swing_modes()) { @@ -291,36 +293,39 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_fan_modes()) { - const char *payload = ""; - switch (this->device_->fan_mode.value()) { - case CLIMATE_FAN_ON: - payload = "on"; - break; - case CLIMATE_FAN_OFF: - payload = "off"; - break; - case CLIMATE_FAN_AUTO: - payload = "auto"; - break; - case CLIMATE_FAN_LOW: - payload = "low"; - break; - case CLIMATE_FAN_MEDIUM: - payload = "medium"; - break; - case CLIMATE_FAN_HIGH: - payload = "high"; - break; - case CLIMATE_FAN_MIDDLE: - payload = "middle"; - break; - case CLIMATE_FAN_FOCUS: - payload = "focus"; - break; - case CLIMATE_FAN_DIFFUSE: - payload = "diffuse"; - break; - } + std::string payload; + if (this->device_->fan_mode.has_value()) + switch (this->device_->fan_mode.value()) { + case CLIMATE_FAN_ON: + payload = "on"; + break; + case CLIMATE_FAN_OFF: + payload = "off"; + break; + case CLIMATE_FAN_AUTO: + payload = "auto"; + break; + case CLIMATE_FAN_LOW: + payload = "low"; + break; + case CLIMATE_FAN_MEDIUM: + payload = "medium"; + break; + case CLIMATE_FAN_HIGH: + payload = "high"; + break; + case CLIMATE_FAN_MIDDLE: + payload = "middle"; + break; + case CLIMATE_FAN_FOCUS: + payload = "focus"; + break; + case CLIMATE_FAN_DIFFUSE: + payload = "diffuse"; + break; + } + if (this->device_->custom_fan_mode.has_value()) + payload = this->device_->custom_fan_mode.value(); if (!this->publish(this->get_fan_mode_state_topic(), payload)) success = false; } From 4f88c2489bb462a6ad36809e50e1175a4a7f1f45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 23:27:08 +0200 Subject: [PATCH 548/643] Bump protobuf from 3.17.0 to 3.17.3 (#1986) Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 3.17.0 to 3.17.3. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.17.0...v3.17.3) --- updated-dependencies: - dependency-name: protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cc9059f0d7..c3562f492d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ PyYAML==5.4.1 paho-mqtt==1.5.1 colorama==0.4.4 tornado==6.1 -protobuf==3.17.0 +protobuf==3.17.3 tzlocal==2.1 pytz==2021.1 pyserial==3.5 From 99f497c3b82e5a5196a3a429191d414184a85440 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 23:28:28 +0200 Subject: [PATCH 549/643] Bump pytest-cov from 2.11.1 to 2.12.1 (#1855) Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.11.1 to 2.12.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.11.1...v2.12.1) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 26755921cd..6eb22ac43f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==6.2.4 -pytest-cov==2.11.1 +pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-asyncio==0.14.0 asyncmock==0.4.2 From b62c47fedee7e4da540bc6e13188c02ec770d3e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Jul 2021 23:28:37 +0200 Subject: [PATCH 550/643] Bump pytest-asyncio from 0.14.0 to 0.15.1 (#1793) Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.14.0 to 0.15.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.14.0...v0.15.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 6eb22ac43f..38717f3f77 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==6.2.4 pytest-cov==2.12.1 pytest-mock==3.6.1 -pytest-asyncio==0.14.0 +pytest-asyncio==0.15.1 asyncmock==0.4.2 hypothesis==5.49.0 From 7ae611256aef1c44bb9cfb0ea690f91b8fddcd27 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 10 Jul 2021 11:23:04 +0200 Subject: [PATCH 551/643] Improve climate mode code docs (#1995) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/climate/climate_mode.h | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 07fbf32b26..476cf5bd84 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -7,19 +7,22 @@ namespace climate { /// Enum for all modes a climate device can be in. enum ClimateMode : uint8_t { - /// The climate device is off (not in auto, heat or cool mode) + /// The climate device is off CLIMATE_MODE_OFF = 0, - /// The climate device is set to automatically change the heating/cooling cycle + /// The climate device is set to heat/cool to reach the target temperature. CLIMATE_MODE_HEAT_COOL = 1, - /// The climate device is manually set to cool mode (not in auto mode!) + /// The climate device is set to cool to reach the target temperature CLIMATE_MODE_COOL = 2, - /// The climate device is manually set to heat mode (not in auto mode!) + /// The climate device is set to heat to reach the target temperature CLIMATE_MODE_HEAT = 3, - /// The climate device is manually set to fan only mode + /// The climate device only has the fan enabled, no heating or cooling is taking place CLIMATE_MODE_FAN_ONLY = 4, - /// The climate device is manually set to dry mode + /// The climate device is set to dry/humidity mode CLIMATE_MODE_DRY = 5, - /// The climate device is manually set to heat-cool mode + /** The climate device is adjusting the temperatre dynamically. + * For example, the target temperature can be adjusted based on a schedule, or learned behavior. + * The target temperature can't be adjusted when in this mode. + */ CLIMATE_MODE_AUTO = 6 }; @@ -27,15 +30,15 @@ enum ClimateMode : uint8_t { enum ClimateAction : uint8_t { /// The climate device is off (inactive or no power) CLIMATE_ACTION_OFF = 0, - /// The climate device is actively cooling (usually in cool or auto mode) + /// The climate device is actively cooling CLIMATE_ACTION_COOLING = 2, - /// The climate device is actively heating (usually in heat or auto mode) + /// The climate device is actively heating CLIMATE_ACTION_HEATING = 3, /// The climate device is idle (monitoring climate but no action needed) CLIMATE_ACTION_IDLE = 4, - /// The climate device is drying (either mode DRY or AUTO) + /// The climate device is drying CLIMATE_ACTION_DRYING = 5, - /// The climate device is in fan only mode (either mode FAN_ONLY or AUTO) + /// The climate device is in fan only mode CLIMATE_ACTION_FAN = 6, }; From cdbc146e5d032773f8f46dc3994ae2d60f5e2e44 Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Sat, 10 Jul 2021 11:37:55 +0200 Subject: [PATCH 552/643] Climate modes COOL and HEAT are auto modes (#1994) --- esphome/components/pid/pid_climate.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 8a61361fb8..dac4426698 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -20,7 +20,12 @@ void PIDClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + if (supports_heat_() && supports_cool_()) + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + else if (supports_cool_()) + this->mode = climate::CLIMATE_MODE_COOL; + else if (supports_heat_()) + this->mode = climate::CLIMATE_MODE_HEAT; this->target_temperature = this->default_target_temperature_; } } @@ -41,11 +46,13 @@ climate::ClimateTraits PIDClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supports_two_point_target_temperature(false); - traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL}); + traits.set_supported_modes({climate::CLIMATE_MODE_OFF}); if (supports_cool_()) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); if (supports_heat_()) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_heat_() && supports_cool_()) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); traits.set_supports_action(true); return traits; @@ -93,13 +100,8 @@ void PIDClimate::write_output_(float value) { } void PIDClimate::handle_non_auto_mode_() { // in non-auto mode, switch directly to appropriate action - // - HEAT mode / COOL mode -> Output at ±100% // - OFF mode -> Output at 0% - if (this->mode == climate::CLIMATE_MODE_HEAT) { - this->write_output_(1.0); - } else if (this->mode == climate::CLIMATE_MODE_COOL) { - this->write_output_(-1.0); - } else if (this->mode == climate::CLIMATE_MODE_OFF) { + if (this->mode == climate::CLIMATE_MODE_OFF) { this->write_output_(0.0); } else { assert(false); @@ -132,7 +134,7 @@ void PIDClimate::update_pid_() { } } - if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) { + if (this->mode == climate::CLIMATE_MODE_OFF) { this->handle_non_auto_mode_(); } else { this->write_output_(value); From 623570a117857180816e9dbb2e426aadba56fbd1 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Sat, 10 Jul 2021 21:52:19 +0200 Subject: [PATCH 553/643] Add state callback to ota component (#1816) Co-authored-by: Maurice Makaay Co-authored-by: Guillermo Ruffino --- esphome/components/ota/__init__.py | 65 ++++++++++++++++++++++ esphome/components/ota/automation.h | 71 ++++++++++++++++++++++++ esphome/components/ota/ota_component.cpp | 20 ++++++- esphome/components/ota/ota_component.h | 11 ++++ tests/test1.yaml | 18 ++++++ 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 esphome/components/ota/automation.h diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 7ee7ef47ca..75641ad399 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,6 +1,7 @@ from esphome.cpp_generator import RawExpression import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.const import ( CONF_ID, CONF_NUM_ATTEMPTS, @@ -8,14 +9,29 @@ from esphome.const import ( CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, + CONF_TRIGGER_ID, ) from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] +CONF_ON_STATE_CHANGE = "on_state_change" +CONF_ON_BEGIN = "on_begin" +CONF_ON_PROGRESS = "on_progress" +CONF_ON_END = "on_end" +CONF_ON_ERROR = "on_error" + ota_ns = cg.esphome_ns.namespace("ota") +OTAState = ota_ns.enum("OTAState") OTAComponent = ota_ns.class_("OTAComponent", cg.Component) +OTAStateChangeTrigger = ota_ns.class_( + "OTAStateChangeTrigger", automation.Trigger.template() +) +OTAStartTrigger = ota_ns.class_("OTAStartTrigger", automation.Trigger.template()) +OTAProgressTrigger = ota_ns.class_("OTAProgressTrigger", automation.Trigger.template()) +OTAEndTrigger = ota_ns.class_("OTAEndTrigger", automation.Trigger.template()) +OTAErrorTrigger = ota_ns.class_("OTAErrorTrigger", automation.Trigger.template()) CONFIG_SCHEMA = cv.Schema( { @@ -27,6 +43,31 @@ CONFIG_SCHEMA = cv.Schema( CONF_REBOOT_TIMEOUT, default="5min" ): cv.positive_time_period_milliseconds, cv.Optional(CONF_NUM_ATTEMPTS, default="10"): cv.positive_not_null_int, + cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStateChangeTrigger), + } + ), + cv.Optional(CONF_ON_BEGIN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAStartTrigger), + } + ), + cv.Optional(CONF_ON_ERROR): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAErrorTrigger), + } + ), + cv.Optional(CONF_ON_PROGRESS): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAProgressTrigger), + } + ), + cv.Optional(CONF_ON_END): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OTAEndTrigger), + } + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -49,3 +90,27 @@ async def to_code(config): cg.add_library("Update", None) elif CORE.is_esp32: cg.add_library("Hash", None) + + use_state_callback = False + for conf in config.get(CONF_ON_STATE_CHANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(OTAState, "state")], conf) + use_state_callback = True + for conf in config.get(CONF_ON_BEGIN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_PROGRESS, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + use_state_callback = True + for conf in config.get(CONF_ON_END, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + use_state_callback = True + for conf in config.get(CONF_ON_ERROR, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(int, "x")], conf) + use_state_callback = True + if use_state_callback: + cg.add_define("USE_OTA_STATE_CALLBACK") diff --git a/esphome/components/ota/automation.h b/esphome/components/ota/automation.h new file mode 100644 index 0000000000..6c8aca3705 --- /dev/null +++ b/esphome/components/ota/automation.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_OTA_STATE_CALLBACK + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/ota/ota_component.h" + +namespace esphome { +namespace ota { + +class OTAStateChangeTrigger : public Trigger { + public: + explicit OTAStateChangeTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (!parent->is_failed()) { + return trigger(state); + } + }); + } +}; + +class OTAStartTrigger : public Trigger<> { + public: + explicit OTAStartTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_STARTED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class OTAProgressTrigger : public Trigger { + public: + explicit OTAProgressTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_IN_PROGRESS && !parent->is_failed()) { + trigger(progress); + } + }); + } +}; + +class OTAEndTrigger : public Trigger<> { + public: + explicit OTAEndTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_COMPLETED && !parent->is_failed()) { + trigger(); + } + }); + } +}; + +class OTAErrorTrigger : public Trigger { + public: + explicit OTAErrorTrigger(OTAComponent *parent) { + parent->add_on_state_callback([this, parent](OTAState state, float progress, uint8_t error) { + if (state == OTA_ERROR && !parent->is_failed()) { + trigger(error); + } + }); + } +}; + +} // namespace ota +} // namespace esphome + +#endif // USE_OTA_STATE_CALLBACK diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 5302b7bc24..71f8101704 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -1,7 +1,6 @@ #include "ota_component.h" #include "esphome/core/log.h" -#include "esphome/core/helpers.h" #include "esphome/core/application.h" #include "esphome/core/util.h" @@ -25,6 +24,7 @@ void OTAComponent::setup() { this->dump_config(); } + void OTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air Updates:"); ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_); @@ -71,6 +71,9 @@ void OTAComponent::handle_() { ESP_LOGD(TAG, "Starting OTA Update from %s...", this->client_.remoteIP().toString().c_str()); this->status_set_warning(); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_STARTED, 0.0f, 0); +#endif if (!this->wait_receive_(buf, 5)) { ESP_LOGW(TAG, "Reading magic bytes failed!"); @@ -241,6 +244,9 @@ void OTAComponent::handle_() { last_progress = now; float percentage = (total * 100.0f) / ota_size; ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_IN_PROGRESS, percentage, 0); +#endif // slow down OTA update to avoid getting killed by task watchdog (task_wdt) delay(10); } @@ -268,6 +274,9 @@ void OTAComponent::handle_() { delay(10); ESP_LOGI(TAG, "OTA update finished!"); this->status_clear_warning(); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_COMPLETED, 100.0f, 0); +#endif delay(100); // NOLINT App.safe_reboot(); @@ -296,6 +305,9 @@ error: #endif this->status_momentary_error("onerror", 5000); +#ifdef USE_OTA_STATE_CALLBACK + this->state_callback_.call(OTA_ERROR, 0.0f, static_cast(error_code)); +#endif #ifdef ARDUINO_ARCH_ESP8266 global_preferences.prevent_write(false); @@ -400,5 +412,11 @@ void OTAComponent::on_safe_shutdown() { this->clean_rtc(); } +#ifdef USE_OTA_STATE_CALLBACK +void OTAComponent::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} +#endif + } // namespace ota } // namespace esphome diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index f16725e324..8b5830295e 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/helpers.h" #include #include @@ -32,6 +33,8 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_UNKNOWN = 255, }; +enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR }; + /// OTAComponent provides a simple way to integrate Over-the-Air updates into your app using ArduinoOTA. class OTAComponent : public Component { public: @@ -49,6 +52,10 @@ class OTAComponent : public Component { bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time); +#ifdef USE_OTA_STATE_CALLBACK + void add_on_state_callback(std::function &&callback); +#endif + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; @@ -82,6 +89,10 @@ class OTAComponent : public Component { uint32_t safe_mode_rtc_value_; uint8_t safe_mode_num_attempts_; ESPPreferenceObject rtc_; + +#ifdef USE_OTA_STATE_CALLBACK + CallbackManager state_callback_{}; +#endif }; } // namespace ota diff --git a/tests/test1.yaml b/tests/test1.yaml index 1f817f0dab..1c522a23d4 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -197,6 +197,24 @@ ota: port: 3286 reboot_timeout: 2min num_attempts: 5 + on_state_change: + then: + lambda: >- + ESP_LOGD("ota", "State %d", state); + on_begin: + then: + logger.log: "OTA begin" + on_progress: + then: + lambda: >- + ESP_LOGD("ota", "Got progress %f", x); + on_end: + then: + logger.log: "OTA end" + on_error: + then: + lambda: >- + ESP_LOGD("ota", "Got error code %d", x); logger: baud_rate: 0 From 1f5c79bd1714d6eebec67a5d1b9585fcf7fab48f Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 11 Jul 2021 06:51:24 +0200 Subject: [PATCH 554/643] Fix deprecation message for old climate swing mode methods (#2003) --- esphome/components/climate/climate_traits.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 8f9e716e1d..fbd6f158e6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -127,13 +127,13 @@ class ClimateTraits { void set_supported_swing_modes(std::set modes) { supported_swing_modes_ = std::move(modes); } void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); } - ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead") + ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead") void set_supports_swing_mode_horizontal(bool supported) { set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported); } From dd37a4e04c8e1220f934de89a97c326381d0fdda Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 13 Jul 2021 07:20:12 +1200 Subject: [PATCH 555/643] Add Number entities (from Home Assistant) (#1971) Co-authored-by: Oxan van Leeuwen --- CODEOWNERS | 1 + esphome/components/api/api.proto | 39 ++++ esphome/components/api/api_connection.cpp | 36 ++++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 173 ++++++++++++++++++ esphome/components/api/api_pb2.h | 39 ++++ esphome/components/api/api_pb2_service.cpp | 36 ++++ esphome/components/api/api_pb2_service.h | 15 ++ esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 4 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 5 + esphome/components/api/subscribe_state.h | 3 + esphome/components/api/util.cpp | 15 ++ esphome/components/api/util.h | 6 + esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_number.cpp | 61 ++++++ esphome/components/mqtt/mqtt_number.h | 46 +++++ esphome/components/number/__init__.py | 154 ++++++++++++++++ esphome/components/number/automation.cpp | 47 +++++ esphome/components/number/automation.h | 76 ++++++++ esphome/components/number/number.cpp | 76 ++++++++ esphome/components/number/number.h | 112 ++++++++++++ .../components/template/number/__init__.py | 63 +++++++ .../template/number/template_number.cpp | 39 ++++ .../template/number/template_number.h | 30 +++ esphome/components/web_server/web_server.cpp | 49 +++++ esphome/components/web_server/web_server.h | 9 + esphome/const.py | 1 + esphome/core/application.h | 19 ++ esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/test5.yaml | 17 ++ 36 files changed, 1206 insertions(+) create mode 100644 esphome/components/mqtt/mqtt_number.cpp create mode 100644 esphome/components/mqtt/mqtt_number.h create mode 100644 esphome/components/number/__init__.py create mode 100644 esphome/components/number/automation.cpp create mode 100644 esphome/components/number/automation.h create mode 100644 esphome/components/number/number.cpp create mode 100644 esphome/components/number/number.h create mode 100644 esphome/components/template/number/__init__.py create mode 100644 esphome/components/template/number/template_number.cpp create mode 100644 esphome/components/template/number/template_number.h diff --git a/CODEOWNERS b/CODEOWNERS index a1c7bf9bfd..2a57e5d81a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,7 @@ esphome/components/midea_dongle/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core esphome/components/nfc/* @jesserockz +esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 8034c980a4..40be1fd0db 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -38,6 +38,7 @@ service APIConnection { rpc switch_command (SwitchCommandRequest) returns (void) {} rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} + rpc number_command (NumberCommandRequest) returns (void) {} } @@ -798,3 +799,41 @@ message ClimateCommandRequest { bool has_custom_preset = 20; string custom_preset = 21; } + +// ==================== NUMBER ==================== +message ListEntitiesNumberResponse { + option (id) = 49; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + float min_value = 6; + float max_value = 7; + float step = 8; +} +message NumberStateResponse { + option (id) = 50; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; + // If the number does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message NumberCommandRequest { + option (id) = 51; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index c36d36a159..8c76583fc7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -553,6 +553,42 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { } #endif +#ifdef USE_NUMBER +bool APIConnection::send_number_state(number::Number *number, float state) { + if (!this->state_subscription_) + return false; + + NumberStateResponse resp{}; + resp.key = number->get_object_id_hash(); + resp.state = state; + resp.missing_state = !number->has_state(); + return this->send_number_state_response(resp); +} +bool APIConnection::send_number_info(number::Number *number) { + ListEntitiesNumberResponse msg; + msg.key = number->get_object_id_hash(); + msg.object_id = number->get_object_id(); + msg.name = number->get_name(); + msg.unique_id = get_default_unique_id("number", number); + msg.icon = number->get_icon(); + + msg.min_value = number->get_min_value(); + msg.max_value = number->get_max_value(); + msg.step = number->get_step(); + + return this->send_list_entities_number_response(msg); +} +void APIConnection::number_command(const NumberCommandRequest &msg) { + number::Number *number = App.get_number_by_key(msg.key); + if (number == nullptr) + return; + + auto call = number->make_call(); + call.set_value(msg.state); + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 3e91ead52c..1d7fc48563 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -62,6 +62,11 @@ class APIConnection : public APIServerConnection { bool send_climate_state(climate::Climate *climate); bool send_climate_info(climate::Climate *climate); void climate_command(const ClimateCommandRequest &msg) override; +#endif +#ifdef USE_NUMBER + bool send_number_state(number::Number *number, float state); + bool send_number_info(number::Number *number); + void number_command(const NumberCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 83ebdd8b68..c3cfc8cd76 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3256,6 +3256,179 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +bool ListEntitiesNumberResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + default: + return false; + } +} +bool ListEntitiesNumberResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + case 6: { + this->min_value = value.as_float(); + return true; + } + case 7: { + this->max_value = value.as_float(); + return true; + } + case 8: { + this->step = value.as_float(); + return true; + } + default: + return false; + } +} +void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + buffer.encode_float(6, this->min_value); + buffer.encode_float(7, this->max_value); + buffer.encode_float(8, this->step); +} +void ListEntitiesNumberResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesNumberResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + out.append(" min_value: "); + sprintf(buffer, "%g", this->min_value); + out.append(buffer); + out.append("\n"); + + out.append(" max_value: "); + sprintf(buffer, "%g", this->max_value); + out.append(buffer); + out.append("\n"); + + out.append(" step: "); + sprintf(buffer, "%g", this->step); + out.append(buffer); + out.append("\n"); + out.append("}"); +} +bool NumberStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool NumberStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->state = value.as_float(); + return true; + } + default: + return false; + } +} +void NumberStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->state); + buffer.encode_bool(3, this->missing_state); +} +void NumberStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("NumberStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + sprintf(buffer, "%g", this->state); + out.append(buffer); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + out.append("}"); +} +bool NumberCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + case 2: { + this->state = value.as_float(); + return true; + } + default: + return false; + } +} +void NumberCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_float(2, this->state); +} +void NumberCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("NumberCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + sprintf(buffer, "%g", this->state); + out.append(buffer); + out.append("\n"); + out.append("}"); +} } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 6873e0d54c..e3bb1d9106 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -782,6 +782,45 @@ class ClimateCommandRequest : public ProtoMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; +class ListEntitiesNumberResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + float min_value{0.0f}; + float max_value{0.0f}; + float step{0.0f}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class NumberStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + float state{0.0f}; + bool missing_state{false}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class NumberCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + float state{0.0f}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4fade19787..440a5d0ab3 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -184,6 +184,20 @@ bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResp #endif #ifdef USE_CLIMATE #endif +#ifdef USE_NUMBER +bool APIServerConnectionBase::send_list_entities_number_response(const ListEntitiesNumberResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_number_response: %s", msg.dump().c_str()); + return this->send_message_(msg, 49); +} +#endif +#ifdef USE_NUMBER +bool APIServerConnectionBase::send_number_state_response(const NumberStateResponse &msg) { + ESP_LOGVV(TAG, "send_number_state_response: %s", msg.dump().c_str()); + return this->send_message_(msg, 50); +} +#endif +#ifdef USE_NUMBER +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -349,6 +363,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, msg.decode(msg_data, msg_size); ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str()); this->on_climate_command_request(msg); +#endif + break; + } + case 51: { +#ifdef USE_NUMBER + NumberCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); + this->on_number_command_request(msg); #endif break; } @@ -547,6 +570,19 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest this->climate_command(msg); } #endif +#ifdef USE_NUMBER +void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->number_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index afbe39e314..398c10a811 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -111,6 +111,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_CLIMATE virtual void on_climate_command_request(const ClimateCommandRequest &value){}; +#endif +#ifdef USE_NUMBER + bool send_list_entities_number_response(const ListEntitiesNumberResponse &msg); +#endif +#ifdef USE_NUMBER + bool send_number_state_response(const NumberStateResponse &msg); +#endif +#ifdef USE_NUMBER + virtual void on_number_command_request(const NumberCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -147,6 +156,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_CLIMATE virtual void climate_command(const ClimateCommandRequest &msg) = 0; +#endif +#ifdef USE_NUMBER + virtual void number_command(const NumberCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -179,6 +191,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_CLIMATE void on_climate_command_request(const ClimateCommandRequest &msg) override; #endif +#ifdef USE_NUMBER + void on_number_command_request(const NumberCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 1373533ae2..7434030565 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -197,6 +197,15 @@ void APIServer::on_climate_update(climate::Climate *obj) { } #endif +#ifdef USE_NUMBER +void APIServer::on_number_update(number::Number *obj, float state) { + if (obj->is_internal()) + return; + for (auto *c : this->clients_) + c->send_number_state(obj, state); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index eb6c91d01c..add22e121e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -60,6 +60,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_CLIMATE void on_climate_update(climate::Climate *obj) override; +#endif +#ifdef USE_NUMBER + void on_number_update(number::Number *obj, float state) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index d4245136ae..8897758073 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -51,5 +51,9 @@ bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) { bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); } #endif +#ifdef USE_NUMBER +bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 6b10a72fdf..c55ba5089e 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -39,6 +39,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_CLIMATE bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 2612a852d3..25aa7c8b31 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -37,6 +37,11 @@ bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) #ifdef USE_CLIMATE bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); } #endif +#ifdef USE_NUMBER +bool InitialStateIterator::on_number(number::Number *number) { + return this->client_->send_number_state(number, number->state); +} +#endif InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 51b9c695e4..f03322ac4a 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -36,6 +36,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_CLIMATE bool on_climate(climate::Climate *climate) override; +#endif +#ifdef USE_NUMBER + bool on_number(number::Number *number) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index f929db5d6a..6e05d49b74 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -167,6 +167,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_NUMBER + case IteratorState::NUMBER: + if (this->at_ >= App.get_numbers().size()) { + advance_platform = true; + } else { + auto *number = App.get_numbers()[this->at_]; + if (number->is_internal()) { + success = true; + break; + } else { + success = this->on_number(number); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index 5a29a48cbe..f8b248056b 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -47,6 +47,9 @@ class ComponentIterator { #endif #ifdef USE_CLIMATE virtual bool on_climate(climate::Climate *climate) = 0; +#endif +#ifdef USE_NUMBER + virtual bool on_number(number::Number *number) = 0; #endif virtual bool on_end(); @@ -81,6 +84,9 @@ class ComponentIterator { #endif #ifdef USE_CLIMATE CLIMATE, +#endif +#ifdef USE_NUMBER + NUMBER, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 906c570b17..3559fce046 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -91,6 +91,7 @@ MQTTJSONLightComponent = mqtt_ns.class_("MQTTJSONLightComponent", MQTTComponent) MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) +MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) def validate_config(value): diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp new file mode 100644 index 0000000000..bb67a225fd --- /dev/null +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -0,0 +1,61 @@ +#include "mqtt_number.h" +#include "esphome/core/log.h" + +#ifdef USE_NUMBER + +#ifdef USE_DEEP_SLEEP +#include "esphome/components/deep_sleep/deep_sleep_component.h" +#endif + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.number"; + +using namespace esphome::number; + +MQTTNumberComponent::MQTTNumberComponent(Number *number) : MQTTComponent(), number_(number) {} + +void MQTTNumberComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto val = parse_float(state); + if (!val.has_value()) { + ESP_LOGE(TAG, "Can't convert '%s' to number!", state.c_str()); + return; + } + auto call = this->number_->make_call(); + call.set_value(*val); + call.perform(); + }); + this->number_->add_on_state_callback([this](float state) { this->publish_state(state); }); +} + +void MQTTNumberComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, false) +} + +std::string MQTTNumberComponent::component_type() const { return "number"; } + +std::string MQTTNumberComponent::friendly_name() const { return this->number_->get_name(); } +void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->number_->get_icon().empty()) + root["icon"] = this->number_->get_icon(); +} +bool MQTTNumberComponent::send_initial_state() { + if (this->number_->has_state()) { + return this->publish_state(this->number_->state); + } else { + return true; + } +} +bool MQTTNumberComponent::is_internal() { return this->number_->is_internal(); } +bool MQTTNumberComponent::publish_state(float value) { + int8_t accuracy = this->number_->get_accuracy_decimals(); + return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); +} + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h new file mode 100644 index 0000000000..f44de91435 --- /dev/null +++ b/esphome/components/mqtt/mqtt_number.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_NUMBER + +#include "esphome/components/number/number.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTNumberComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTNumberComponent instance with the provided friendly_name and number + * + * @param number The number. + */ + explicit MQTTNumberComponent(number::Number *number); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + bool is_internal() override; + + bool publish_state(float value); + + protected: + /// Override for MQTTComponent, returns "number". + std::string component_type() const override; + + std::string friendly_name() const override; + + number::Number *number_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py new file mode 100644 index 0000000000..ed33931d8b --- /dev/null +++ b/esphome/components/number/__init__.py @@ -0,0 +1,154 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ABOVE, + CONF_BELOW, + CONF_ICON, + CONF_ID, + CONF_INTERNAL, + CONF_ON_VALUE, + CONF_ON_VALUE_RANGE, + CONF_TRIGGER_ID, + CONF_NAME, + CONF_MQTT_ID, + CONF_VALUE, + ICON_EMPTY, +) +from esphome.core import CORE, coroutine_with_priority + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +number_ns = cg.esphome_ns.namespace("number") +Number = number_ns.class_("Number", cg.Nameable) +NumberPtr = Number.operator("ptr") + +# Triggers +NumberStateTrigger = number_ns.class_( + "NumberStateTrigger", automation.Trigger.template(cg.float_) +) +ValueRangeTrigger = number_ns.class_( + "ValueRangeTrigger", automation.Trigger.template(cg.float_), cg.Component +) + +# Actions +NumberSetAction = number_ns.class_("NumberSetAction", automation.Action) + +# Conditions +NumberInRangeCondition = number_ns.class_( + "NumberInRangeCondition", automation.Condition +) + +icon = cv.icon + + +NUMBER_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), + cv.GenerateID(): cv.declare_id(Number), + cv.Optional(CONF_ICON, default=ICON_EMPTY): icon, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(NumberStateTrigger), + } + ), + cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ValueRangeTrigger), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), + ), + } +) + + +async def setup_number_core_(var, config): + cg.add(var.set_name(config[CONF_NAME])) + if CONF_INTERNAL in config: + cg.add(var.set_internal(config[CONF_INTERNAL])) + + cg.add(var.set_icon(config[CONF_ICON])) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(float, "x")], conf) + for conf in config.get(CONF_ON_VALUE_RANGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await cg.register_component(trigger, conf) + if CONF_ABOVE in conf: + template_ = await cg.templatable(conf[CONF_ABOVE], [(float, "x")], float) + cg.add(trigger.set_min(template_)) + if CONF_BELOW in conf: + template_ = await cg.templatable(conf[CONF_BELOW], [(float, "x")], float) + cg.add(trigger.set_max(template_)) + await automation.build_automation(trigger, [(float, "x")], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_number(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_number(var)) + await setup_number_core_(var, config) + + +async def new_number(config): + var = cg.new_Pvariable(config[CONF_ID]) + await register_number(var, config) + return var + + +NUMBER_IN_RANGE_CONDITION_SCHEMA = cv.All( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Optional(CONF_ABOVE): cv.float_, + cv.Optional(CONF_BELOW): cv.float_, + }, + cv.has_at_least_one_key(CONF_ABOVE, CONF_BELOW), +) + + +@automation.register_condition( + "number.in_range", NumberInRangeCondition, NUMBER_IN_RANGE_CONDITION_SCHEMA +) +async def number_in_range_to_code(config, condition_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + + if CONF_ABOVE in config: + cg.add(var.set_min(config[CONF_ABOVE])) + if CONF_BELOW in config: + cg.add(var.set_max(config[CONF_BELOW])) + + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_NUMBER") + cg.add_global(number_ns.using) + + +@automation.register_action( + "number.set", + NumberSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Number), + cv.Required(CONF_VALUE): cv.templatable(cv.float_), + } + ), +) +async def number_set_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_VALUE], args, float) + cg.add(var.set_value(template_)) + return var diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp new file mode 100644 index 0000000000..a0b169427f --- /dev/null +++ b/esphome/components/number/automation.cpp @@ -0,0 +1,47 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number.automation"; + +void ValueRangeTrigger::setup() { + this->rtc_ = global_preferences.make_preference(this->parent_->get_object_id_hash()); + bool initial_state; + if (this->rtc_.load(&initial_state)) { + this->previous_in_range_ = initial_state; + } + + this->parent_->add_on_state_callback([this](float state) { this->on_state_(state); }); +} +float ValueRangeTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } + +void ValueRangeTrigger::on_state_(float state) { + if (isnan(state)) + return; + + float local_min = this->min_.value(state); + float local_max = this->max_.value(state); + + bool in_range; + if (isnan(local_min) && isnan(local_max)) { + in_range = this->previous_in_range_; + } else if (isnan(local_min)) { + in_range = state <= local_max; + } else if (isnan(local_max)) { + in_range = state >= local_min; + } else { + in_range = local_min <= state && state <= local_max; + } + + if (in_range != this->previous_in_range_ && in_range) { + this->trigger(state); + } + + this->previous_in_range_ = in_range; + this->rtc_.save(&in_range); +} + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/automation.h b/esphome/components/number/automation.h new file mode 100644 index 0000000000..9e812f8c49 --- /dev/null +++ b/esphome/components/number/automation.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace number { + +class NumberStateTrigger : public Trigger { + public: + explicit NumberStateTrigger(Number *parent) { + parent->add_on_state_callback([this](float value) { this->trigger(value); }); + } +}; + +template class NumberSetAction : public Action { + public: + NumberSetAction(Number *number) : number_(number) {} + TEMPLATABLE_VALUE(float, value) + + void play(Ts... x) override { + auto call = this->number_->make_call(); + call.set_value(this->value_.value(x...)); + call.perform(); + } + + protected: + Number *number_; +}; + +class ValueRangeTrigger : public Trigger, public Component { + public: + explicit ValueRangeTrigger(Number *parent) : parent_(parent) {} + + template void set_min(V min) { this->min_ = min; } + template void set_max(V max) { this->max_ = max; } + + void setup() override; + float get_setup_priority() const override; + + protected: + void on_state_(float state); + + Number *parent_; + ESPPreferenceObject rtc_; + bool previous_in_range_{false}; + TemplatableValue min_{NAN}; + TemplatableValue max_{NAN}; +}; + +template class NumberInRangeCondition : public Condition { + public: + NumberInRangeCondition(Number *parent) : parent_(parent) {} + + void set_min(float min) { this->min_ = min; } + void set_max(float max) { this->max_ = max; } + bool check(Ts... x) override { + const float state = this->parent_->state; + if (isnan(this->min_)) { + return state <= this->max_; + } else if (isnan(this->max_)) { + return state >= this->min_; + } else { + return this->min_ <= state && state <= this->max_; + } + } + + protected: + Number *parent_; + float min_{NAN}; + float max_{NAN}; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp new file mode 100644 index 0000000000..eaee5d4e69 --- /dev/null +++ b/esphome/components/number/number.cpp @@ -0,0 +1,76 @@ +#include "number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace number { + +static const char *const TAG = "number"; + +void NumberCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (this->value_.has_value()) { + auto value = *this->value_; + uint8_t accuracy = this->parent_->get_accuracy_decimals(); + float min_value = this->parent_->get_min_value(); + if (value < min_value) { + ESP_LOGW(TAG, " Value %s must not be less than minimum %s", value_accuracy_to_string(value, accuracy).c_str(), + value_accuracy_to_string(min_value, accuracy).c_str()); + this->value_.reset(); + return; + } + float max_value = this->parent_->get_max_value(); + if (value > max_value) { + ESP_LOGW(TAG, " Value %s must not be larger than maximum %s", value_accuracy_to_string(value, accuracy).c_str(), + value_accuracy_to_string(max_value, accuracy).c_str()); + this->value_.reset(); + return; + } + ESP_LOGD(TAG, " Value: %s", value_accuracy_to_string(*this->value_, accuracy).c_str()); + this->parent_->set(*this->value_); + } +} + +NumberCall &NumberCall::set_value(float value) { + this->value_ = value; + return *this; +} + +const optional &NumberCall::get_value() const { return this->value_; } + +NumberCall Number::make_call() { return NumberCall(this); } + +void Number::publish_state(float state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %.5f", this->get_name().c_str(), state); + this->state_callback_.call(state); +} + +uint32_t Number::update_interval() { return 0; } +Number::Number(const std::string &name) : Nameable(name), state(NAN) {} +Number::Number() : Number("") {} + +void Number::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} +void Number::set_icon(const std::string &icon) { this->icon_ = icon; } +std::string Number::get_icon() { return *this->icon_; } +int8_t Number::get_accuracy_decimals() { + // use printf %g to find number of digits based on step + char buf[32]; + sprintf(buf, "%.5g", this->step_); + std::string str{buf}; + size_t dot_pos = str.find('.'); + if (dot_pos == std::string::npos) + return 0; + + return str.length() - dot_pos - 1; +} +float Number::get_state() const { return this->state; } + +bool Number::has_state() const { return this->has_state_; } + +uint32_t Number::hash_base() { return 2282307003UL; } + +} // namespace number +} // namespace esphome diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h new file mode 100644 index 0000000000..4fe9692a6b --- /dev/null +++ b/esphome/components/number/number.h @@ -0,0 +1,112 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace number { + +#define LOG_NUMBER(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + if (!(obj)->get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + } \ + } + +class Number; + +class NumberCall { + public: + explicit NumberCall(Number *parent) : parent_(parent) {} + NumberCall &set_value(float value); + void perform(); + + const optional &get_value() const; + + protected: + Number *const parent_; + optional value_; +}; + +/** Base-class for all numbers. + * + * A number can use publish_state to send out a new value. + */ +class Number : public Nameable { + public: + explicit Number(); + explicit Number(const std::string &name); + + /** Manually set the icon of this number. By default the number's default defined by icon() is used. + * + * @param icon The icon, for example "mdi:flash". "" to disable. + */ + void set_icon(const std::string &icon); + /// Get the Home Assistant Icon. Uses the manual override if specified or the default value instead. + std::string get_icon(); + + /// Getter-syntax for .state. + float get_state() const; + + /// Get the accuracy in decimals. Based on the step value. + int8_t get_accuracy_decimals(); + + /** Publish the current state to the front-end. + */ + void publish_state(float state); + + NumberCall make_call(); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Add a callback that will be called every time the state changes. + void add_on_state_callback(std::function &&callback); + + /** This member variable stores the last state. + * + * On startup, when no state is available yet, this is NAN (not-a-number) and the validity + * can be checked using has_state(). + * + * This is exposed through a member variable for ease of use in esphome lambdas. + */ + float state; + + /// Return whether this number has gotten a full state yet. + bool has_state() const; + + /// Return with which interval the number is polled. Return 0 for non-polling mode. + virtual uint32_t update_interval(); + + void set_min_value(float min_value) { this->min_value_ = min_value; } + void set_max_value(float max_value) { this->max_value_ = max_value; } + void set_step(float step) { this->step_ = step; } + + float get_min_value() const { return this->min_value_; } + float get_max_value() const { return this->max_value_; } + float get_step() const { return this->step_; } + + protected: + friend class NumberCall; + + /** Set the value of the number, this is a virtual method that each number integration must implement. + * + * This method is called by the NumberCall. + * + * @param value The value as validated by the NumberCall. + */ + virtual void set(float value) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_; + /// Override the icon advertised to Home Assistant, otherwise number's icon will be used. + optional icon_; + bool has_state_{false}; + float step_{1.0}; + float min_value_{0}; + float max_value_{100}; +}; + +} // namespace number +} // namespace esphome diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py new file mode 100644 index 0000000000..cf70a48c4d --- /dev/null +++ b/esphome/components/template/number/__init__.py @@ -0,0 +1,63 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import number +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_OPTIMISTIC, + CONF_STEP, +) +from .. import template_ns + +TemplateNumber = template_ns.class_( + "TemplateNumber", number.Number, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +CONFIG_SCHEMA = cv.All( + number.NUMBER_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateNumber), + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.positive_float, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + } + ).extend(cv.polling_component_schema("60s")), + validate_min_max, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await number.register_number(var, config) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(float) + ) + cg.add(var.set_template(template_)) + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] + ) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + cg.add(var.set_min_value(config[CONF_MIN_VALUE])) + cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + cg.add(var.set_step(config[CONF_STEP])) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp new file mode 100644 index 0000000000..69c5d62684 --- /dev/null +++ b/esphome/components/template/number/template_number.cpp @@ -0,0 +1,39 @@ +#include "template_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.number"; + +TemplateNumber::TemplateNumber() : set_trigger_(new Trigger()) {} + +void TemplateNumber::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); + } +} + +void TemplateNumber::set(float value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); +} +float TemplateNumber::get_setup_priority() const { return setup_priority::HARDWARE; } +void TemplateNumber::set_template(std::function()> &&f) { this->f_ = f; } +void TemplateNumber::dump_config() { + LOG_NUMBER("", "Template Number", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + LOG_UPDATE_INTERVAL(this); +} + +void TemplateNumber::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } +Trigger *TemplateNumber::get_set_trigger() const { return this->set_trigger_; }; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h new file mode 100644 index 0000000000..4c633e3b53 --- /dev/null +++ b/esphome/components/template/number/template_number.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace template_ { + +class TemplateNumber : public number::Number, public PollingComponent { + public: + TemplateNumber(); + void set_template(std::function()> &&f); + + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + Trigger *get_set_trigger() const; + void set_optimistic(bool optimistic); + + protected: + void set(float value) override; + bool optimistic_{false}; + Trigger *set_trigger_; + optional()>> f_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 7a6d877d8f..57eef7a946 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -119,6 +119,12 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->cover_json(obj).c_str(), "state"); #endif + +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + if (!obj->is_internal()) + client->send(this->number_json(obj, obj->state).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -196,6 +202,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "cover", ""); #endif +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) + write_row(stream, obj, "number", ""); +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

events_.send(this->number_json(obj, state).c_str(), "state"); +} +void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_numbers()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->number_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::number_json(number::Number *obj, float value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "number-" + obj->get_object_id(); + std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); + root["state"] = state; + root["value"] = value; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -636,6 +673,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_NUMBER + if (request->method() == HTTP_GET && match.domain == "number") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -711,6 +753,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_NUMBER + if (match.domain == "number") { + this->handle_number_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 89e23b7071..4789c6e1c0 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -154,6 +154,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string cover_json(cover::Cover *obj); #endif +#ifdef USE_NUMBER + void on_number_update(number::Number *obj, float state) override; + /// Handle a number request under '/number/'. + void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the number state with its value as a JSON string. + std::string number_json(number::Number *obj, float value); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/const.py b/esphome/const.py index 9b7490878d..e82b66ee37 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -553,6 +553,7 @@ CONF_STATE_CLASS = "state_class" CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" CONF_STATUS = "status" +CONF_STEP = "step" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" diff --git a/esphome/core/application.h b/esphome/core/application.h index 774f6e3aa8..e065552a74 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -32,6 +32,9 @@ #ifdef USE_COVER #include "esphome/components/cover/cover.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif namespace esphome { @@ -82,6 +85,10 @@ class Application { void register_light(light::LightState *light) { this->lights_.push_back(light); } #endif +#ifdef USE_NUMBER + void register_number(number::Number *number) { this->numbers_.push_back(number); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -208,6 +215,15 @@ class Application { return nullptr; } #endif +#ifdef USE_NUMBER + const std::vector &get_numbers() { return this->numbers_; } + number::Number *get_number_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->numbers_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif Scheduler scheduler; @@ -245,6 +261,9 @@ class Application { #ifdef USE_LIGHT std::vector lights_{}; #endif +#ifdef USE_NUMBER + std::vector numbers_{}; +#endif std::string name_; std::string compilation_time_; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index f3d10b23ff..305fe93532 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -53,6 +53,12 @@ void Controller::setup_controller() { obj->add_on_state_callback([this, obj]() { this->on_climate_update(obj); }); } #endif +#ifdef USE_NUMBER + for (auto *obj : App.get_numbers()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 0e94a43c4c..746658075f 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -25,6 +25,9 @@ #ifdef USE_CLIMATE #include "esphome/components/climate/climate.h" #endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif namespace esphome { @@ -55,6 +58,9 @@ class Controller { #ifdef USE_CLIMATE virtual void on_climate_update(climate::Climate *obj){}; #endif +#ifdef USE_NUMBER + virtual void on_number_update(number::Number *obj, float state){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 90562510b9..cac03fc703 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -13,6 +13,7 @@ #define USE_COVER #define USE_LIGHT #define USE_CLIMATE +#define USE_NUMBER #define USE_MQTT #define USE_POWER_SUPPLY #define USE_HOMEASSISTANT_TIME diff --git a/script/ci-custom.py b/script/ci-custom.py index 4ec7c664a4..02f193c6e0 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -559,6 +559,7 @@ def lint_inclusive_language(fname, match): "esphome/components/display/display_buffer.h", "esphome/components/i2c/i2c.h", "esphome/components/mqtt/mqtt_component.h", + "esphome/components/number/number.h", "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", "esphome/components/sensor/sensor.h", diff --git a/tests/test5.yaml b/tests/test5.yaml index ba047721e2..35225402a3 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -38,3 +38,20 @@ esp32_improv: authorizer: io0_button authorized_duration: 1min status_indicator: built_in_led + +number: + - platform: template + name: My template number + id: template_number_id + optimistic: true + on_value: + - logger.log: + format: "Number changed to %f" + args: ["x"] + set_action: + - logger.log: + format: "Template Number set to %f" + args: ["x"] + max_value: 100 + min_value: 0 + step: 5 From d77c3abdc05ed4d4414ace1dfd825dea06bb1cb2 Mon Sep 17 00:00:00 2001 From: monkeyclass <30174727+monkeyclass@users.noreply.github.com> Date: Mon, 12 Jul 2021 21:37:34 +0200 Subject: [PATCH 556/643] Fixed lolin32 lite key (#2001) Co-authored-by: monkeyclass --- esphome/pins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/pins.py b/esphome/pins.py index fef77f3946..6356ae9bd0 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -538,7 +538,7 @@ ESP32_BOARD_PINS = { "iotbusio": {}, "iotbusproteus": {}, "lolin32": {"LED": 5}, - "lolin32-lite": {"LED": 22}, + "lolin32_lite": {"LED": 22}, "lolin_d32": {"LED": 5, "_VBAT": 35}, "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, "lopy": { From cc9f0b3f47e5310aac38cb7d90c89e320d8c25af Mon Sep 17 00:00:00 2001 From: Mikko Tervala Date: Mon, 12 Jul 2021 23:55:53 +0300 Subject: [PATCH 557/643] Add support for IBS-TH1 External Sensor (#1983) --- .../inkbird_ibsth1_mini.cpp | 30 +++++++++++++++++-- .../inkbird_ibsth1_mini/inkbird_ibsth1_mini.h | 2 ++ .../components/inkbird_ibsth1_mini/sensor.py | 12 ++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 03bc4f9f92..45be7c1acf 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -11,6 +11,7 @@ static const char *const TAG = "inkbird_ibsth1_mini"; void InkbirdIBSTH1_MINI::dump_config() { ESP_LOGCONFIG(TAG, "Inkbird IBS TH1 MINI"); LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "External Temperature", this->external_temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); } @@ -54,7 +55,7 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi ESP_LOGVV(TAG, "parse_device(): manufacturer data element length is expected to be of length 7"); return false; } - if ((mnfData.data[2] != 0) || (mnfData.data[6] != 8)) { + if (mnfData.data[6] != 8) { ESP_LOGVV(TAG, "parse_device(): unexpected data"); return false; } @@ -63,13 +64,36 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // data[5] is a battery level // data[0] and data[1] is humidity * 100 (in pct) // uuid is a temperature * 100 (in Celcius) + // when data[2] == 0 temperature is from internal sensor (IBS-TH1 or IBS-TH1 Mini) + // when data[2] == 1 temperature is from external sensor (IBS-TH1 only) + + // Create empty variables to pass automatic checks + auto temperature = NAN; + auto external_temperature = NAN; + + // Read bluetooth data into variable + auto measured_temperature = mnfData.uuid.get_uuid().uuid.uuid16 / 100.0f; + + // Set temperature or external_temperature based on which sensor is in use + if (mnfData.data[2] == 0) { + temperature = measured_temperature; + } else if (mnfData.data[2] == 1) { + external_temperature = measured_temperature; + } else { + ESP_LOGVV(TAG, "parse_device(): unknown sensor type"); + return false; + } + auto battery_level = mnfData.data[5]; - auto temperature = mnfData.uuid.get_uuid().uuid.uuid16 / 100.0f; auto humidity = ((mnfData.data[1] << 8) + mnfData.data[0]) / 100.0f; - if (this->temperature_ != nullptr) { + // Send temperature only if the value is set + if (!isnan(temperature) && this->temperature_ != nullptr) { this->temperature_->publish_state(temperature); } + if (!isnan(external_temperature) && this->external_temperature_ != nullptr) { + this->external_temperature_->publish_state(external_temperature); + } if (this->humidity_ != nullptr) { this->humidity_->publish_state(humidity); } diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h index 38e72dad17..c3a9f7062d 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.h @@ -18,12 +18,14 @@ class InkbirdIBSTH1_MINI : public Component, public esp32_ble_tracker::ESPBTDevi void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_external_temperature(sensor::Sensor *external_temperature) { external_temperature_ = external_temperature; } void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } protected: uint64_t address_; sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *external_temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; }; diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index 044e7fe67d..aaaaddb890 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -19,6 +19,8 @@ from esphome.const import ( CODEOWNERS = ["@fkirill"] DEPENDENCIES = ["esp32_ble_tracker"] +CONF_EXTERNAL_TEMPERATURE = "external_temperature" + inkbird_ibsth1_mini_ns = cg.esphome_ns.namespace("inkbird_ibsth1_mini") InkbirdUBSTH1_MINI = inkbird_ibsth1_mini_ns.class_( "InkbirdIBSTH1_MINI", esp32_ble_tracker.ESPBTDeviceListener, cg.Component @@ -36,6 +38,13 @@ CONFIG_SCHEMA = ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, + ICON_EMPTY, + 1, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( UNIT_PERCENT, ICON_EMPTY, @@ -67,6 +76,9 @@ async def to_code(config): if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) + if CONF_EXTERNAL_TEMPERATURE in config: + sens = await sensor.new_sensor(config[CONF_EXTERNAL_TEMPERATURE]) + cg.add(var.set_external_temperature(sens)) if CONF_HUMIDITY in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) From 551e9c6111d5f0cd797778bccafc878f8d81875b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 12 Jul 2021 22:56:55 +0200 Subject: [PATCH 558/643] Bang bang climate new mode meanings (#1996) --- .../components/bang_bang/bang_bang_climate.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index c043f6b7de..4b41684707 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -21,7 +21,12 @@ void BangBangClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles those for us - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + if (supports_cool_ && supports_heat_) + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + else if (supports_cool_) + this->mode = climate::CLIMATE_MODE_COOL; + else if (supports_heat_) + this->mode = climate::CLIMATE_MODE_HEAT; this->change_away_(false); } } @@ -43,12 +48,13 @@ climate::ClimateTraits BangBangClimate::traits() { traits.set_supports_current_temperature(true); traits.set_supported_modes({ climate::CLIMATE_MODE_OFF, - climate::CLIMATE_MODE_HEAT_COOL, }); if (supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); if (supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); + if (supports_cool_ && supports_heat_) + traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); traits.set_supports_two_point_target_temperature(true); if (supports_away_) traits.set_supported_presets({ @@ -59,12 +65,8 @@ climate::ClimateTraits BangBangClimate::traits() { return traits; } void BangBangClimate::compute_state_() { - if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) { - // in non-auto mode, switch directly to appropriate action - // - HEAT mode -> HEATING action - // - COOL mode -> COOLING action - // - OFF mode -> OFF action (not IDLE!) - this->switch_to_action_(static_cast(this->mode)); + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { From 7dd16df846989ddba21b5927912c8ea8d4545634 Mon Sep 17 00:00:00 2001 From: Huub Eikens Date: Mon, 12 Jul 2021 23:21:54 +0200 Subject: [PATCH 559/643] Sgp30 sensor improvements (#1510) Co-authored-by: Umberto73 Co-authored-by: Guillermo Ruffino --- esphome/components/ccs811/sensor.py | 5 +-- esphome/components/sgp30/sensor.py | 28 +++++++++++-- esphome/components/sgp30/sgp30.cpp | 65 +++++++++++++++++++++++++++-- esphome/components/sgp30/sgp30.h | 16 +++++++ esphome/const.py | 2 + 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4c4f8802d4..4e81d6ac10 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -8,6 +8,8 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, + CONF_BASELINE, + CONF_ECO2, CONF_TEMPERATURE, CONF_TVOC, CONF_HUMIDITY, @@ -21,9 +23,6 @@ CCS811Component = ccs811_ns.class_( "CCS811Component", cg.PollingComponent, i2c.I2CDevice ) -CONF_ECO2 = "eco2" -CONF_BASELINE = "baseline" - CONFIG_SCHEMA = ( cv.Schema( { diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index f393627eda..7a3e870f6d 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -3,13 +3,16 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, + CONF_BASELINE, DEVICE_CLASS_EMPTY, + CONF_ECO2, + CONF_TVOC, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, + UNIT_EMPTY, ICON_MOLECULE_CO2, - CONF_TVOC, ) DEPENDENCIES = ["i2c"] @@ -17,10 +20,9 @@ DEPENDENCIES = ["i2c"] sgp30_ns = cg.esphome_ns.namespace("sgp30") SGP30Component = sgp30_ns.class_("SGP30Component", cg.PollingComponent, i2c.I2CDevice) -CONF_ECO2 = "eco2" -CONF_BASELINE = "baseline" CONF_ECO2_BASELINE = "eco2_baseline" CONF_TVOC_BASELINE = "tvoc_baseline" +CONF_STORE_BASELINE = "store_baseline" CONF_UPTIME = "uptime" CONF_COMPENSATION = "compensation" CONF_HUMIDITY_SOURCE = "humidity_source" @@ -44,6 +46,13 @@ CONFIG_SCHEMA = ( DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( + UNIT_EMPTY, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema( + UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_BASELINE): cv.Schema( { cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, @@ -58,7 +67,7 @@ CONFIG_SCHEMA = ( ), } ) - .extend(cv.polling_component_schema("60s")) + .extend(cv.polling_component_schema("1s")) .extend(i2c.i2c_device_schema(0x58)) ) @@ -76,6 +85,17 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_tvoc_sensor(sens)) + if CONF_ECO2_BASELINE in config: + sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE]) + cg.add(var.set_eco2_baseline_sensor(sens)) + + if CONF_TVOC_BASELINE in config: + sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE]) + cg.add(var.set_tvoc_baseline_sensor(sens)) + + if CONF_STORE_BASELINE in config: + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + if CONF_BASELINE in config: baseline_config = config[CONF_BASELINE] cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 56e5c7214c..fdc6ae031d 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,5 +1,6 @@ #include "sgp30.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace sgp30 { @@ -22,6 +23,13 @@ const uint32_t IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; // if the sensor starts without any prior baseline value provided const uint32_t IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; +// Shortest time interval of 1H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 3600; + +// Store anyway if the baseline difference exceeds the max storage diff value +const uint32_t MAXIMUM_STORAGE_DIFF = 50; + void SGP30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SGP30..."); @@ -73,6 +81,21 @@ void SGP30Component::setup() { return; } + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences.make_preference(hash, true); + + if (this->pref_.load(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Loaded eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + baselines_storage_.tvoc); + this->eco2_baseline_ = this->baselines_storage_.eco2; + this->tvoc_baseline_ = this->baselines_storage_.tvoc; + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + // Sensor baseline reliability timer if (this->eco2_baseline_ > 0 && this->tvoc_baseline_ > 0) { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; @@ -110,6 +133,31 @@ void SGP30Component::read_iaq_baseline_() { uint16_t tvocbaseline = (raw_data[1]); ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); + if (eco2baseline != this->eco2_baseline_ || tvocbaseline != this->tvoc_baseline_) { + this->eco2_baseline_ = eco2baseline; + this->tvoc_baseline_ = tvocbaseline; + if (this->eco2_sensor_baseline_ != nullptr) + this->eco2_sensor_baseline_->publish_state(this->eco2_baseline_); + if (this->tvoc_sensor_baseline_ != nullptr) + this->tvoc_sensor_baseline_->publish_state(this->tvoc_baseline_); + + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && + (this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL || + abs(this->baselines_storage_.eco2 - this->eco2_baseline_) > MAXIMUM_STORAGE_DIFF || + abs(this->baselines_storage_.tvoc - this->tvoc_baseline_) > MAXIMUM_STORAGE_DIFF)) { + this->seconds_since_last_store_ = 0; + this->baselines_storage_.eco2 = this->eco2_baseline_; + this->baselines_storage_.tvoc = this->tvoc_baseline_; + if (this->pref_.save(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Store eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + this->baselines_storage_.tvoc); + } else { + ESP_LOGW(TAG, "Could not store eCO2 and TVOC baselines"); + } + } + } this->status_clear_warning(); }); } else { @@ -171,7 +219,8 @@ void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_b if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); } else - ESP_LOGI(TAG, "Initial eCO2 and TVOC baselines applied successfully!"); + ESP_LOGI(TAG, "Initial baselines applied successfully! eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, + tvoc_baseline); } void SGP30Component::dump_config() { @@ -207,8 +256,11 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, " Warm up time: %us", this->required_warm_up_time_); } LOG_UPDATE_INTERVAL(this); - LOG_SENSOR(" ", "eCO2", this->eco2_sensor_); - LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + LOG_SENSOR(" ", "eCO2 sensor", this->eco2_sensor_); + LOG_SENSOR(" ", "TVOC sensor", this->tvoc_sensor_); + LOG_SENSOR(" ", "eCO2 baseline sensor", this->eco2_sensor_baseline_); + LOG_SENSOR(" ", "TVOC baseline sensor", this->tvoc_sensor_baseline_); + ESP_LOGCONFIG(TAG, "Store baseline: %s", YESNO(this->store_baseline_)); if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); @@ -223,7 +275,7 @@ void SGP30Component::update() { this->status_set_warning(); return; } - + this->seconds_since_last_store_ += this->update_interval_ / 1000; this->set_timeout(50, [this]() { uint16_t raw_data[2]; if (!this->read_data_(raw_data, 2)) { @@ -239,6 +291,11 @@ void SGP30Component::update() { this->eco2_sensor_->publish_state(eco2); if (this->tvoc_sensor_ != nullptr) this->tvoc_sensor_->publish_state(tvoc); + + if (this->get_update_interval() != 1000) { + ESP_LOGW(TAG, "Update interval for SGP30 sensor must be set to 1s for optimized readout"); + } + this->status_clear_warning(); this->send_env_data_(); this->read_iaq_baseline_(); diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 298a78e8dd..91a1c1e9c7 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -3,16 +3,25 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/core/preferences.h" #include namespace esphome { namespace sgp30 { +struct SGP30Baselines { + uint16_t eco2; + uint16_t tvoc; +} PACKED; + /// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. class SGP30Component : public PollingComponent, public i2c::I2CDevice { public: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_eco2_baseline_sensor(sensor::Sensor *eco2_baseline) { eco2_sensor_baseline_ = eco2_baseline; } + void set_tvoc_baseline_sensor(sensor::Sensor *tvoc_baseline) { tvoc_sensor_baseline_ = tvoc_baseline; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } void set_eco2_baseline(uint16_t eco2_baseline) { eco2_baseline_ = eco2_baseline; } void set_tvoc_baseline(uint16_t tvoc_baseline) { tvoc_baseline_ = tvoc_baseline; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } @@ -34,6 +43,9 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { uint64_t serial_number_; uint16_t featureset_; uint32_t required_warm_up_time_; + uint32_t seconds_since_last_store_; + SGP30Baselines baselines_storage_; + ESPPreferenceObject pref_; enum ErrorCode { COMMUNICATION_FAILED, @@ -45,8 +57,12 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; + sensor::Sensor *eco2_sensor_baseline_{nullptr}; + sensor::Sensor *tvoc_sensor_baseline_{nullptr}; uint16_t eco2_baseline_{0x0000}; uint16_t tvoc_baseline_{0x0000}; + bool store_baseline_; + /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/const.py b/esphome/const.py index e82b66ee37..e676056581 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -77,6 +77,7 @@ CONF_AVAILABILITY = "availability" CONF_AWAY = "away" CONF_AWAY_CONFIG = "away_config" CONF_BACKLIGHT_PIN = "backlight_pin" +CONF_BASELINE = "baseline" CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_VOLTAGE = "battery_voltage" CONF_BAUD_RATE = "baud_rate" @@ -189,6 +190,7 @@ CONF_DUMP = "dump" CONF_DURATION = "duration" CONF_EAP = "eap" CONF_ECHO_PIN = "echo_pin" +CONF_ECO2 = "eco2" CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" From fb8ec79a52120001ab59feb5205a5a06b2276cfb Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 13 Jul 2021 02:28:29 +0200 Subject: [PATCH 560/643] Color brightness fixes (#2008) --- esphome/components/light/light_call.cpp | 23 ++++++++++++------- esphome/components/light/light_color_values.h | 7 +++--- esphome/components/light/light_state.cpp | 3 +++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 557d001321..d7e8ce6298 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -32,19 +32,26 @@ LightCall &LightCall::parse_color_json(JsonObject &root) { if (root.containsKey("color")) { JsonObject &color = root["color"]; + // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. + float max_rgb = 0.0f; if (color.containsKey("r")) { - this->set_red(float(color["r"]) / 255.0f); + float r = float(color["r"]) / 255.0f; + max_rgb = fmaxf(max_rgb, r); + this->set_red(r); } if (color.containsKey("g")) { - this->set_green(float(color["g"]) / 255.0f); + float g = float(color["g"]) / 255.0f; + max_rgb = fmaxf(max_rgb, g); + this->set_green(g); } if (color.containsKey("b")) { - this->set_blue(float(color["b"]) / 255.0f); + float b = float(color["b"]) / 255.0f; + max_rgb = fmaxf(max_rgb, b); + this->set_blue(b); + } + if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + this->set_color_brightness(max_rgb); } - } - - if (root.containsKey("color_brightness")) { - this->set_color_brightness(float(root["color_brightness"]) / 255.0f); } if (root.containsKey("white_value")) { @@ -418,7 +425,7 @@ LightCall &LightCall::set_brightness_if_supported(float brightness) { } LightCall &LightCall::set_color_brightness_if_supported(float brightness) { if (this->parent_->get_traits().get_supports_rgb_white_value()) - this->set_brightness(brightness); + this->set_color_brightness(brightness); return *this; } LightCall &LightCall::set_red_if_supported(float red) { diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 4b6ca9e576..54dcaea5a3 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -135,12 +135,11 @@ class LightColorValues { root["brightness"] = uint8_t(this->get_brightness() * 255); if (traits.get_supports_rgb()) { JsonObject &color = root.createNestedObject("color"); - color["r"] = uint8_t(this->get_red() * 255); - color["g"] = uint8_t(this->get_green() * 255); - color["b"] = uint8_t(this->get_blue() * 255); + color["r"] = uint8_t(this->get_color_brightness() * this->get_red() * 255); + color["g"] = uint8_t(this->get_color_brightness() * this->get_green() * 255); + color["b"] = uint8_t(this->get_color_brightness() * this->get_blue() * 255); } if (traits.get_supports_rgb_white_value()) { - root["color_brightness"] = uint8_t(this->get_color_brightness() * 255); root["white_value"] = uint8_t(this->get_white() * 255); } if (traits.get_supports_color_temperature()) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 69d147b6f3..a97e4f6790 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -18,6 +18,7 @@ LightCall LightState::make_call() { return LightCall(this); } struct LightStateRTCState { bool state{false}; float brightness{1.0f}; + float color_brightness{1.0f}; float red{1.0f}; float green{1.0f}; float blue{1.0f}; @@ -65,6 +66,7 @@ void LightState::setup() { call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); + call.set_color_brightness_if_supported(recovered.color_brightness); call.set_red_if_supported(recovered.red); call.set_green_if_supported(recovered.green); call.set_blue_if_supported(recovered.blue); @@ -244,6 +246,7 @@ void LightState::save_remote_values_() { LightStateRTCState saved; saved.state = this->remote_values.is_on(); saved.brightness = this->remote_values.get_brightness(); + saved.color_brightness = this->remote_values.get_color_brightness(); saved.red = this->remote_values.get_red(); saved.green = this->remote_values.get_green(); saved.blue = this->remote_values.get_blue(); From b632344596bad99b1f2890c0eecaf994382e2224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jul 2021 11:39:04 +0200 Subject: [PATCH 561/643] Bump black from 21.5b1 to 21.6b0 (#2011) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 38717f3f77..f9566d5adc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==2.8.2 flake8==3.9.2 -black==21.5b1 +black==21.6b0 pexpect==4.8.0 pre-commit From 04c3a43c17a1383597696be0483ffbebb8bedaad Mon Sep 17 00:00:00 2001 From: Sourabh Jaiswal Date: Wed, 14 Jul 2021 06:35:51 +0530 Subject: [PATCH 562/643] Added support for havells_solar sensor (#1988) Co-authored-by: Oxan van Leeuwen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/havells_solar/__init__.py | 0 .../havells_solar/havells_solar.cpp | 165 ++++++++++ .../components/havells_solar/havells_solar.h | 115 +++++++ .../havells_solar/havells_solar_registers.h | 49 +++ esphome/components/havells_solar/sensor.py | 293 ++++++++++++++++++ 6 files changed, 623 insertions(+) create mode 100644 esphome/components/havells_solar/__init__.py create mode 100644 esphome/components/havells_solar/havells_solar.cpp create mode 100644 esphome/components/havells_solar/havells_solar.h create mode 100644 esphome/components/havells_solar/havells_solar_registers.h create mode 100644 esphome/components/havells_solar/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 2a57e5d81a..d6769800cd 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -47,6 +47,7 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle +esphome/components/havells_solar/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter esphome/components/i2c/* @esphome/core esphome/components/improv/* @jesserockz diff --git a/esphome/components/havells_solar/__init__.py b/esphome/components/havells_solar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp new file mode 100644 index 0000000000..f029df10ad --- /dev/null +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -0,0 +1,165 @@ +#include "havells_solar.h" +#include "havells_solar_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace havells_solar { + +static const char *const TAG = "havells_solar"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x03; +static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers + +void HavellsSolar::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for HavellsSolar!"); + return; + } + + /* Usage: returns the float value of 1 register read by modbus + Arg1: Register address * number of bytes per register + Arg2: Multiplier for final register value + */ + auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); + return temp * unit; + }; + + /* Usage: returns the float value of 2 registers read by modbus + Arg1: Register address * number of bytes per register + Arg2: Multiplier for final register value + */ + auto havells_solar_get_1_register = [&](size_t i, float unit) -> float { + uint16_t temp = encode_uint16(data[i], data[i + 1]); + return temp * unit; + }; + + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + + float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); + float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); + + if (phase.voltage_sensor_ != nullptr) + phase.voltage_sensor_->publish_state(voltage); + if (phase.current_sensor_ != nullptr) + phase.current_sensor_->publish_state(current); + } + + for (uint8_t i = 0; i < 2; i++) { + auto pv = this->pvs_[i]; + if (!pv.setup) + continue; + + float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); + float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); + float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT); + float voltage_sampled_by_secondary_cpu = + havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT); + float insulation_of_p_to_ground = + havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT); + + if (pv.voltage_sensor_ != nullptr) + pv.voltage_sensor_->publish_state(voltage); + if (pv.current_sensor_ != nullptr) + pv.current_sensor_->publish_state(current); + if (pv.active_power_sensor_ != nullptr) + pv.active_power_sensor_->publish_state(active_power); + if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr) + pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu); + if (pv.insulation_of_p_to_ground_sensor_ != nullptr) + pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground); + } + + float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT); + float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT); + float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT); + float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT); + float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT); + float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT); + float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT); + float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT); + float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT); + float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT); + float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT); + float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT); + float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT); + float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT); + float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT); + + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->today_production_sensor_ != nullptr) + this->today_production_sensor_->publish_state(today_production); + if (this->total_energy_production_sensor_ != nullptr) + this->total_energy_production_sensor_->publish_state(total_energy_production); + if (this->total_generation_time_sensor_ != nullptr) + this->total_generation_time_sensor_->publish_state(total_generation_time); + if (this->today_generation_time_sensor_ != nullptr) + this->today_generation_time_sensor_->publish_state(today_generation_time); + if (this->inverter_module_temp_sensor_ != nullptr) + this->inverter_module_temp_sensor_->publish_state(inverter_module_temp); + if (this->inverter_inner_temp_sensor_ != nullptr) + this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp); + if (this->inverter_bus_voltage_sensor_ != nullptr) + this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage); + if (this->insulation_pv_n_to_ground_sensor_ != nullptr) + this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground); + if (this->gfci_value_sensor_ != nullptr) + this->gfci_value_sensor_->publish_state(gfci_value); + if (this->dci_of_r_sensor_ != nullptr) + this->dci_of_r_sensor_->publish_state(dci_of_r); + if (this->dci_of_s_sensor_ != nullptr) + this->dci_of_s_sensor_->publish_state(dci_of_s); + if (this->dci_of_t_sensor_ != nullptr) + this->dci_of_t_sensor_->publish_state(dci_of_t); +} + +void HavellsSolar::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void HavellsSolar::dump_config() { + ESP_LOGCONFIG(TAG, "HAVELLS Solar:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + for (uint8_t i = 0; i < 3; i++) { + auto phase = this->phases_[i]; + if (!phase.setup) + continue; + ESP_LOGCONFIG(TAG, " Phase %c", i + 'A'); + LOG_SENSOR(" ", "Voltage", phase.voltage_sensor_); + LOG_SENSOR(" ", "Current", phase.current_sensor_); + } + for (uint8_t i = 0; i < 2; i++) { + auto pv = this->pvs_[i]; + if (!pv.setup) + continue; + ESP_LOGCONFIG(TAG, " PV %d", i + 1); + LOG_SENSOR(" ", "Voltage", pv.voltage_sensor_); + LOG_SENSOR(" ", "Current", pv.current_sensor_); + LOG_SENSOR(" ", "Active Power", pv.active_power_sensor_); + LOG_SENSOR(" ", "Voltage Sampled By Secondary CPU", pv.voltage_sampled_by_secondary_cpu_sensor_); + LOG_SENSOR(" ", "Insulation Of PV+ To Ground", pv.insulation_of_p_to_ground_sensor_); + } + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Today Generation", this->today_production_sensor_); + LOG_SENSOR(" ", "Total Generation", this->total_energy_production_sensor_); + LOG_SENSOR(" ", "Total Generation Time", this->total_generation_time_sensor_); + LOG_SENSOR(" ", "Today Generation Time", this->today_generation_time_sensor_); + LOG_SENSOR(" ", "Inverter Module Temp", this->inverter_module_temp_sensor_); + LOG_SENSOR(" ", "Inverter Inner Temp", this->inverter_inner_temp_sensor_); + LOG_SENSOR(" ", "Inverter Bus Voltage", this->inverter_bus_voltage_sensor_); + LOG_SENSOR(" ", "Insulation Of PV- To Ground", this->insulation_pv_n_to_ground_sensor_); + LOG_SENSOR(" ", "GFCI Value", this->gfci_value_sensor_); + LOG_SENSOR(" ", "DCI Of R", this->dci_of_r_sensor_); + LOG_SENSOR(" ", "DCI Of S", this->dci_of_s_sensor_); + LOG_SENSOR(" ", "DCI Of T", this->dci_of_t_sensor_); +} + +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h new file mode 100644 index 0000000000..2ccc8be3d4 --- /dev/null +++ b/esphome/components/havells_solar/havells_solar.h @@ -0,0 +1,115 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace havells_solar { + +class HavellsSolar : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(uint8_t phase, sensor::Sensor *voltage_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor(uint8_t phase, sensor::Sensor *current_sensor) { + this->phases_[phase].setup = true; + this->phases_[phase].current_sensor_ = current_sensor; + } + void set_voltage_sensor_pv(uint8_t pv, sensor::Sensor *voltage_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].voltage_sensor_ = voltage_sensor; + } + void set_current_sensor_pv(uint8_t pv, sensor::Sensor *current_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].current_sensor_ = current_sensor; + } + void set_active_power_sensor_pv(uint8_t pv, sensor::Sensor *active_power_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].active_power_sensor_ = active_power_sensor; + } + void set_voltage_sampled_by_secondary_cpu_sensor_pv(uint8_t pv, + sensor::Sensor *voltage_sampled_by_secondary_cpu_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].voltage_sampled_by_secondary_cpu_sensor_ = voltage_sampled_by_secondary_cpu_sensor; + } + void set_insulation_of_p_to_ground_sensor_pv(uint8_t pv, sensor::Sensor *insulation_of_p_to_ground_sensor) { + this->pvs_[pv].setup = true; + this->pvs_[pv].insulation_of_p_to_ground_sensor_ = insulation_of_p_to_ground_sensor; + } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } + void set_active_power_sensor(sensor::Sensor *active_power_sensor) { + this->active_power_sensor_ = active_power_sensor; + } + void set_reactive_power_sensor(sensor::Sensor *reactive_power_sensor) { + this->reactive_power_sensor_ = reactive_power_sensor; + } + void set_today_production_sensor(sensor::Sensor *today_production_sensor) { + this->today_production_sensor_ = today_production_sensor; + } + void set_total_energy_production_sensor(sensor::Sensor *total_energy_production_sensor) { + this->total_energy_production_sensor_ = total_energy_production_sensor; + } + void set_total_generation_time_sensor(sensor::Sensor *total_generation_time_sensor) { + this->total_generation_time_sensor_ = total_generation_time_sensor; + } + void set_today_generation_time_sensor(sensor::Sensor *today_generation_time_sensor) { + this->today_generation_time_sensor_ = today_generation_time_sensor; + } + void set_inverter_module_temp_sensor(sensor::Sensor *inverter_module_temp_sensor) { + this->inverter_module_temp_sensor_ = inverter_module_temp_sensor; + } + void set_inverter_inner_temp_sensor(sensor::Sensor *inverter_inner_temp_sensor) { + this->inverter_inner_temp_sensor_ = inverter_inner_temp_sensor; + } + void set_inverter_bus_voltage_sensor(sensor::Sensor *inverter_bus_voltage_sensor) { + this->inverter_bus_voltage_sensor_ = inverter_bus_voltage_sensor; + } + void set_insulation_pv_n_to_ground_sensor(sensor::Sensor *insulation_pv_n_to_ground_sensor) { + this->insulation_pv_n_to_ground_sensor_ = insulation_pv_n_to_ground_sensor; + } + void set_gfci_value_sensor(sensor::Sensor *gfci_value_sensor) { this->gfci_value_sensor_ = gfci_value_sensor; } + void set_dci_of_r_sensor(sensor::Sensor *dci_of_r_sensor) { this->dci_of_r_sensor_ = dci_of_r_sensor; } + void set_dci_of_s_sensor(sensor::Sensor *dci_of_s_sensor) { this->dci_of_s_sensor_ = dci_of_s_sensor; } + void set_dci_of_t_sensor(sensor::Sensor *dci_of_t_sensor) { this->dci_of_t_sensor_ = dci_of_t_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + struct HAVELLSPhase { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + } phases_[3]; + struct HAVELLSPV { + bool setup{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *voltage_sampled_by_secondary_cpu_sensor_{nullptr}; + sensor::Sensor *insulation_of_p_to_ground_sensor_{nullptr}; + } pvs_[2]; + sensor::Sensor *frequency_sensor_{nullptr}; + sensor::Sensor *active_power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *today_production_sensor_{nullptr}; + sensor::Sensor *total_energy_production_sensor_{nullptr}; + sensor::Sensor *total_generation_time_sensor_{nullptr}; + sensor::Sensor *today_generation_time_sensor_{nullptr}; + sensor::Sensor *inverter_module_temp_sensor_{nullptr}; + sensor::Sensor *inverter_inner_temp_sensor_{nullptr}; + sensor::Sensor *inverter_bus_voltage_sensor_{nullptr}; + sensor::Sensor *insulation_pv_n_to_ground_sensor_{nullptr}; + sensor::Sensor *gfci_value_sensor_{nullptr}; + sensor::Sensor *dci_of_r_sensor_{nullptr}; + sensor::Sensor *dci_of_s_sensor_{nullptr}; + sensor::Sensor *dci_of_t_sensor_{nullptr}; +}; + +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/havells_solar_registers.h b/esphome/components/havells_solar/havells_solar_registers.h new file mode 100644 index 0000000000..8e1cb3ec7a --- /dev/null +++ b/esphome/components/havells_solar/havells_solar_registers.h @@ -0,0 +1,49 @@ +#pragma once +namespace esphome { +namespace havells_solar { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; + +/* PV Input Message */ +static const uint16_t HAVELLS_PV_1_VOLTAGE = 0x0006; +static const uint16_t HAVELLS_PV_1_CURRENT = 0x0007; +static const uint16_t HAVELLS_PV_2_VOLTAGE = 0x0008; +static const uint16_t HAVELLS_PV_2_CURRENT = 0x0009; +static const uint16_t HAVELLS_PV_1_POWER = 0x000A; +static const uint16_t HAVELLS_PV_2_POWER = 0x000B; + +/* Output Grid Message */ +static const uint16_t HAVELLS_SYSTEM_ACTIVE_POWER = 0x000C; +static const uint16_t HAVELLS_SYSTEM_REACTIVE_POWER = 0x000D; +static const uint16_t HAVELLS_GRID_FREQUENCY = 0x000E; +static const uint16_t HAVELLS_PHASE_1_VOLTAGE = 0x000F; +static const uint16_t HAVELLS_PHASE_1_CURRENT = 0x0010; +static const uint16_t HAVELLS_PHASE_2_VOLTAGE = 0x0011; +static const uint16_t HAVELLS_PHASE_2_CURRENT = 0x0012; +static const uint16_t HAVELLS_PHASE_3_VOLTAGE = 0x0013; +static const uint16_t HAVELLS_PHASE_3_CURRENT = 0x0014; + +/* Inverter Generation message */ +static const uint16_t HAVELLS_TOTAL_ENERGY_PRODUCTION = 0x0015; +static const uint16_t HAVELLS_TOTAL_GENERATION_TIME = 0x0017; +static const uint16_t HAVELLS_TODAY_PRODUCTION = 0x0019; +static const uint16_t HAVELLS_TODAY_GENERATION_TIME = 0x001A; + +/* Inverter inner message */ +static const uint16_t HAVELLS_INVERTER_MODULE_TEMP = 0x001B; +static const uint16_t HAVELLS_INVERTER_INNER_TEMP = 0x001C; +static const uint16_t HAVELLS_INVERTER_BUS_VOLTAGE = 0x001D; +static const uint16_t HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = 0x001E; +static const uint16_t HAVELLS_PV2_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = 0x001F; +static const uint16_t HAVELLS_PV1_INSULATION_OF_P_TO_GROUND = 0x0024; +static const uint16_t HAVELLS_PV2_INSULATION_OF_P_TO_GROUND = 0x0025; +static const uint16_t HAVELLS_INSULATION_OF_PV_N_TO_GROUND = 0x0026; +static const uint16_t HAVELLS_GFCI_VALUE = 0x002A; +static const uint16_t HAVELLS_DCI_OF_R = 0x002B; +static const uint16_t HAVELLS_DCI_OF_S = 0x002C; +static const uint16_t HAVELLS_DCI_OF_T = 0x002D; +} // namespace havells_solar +} // namespace esphome diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py new file mode 100644 index 0000000000..7d1e2be581 --- /dev/null +++ b/esphome/components/havells_solar/sensor.py @@ -0,0 +1,293 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_CURRENT, + CONF_FREQUENCY, + CONF_ID, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_AMPERE, + UNIT_DEGREES, + UNIT_HERTZ, + UNIT_MINUTE, + UNIT_VOLT, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +CONF_PHASE_A = "phase_a" +CONF_PHASE_B = "phase_b" +CONF_PHASE_C = "phase_c" +CONF_ENERGY_PRODUCTION_DAY = "energy_production_day" +CONF_TOTAL_ENERGY_PRODUCTION = "total_energy_production" +CONF_TOTAL_GENERATION_TIME = "total_generation_time" +CONF_TODAY_GENERATION_TIME = "today_generation_time" +CONF_PV1 = "pv1" +CONF_PV2 = "pv2" +UNIT_KILOWATT_HOURS = "kWh" +UNIT_HOURS = "h" +UNIT_KOHM = "kΩ" +UNIT_MILLIAMPERE = "mA" + + +CONF_INVERTER_MODULE_TEMP = "inverter_module_temp" +CONF_INVERTER_INNER_TEMP = "inverter_inner_temp" +CONF_INVERTER_BUS_VOLTAGE = "inverter_bus_voltage" +CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU = "voltage_sampled_by_secondary_cpu" +CONF_INSULATION_OF_P_TO_GROUND = "insulation_of_p_to_ground" +CONF_INSULATION_OF_PV_N_TO_GROUND = "insulation_of_pv_n_to_ground" +CONF_GFCI_VALUE = "gfci_value" +CONF_DCI_OF_R = "dci_of_r" +CONF_DCI_OF_S = "dci_of_s" +CONF_DCI_OF_T = "dci_of_t" + + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +havells_solar_ns = cg.esphome_ns.namespace("havells_solar") +HavellsSolar = havells_solar_ns.class_( + "HavellsSolar", cg.PollingComponent, modbus.ModbusDevice +) + +PHASE_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), +} +PV_SENSORS = { + CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( + UNIT_KOHM, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), +} + +PHASE_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PHASE_SENSORS.items()} +) +PV_SCHEMA = cv.Schema( + {cv.Optional(sensor): schema for sensor, schema in PV_SENSORS.items()} +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HavellsSolar), + cv.Optional(CONF_PHASE_A): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, + cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, + cv.Optional(CONF_PV1): PV_SCHEMA, + cv.Optional(CONF_PV2): PV_SCHEMA, + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( + UNIT_HERTZ, + ICON_CURRENT_AC, + 2, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, + ICON_EMPTY, + 2, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 0, + DEVICE_CLASS_ENERGY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( + UNIT_HOURS, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( + UNIT_MINUTE, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_NONE, + ), + cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( + UNIT_DEGREES, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INVERTER_INNER_TEMP): sensor.sensor_schema( + UNIT_DEGREES, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( + UNIT_KOHM, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( + UNIT_MILLIAMPERE, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( + UNIT_MILLIAMPERE, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( + UNIT_MILLIAMPERE, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( + UNIT_MILLIAMPERE, + ICON_EMPTY, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + + if CONF_FREQUENCY in config: + sens = await sensor.new_sensor(config[CONF_FREQUENCY]) + cg.add(var.set_frequency_sensor(sens)) + + if CONF_ACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_ACTIVE_POWER]) + cg.add(var.set_active_power_sensor(sens)) + + if CONF_REACTIVE_POWER in config: + sens = await sensor.new_sensor(config[CONF_REACTIVE_POWER]) + cg.add(var.set_reactive_power_sensor(sens)) + + if CONF_ENERGY_PRODUCTION_DAY in config: + sens = await sensor.new_sensor(config[CONF_ENERGY_PRODUCTION_DAY]) + cg.add(var.set_today_production_sensor(sens)) + + if CONF_TOTAL_ENERGY_PRODUCTION in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_ENERGY_PRODUCTION]) + cg.add(var.set_total_energy_production_sensor(sens)) + + if CONF_TOTAL_GENERATION_TIME in config: + sens = await sensor.new_sensor(config[CONF_TOTAL_GENERATION_TIME]) + cg.add(var.set_total_generation_time_sensor(sens)) + + if CONF_TODAY_GENERATION_TIME in config: + sens = await sensor.new_sensor(config[CONF_TODAY_GENERATION_TIME]) + cg.add(var.set_today_generation_time_sensor(sens)) + + if CONF_INVERTER_MODULE_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_MODULE_TEMP]) + cg.add(var.set_inverter_module_temp_sensor(sens)) + + if CONF_INVERTER_INNER_TEMP in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_INNER_TEMP]) + cg.add(var.set_inverter_inner_temp_sensor(sens)) + + if CONF_INVERTER_BUS_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_INVERTER_BUS_VOLTAGE]) + cg.add(var.set_inverter_bus_voltage_sensor(sens)) + + if CONF_INSULATION_OF_PV_N_TO_GROUND in config: + sens = await sensor.new_sensor(config[CONF_INSULATION_OF_PV_N_TO_GROUND]) + cg.add(var.set_insulation_pv_n_to_ground_sensor(sens)) + + if CONF_GFCI_VALUE in config: + sens = await sensor.new_sensor(config[CONF_GFCI_VALUE]) + cg.add(var.set_gfci_value_sensor(sens)) + + if CONF_DCI_OF_R in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_R]) + cg.add(var.set_dci_of_r_sensor(sens)) + + if CONF_DCI_OF_S in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_S]) + cg.add(var.set_dci_of_s_sensor(sens)) + + if CONF_DCI_OF_T in config: + sens = await sensor.new_sensor(config[CONF_DCI_OF_T]) + cg.add(var.set_dci_of_t_sensor(sens)) + + for i, phase in enumerate([CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]): + if phase not in config: + continue + + phase_config = config[phase] + for sensor_type in PHASE_SENSORS: + if sensor_type in phase_config: + sens = await sensor.new_sensor(phase_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor")(i, sens)) + + for i, pv in enumerate([CONF_PV1, CONF_PV2]): + if pv not in config: + continue + + pv_config = config[pv] + for sensor_type in pv_config: + if sensor_type in pv_config: + sens = await sensor.new_sensor(pv_config[sensor_type]) + cg.add(getattr(var, f"set_{sensor_type}_sensor_pv")(i, sens)) From 07ae8ec553e7a81633e30f54b4343741f10aac80 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 14 Jul 2021 14:42:16 +1200 Subject: [PATCH 563/643] Remove a whole bunch of deprecated/removed stuff (#1981) --- esphome/components/binary_sensor/__init__.py | 10 ------ esphome/components/cover/cover.h | 15 ++++----- esphome/components/deep_sleep/__init__.py | 6 ---- .../components/esp32_ble_tracker/__init__.py | 3 -- .../esp32_ble_tracker/binary_sensor.py | 3 -- .../esp32_ble_tracker/esp32_ble_tracker.h | 6 ---- esphome/components/ethernet/__init__.py | 3 -- esphome/components/ledc/output.py | 5 --- esphome/components/light/light_state.cpp | 3 -- esphome/components/light/light_state.h | 8 ----- esphome/components/pcf8574/__init__.py | 18 ++++------ .../components/remote_transmitter/switch.py | 33 ------------------- esphome/components/sensor/sensor.cpp | 3 -- esphome/components/sensor/sensor.h | 10 ------ esphome/components/time/real_time_clock.h | 7 ++-- esphome/components/ultrasonic/sensor.py | 7 ---- esphome/components/wifi/__init__.py | 3 -- esphome/components/xiaomi_miflora/__init__.py | 0 esphome/components/xiaomi_miflora/sensor.py | 3 -- esphome/components/xiaomi_mijia/__init__.py | 0 esphome/components/xiaomi_mijia/sensor.py | 3 -- esphome/const.py | 1 - esphome/core/config.py | 5 --- 23 files changed, 14 insertions(+), 141 deletions(-) delete mode 100644 esphome/components/esp32_ble_tracker/binary_sensor.py delete mode 100644 esphome/components/remote_transmitter/switch.py delete mode 100644 esphome/components/xiaomi_miflora/__init__.py delete mode 100644 esphome/components/xiaomi_miflora/sensor.py delete mode 100644 esphome/components/xiaomi_mijia/__init__.py delete mode 100644 esphome/components/xiaomi_mijia/sensor.py diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 68d4d3e324..8f66978320 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -22,7 +22,6 @@ from esphome.const import ( CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, - CONF_FOR, CONF_NAME, CONF_MQTT_ID, DEVICE_CLASS_EMPTY, @@ -372,11 +371,6 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), } ), - cv.Optional(CONF_INVERTED): cv.invalid( - "The inverted binary_sensor property has been replaced by the " - "new 'invert' binary sensor filter. Please see " - "https://esphome.io/components/binary_sensor/index.html." - ), } ) @@ -455,10 +449,6 @@ async def new_binary_sensor(config): BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(BinarySensor), - cv.Optional(CONF_FOR): cv.invalid( - "This option has been removed in 1.13, please use the " - "'for' condition instead." - ), } ) diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 1af4f9cbea..8f30750fbd 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -110,15 +110,12 @@ class Cover : public Nameable { /// The current operation of the cover (idle, opening, closing). CoverOperation current_operation{COVER_OPERATION_IDLE}; - union { - /** The position of the cover from 0.0 (fully closed) to 1.0 (fully open). - * - * For binary covers this is always equals to 0.0 or 1.0 (see also COVER_OPEN and - * COVER_CLOSED constants). - */ - float position; - ESPDEPRECATED(".state is deprecated, please use .position instead") float state; - }; + /** The position of the cover from 0.0 (fully closed) to 1.0 (fully open). + * + * For binary covers this is always equals to 0.0 or 1.0 (see also COVER_OPEN and + * COVER_CLOSED constants). + */ + float position; /// The current tilt value of the cover from 0.0 to 1.0. float tilt{COVER_OPEN}; diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 7011081774..b7bf27e79a 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_MODE, CONF_NUMBER, CONF_PINS, - CONF_RUN_CYCLES, CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN, @@ -69,11 +68,6 @@ CONFIG_SCHEMA = cv.Schema( } ), ), - cv.Optional(CONF_RUN_CYCLES): cv.invalid( - "The run_cycles option has been removed in 1.11.0 as " - "it was essentially the same as a run_duration of 0s." - "Please use run_duration now." - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 5c70ddb27f..18f1c46ff2 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -163,9 +163,6 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_MANUFACTURER_ID): bt_uuid, } ), - cv.Optional("scan_interval"): cv.invalid( - "This option has been removed in 1.14 (Reason: " "it never had an effect)" - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/esp32_ble_tracker/binary_sensor.py b/esphome/components/esp32_ble_tracker/binary_sensor.py deleted file mode 100644 index 3bea6d9900..0000000000 --- a/esphome/components/esp32_ble_tracker/binary_sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This platform has been renamed to ble_presence in 1.13") diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 6f0c28a73c..0594c4a811 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -85,12 +85,6 @@ class ESPBTDevice { int get_rssi() const { return rssi_; } const std::string &get_name() const { return this->name_; } - ESPDEPRECATED("Use get_tx_powers() instead") - optional get_tx_power() const { - if (this->tx_powers_.empty()) - return {}; - return this->tx_powers_[0]; - } const std::vector &get_tx_powers() const { return tx_powers_; } const optional &get_appearance() const { return appearance_; } diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 94c9ddd2e9..95b7e40151 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -85,9 +85,6 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, - cv.Optional("hostname"): cv.invalid( - "The hostname option has been removed in 1.11.0" - ), } ).extend(cv.COMPONENT_SCHEMA), _validate, diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index 150c5fa410..83b1c6f096 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -3,7 +3,6 @@ from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import ( - CONF_BIT_DEPTH, CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, @@ -50,10 +49,6 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency, cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), - cv.Optional(CONF_BIT_DEPTH): cv.invalid( - "The bit_depth option has been removed in v1.14, the " - "best bit depth is now automatically calculated." - ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index a97e4f6790..e32b15daf1 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -121,9 +121,6 @@ void LightState::loop() { float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } uint32_t LightState::hash_base() { return 1114400283; } -LightColorValues LightState::get_current_values() { return this->current_values; } -LightColorValues LightState::get_remote_values() { return this->remote_values; } - void LightState::publish_state() { this->remote_values_callback_.call(); this->next_write_ = true; diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 5f3441daf7..cd5e6ad1cb 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -68,14 +68,6 @@ class LightState : public Nameable, public Component { */ LightColorValues remote_values; - /// Deprecated method to access current_values. - ESPDEPRECATED("get_current_values() is deprecated, please use .current_values instead.") - LightColorValues get_current_values(); - - /// Deprecated method to access remote_values. - ESPDEPRECATED("get_remote_values() is deprecated, please use .remote_values instead.") - LightColorValues get_remote_values(); - /// Publish the currently active state to the frontend. void publish_state(); diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index e96c526cb0..52e38febaa 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -38,21 +38,13 @@ async def to_code(config): cg.add(var.set_pcf8575(config[CONF_PCF8575])) -def validate_pcf8574_gpio_mode(value): - value = cv.string(value) - if value.upper() == "INPUT_PULLUP": - raise cv.Invalid( - "INPUT_PULLUP mode has been removed in 1.14 and been combined into " - "INPUT mode (they were the same thing). Please use INPUT instead." - ) - return cv.enum(PCF8674_GPIO_MODES, upper=True)(value) - - PCF8574_OUTPUT_PIN_SCHEMA = cv.Schema( { cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="OUTPUT"): validate_pcf8574_gpio_mode, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum( + PCF8674_GPIO_MODES, upper=True + ), cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) @@ -60,7 +52,9 @@ PCF8574_INPUT_PIN_SCHEMA = cv.Schema( { cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component), cv.Required(CONF_NUMBER): cv.int_, - cv.Optional(CONF_MODE, default="INPUT"): validate_pcf8574_gpio_mode, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum( + PCF8674_GPIO_MODES, upper=True + ), cv.Optional(CONF_INVERTED, default=False): cv.boolean, } ) diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py deleted file mode 100644 index 3a2e43a31a..0000000000 --- a/esphome/components/remote_transmitter/switch.py +++ /dev/null @@ -1,33 +0,0 @@ -import esphome.config_validation as cv -from esphome.components.remote_base import BINARY_SENSOR_REGISTRY -from esphome.util import OrderedDict - - -def show_new(value): - from esphome import yaml_util - - for key in BINARY_SENSOR_REGISTRY: - if key in value: - break - else: - raise cv.Invalid( - "This platform has been removed in 1.13, please see the docs for updated " - "instructions." - ) - - val = value[key] - args = [("platform", "template")] - if "id" in value: - args.append(("id", value["id"])) - if "name" in value: - args.append(("name", value["name"])) - args.append(("turn_on_action", {f"remote_transmitter.transmit_{key}": val})) - - text = yaml_util.dump([OrderedDict(args)]) - raise cv.Invalid( - "This platform has been removed in 1.13, please change to:\n\n{}\n\n." - "".format(text) - ) - - -CONFIG_SCHEMA = show_new diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index b19d8be634..fe92f88308 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -28,7 +28,6 @@ void Sensor::publish_state(float state) { this->filter_list_->input(state); } } -void Sensor::push_new_value(float state) { this->publish_state(state); } std::string Sensor::unit_of_measurement() { return ""; } std::string Sensor::icon() { return ""; } uint32_t Sensor::update_interval() { return 0; } @@ -104,9 +103,7 @@ void Sensor::clear_filters() { } this->filter_list_ = nullptr; } -float Sensor::get_value() const { return this->state; } float Sensor::get_state() const { return this->state; } -float Sensor::get_raw_value() const { return this->raw_state; } float Sensor::get_raw_state() const { return this->raw_state; } std::string Sensor::unique_id() { return ""; } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 9b1f4d7f86..123e7eddb3 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -87,12 +87,8 @@ class Sensor : public Nameable { /// Clear the entire filter chain. void clear_filters(); - /// Getter-syntax for .value. Please use .state instead. - float get_value() const ESPDEPRECATED(".value is deprecated, please use .state"); /// Getter-syntax for .state. float get_state() const; - /// Getter-syntax for .raw_value. Please use .raw_state instead. - float get_raw_value() const ESPDEPRECATED(".raw_value is deprecated, please use .raw_state"); /// Getter-syntax for .raw_state float get_raw_state() const; @@ -114,12 +110,6 @@ class Sensor : public Nameable { */ void publish_state(float state); - /** Push a new value to the MQTT front-end. - * - * Note: deprecated, please use publish_state. - */ - void push_new_value(float state) ESPDEPRECATED("push_new_value is deprecated. Please use .publish_state instead"); - // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Add a callback that will be called every time a filtered value arrives. diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 92a25fe993..0c6fa6f3a0 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -32,11 +32,8 @@ struct ESPTime { uint16_t year; /// daylight saving time flag bool is_dst; - union { - ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t timestamp; - }; + /// unix epoch time (seconds since UTC Midnight January 1, 1970) + time_t timestamp; /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. * Up to buffer_len bytes are written. diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 77b08b3324..d5f2cef05f 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -37,13 +37,6 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, - cv.Optional("timeout_meter"): cv.invalid( - "The timeout_meter option has been renamed " "to 'timeout' in 1.12." - ), - cv.Optional("timeout_time"): cv.invalid( - "The timeout_time option has been removed. Please " - "use 'timeout' in 1.12." - ), } ) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 7a1a01bcc4..d066570cc8 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -249,9 +249,6 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All( cv.decibel, cv.float_range(min=10.0, max=20.5) ), - cv.Optional("hostname"): cv.invalid( - "The hostname option has been removed in 1.11.0" - ), } ), _validate, diff --git a/esphome/components/xiaomi_miflora/__init__.py b/esphome/components/xiaomi_miflora/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py deleted file mode 100644 index 0a0b3ff63f..0000000000 --- a/esphome/components/xiaomi_miflora/sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_hhccjcy01") diff --git a/esphome/components/xiaomi_mijia/__init__.py b/esphome/components/xiaomi_mijia/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py deleted file mode 100644 index 597d8d1bce..0000000000 --- a/esphome/components/xiaomi_mijia/sensor.py +++ /dev/null @@ -1,3 +0,0 @@ -import esphome.config_validation as cv - -CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_lywsdcgq") diff --git a/esphome/const.py b/esphome/const.py index e676056581..f02c17e0da 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -501,7 +501,6 @@ CONF_ROTATION = "rotation" CONF_RS_PIN = "rs_pin" CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance" CONF_RTD_WIRES = "rtd_wires" -CONF_RUN_CYCLES = "run_cycles" CONF_RUN_DURATION = "run_duration" CONF_RW_PIN = "rw_pin" CONF_RX_BUFFER_SIZE = "rx_buffer_size" diff --git a/esphome/core/config.py b/esphome/core/config.py index f55acc1892..9475225f4d 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -201,11 +201,6 @@ CONFIG_SCHEMA = cv.Schema( cv.Required(CONF_VERSION): cv.string_strict, } ), - cv.Optional("esphome_core_version"): cv.invalid( - "The esphome_core_version option has been " - "removed in 1.13 - the esphome core source " - "files are now bundled with ESPHome." - ), } ) From 08b67e7aead8079e80ed526e80f41bc772256fa9 Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Wed, 14 Jul 2021 12:43:30 +1000 Subject: [PATCH 564/643] catch 0.0 in float set_level pre-adjustment (#2013) --- esphome/components/output/float_output.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index f44383db36..99ba798cb9 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -31,6 +31,10 @@ void FloatOutput::set_level(float state) { #endif if (this->is_inverted()) state = 1.0f - state; + if (state == 0.0f) { // regardless of min_power_, 0.0 means off + this->write_state(state); + return; + } float adjusted_value = (state * (this->max_power_ - this->min_power_)) + this->min_power_; this->write_state(adjusted_value); } From 5cb0c11feb8b0283762c16fa741eebf8f09b0f3b Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 14 Jul 2021 07:08:18 +0200 Subject: [PATCH 565/643] Introduce clamp as a template function (#1953) --- esphome/components/climate_ir_lg/climate_ir_lg.cpp | 2 +- esphome/components/coolix/coolix.cpp | 2 +- esphome/components/daikin/daikin.cpp | 2 +- esphome/components/fan/fan_helpers.cpp | 2 +- esphome/components/fan/fan_state.cpp | 2 +- esphome/components/fujitsu_general/fujitsu_general.cpp | 2 +- esphome/components/mitsubishi/mitsubishi.cpp | 4 ++-- esphome/components/ssd1306_base/ssd1306_base.cpp | 2 +- esphome/components/ssd1322_base/ssd1322_base.cpp | 2 +- esphome/components/ssd1327_base/ssd1327_base.cpp | 2 +- esphome/components/ssd1331_base/ssd1331_base.cpp | 2 +- esphome/core/helpers.cpp | 5 ++++- esphome/core/helpers.h | 2 +- 13 files changed, 17 insertions(+), 14 deletions(-) diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 4c721d5e64..cbb1f7699b 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -94,7 +94,7 @@ void LgIrClimate::transmit_state() { // remote_state |= FAN_MODE_AUTO_DRY; } if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); remote_state |= ((temp - 15) << TEMP_SHIFT); } } diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 653d8344e0..c9145e4ecf 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -84,7 +84,7 @@ void CoolixClimate::transmit_state() { } if (this->mode != climate::CLIMATE_MODE_OFF) { if (this->mode != climate::CLIMATE_MODE_FAN_ONLY) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); + auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; } else { remote_state |= COOLIX_FAN_TEMP_CODE; diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index 40734203da..5f8d0288e2 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -135,7 +135,7 @@ uint8_t DaikinClimate::temperature_() { case climate::CLIMATE_MODE_DRY: return 0xc0; default: - uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); + uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); return temperature << 1; } } diff --git a/esphome/components/fan/fan_helpers.cpp b/esphome/components/fan/fan_helpers.cpp index be16e6bb64..09be20991b 100644 --- a/esphome/components/fan/fan_helpers.cpp +++ b/esphome/components/fan/fan_helpers.cpp @@ -6,7 +6,7 @@ namespace fan { FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { const auto speed_ratio = static_cast(speed_level) / (supported_speed_levels + 1); - const auto legacy_level = static_cast(clamp(ceilf(speed_ratio * 3), 1, 3)); + const auto legacy_level = clamp(static_cast(ceilf(speed_ratio * 3)), 1, 3); return static_cast(legacy_level - 1); } diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index 7cfe3afef7..9b4ae53937 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -54,7 +54,7 @@ void FanStateCall::perform() const { } if (this->speed_.has_value()) { const int speed_count = this->state_->get_traits().supported_speed_count(); - this->state_->speed = static_cast(clamp(*this->speed_, 1, speed_count)); + this->state_->speed = clamp(*this->speed_, 1, speed_count); } FanStateRTCState saved{}; diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 6dd569fa21..8d789bbcfc 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -110,7 +110,7 @@ void FujitsuGeneralClimate::transmit_state() { // Set temperature uint8_t temperature_clamped = - (uint8_t) roundf(clamp(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX)); + (uint8_t) roundf(clamp(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX)); uint8_t temperature_offset = temperature_clamped - FUJITSU_GENERAL_TEMP_MIN; SET_NIBBLE(remote_state, FUJITSU_GENERAL_TEMPERATURE_NIBBLE, temperature_offset); diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp index 0b4b9be1eb..43397770d1 100644 --- a/esphome/components/mitsubishi/mitsubishi.cpp +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -42,8 +42,8 @@ void MitsubishiClimate::transmit_state() { break; } - remote_state[7] = - (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN); + remote_state[7] = (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - + MITSUBISHI_TEMP_MIN); ESP_LOGV(TAG, "Sending Mitsubishi target temp: %.1f state: %02X mode: %02X temp: %02X", this->target_temperature, remote_state[5], remote_state[6], remote_state[7]); diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index 58f86fd182..10e66df784 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -130,7 +130,7 @@ void SSD1306::update() { } void SSD1306::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1306_COMMAND_SET_CONTRAST); this->command(int(SSD1306_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp index 0a3233acfe..007f61d5c8 100644 --- a/esphome/components/ssd1322_base/ssd1322_base.cpp +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -126,7 +126,7 @@ void SSD1322::update() { this->display(); } void SSD1322::set_brightness(float brightness) { - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1322_SETCONTRAST); this->data(int(SSD1322_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp index 798f67e4fc..ae94be87df 100644 --- a/esphome/components/ssd1327_base/ssd1327_base.cpp +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -100,7 +100,7 @@ void SSD1327::update() { } void SSD1327::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1327_SETCONTRAST); this->command(int(SSD1327_MAX_CONTRAST * (this->brightness_))); diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index a4bbad508c..f25ef50075 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -97,7 +97,7 @@ void SSD1331::update() { } void SSD1331::set_brightness(float brightness) { // validation - this->brightness_ = clamp(brightness, 0, 1); + this->brightness_ = clamp(brightness, 0.0F, 1.0F); // now write the new brightness level to the display this->command(SSD1331_CONTRASTA); // 0x81 this->command(int(SSD1331_MAX_CONTRASTA * (this->brightness_))); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 194ab08af3..a6cf8b779c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -287,13 +287,16 @@ void HighFrequencyLoopRequester::stop() { } bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } -float clamp(float val, float min, float max) { +template T clamp(const T val, const T min, const T max) { if (val < min) return min; if (val > max) return max; return val; } +template float clamp(float, float, float); +template int clamp(int, int, int); + float lerp(float completion, float start, float end) { return start + (end - start) * completion; } bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8096228a0f..808f96d4b8 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -80,7 +80,7 @@ class HighFrequencyLoopRequester { * @param max The maximum value. * @return val clamped in between min and max. */ -float clamp(float val, float min, float max); +template T clamp(T val, T min, T max); /** Linearly interpolate between end start and end by completion. * From c399905675a5404abc04731fc2a8b31de113f1df Mon Sep 17 00:00:00 2001 From: St4n <2866240+St4n@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:21:39 +0200 Subject: [PATCH 566/643] [Teleinfo] do not stop parsing frame if there is only a CRC error (#1999) Co-authored-by: Stephane Angot Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/teleinfo/teleinfo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index fa76dcfe76..8240615cc5 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -120,7 +120,7 @@ void TeleInfo::loop() { } if (!check_crc_(buf_finger, grp_end)) - break; + continue; /* Get tag */ field_len = get_field(tag_, buf_finger, grp_end, separator_); From 0992609bf49d2cdac25fd2f815021079ed3bf51e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 15 Jul 2021 07:45:05 +1200 Subject: [PATCH 567/643] Bump version to v1.21.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index f02c17e0da..7f1e3a2c58 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,7 +1,7 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 20 +MINOR_VERSION = 21 PATCH_VERSION = "0-dev" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 0651716b9657dc109b168235ea280a9848b583ad Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Wed, 14 Jul 2021 20:51:15 -0400 Subject: [PATCH 568/643] Nextion upload and sensors (#1464) Co-authored-by: Senex Crenshaw --- CODEOWNERS | 5 + esphome/components/nextion/__init__.py | 5 + esphome/components/nextion/automation.h | 30 + esphome/components/nextion/base_component.py | 126 ++ esphome/components/nextion/binary_sensor.py | 34 - .../nextion/binary_sensor/__init__.py | 54 + .../binary_sensor/nextion_binarysensor.cpp | 69 + .../binary_sensor/nextion_binarysensor.h | 42 + esphome/components/nextion/display.py | 75 +- esphome/components/nextion/nextion.cpp | 1143 ++++++++++++++--- esphome/components/nextion/nextion.h | 527 +++++++- esphome/components/nextion/nextion_base.h | 58 + .../components/nextion/nextion_commands.cpp | 234 ++++ .../components/nextion/nextion_component.cpp | 116 ++ .../components/nextion/nextion_component.h | 49 + .../nextion/nextion_component_base.h | 95 ++ esphome/components/nextion/nextion_upload.cpp | 343 +++++ esphome/components/nextion/sensor/__init__.py | 99 ++ .../nextion/sensor/nextion_sensor.cpp | 110 ++ .../nextion/sensor/nextion_sensor.h | 49 + esphome/components/nextion/switch/__init__.py | 39 + .../nextion/switch/nextion_switch.cpp | 52 + .../nextion/switch/nextion_switch.h | 34 + .../nextion/text_sensor/__init__.py | 38 + .../text_sensor/nextion_textsensor.cpp | 49 + .../nextion/text_sensor/nextion_textsensor.h | 32 + esphome/components/uart/uart.h | 1 + script/ci-custom.py | 3 +- tests/test1.yaml | 9 - tests/test3.yaml | 38 +- 30 files changed, 3295 insertions(+), 263 deletions(-) create mode 100644 esphome/components/nextion/automation.h create mode 100644 esphome/components/nextion/base_component.py delete mode 100644 esphome/components/nextion/binary_sensor.py create mode 100644 esphome/components/nextion/binary_sensor/__init__.py create mode 100644 esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp create mode 100644 esphome/components/nextion/binary_sensor/nextion_binarysensor.h create mode 100644 esphome/components/nextion/nextion_base.h create mode 100644 esphome/components/nextion/nextion_commands.cpp create mode 100644 esphome/components/nextion/nextion_component.cpp create mode 100644 esphome/components/nextion/nextion_component.h create mode 100644 esphome/components/nextion/nextion_component_base.h create mode 100644 esphome/components/nextion/nextion_upload.cpp create mode 100644 esphome/components/nextion/sensor/__init__.py create mode 100644 esphome/components/nextion/sensor/nextion_sensor.cpp create mode 100644 esphome/components/nextion/sensor/nextion_sensor.h create mode 100644 esphome/components/nextion/switch/__init__.py create mode 100644 esphome/components/nextion/switch/nextion_switch.cpp create mode 100644 esphome/components/nextion/switch/nextion_switch.h create mode 100644 esphome/components/nextion/text_sensor/__init__.py create mode 100644 esphome/components/nextion/text_sensor/nextion_textsensor.cpp create mode 100644 esphome/components/nextion/text_sensor/nextion_textsensor.h diff --git a/CODEOWNERS b/CODEOWNERS index d6769800cd..557fe7cc08 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -73,6 +73,11 @@ esphome/components/midea_ac/* @dudanov esphome/components/midea_dongle/* @dudanov esphome/components/mitsubishi/* @RubyBailey esphome/components/network/* @esphome/core +esphome/components/nextion/* @senexcrenshaw +esphome/components/nextion/binary_sensor/* @senexcrenshaw +esphome/components/nextion/sensor/* @senexcrenshaw +esphome/components/nextion/switch/* @senexcrenshaw +esphome/components/nextion/text_sensor/* @senexcrenshaw esphome/components/nfc/* @jesserockz esphome/components/number/* @esphome/core esphome/components/ota/* @esphome/core diff --git a/esphome/components/nextion/__init__.py b/esphome/components/nextion/__init__.py index 67a49df9fa..924d58198d 100644 --- a/esphome/components/nextion/__init__.py +++ b/esphome/components/nextion/__init__.py @@ -1,3 +1,8 @@ import esphome.codegen as cg +from esphome.components import uart nextion_ns = cg.esphome_ns.namespace("nextion") +Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) +nextion_ref = Nextion.operator("ref") + +CONF_NEXTION_ID = "nextion_id" diff --git a/esphome/components/nextion/automation.h b/esphome/components/nextion/automation.h new file mode 100644 index 0000000000..5f4219acb1 --- /dev/null +++ b/esphome/components/nextion/automation.h @@ -0,0 +1,30 @@ +#pragma once +#include "esphome/core/automation.h" +#include "nextion.h" + +namespace esphome { +namespace nextion { + +class SetupTrigger : public Trigger<> { + public: + explicit SetupTrigger(Nextion *nextion) { + nextion->add_setup_state_callback([this]() { this->trigger(); }); + } +}; + +class SleepTrigger : public Trigger<> { + public: + explicit SleepTrigger(Nextion *nextion) { + nextion->add_sleep_state_callback([this]() { this->trigger(); }); + } +}; + +class WakeTrigger : public Trigger<> { + public: + explicit WakeTrigger(Nextion *nextion) { + nextion->add_wake_state_callback([this]() { this->trigger(); }); + } +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/base_component.py b/esphome/components/nextion/base_component.py new file mode 100644 index 0000000000..3bf828aafa --- /dev/null +++ b/esphome/components/nextion/base_component.py @@ -0,0 +1,126 @@ +from string import ascii_letters, digits +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.components import color + +from . import CONF_NEXTION_ID +from . import Nextion + +CONF_VARIABLE_NAME = "variable_name" +CONF_COMPONENT_NAME = "component_name" +CONF_WAVE_CHANNEL_ID = "wave_channel_id" +CONF_WAVE_MAX_VALUE = "wave_max_value" +CONF_PRECISION = "precision" +CONF_WAVEFORM_SEND_LAST_VALUE = "waveform_send_last_value" +CONF_TFT_URL = "tft_url" +CONF_ON_SLEEP = "on_sleep" +CONF_ON_WAKE = "on_wake" +CONF_ON_SETUP = "on_setup" +CONF_TOUCH_SLEEP_TIMEOUT = "touch_sleep_timeout" +CONF_WAKE_UP_PAGE = "wake_up_page" +CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" +CONF_WAVE_MAX_LENGTH = "wave_max_length" +CONF_BACKGROUND_COLOR = "background_color" +CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" +CONF_FOREGROUND_COLOR = "foreground_color" +CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" +CONF_FONT_ID = "font_id" +CONF_VISIBLE = "visible" + + +def NextionName(value): + valid_chars = ascii_letters + digits + "." + if not isinstance(value, str) or len(value) > 29: + raise cv.Invalid("Must be a string less than 29 characters") + + for char in value: + if char not in valid_chars: + raise cv.Invalid( + "Must only consist of upper/lowercase characters, numbers and the period '.'. The character '{}' cannot be used.".format( + char + ) + ) + + return value + + +CONFIG_BASE_COMPONENT_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), + cv.Optional(CONF_BACKGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_FOREGROUND_COLOR): cv.use_id(color), + cv.Optional(CONF_VISIBLE, default=True): cv.boolean, + } +) + + +CONFIG_TEXT_COMPONENT_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Required(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + +CONFIG_BINARY_SENSOR_SCHEMA = CONFIG_BASE_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_COMPONENT_NAME): NextionName, + cv.Optional(CONF_VARIABLE_NAME): NextionName, + } + ) +) + +CONFIG_SENSOR_COMPONENT_SCHEMA = CONFIG_BINARY_SENSOR_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FONT_ID): cv.int_range(min=0, max=255), + } + ) +) + + +CONFIG_SWITCH_COMPONENT_SCHEMA = CONFIG_SENSOR_COMPONENT_SCHEMA.extend( + cv.Schema( + { + cv.Optional(CONF_FOREGROUND_PRESSED_COLOR): cv.use_id(color), + cv.Optional(CONF_BACKGROUND_PRESSED_COLOR): cv.use_id(color), + } + ) +) + + +async def setup_component_core_(var, config, arg): + + if CONF_VARIABLE_NAME in config: + cg.add(var.set_variable_name(config[CONF_VARIABLE_NAME])) + elif CONF_COMPONENT_NAME in config: + cg.add( + var.set_variable_name( + config[CONF_COMPONENT_NAME], + config[CONF_COMPONENT_NAME] + arg, + ) + ) + + if CONF_BACKGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_COLOR]) + cg.add(var.set_background_color(color_component)) + + if CONF_BACKGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_BACKGROUND_PRESSED_COLOR]) + cg.add(var.set_background_pressed_color(color_component)) + + if CONF_FOREGROUND_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_COLOR]) + cg.add(var.set_foreground_color(color_component)) + + if CONF_FOREGROUND_PRESSED_COLOR in config: + color_component = await cg.get_variable(config[CONF_FOREGROUND_PRESSED_COLOR]) + cg.add(var.set_foreground_pressed_color(color_component)) + + if CONF_FONT_ID in config: + cg.add(var.set_font_id(config[CONF_FONT_ID])) + + if CONF_VISIBLE in config: + cg.add(var.set_visible(config[CONF_VISIBLE])) diff --git a/esphome/components/nextion/binary_sensor.py b/esphome/components/nextion/binary_sensor.py deleted file mode 100644 index ed4e8d832a..0000000000 --- a/esphome/components/nextion/binary_sensor.py +++ /dev/null @@ -1,34 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import binary_sensor -from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID -from . import nextion_ns -from .display import Nextion - -DEPENDENCIES = ["display"] - -CONF_NEXTION_ID = "nextion_id" - -NextionTouchComponent = nextion_ns.class_( - "NextionTouchComponent", binary_sensor.BinarySensor -) - -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(NextionTouchComponent), - cv.GenerateID(CONF_NEXTION_ID): cv.use_id(Nextion), - cv.Required(CONF_PAGE_ID): cv.uint8_t, - cv.Required(CONF_COMPONENT_ID): cv.uint8_t, - } -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await binary_sensor.register_binary_sensor(var, config) - - hub = await cg.get_variable(config[CONF_NEXTION_ID]) - cg.add(hub.register_touch_component(var)) - - cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) - cg.add(var.set_page_id(config[CONF_PAGE_ID])) diff --git a/esphome/components/nextion/binary_sensor/__init__.py b/esphome/components/nextion/binary_sensor/__init__.py new file mode 100644 index 0000000000..090fae3429 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor + +from esphome.const import CONF_COMPONENT_ID, CONF_PAGE_ID, CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + + +from ..base_component import ( + setup_component_core_, + CONFIG_BINARY_SENSOR_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionBinarySensor = nextion_ns.class_( + "NextionBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) + +CONFIG_SCHEMA = cv.All( + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionBinarySensor), + cv.Optional(CONF_PAGE_ID): cv.uint8_t, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + } + ) + .extend(CONFIG_BINARY_SENSOR_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_at_least_one_key( + CONF_PAGE_ID, + CONF_COMPONENT_ID, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + ), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await binary_sensor.register_binary_sensor(var, config) + await cg.register_component(var, config) + + if config.keys() >= {CONF_PAGE_ID, CONF_COMPONENT_ID}: + cg.add(hub.register_touch_component(var)) + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + cg.add(var.set_page_id(config[CONF_PAGE_ID])) + + if CONF_COMPONENT_NAME in config or CONF_VARIABLE_NAME in config: + await setup_component_core_(var, config, ".val") + cg.add(hub.register_binarysensor_component(var)) diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp new file mode 100644 index 0000000000..bf6e74cb38 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.cpp @@ -0,0 +1,69 @@ +#include "nextion_binarysensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_binarysensor"; + +void NextionBinarySensor::process_bool(const std::string &variable_name, bool state) { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + if (this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed binarysensor \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionBinarySensor::process_touch(uint8_t page_id, uint8_t component_id, bool state) { + if (this->page_id_ == page_id && this->component_id_ == component_id) { + this->publish_state(state); + } +} + +void NextionBinarySensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->variable_name_.empty()) // This is a touch component + return; + + this->nextion_->add_to_get_queue(this); +} + +void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (this->component_id_ == 0) // This is a legacy touch component + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %s", this->variable_name_.c_str(), + ONOFF(this->variable_name_.c_str())); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/binary_sensor/nextion_binarysensor.h b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h new file mode 100644 index 0000000000..b6b23ada85 --- /dev/null +++ b/esphome/components/nextion/binary_sensor/nextion_binarysensor.h @@ -0,0 +1,42 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionBinarySensor; + +class NextionBinarySensor : public NextionComponent, + public binary_sensor::BinarySensorInitiallyOff, + public PollingComponent { + public: + NextionBinarySensor(NextionBase *nextion) { this->nextion_ = nextion; } + + void update_component() override { this->update(); } + void update() override; + void send_state_to_nextion() override { this->set_state(this->state, false); }; + void process_bool(const std::string &variable_name, bool state) override; + void process_touch(uint8_t page_id, uint8_t component_id, bool state) override; + + // Set the components page id for Nextion Touch Component + void set_page_id(uint8_t page_id) { page_id_ = page_id; } + // Set the components component id for Nextion Touch Component + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + NextionQueueType get_queue_type() override { return NextionQueueType::BINARY_SENSOR; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + uint8_t page_id_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 7d7018a4c4..e693b2f1ec 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,20 +1,58 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS -from . import nextion_ns +from esphome.const import ( + CONF_ID, + CONF_LAMBDA, + CONF_BRIGHTNESS, + CONF_TRIGGER_ID, +) + +from . import Nextion, nextion_ns, nextion_ref +from .base_component import ( + CONF_ON_SLEEP, + CONF_ON_WAKE, + CONF_ON_SETUP, + CONF_TFT_URL, + CONF_TOUCH_SLEEP_TIMEOUT, + CONF_WAKE_UP_PAGE, + CONF_AUTO_WAKE_ON_TOUCH, +) + +CODEOWNERS = ["@senexcrenshaw"] DEPENDENCIES = ["uart"] -AUTO_LOAD = ["binary_sensor"] +AUTO_LOAD = ["binary_sensor", "switch", "sensor", "text_sensor"] -Nextion = nextion_ns.class_("Nextion", cg.PollingComponent, uart.UARTDevice) -NextionRef = Nextion.operator("ref") +SetupTrigger = nextion_ns.class_("SetupTrigger", automation.Trigger.template()) +SleepTrigger = nextion_ns.class_("SleepTrigger", automation.Trigger.template()) +WakeTrigger = nextion_ns.class_("WakeTrigger", automation.Trigger.template()) CONFIG_SCHEMA = ( display.BASIC_DISPLAY_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(Nextion), + cv.Optional(CONF_TFT_URL): cv.string, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_ON_SETUP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SetupTrigger), + } + ), + cv.Optional(CONF_ON_SLEEP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SleepTrigger), + } + ), + cv.Optional(CONF_ON_WAKE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger), + } + ), + cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535), + cv.Optional(CONF_WAKE_UP_PAGE): cv.positive_int, + cv.Optional(CONF_AUTO_WAKE_ON_TOUCH, default=True): cv.boolean, } ) .extend(cv.polling_component_schema("5s")) @@ -31,8 +69,33 @@ async def to_code(config): cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(NextionRef, "it")], return_type=cg.void + config[CONF_LAMBDA], [(nextion_ref, "it")], return_type=cg.void ) cg.add(var.set_writer(lambda_)) + if CONF_TFT_URL in config: + cg.add_define("USE_TFT_UPLOAD") + cg.add(var.set_tft_url(config[CONF_TFT_URL])) + + if CONF_TOUCH_SLEEP_TIMEOUT in config: + cg.add(var.set_touch_sleep_timeout_internal(config[CONF_TOUCH_SLEEP_TIMEOUT])) + + if CONF_WAKE_UP_PAGE in config: + cg.add(var.set_wake_up_page_internal(config[CONF_WAKE_UP_PAGE])) + + if CONF_AUTO_WAKE_ON_TOUCH in config: + cg.add(var.set_auto_wake_on_touch_internal(config[CONF_AUTO_WAKE_ON_TOUCH])) + await display.register_display(var, config) + + for conf in config.get(CONF_ON_SETUP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_SLEEP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_WAKE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index fe0767342b..9a5424917f 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,5 +1,7 @@ #include "nextion.h" +#include "esphome/core/util.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome { namespace nextion { @@ -7,69 +9,171 @@ namespace nextion { static const char *const TAG = "nextion"; void Nextion::setup() { - this->send_command_no_ack(""); - this->send_command_printf("bkcmd=3"); - this->set_backlight_brightness(static_cast(brightness_ * 100)); - this->goto_page("0"); + this->is_setup_ = false; + this->ignore_is_setup_ = true; + + // Wake up the nextion + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + this->send_command_("bkcmd=0"); + this->send_command_("sleep=0"); + + // Reboot it + this->send_command_("rest"); + + this->ignore_is_setup_ = false; } -float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } + +bool Nextion::send_command_(const std::string &command) { + if (!this->ignore_is_setup_ && !this->is_setup()) { + return false; + } + + ESP_LOGN(TAG, "send_command %s", command.c_str()); + + this->write_str(command.c_str()); + const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF}; + this->write_array(to_send, sizeof(to_send)); + return true; +} + +bool Nextion::check_connect_() { + if (this->get_is_connected_()) + return true; + + if (this->comok_sent_ == 0) { + this->reset_(false); + + this->ignore_is_setup_ = true; + this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating + this->send_command_("connect"); + + this->comok_sent_ = millis(); + this->ignore_is_setup_ = false; + + return false; + } + + if (millis() - this->comok_sent_ <= 500) // Wait 500 ms + return false; + + std::string response; + + this->recv_ret_string_(response, 0, false); + if (response.empty() || response.find("comok") == std::string::npos) { +#ifdef NEXTION_PROTOCOL_LOG + ESP_LOGN(TAG, "Bad connect request %s", response.c_str()); + for (int i = 0; i < response.length(); i++) { + ESP_LOGN(TAG, "response %s %d %d %c", response.c_str(), i, response[i], response[i]); + } +#endif + + ESP_LOGW(TAG, "Nextion is not connected! "); + comok_sent_ = 0; + return false; + } + + this->ignore_is_setup_ = true; + ESP_LOGI(TAG, "Nextion is connected"); + this->is_connected_ = true; + + ESP_LOGN(TAG, "connect request %s", response.c_str()); + + size_t start; + size_t end = 0; + std::vector connect_info; + while ((start = response.find_first_not_of(',', end)) != std::string::npos) { + end = response.find(',', start); + connect_info.push_back(response.substr(start, end - start)); + } + + if (connect_info.size() == 7) { + ESP_LOGN(TAG, "Received connect_info %zu", connect_info.size()); + + this->device_model_ = connect_info[2]; + this->firmware_version_ = connect_info[3]; + this->serial_number_ = connect_info[5]; + this->flash_size_ = connect_info[6]; + } else { + ESP_LOGE(TAG, "Nextion returned bad connect value \"%s\"", response.c_str()); + } + + this->ignore_is_setup_ = false; + this->dump_config(); + return true; +} + +void Nextion::reset_(bool reset_nextion) { + uint8_t d; + + while (this->available()) { // Clear receive buffer + this->read_byte(&d); + }; + this->nextion_queue_.clear(); +} + +void Nextion::dump_config() { + ESP_LOGCONFIG(TAG, "Nextion:"); + ESP_LOGCONFIG(TAG, " Device Model: %s", this->device_model_.c_str()); + ESP_LOGCONFIG(TAG, " Firmware Version: %s", this->firmware_version_.c_str()); + ESP_LOGCONFIG(TAG, " Serial Number: %s", this->serial_number_.c_str()); + ESP_LOGCONFIG(TAG, " Flash Size: %s", this->flash_size_.c_str()); + ESP_LOGCONFIG(TAG, " Wake On Touch: %s", this->auto_wake_on_touch_ ? "True" : "False"); + + if (this->touch_sleep_timeout_ != 0) { + ESP_LOGCONFIG(TAG, " Touch Timeout: %d", this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + ESP_LOGCONFIG(TAG, " Wake Up Page : %d", this->wake_up_page_); + } +} + +float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { + if (!this->is_setup()) { + return; + } if (this->writer_.has_value()) { (*this->writer_)(*this); } } -void Nextion::send_command_no_ack(const char *command) { - // Flush RX... - this->loop(); - this->write_str(command); - const uint8_t data[3] = {0xFF, 0xFF, 0xFF}; - this->write_array(data, sizeof(data)); +void Nextion::add_sleep_state_callback(std::function &&callback) { + this->sleep_callback_.add(std::move(callback)); } -bool Nextion::ack_() { - if (!this->wait_for_ack_) - return true; +void Nextion::add_wake_state_callback(std::function &&callback) { + this->wake_callback_.add(std::move(callback)); +} - uint32_t start = millis(); - while (!this->read_until_ack_()) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Waiting for ACK timed out!"); - return false; - } +void Nextion::add_setup_state_callback(std::function &&callback) { + this->setup_callback_.add(std::move(callback)); +} + +void Nextion::update_all_components() { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->update_component(); + } + for (auto *sensortype : this->sensortype_) { + sensortype->update_component(); + } + for (auto *switchtype : this->switchtype_) { + switchtype->update_component(); + } + for (auto *textsensortype : this->textsensortype_) { + textsensortype->update_component(); } - return true; } -void Nextion::set_component_text(const char *component, const char *text) { - this->send_command_printf("%s.txt=\"%s\"", component, text); -} -void Nextion::set_component_value(const char *component, int value) { - this->send_command_printf("%s.val=%d", component, value); -} -void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id); -} -void Nextion::set_component_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_background_color(const char *component, const char *color) { - this->send_command_printf("%s.bco2=\"%s\"", component, color); -} -void Nextion::set_component_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco=\"%s\"", component, color); -} -void Nextion::set_component_pressed_font_color(const char *component, const char *color) { - this->send_command_printf("%s.pco2=\"%s\"", component, color); -} -void Nextion::set_component_coordinates(const char *component, int x, int y) { - this->send_command_printf("%s.xcen=%d", component, x); - this->send_command_printf("%s.ycen=%d", component, y); -} -void Nextion::set_component_font(const char *component, uint8_t font_id) { - this->send_command_printf("%s.font=%d", component, font_id); -} -void Nextion::goto_page(const char *page) { this->send_command_printf("page %s", page); } + bool Nextion::send_command_printf(const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + char buffer[256]; va_list arg; va_start(arg, format); @@ -79,208 +183,911 @@ bool Nextion::send_command_printf(const char *format, ...) { ESP_LOGW(TAG, "Building command for format '%s' failed!", format); return false; } - this->send_command_no_ack(buffer); - if (!this->ack_()) { - ESP_LOGW(TAG, "Sending command '%s' failed because no ACK was received", buffer); + + if (this->send_command_(buffer)) { + this->add_no_result_to_queue_("send_command_printf"); + return true; + } + return false; +} + +#ifdef NEXTION_PROTOCOL_LOG +void Nextion::print_queue_members_() { + ESP_LOGN(TAG, "print_queue_members_ (top 10) size %zu", this->nextion_queue_.size()); + ESP_LOGN(TAG, "*******************************************"); + int count = 0; + for (auto *i : this->nextion_queue_) { + if (count++ == 10) + break; + + if (i == nullptr) { + ESP_LOGN(TAG, "Nextion queue is null"); + } else { + ESP_LOGN(TAG, "Nextion queue type: %d:%s , name: %s", i->component->get_queue_type(), + i->component->get_queue_type_string().c_str(), i->component->get_variable_name().c_str()); + } + } + ESP_LOGN(TAG, "*******************************************"); +} +#endif + +void Nextion::loop() { + if (!this->check_connect_() || this->is_updating_) + return; + + if (this->nextion_reports_is_setup_ && !this->sent_setup_commands_) { + this->ignore_is_setup_ = true; + this->sent_setup_commands_ = true; + this->send_command_("bkcmd=3"); // Always, returns 0x00 to 0x23 result of serial command. + + this->set_backlight_brightness(this->brightness_); + this->goto_page("0"); + + this->set_auto_wake_on_touch(this->auto_wake_on_touch_); + + if (this->touch_sleep_timeout_ != 0) { + this->set_touch_sleep_timeout(this->touch_sleep_timeout_); + } + + if (this->wake_up_page_ != -1) { + this->set_wake_up_page(this->wake_up_page_); + } + + this->ignore_is_setup_ = false; + } + + this->process_serial_(); // Receive serial data + this->process_nextion_commands_(); // Process nextion return commands + + if (!this->nextion_reports_is_setup_) { + if (this->started_ms_ == 0) + this->started_ms_ = millis(); + + if (this->started_ms_ + this->startup_override_ms_ < millis()) { + ESP_LOGD(TAG, "Manually set nextion report ready"); + this->nextion_reports_is_setup_ = true; + } + } +} + +bool Nextion::remove_from_q_(bool report_empty) { + if (this->nextion_queue_.empty()) { + if (report_empty) + ESP_LOGE(TAG, "Nextion queue is empty!"); return false; } + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + ESP_LOGN(TAG, "Removing %s from the queue", component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + delete component; + } + delete nb; + this->nextion_queue_.pop_front(); return true; } -void Nextion::hide_component(const char *component) { this->send_command_printf("vis %s,0", component); } -void Nextion::show_component(const char *component) { this->send_command_printf("vis %s,1", component); } -void Nextion::enable_component_touch(const char *component) { this->send_command_printf("tsw %s,1", component); } -void Nextion::disable_component_touch(const char *component) { this->send_command_printf("tsw %s,0", component); } -void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { - this->send_command_printf("add %d,%u,%u", component_id, channel_number, value); -} -void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("fill %d,%d,%d,%d,%s", x1, y1, width, height, color); -} -void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { - this->send_command_printf("line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); -} -void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { - this->send_command_printf("draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); -} -void Nextion::circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cir %d,%d,%d,%s", center_x, center_y, radius, color); -} -void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { - this->send_command_printf("cirs %d,%d,%d,%s", center_x, center_y, radius, color); -} -bool Nextion::read_until_ack_() { - while (this->available() >= 4) { - // flush preceding filler bytes - uint8_t temp; - while (this->available() && this->peek_byte(&temp) && temp == 0xFF) - this->read_byte(&temp); - if (!this->available()) - break; +void Nextion::process_serial_() { + uint8_t d; - uint8_t event; - // event type - this->read_byte(&event); + while (this->available()) { + read_byte(&d); + this->command_data_ += d; + } +} +// nextion.tech/instruction-set/ +void Nextion::process_nextion_commands_() { + if (this->command_data_.length() == 0) { + return; + } - uint8_t data[255]; - // total length of data (including end bytes) - uint8_t data_length = 0; - // message is terminated by three consecutive 0xFF - // this variable keeps track of ohow many of those have - // been received - uint8_t end_length = 0; - while (this->available() && end_length < 3 && data_length < sizeof(data)) { - uint8_t byte; - this->read_byte(&byte); - if (byte == 0xFF) { - end_length++; - } else { - end_length = 0; - } - data[data_length++] = byte; + size_t to_process_length = 0; + std::string to_process; + + ESP_LOGN(TAG, "this->command_data_ %s length %d", this->command_data_.c_str(), this->command_data_.length()); +#ifdef NEXTION_PROTOCOL_LOG + this->print_queue_members_(); +#endif + while ((to_process_length = this->command_data_.find(COMMAND_DELIMITER)) != std::string::npos) { + ESP_LOGN(TAG, "print_queue_members_ size %zu", this->nextion_queue_.size()); + while (to_process_length + COMMAND_DELIMITER.length() < this->command_data_.length() && + static_cast(this->command_data_[to_process_length + COMMAND_DELIMITER.length()]) == 0xFF) { + ++to_process_length; + ESP_LOGN(TAG, "Add extra 0xFF to process"); } - if (end_length != 3) { - ESP_LOGW(TAG, "Received unknown filler end bytes from Nextion!"); - continue; - } + this->nextion_event_ = this->command_data_[0]; - data_length -= 3; // remove filler bytes + to_process_length -= 1; + to_process = this->command_data_.substr(1, to_process_length); - bool invalid_data_length = false; - switch (event) { - case 0x01: // successful execution of instruction (ACK) - return true; - case 0x00: // invalid instruction + switch (this->nextion_event_) { + case 0x00: // instruction sent by user has failed ESP_LOGW(TAG, "Nextion reported invalid instruction!"); + this->remove_from_q_(); + break; - case 0x02: // component ID invalid - ESP_LOGW(TAG, "Nextion reported component ID invalid!"); + case 0x01: // instruction sent by user was successful + + ESP_LOGVV(TAG, "instruction sent by user was successful"); + ESP_LOGN(TAG, "this->nextion_queue_.empty() %s", this->nextion_queue_.empty() ? "True" : "False"); + + this->remove_from_q_(); + if (!this->is_setup_) { + if (this->nextion_queue_.empty()) { + ESP_LOGD(TAG, "Nextion is setup"); + this->is_setup_ = true; + this->setup_callback_.call(); + } + } + break; - case 0x03: // page ID invalid + case 0x02: // invalid Component ID or name was used + this->remove_from_q_(); + break; + case 0x03: // invalid Page ID or name was used ESP_LOGW(TAG, "Nextion reported page ID invalid!"); + this->remove_from_q_(); break; - case 0x04: // picture ID invalid + case 0x04: // invalid Picture ID was used ESP_LOGW(TAG, "Nextion reported picture ID invalid!"); + this->remove_from_q_(); break; - case 0x05: // font ID invalid + case 0x05: // invalid Font ID was used ESP_LOGW(TAG, "Nextion reported font ID invalid!"); + this->remove_from_q_(); break; - case 0x11: // baud rate setting invalid + case 0x06: // File operation fails + ESP_LOGW(TAG, "Nextion File operation fail!"); + break; + case 0x09: // Instructions with CRC validation fails their CRC check + ESP_LOGW(TAG, "Nextion Instructions with CRC validation fails their CRC check!"); + break; + case 0x11: // invalid Baud rate was used ESP_LOGW(TAG, "Nextion reported baud rate invalid!"); break; - case 0x12: // curve control ID number or channel number is invalid - ESP_LOGW(TAG, "Nextion reported control/channel ID invalid!"); + case 0x12: // invalid Waveform ID or Channel # was used + + if (!this->nextion_queue_.empty()) { + int index = 0; + int found = -1; + for (auto &nb : this->nextion_queue_) { + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { + ESP_LOGW(TAG, "Nextion reported invalid Waveform ID %d or Channel # %d was used!", + component->get_component_id(), component->get_wave_channel_id()); + + ESP_LOGN(TAG, "Removing waveform from queue with component id %d and waveform id %d", + component->get_component_id(), component->get_wave_channel_id()); + + found = index; + + delete component; + delete nb; + + break; + } + ++index; + } + + if (found != -1) { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } else { + ESP_LOGW( + TAG, + "Nextion reported invalid Waveform ID or Channel # was used but no waveform sensor in queue found!"); + } + } break; case 0x1A: // variable name invalid - ESP_LOGW(TAG, "Nextion reported variable name invalid!"); + this->remove_from_q_(); + break; case 0x1B: // variable operation invalid ESP_LOGW(TAG, "Nextion reported variable operation invalid!"); + this->remove_from_q_(); break; case 0x1C: // failed to assign ESP_LOGW(TAG, "Nextion reported failed to assign variable!"); + this->remove_from_q_(); break; case 0x1D: // operate EEPROM failed ESP_LOGW(TAG, "Nextion reported operating EEPROM failed!"); break; case 0x1E: // parameter quantity invalid ESP_LOGW(TAG, "Nextion reported parameter quantity invalid!"); + this->remove_from_q_(); break; case 0x1F: // IO operation failed ESP_LOGW(TAG, "Nextion reported component I/O operation invalid!"); break; case 0x20: // undefined escape characters ESP_LOGW(TAG, "Nextion reported undefined escape characters!"); + this->remove_from_q_(); break; case 0x23: // too long variable name ESP_LOGW(TAG, "Nextion reported too long variable name!"); + this->remove_from_q_(); + + break; + case 0x24: // Serial Buffer overflow occurs + ESP_LOGW(TAG, "Nextion reported Serial Buffer overflow!"); break; case 0x65: { // touch event return data - if (data_length != 3) { - invalid_data_length = true; + if (to_process_length != 3) { + ESP_LOGW(TAG, "Touch event data is expecting 3, received %zu", to_process_length); + break; } - uint8_t page_id = data[0]; - uint8_t component_id = data[1]; - uint8_t touch_event = data[2]; // 0 -> release, 1 -> press + uint8_t page_id = to_process[0]; + uint8_t component_id = to_process[1]; + uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press ESP_LOGD(TAG, "Got touch page=%u component=%u type=%s", page_id, component_id, touch_event ? "PRESS" : "RELEASE"); for (auto *touch : this->touch_) { - touch->process(page_id, component_id, touch_event); + touch->process_touch(page_id, component_id, touch_event != 0); } break; } - case 0x67: - case 0x68: { // touch coordinate data - if (data_length != 5) { - invalid_data_length = true; + case 0x67: { // Touch Coordinate (awake) + break; + } + case 0x68: { // touch coordinate data (sleep) + + if (to_process_length != 5) { + ESP_LOGW(TAG, "Touch coordinate data is expecting 5, received %zu", to_process_length); + ESP_LOGW(TAG, "%s", to_process.c_str()); break; } - uint16_t x = (uint16_t(data[0]) << 8) | data[1]; - uint16_t y = (uint16_t(data[2]) << 8) | data[3]; - uint8_t touch_event = data[4]; // 0 -> release, 1 -> press + + uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; + uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; + uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press ESP_LOGD(TAG, "Got touch at x=%u y=%u type=%s", x, y, touch_event ? "PRESS" : "RELEASE"); break; } - case 0x66: // sendme page id + case 0x66: { + break; + } // sendme page id + + // 0x70 0x61 0x62 0x31 0x32 0x33 0xFF 0xFF 0xFF + // Returned when using get command for a string. + // Each byte is converted to char. + // data: ab123 case 0x70: // string variable data return + { + if (this->nextion_queue_.empty()) { + ESP_LOGW(TAG, "ERROR: Received string return but the queue is empty"); + break; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() != NextionQueueType::TEXT_SENSOR) { + ESP_LOGE(TAG, "ERROR: Received string return but next in queue \"%s\" is not a text sensor", + component->get_variable_name().c_str()); + } else { + ESP_LOGN(TAG, "Received get_string response: \"%s\" for component id: %s, type: %s", to_process.c_str(), + component->get_variable_name().c_str(), component->get_queue_type_string().c_str()); + component->set_state_from_string(to_process, true, false); + } + + delete nb; + this->nextion_queue_.pop_front(); + + break; + } + // 0x71 0x01 0x02 0x03 0x04 0xFF 0xFF 0xFF + // Returned when get command to return a number + // 4 byte 32-bit value in little endian order. + // (0x01+0x02*256+0x03*65536+0x04*16777216) + // data: 67305985 case 0x71: // numeric variable data return - case 0x86: // device automatically enters into sleep mode + { + if (this->nextion_queue_.empty()) { + ESP_LOGE(TAG, "ERROR: Received numeric return but the queue is empty"); + break; + } + + if (to_process_length == 0) { + ESP_LOGE(TAG, "ERROR: Received numeric return but no data!"); + break; + } + + int dataindex = 0; + + int value = 0; + + for (int i = 0; i < 4; ++i) { + value += to_process[i] << (8 * i); + ++dataindex; + } + + NextionQueue *nb = this->nextion_queue_.front(); + NextionComponentBase *component = nb->component; + + if (component->get_queue_type() != NextionQueueType::SENSOR && + component->get_queue_type() != NextionQueueType::BINARY_SENSOR && + component->get_queue_type() != NextionQueueType::SWITCH) { + ESP_LOGE(TAG, "ERROR: Received numeric return but next in queue \"%s\" is not a valid sensor type %d", + component->get_variable_name().c_str(), component->get_queue_type()); + } else { + ESP_LOGN(TAG, "Received numeric return for variable %s, queue type %d:%s, value %d", + component->get_variable_name().c_str(), component->get_queue_type(), + component->get_queue_type_string().c_str(), value); + component->set_state_from_int(value, true, false); + } + + delete nb; + this->nextion_queue_.pop_front(); + + break; + } + + case 0x86: { // device automatically enters into sleep mode + ESP_LOGVV(TAG, "Received Nextion entering sleep automatically"); + this->is_sleeping_ = true; + this->sleep_callback_.call(); + break; + } case 0x87: // device automatically wakes up + { + ESP_LOGVV(TAG, "Received Nextion leaves sleep automatically"); + this->is_sleeping_ = false; + this->wake_callback_.call(); + break; + } case 0x88: // system successful start up - case 0x89: // start SD card upgrade - case 0xFD: // data transparent transmit finished - case 0xFE: // data transparent transmit ready + { + ESP_LOGD(TAG, "system successful start up %zu", to_process_length); + this->nextion_reports_is_setup_ = true; break; + } + case 0x89: { // start SD card upgrade + break; + } + // Data from nextion is + // 0x90 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x90: { // Switched component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad switch component data received for 0x90 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + ESP_LOGN(TAG, "Got Switch variable_name=%s value=%d", variable_name.c_str(), to_process[0] != 0); + + for (auto *switchtype : this->switchtype_) { + switchtype->process_bool(variable_name, to_process[index] != 0); + } + break; + } + // Data from nextion is + // 0x91 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x71 return data: prints temp1.val,0 + // FF FF FF - End + case 0x91: { // Sensor component + std::string variable_name; + uint8_t index = 0; + + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) != 4) { + ESP_LOGE(TAG, "Bad sensor component data received for 0x91 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + index = to_process.find('\0'); + variable_name = to_process.substr(0, index); + // // Get variable name + int value = 0; + for (int i = 0; i < 4; ++i) { + value += to_process[i + index + 1] << (8 * i); + } + + ESP_LOGN(TAG, "Got sensor variable_name=%s value=%d", variable_name.c_str(), value); + + for (auto *sensor : this->sensortype_) { + sensor->process_sensor(variable_name, value); + } + break; + } + + // Data from nextion is + // 0x92 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // variable length of 0x70 return formatted data (bytes) that contain the text prints temp1.txt,0 + // 00 - NULL + // FF FF FF - End + case 0x92: { // Text Sensor Component + std::string variable_name; + std::string text_value; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad text sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + text_value = to_process.substr(index); + + ESP_LOGN(TAG, "Got Text Sensor variable_name=%s value=%s", variable_name.c_str(), text_value.c_str()); + + // NextionTextSensorResponseQueue *nq = new NextionTextSensorResponseQueue; + // nq->variable_name = variable_name; + // nq->state = text_value; + // this->textsensorq_.push_back(nq); + for (auto *textsensortype : this->textsensortype_) { + textsensortype->process_text(variable_name, text_value); + } + break; + } + // Data from nextion is + // 0x93 - Start + // variable length of 0x70 return formatted data (bytes) that contain the variable name: prints "temp1",0 + // 00 - NULL + // 00/01 - Single byte for on/off + // FF FF FF - End + case 0x93: { // Binary Sensor component + std::string variable_name; + uint8_t index = 0; + + // Get variable name + index = to_process.find('\0'); + if (static_cast(index) == std::string::npos || (to_process_length - index - 1) < 1) { + ESP_LOGE(TAG, "Bad binary sensor component data received for 0x92 event!"); + ESP_LOGN(TAG, "to_process %s %zu %d", to_process.c_str(), to_process_length, index); + break; + } + + variable_name = to_process.substr(0, index); + ++index; + + ESP_LOGN(TAG, "Got Binary Sensor variable_name=%s value=%d", variable_name.c_str(), to_process[index] != 0); + + for (auto *binarysensortype : this->binarysensortype_) { + binarysensortype->process_bool(&variable_name[0], to_process[index] != 0); + } + break; + } + case 0xFD: { // data transparent transmit finished + ESP_LOGVV(TAG, "Nextion reported data transmit finished!"); + break; + } + case 0xFE: { // data transparent transmit ready + ESP_LOGVV(TAG, "Nextion reported ready for transmit!"); + + int index = 0; + int found = -1; + for (auto &nb : this->nextion_queue_) { + auto component = nb->component; + if (component->get_queue_type() == NextionQueueType::WAVEFORM_SENSOR) { + size_t buffer_to_send = component->get_wave_buffer().size() < 255 ? component->get_wave_buffer().size() + : 255; // ADDT command can only send 255 + + this->write_array(component->get_wave_buffer().data(), static_cast(buffer_to_send)); + + ESP_LOGN(TAG, "Nextion sending waveform data for component id %d and waveform id %d, size %zu", + component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); + + if (component->get_wave_buffer().size() <= 255) { + component->get_wave_buffer().clear(); + } else { + component->get_wave_buffer().erase(component->get_wave_buffer().begin(), + component->get_wave_buffer().begin() + buffer_to_send); + } + found = index; + delete component; + delete nb; + break; + } + ++index; + } + + if (found == -1) { + ESP_LOGE(TAG, "No waveforms in queue to send data!"); + break; + } else { + this->nextion_queue_.erase(this->nextion_queue_.begin() + found); + } + break; + } default: - ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", event); + ESP_LOGW(TAG, "Received unknown event from nextion: 0x%02X", this->nextion_event_); break; } - if (invalid_data_length) { - ESP_LOGW(TAG, "Invalid data length from nextion!"); + + // ESP_LOGN(TAG, "nextion_event_ deleting from 0 to %d", to_process_length + COMMAND_DELIMITER.length() + 1); + this->command_data_.erase(0, to_process_length + COMMAND_DELIMITER.length() + 1); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); + } + + uint32_t ms = millis(); + + if (!this->nextion_queue_.empty() && this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) { + for (int i = 0; i < this->nextion_queue_.size(); i++) { + NextionComponentBase *component = this->nextion_queue_[i]->component; + if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) { + if (this->nextion_queue_[i]->queue_time == 0) + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\" queue_time 0", + component->get_queue_type_string().c_str(), component->get_variable_name().c_str()); + + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + + ESP_LOGD(TAG, "Removing old queue type \"%s\" name \"%s\"", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + if (component->get_queue_type() == NextionQueueType::NO_RESULT) { + if (component->get_variable_name() == "sleep_wake") { + this->is_sleeping_ = false; + } + delete component; + } + + delete this->nextion_queue_[i]; + + this->nextion_queue_.erase(this->nextion_queue_.begin() + i); + + } else { + break; + } + } + } + ESP_LOGN(TAG, "Loop End"); + // App.feed_wdt(); Remove before master merge + this->process_serial_(); +} // namespace nextion + +void Nextion::set_nextion_sensor_state(int queue_type, const std::string &name, float state) { + this->set_nextion_sensor_state(static_cast(queue_type), name, state); +} + +void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state) { + ESP_LOGN(TAG, "Received state for variable %s, state %lf for queue type %d", name.c_str(), state, queue_type); + + switch (queue_type) { + case NextionQueueType::SENSOR: { + for (auto *sensor : this->sensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } + break; + } + case NextionQueueType::BINARY_SENSOR: { + for (auto *sensor : this->binarysensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + case NextionQueueType::SWITCH: { + for (auto *sensor : this->switchtype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state != 0, true, true); + break; + } + } + break; + } + default: { + ESP_LOGW(TAG, "set_nextion_sensor_state does not support a queue type %d", queue_type); + } + } +} + +void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) { + ESP_LOGD(TAG, "Received state for variable %s, state %s", name.c_str(), state.c_str()); + + for (auto *sensor : this->textsensortype_) { + if (name == sensor->get_variable_name()) { + sensor->set_state(state, true, true); + break; + } + } +} + +void Nextion::all_components_send_state_(bool force_update) { + ESP_LOGD(TAG, "all_components_send_state_ "); + for (auto *binarysensortype : this->binarysensortype_) { + if (force_update || binarysensortype->get_needs_to_send_update()) + binarysensortype->send_state_to_nextion(); + } + for (auto *sensortype : this->sensortype_) { + if ((force_update || sensortype->get_needs_to_send_update()) && sensortype->get_wave_chan_id() == 0) + sensortype->send_state_to_nextion(); + } + for (auto *switchtype : this->switchtype_) { + if (force_update || switchtype->get_needs_to_send_update()) + switchtype->send_state_to_nextion(); + } + for (auto *textsensortype : this->textsensortype_) { + if (force_update || textsensortype->get_needs_to_send_update()) + textsensortype->send_state_to_nextion(); + } +} + +void Nextion::update_components_by_prefix(const std::string &prefix) { + for (auto *binarysensortype : this->binarysensortype_) { + if (binarysensortype->get_variable_name().find(prefix, 0) != std::string::npos) + binarysensortype->update_component_settings(true); + } + for (auto *sensortype : this->sensortype_) { + if (sensortype->get_variable_name().find(prefix, 0) != std::string::npos) + sensortype->update_component_settings(true); + } + for (auto *switchtype : this->switchtype_) { + if (switchtype->get_variable_name().find(prefix, 0) != std::string::npos) + switchtype->update_component_settings(true); + } + for (auto *textsensortype : this->textsensortype_) { + if (textsensortype->get_variable_name().find(prefix, 0) != std::string::npos) + textsensortype->update_component_settings(true); + } +} + +uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag) { + uint16_t ret = 0; + uint8_t c = 0; + uint8_t nr_of_ff_bytes = 0; + uint64_t start; + bool exit_flag = false; + bool ff_flag = false; + + start = millis(); + + while ((timeout == 0 && this->available()) || millis() - start <= timeout) { + this->read_byte(&c); + if (c == 0xFF) + nr_of_ff_bytes++; + else { + nr_of_ff_bytes = 0; + ff_flag = false; + } + + if (nr_of_ff_bytes >= 3) + ff_flag = true; + + response += (char) c; + if (recv_flag) { + if (response.find(0x05) != std::string::npos) { + exit_flag = true; + } + } + App.feed_wdt(); + delay(1); + + if (exit_flag || ff_flag) { + break; } } - return false; + if (ff_flag) + response = response.substr(0, response.length() - 3); // Remove last 3 0xFF + + ret = response.length(); + return ret; } -void Nextion::loop() { - while (this->available() >= 4) { - this->read_until_ack_(); + +/** + * @brief + * + * @param variable_name Name for the queue + */ +void Nextion::add_no_result_to_queue_(const std::string &variable_name) { + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + + nextion_queue->queue_time = millis(); + + this->nextion_queue_.push_back(nextion_queue); + + ESP_LOGN(TAG, "Add to queue type: NORESULT component %s", nextion_queue->component->get_variable_name().c_str()); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param command + */ +void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command) { + if ((!this->is_setup() && !this->ignore_is_setup_) || command.empty()) + return; + + if (this->send_command_(command)) { + this->add_no_result_to_queue_(variable_name); } } -#ifdef USE_TIME -void Nextion::set_nextion_rtc_time(time::ESPTime time) { - this->send_command_printf("rtc0=%u", time.year); - this->send_command_printf("rtc1=%u", time.month); - this->send_command_printf("rtc2=%u", time.day_of_month); - this->send_command_printf("rtc3=%u", time.hour); - this->send_command_printf("rtc4=%u", time.minute); - this->send_command_printf("rtc5=%u", time.second); -} -#endif -void Nextion::set_backlight_brightness(uint8_t brightness) { this->send_command_printf("dim=%u", brightness); } -void Nextion::set_touch_sleep_timeout(uint16_t timeout) { this->send_command_printf("thsp=%u", timeout); } +bool Nextion::add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, + ...) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return false; -void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } -void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + char buffer[256]; va_list arg; va_start(arg, format); - char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); va_end(arg); - if (ret > 0) - this->set_component_text(component, buffer); -} -void Nextion::set_wait_for_ack(bool wait_for_ack) { this->wait_for_ack_ = wait_for_ack; } + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } -void NextionTouchComponent::process(uint8_t page_id, uint8_t component_id, bool on) { - if (this->page_id_ == page_id && this->component_id_ == component_id) { - this->publish_state(on); + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief Sends a formatted command to the nextion + * + * @param variable_name Variable name for the queue + * @param format The printf-style command format, like "vis %s,0" + * @param ... The format arguments + */ +bool Nextion::add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return false; + + char buffer[256]; + va_list arg; + va_start(arg, format); + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret <= 0) { + ESP_LOGW(TAG, "Building command for format '%s' failed!", format); + return false; + } + + this->add_no_result_to_queue_with_command_(variable_name, buffer); + return true; +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value Value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ + +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) { + this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), + state_value); +} + +void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value) { + this->add_no_result_to_queue_with_set_internal_(variable_name, variable_name_to_send, state_value); +} + +void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value, + bool is_sleep_safe) { + if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + return; + + this->add_no_result_to_queue_with_ignore_sleep_printf_(variable_name, "%s=%d", variable_name_to_send.c_str(), + state_value); +} + +/** + * @brief + * + * @param variable_name Variable name for the queue + * @param variable_name_to_send Variable name for the left of the command + * @param state_value Sting value to set + * @param is_sleep_safe The command is safe to send when the Nextion is sleeping + */ +void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) { + this->add_no_result_to_queue_with_set(component->get_variable_name(), component->get_variable_name_to_send(), + state_value); +} +void Nextion::add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value) { + this->add_no_result_to_queue_with_set_internal_(variable_name, variable_name_to_send, state_value); +} + +void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value, bool is_sleep_safe) { + if ((!this->is_setup() && !this->ignore_is_setup_) || (!is_sleep_safe && this->is_sleeping())) + return; + + this->add_no_result_to_queue_with_printf_(variable_name, "%s=\"%s\"", variable_name_to_send.c_str(), + state_value.c_str()); +} + +void Nextion::add_to_get_queue(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_)) + return; + + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = component; + nextion_queue->queue_time = millis(); + + ESP_LOGN(TAG, "Add to queue type: %s component %s", component->get_queue_type_string().c_str(), + component->get_variable_name().c_str()); + + std::string command = "get " + component->get_variable_name_to_send(); + + if (this->send_command_(command)) { + this->nextion_queue_.push_back(nextion_queue); } } +/** + * @brief Add addt command to the queue + * + * @param component_id The waveform component id + * @param wave_chan_id The waveform channel to send it to + * @param buffer_to_send The buffer size + * @param buffer_size The buffer data + */ +void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { + if ((!this->is_setup() && !this->ignore_is_setup_) || this->is_sleeping()) + return; + + nextion::NextionQueue *nextion_queue = new nextion::NextionQueue; + + nextion_queue->component = new nextion::NextionComponentBase; + nextion_queue->queue_time = millis(); + + size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() + : 255; // ADDT command can only send 255 + + std::string command = "addt " + to_string(component->get_component_id()) + "," + + to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); + if (this->send_command_(command)) { + this->nextion_queue_.push_back(nextion_queue); + } +} + +void Nextion::set_writer(const nextion_writer_t &writer) { this->writer_ = writer; } + +ESPDEPRECATED("set_wait_for_ack(bool) is deprecated and has no effect") +void Nextion::set_wait_for_ack(bool wait_for_ack) { ESP_LOGE(TAG, "This command is depreciated"); } + } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index a55ff747ee..2389cc6235 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1,9 +1,21 @@ #pragma once -#include "esphome/core/component.h" +#include #include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" -#include "esphome/components/binary_sensor/binary_sensor.h" +#include "nextion_base.h" +#include "nextion_component.h" +#include "esphome/components/display/display_color_utils.h" + +#if defined(USE_ETHERNET) || defined(USE_WIFI) +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif +#endif #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -12,12 +24,14 @@ namespace esphome { namespace nextion { -class NextionTouchComponent; class Nextion; +class NextionComponentBase; using nextion_writer_t = std::function; -class Nextion : public PollingComponent, public uart::UARTDevice { +static const std::string COMMAND_DELIMITER{static_cast(255), static_cast(255), static_cast(255)}; + +class Nextion : public NextionBase, public PollingComponent, public uart::UARTDevice { public: /** * Set the text of a component to a static string. @@ -73,9 +87,20 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * This will change the image of the component `pic` to the image with ID `4`. */ - void set_component_picture(const char *component, const char *picture) { - this->send_command_printf("%s.val=%s", component, picture); - } + void set_component_picture(const char *component, const char *picture); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as a uint32_t). + * + * Example: + * ```cpp + * it.set_component_background_color("button", 0xFF0000); + * ``` + * + * This will change the background color of the component `button` to red. + */ + void set_component_background_color(const char *component, uint32_t color); /** * Set the background color of a component. * @param component The component name. @@ -83,7 +108,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_background_color("button", "17013"); + * it.set_component_background_color("button", "RED"); * ``` * * This will change the background color of the component `button` to blue. @@ -91,6 +116,33 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_background_color(const char *component, const char *color); + /** + * Set the background color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_background_color("button", color); + * ``` + * + * This will change the background color of the component `button` to what color contains. + */ + void set_component_background_color(const char *component, Color color) override; + /** + * Set the pressed background color of a component. + * @param component The component name. + * @param color The color (as a int). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", 0xFF0000 ); + * ``` + * + * This will change the pressed background color of the component `button` to red. This is the background color that + * is shown when the component is pressed. + */ + void set_component_pressed_background_color(const char *component, uint32_t color); /** * Set the pressed background color of a component. * @param component The component name. @@ -98,7 +150,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_background_color("button", "17013"); + * it.set_component_pressed_background_color("button", "RED"); * ``` * * This will change the pressed background color of the component `button` to blue. This is the background color that @@ -107,6 +159,63 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void set_component_pressed_background_color(const char *component, const char *color); + /** + * Set the pressed background color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_background_color("button", color); + * ``` + * + * This will change the pressed background color of the component `button` to blue. This is the background color that + * is shown when the component is pressed. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void set_component_pressed_background_color(const char *component, Color color) override; + + /** + * Set the picture id of a component. + * @param component The component name. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_pic("textview", 1); + * ``` + * + * This will change the picture id of the component `textview`. + */ + void set_component_pic(const char *component, uint8_t pic_id); + /** + * Set the background picture id of component. + * @param component The component name. + * @param pic_id The picture ID. + * + * Example: + * ```cpp + * it.set_component_picc("textview", 1); + * ``` + * + * This will change the background picture id of the component `textview`. + */ + void set_component_picc(const char *component, uint8_t pic_id); + + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as a uint32_t ). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", 0xFF0000); + * ``` + * + * This will change the font color of the component `textview` to a red color. + */ + void set_component_font_color(const char *component, uint32_t color); /** * Set the font color of a component. * @param component The component name. @@ -114,7 +223,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_font_color("textview", "17013"); + * it.set_component_font_color("textview", "RED"); * ``` * * This will change the font color of the component `textview` to a blue color. @@ -122,6 +231,34 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_font_color(const char *component, const char *color); + /** + * Set the font color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_font_color("textview", color); + * ``` + * + * This will change the font color of the component `textview` to a blue color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_font_color(const char *component, Color color) override; + /** + * Set the pressed font color of a component. + * @param component The component name. + * @param color The color (as a uint32_t). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", 0xFF0000); + * ``` + * + * This will change the pressed font color of the component `button` to a red. + */ + void set_component_pressed_font_color(const char *component, uint32_t color); /** * Set the pressed font color of a component. * @param component The component name. @@ -129,7 +266,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * it.set_component_pressed_font_color("button", "17013"); + * it.set_component_pressed_font_color("button", "RED"); * ``` * * This will change the pressed font color of the component `button` to a blue color. @@ -137,6 +274,21 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void set_component_pressed_font_color(const char *component, const char *color); + /** + * Set the pressed font color of a component. + * @param component The component name. + * @param color The color (as Color). + * + * Example: + * ```cpp + * it.set_component_pressed_font_color("button", color); + * ``` + * + * This will change the pressed font color of the component `button` to a blue color. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void set_component_pressed_font_color(const char *component, Color color) override; /** * Set the coordinates of a component on screen. * @param component The component name. @@ -163,7 +315,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Changes the font of the component named `textveiw`. Font IDs are set in the Nextion Editor. */ - void set_component_font(const char *component, uint8_t font_id); + void set_component_font(const char *component, uint8_t font_id) override; #ifdef USE_TIME /** * Send the current time to the nextion display. @@ -195,7 +347,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Hides the component named `button`. */ - void hide_component(const char *component); + void hide_component(const char *component) override; /** * Show a component. * @param component The component name. @@ -207,7 +359,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Shows the component named `button`. */ - void show_component(const char *component); + void show_component(const char *component) override; /** * Enable touch for a component. * @param component The component name. @@ -239,6 +391,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @param value The value to write. */ void add_waveform_data(int component_id, uint8_t channel_number, uint8_t value); + void open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value); /** * Display a picture at coordinates. * @param picture_id The picture id. @@ -263,7 +416,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * * Example: * ```cpp - * fill_area(50, 50, 100, 100, "17013"); + * fill_area(50, 50, 100, 100, "RED"); * ``` * * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with @@ -271,6 +424,24 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * convert color codes to Nextion HMI colors */ void fill_area(int x1, int y1, int width, int height, const char *color); + /** + * Fill a rectangle with a color. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width to draw. + * @param height The height to draw. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * fill_area(50, 50, 100, 100, color); + * ``` + * + * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to + * convert color codes to Nextion HMI colors + */ + void fill_area(int x1, int y1, int width, int height, Color color); /** * Draw a line on the screen. * @param x1 The starting x coordinate. @@ -290,6 +461,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void line(int x1, int y1, int x2, int y2, const char *color); + /** + * Draw a line on the screen. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param x2 The ending x coordinate. + * @param y2 The ending y coordinate. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * it.line(50, 50, 75, 75, "17013"); + * ``` + * + * Makes a line that starts at x coordinate `50` and y coordinate `50` and ends at x coordinate `75` and y coordinate + * `75` with the color of blue. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void line(int x1, int y1, int x2, int y2, Color color); /** * Draw a rectangle outline. * @param x1 The starting x coordinate. @@ -309,6 +499,25 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * colors. */ void rectangle(int x1, int y1, int width, int height, const char *color); + /** + * Draw a rectangle outline. + * @param x1 The starting x coordinate. + * @param y1 The starting y coordinate. + * @param width The width of the rectangle. + * @param height The height of the rectangle. + * @param color The color to draw with (as Color). + * + * Example: + * ```cpp + * it.rectangle(25, 35, 40, 50, "17013"); + * ``` + * + * Makes a outline of a rectangle that starts at x coordinate `25` and y coordinate `35` and has a width of `40` and a + * length of `50` with color of blue. Use this [color + * picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to Nextion HMI + * colors. + */ + void rectangle(int x1, int y1, int width, int height, Color color); /** * Draw a circle outline * @param center_x The center x coordinate. @@ -317,6 +526,14 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * @param color The color to draw with (as a string). */ void circle(int center_x, int center_y, int radius, const char *color); + /** + * Draw a circle outline + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (as Color). + */ + void circle(int center_x, int center_y, int radius, Color color); /** * Draw a filled circled. * @param center_x The center x coordinate. @@ -334,19 +551,36 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * Nextion HMI colors. */ void filled_circle(int center_x, int center_y, int radius, const char *color); - - /** Set the brightness of the backlight. - * - * @param brightness The brightness, from 0 to 100. + /** + * Draw a filled circled. + * @param center_x The center x coordinate. + * @param center_y The center y coordinate. + * @param radius The circle radius. + * @param color The color to draw with (as Color). * * Example: * ```cpp - * it.set_backlight_brightness(30); + * it.filled_cricle(25, 25, 10, color); + * ``` + * + * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to + * Nextion HMI colors. + */ + void filled_circle(int center_x, int center_y, int radius, Color color); + + /** Set the brightness of the backlight. + * + * @param brightness The brightness percentage from 0 to 1.0. + * + * Example: + * ```cpp + * it.set_backlight_brightness(.3); * ``` * * Changes the brightness of the display to 30%. */ - void set_backlight_brightness(uint8_t brightness); + void set_backlight_brightness(float brightness); /** * Set the touch sleep timeout of the display. * @param timeout Timeout in seconds. @@ -360,10 +594,46 @@ class Nextion : public PollingComponent, public uart::UARTDevice { * `thup`. */ void set_touch_sleep_timeout(uint16_t timeout); + /** + * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. + * @param page_id The page id, from 0 to the lage page in Nextion. Set 255 (not set to any existing page) to + * wakes up to current page. + * + * Example: + * ```cpp + * it.set_wake_up_page(2); + * ``` + * + * The display will wake up to page 2. + */ + void set_wake_up_page(uint8_t page_id = 255); + /** + * Sets if Nextion should auto-wake from sleep when touch press occurs. + * @param auto_wake True or false. When auto_wake is true and Nextion is in sleep mode, + * the first touch will only trigger the auto wake mode and not trigger a Touch Event. + * + * Example: + * ```cpp + * it.set_auto_wake_on_touch(true); + * ``` + * + * The display will wake up by touch. + */ + void set_auto_wake_on_touch(bool auto_wake); + /** + * Sets Nextion mode between sleep and awake + * @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode. + */ + void sleep(bool sleep); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } + void register_touch_component(NextionComponentBase *obj) { this->touch_.push_back(obj); } + void register_switch_component(NextionComponentBase *obj) { this->switchtype_.push_back(obj); } + void register_binarysensor_component(NextionComponentBase *obj) { this->binarysensortype_.push_back(obj); } + void register_sensor_component(NextionComponentBase *obj) { this->sensortype_.push_back(obj); } + void register_textsensor_component(NextionComponentBase *obj) { this->textsensortype_.push_back(obj); } + void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; @@ -371,11 +641,9 @@ class Nextion : public PollingComponent, public uart::UARTDevice { void loop() override; void set_writer(const nextion_writer_t &writer); - /** - * Manually send a raw command to the display and don't wait for an acknowledgement packet. - * @param command The command to write, for example "vis b0,0". - */ - void send_command_no_ack(const char *command); + // This function has been deprecated + void set_wait_for_ack(bool wait_for_ack); + /** * Manually send a raw formatted command to the display. * @param format The printf-style command format, like "vis %s,0" @@ -384,28 +652,199 @@ class Nextion : public PollingComponent, public uart::UARTDevice { */ bool send_command_printf(const char *format, ...) __attribute__((format(printf, 2, 3))); - void set_wait_for_ack(bool wait_for_ack); +#ifdef USE_TFT_UPLOAD + /** + * Set the tft file URL. https seems problamtic with arduino.. + */ + void set_tft_url(const std::string &tft_url) { this->tft_url_ = tft_url; } + +#endif + + /** + * Upload the tft file and softreset the Nextion + */ + void upload_tft(); + void dump_config() override; + + /** + * Softreset the Nextion + */ + void soft_reset(); + + /** Add a callback to be notified of sleep state changes. + * + * @param callback The void() callback. + */ + void add_sleep_state_callback(std::function &&callback); + + /** Add a callback to be notified of wake state changes. + * + * @param callback The void() callback. + */ + void add_wake_state_callback(std::function &&callback); + + /** Add a callback to be notified when the nextion completes its initialize setup. + * + * @param callback The void() callback. + */ + void add_setup_state_callback(std::function &&callback); + + void update_all_components(); + + /** + * @brief Set the nextion sensor state object. + * + * @param[in] queue_type + * Index of NextionQueueType. + * + * @param[in] name + * Component/variable name. + * + * @param[in] state + * State to set. + */ + void set_nextion_sensor_state(int queue_type, const std::string &name, float state); + void set_nextion_sensor_state(NextionQueueType queue_type, const std::string &name, float state); + void set_nextion_text_state(const std::string &name, const std::string &state); + + void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) override; + void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, + int state_value) override; + + void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) override; + void add_no_result_to_queue_with_set(const std::string &variable_name, const std::string &variable_name_to_send, + const std::string &state_value) override; + + void add_to_get_queue(NextionComponentBase *component) override; + + void add_addt_command_to_queue(NextionComponentBase *component) override; + + void update_components_by_prefix(const std::string &prefix); + + void set_touch_sleep_timeout_internal(uint32_t touch_sleep_timeout) { + this->touch_sleep_timeout_ = touch_sleep_timeout; + } + void set_wake_up_page_internal(uint8_t wake_up_page) { this->wake_up_page_ = wake_up_page; } + void set_auto_wake_on_touch_internal(bool auto_wake_on_touch) { this->auto_wake_on_touch_ = auto_wake_on_touch; } protected: - bool ack_(); - bool read_until_ack_(); + std::deque nextion_queue_; + uint16_t recv_ret_string_(std::string &response, uint32_t timeout, bool recv_flag); + void all_components_send_state_(bool force_update = false); + uint64_t comok_sent_ = 0; + bool remove_from_q_(bool report_empty = true); + /** + * @brief + * Sends commands ignoring of the Nextion has been setup. + */ + bool ignore_is_setup_ = false; + bool nextion_reports_is_setup_ = false; + uint8_t nextion_event_; + + void process_nextion_commands_(); + void process_serial_(); + bool is_updating_ = false; + uint32_t touch_sleep_timeout_ = 0; + int wake_up_page_ = -1; + bool auto_wake_on_touch_ = true; + + /** + * Manually send a raw command to the display and don't wait for an acknowledgement packet. + * @param command The command to write, for example "vis b0,0". + */ + bool send_command_(const std::string &command); + void add_no_result_to_queue_(const std::string &variable_name); + bool add_no_result_to_queue_with_ignore_sleep_printf_(const std::string &variable_name, const char *format, ...) + __attribute__((format(printf, 3, 4))); + void add_no_result_to_queue_with_command_(const std::string &variable_name, const std::string &command); + + bool add_no_result_to_queue_with_printf_(const std::string &variable_name, const char *format, ...) + __attribute__((format(printf, 3, 4))); + + void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value, + bool is_sleep_safe = false); + + void add_no_result_to_queue_with_set_internal_(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value, bool is_sleep_safe = false); + +#ifdef USE_TFT_UPLOAD +#if defined(USE_ETHERNET) || defined(USE_WIFI) +#ifdef ARDUINO_ARCH_ESP8266 + WiFiClient *wifi_client_{nullptr}; + BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; + WiFiClient *get_wifi_client_(); +#endif + + /** + * will request chunk_size chunks from the web server + * and send each to the nextion + * @param int contentLength Total size of the file + * @param uint32_t chunk_size + * @return true if success, false for failure. + */ + int content_length_ = 0; + int tft_size_ = 0; + int upload_by_chunks_(HTTPClient *http, int range_start); + + bool upload_with_range_(uint32_t range_start, uint32_t range_end); + + /** + * start update tft file to nextion. + * + * @param const uint8_t *file_buf + * @param size_t buf_size + * @return true if success, false for failure. + */ + bool upload_from_buffer_(const uint8_t *file_buf, size_t buf_size); + void upload_end_(); + +#endif + +#endif + + bool get_is_connected_() { return this->is_connected_; } + + bool check_connect_(); + + std::vector touch_; + std::vector switchtype_; + std::vector sensortype_; + std::vector textsensortype_; + std::vector binarysensortype_; + CallbackManager setup_callback_{}; + CallbackManager sleep_callback_{}; + CallbackManager wake_callback_{}; - std::vector touch_; optional writer_; - bool wait_for_ack_{true}; float brightness_{1.0}; + + std::string device_model_; + std::string firmware_version_; + std::string serial_number_; + std::string flash_size_; + + void remove_front_no_sensors_(); + +#ifdef USE_TFT_UPLOAD + std::string tft_url_; + uint8_t *transfer_buffer_{nullptr}; + size_t transfer_buffer_size_; + bool upload_first_chunk_sent_ = false; +#endif + +#ifdef NEXTION_PROTOCOL_LOG + void print_queue_members_(); +#endif + void reset_(bool reset_nextion = true); + + std::string command_data_; + bool is_connected_ = false; + uint32_t startup_override_ms_ = 8000; + uint32_t max_q_age_ms_ = 8000; + uint32_t started_ms_ = 0; + bool sent_setup_commands_ = false; }; - -class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { - public: - void set_page_id(uint8_t page_id) { page_id_ = page_id; } - void set_component_id(uint8_t component_id) { component_id_ = component_id; } - void process(uint8_t page_id, uint8_t component_id, bool on); - - protected: - uint8_t page_id_; - uint8_t component_id_; -}; - } // namespace nextion } // namespace esphome diff --git a/esphome/components/nextion/nextion_base.h b/esphome/components/nextion/nextion_base.h new file mode 100644 index 0000000000..a24fd74060 --- /dev/null +++ b/esphome/components/nextion/nextion_base.h @@ -0,0 +1,58 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_component_base.h" +namespace esphome { +namespace nextion { + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define NEXTION_PROTOCOL_LOG +#endif + +#ifdef NEXTION_PROTOCOL_LOG +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE +#define ESP_LOGN(tag, ...) ESP_LOGVV(tag, __VA_ARGS__) +#else +#define ESP_LOGN(tag, ...) ESP_LOGD(tag, __VA_ARGS__) +#endif +#else +#define ESP_LOGN(tag, ...) \ + {} +#endif + +class NextionBase; + +class NextionBase { + public: + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, int state_value) = 0; + virtual void add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, int state_value) = 0; + + virtual void add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) = 0; + virtual void add_no_result_to_queue_with_set(const std::string &variable_name, + const std::string &variable_name_to_send, + const std::string &state_value) = 0; + + virtual void add_addt_command_to_queue(NextionComponentBase *component) = 0; + + virtual void add_to_get_queue(NextionComponentBase *component) = 0; + + virtual void set_component_background_color(const char *component, Color color) = 0; + virtual void set_component_pressed_background_color(const char *component, Color color) = 0; + virtual void set_component_font_color(const char *component, Color color) = 0; + virtual void set_component_pressed_font_color(const char *component, Color color) = 0; + virtual void set_component_font(const char *component, uint8_t font_id) = 0; + + virtual void show_component(const char *component) = 0; + virtual void hide_component(const char *component) = 0; + + bool is_sleeping() { return this->is_sleeping_; } + bool is_setup() { return this->is_setup_; } + + protected: + bool is_setup_ = false; + bool is_sleeping_ = false; +}; + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp new file mode 100644 index 0000000000..931b934ba2 --- /dev/null +++ b/esphome/components/nextion/nextion_commands.cpp @@ -0,0 +1,234 @@ +#include "nextion.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion"; + +// Sleep safe commands +void Nextion::soft_reset() { this->send_command_("rest"); } + +void Nextion::set_wake_up_page(uint8_t page_id) { + if (page_id > 255) { + ESP_LOGD(TAG, "Wake up page of bounds, range 0-255"); + return; + } + this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", page_id, true); +} + +void Nextion::set_touch_sleep_timeout(uint16_t timeout) { + if (timeout < 3 || timeout > 65535) { + ESP_LOGD(TAG, "Sleep timeout out of bounds, range 3-65535"); + return; + } + + this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", timeout, true); +} + +void Nextion::sleep(bool sleep) { + if (sleep) { // Set sleep + this->is_sleeping_ = true; + this->add_no_result_to_queue_with_set_internal_("sleep", "sleep", 1, true); + } else { // Turn off sleep. Wait for a sleep_wake return before setting sleep off + this->add_no_result_to_queue_with_set_internal_("sleep_wake", "sleep", 0, true); + } +} +// End sleep safe commands + +// Set Colors +void Nextion::set_component_background_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, color); +} + +void Nextion::set_component_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%s", component, color); +} + +void Nextion::set_component_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_background_color", "%s.bco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pressed_background_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, color); +} + +void Nextion::set_component_pressed_background_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%s", component, color); +} + +void Nextion::set_component_pressed_background_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_background_color", "%s.bco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pic(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.pic=%d", component, pic_id); +} + +void Nextion::set_component_picc(const char *component, uint8_t pic_id) { + this->add_no_result_to_queue_with_printf_("set_component_pic", "%s.picc=%d", component, pic_id); +} + +void Nextion::set_component_font_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, color); +} + +void Nextion::set_component_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%s", component, color); +} + +void Nextion::set_component_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_font_color", "%s.pco=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_pressed_font_color(const char *component, uint32_t color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, color); +} + +void Nextion::set_component_pressed_font_color(const char *component, const char *color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", " %s.pco2=%s", component, color); +} + +void Nextion::set_component_pressed_font_color(const char *component, Color color) { + this->add_no_result_to_queue_with_printf_("set_component_pressed_font_color", "%s.pco2=%d", component, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::set_component_text_printf(const char *component, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[256]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + this->set_component_text(component, buffer); +} + +// General Nextion +void Nextion::goto_page(const char *page) { this->add_no_result_to_queue_with_printf_("goto_page", "page %s", page); } + +void Nextion::set_backlight_brightness(float brightness) { + if (brightness < 0 || brightness > 1.0) { + ESP_LOGD(TAG, "Brightness out of bounds, percentage range 0-1.0"); + return; + } + this->add_no_result_to_queue_with_set("backlight_brightness", "dim", static_cast(brightness * 100)); +} + +void Nextion::set_auto_wake_on_touch(bool auto_wake) { + this->add_no_result_to_queue_with_set("auto_wake_on_touch", "thup", auto_wake ? 1 : 0); +} + +// General Component +void Nextion::set_component_font(const char *component, uint8_t font_id) { + this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%d", component, font_id); +} + +void Nextion::hide_component(const char *component) { + this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component); +} + +void Nextion::show_component(const char *component) { + this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component); +} + +void Nextion::enable_component_touch(const char *component) { + this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component); +} + +void Nextion::disable_component_touch(const char *component) { + this->add_no_result_to_queue_with_printf_("disable_component_touch", "tsw %s,0", component); +} + +void Nextion::set_component_picture(const char *component, const char *picture) { + this->add_no_result_to_queue_with_printf_("set_component_picture", "%s.val=%s", component, picture); +} + +void Nextion::set_component_text(const char *component, const char *text) { + this->add_no_result_to_queue_with_printf_("set_component_text", "%s.txt=\"%s\"", component, text); +} + +void Nextion::set_component_value(const char *component, int value) { + this->add_no_result_to_queue_with_printf_("set_component_value", "%s.val=%d", component, value); +} + +void Nextion::add_waveform_data(int component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("add_waveform_data", "add %d,%u,%u", component_id, channel_number, value); +} + +void Nextion::open_waveform_channel(int component_id, uint8_t channel_number, uint8_t value) { + this->add_no_result_to_queue_with_printf_("open_waveform_channel", "addt %d,%u,%u", component_id, channel_number, + value); +} + +void Nextion::set_component_coordinates(const char *component, int x, int y) { + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 1", "%s.xcen=%d", component, x); + this->add_no_result_to_queue_with_printf_("set_component_coordinates command 2", "%s.ycen=%d", component, y); +} + +// Drawing +void Nextion::display_picture(int picture_id, int x_start, int y_start) { + this->add_no_result_to_queue_with_printf_("display_picture", "pic %d %d %d", x_start, y_start, picture_id); +} + +void Nextion::fill_area(int x1, int y1, int width, int height, const char *color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%s", x1, y1, width, height, color); +} + +void Nextion::fill_area(int x1, int y1, int width, int height, Color color) { + this->add_no_result_to_queue_with_printf_("fill_area", "fill %d,%d,%d,%d,%d", x1, y1, width, height, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::line(int x1, int y1, int x2, int y2, const char *color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%s", x1, y1, x2, y2, color); +} + +void Nextion::line(int x1, int y1, int x2, int y2, Color color) { + this->add_no_result_to_queue_with_printf_("line", "line %d,%d,%d,%d,%d", x1, y1, x2, y2, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::rectangle(int x1, int y1, int width, int height, const char *color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%s", x1, y1, x1 + width, y1 + height, color); +} + +void Nextion::rectangle(int x1, int y1, int width, int height, Color color) { + this->add_no_result_to_queue_with_printf_("draw", "draw %d,%d,%d,%d,%d", x1, y1, x1 + width, y1 + height, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::circle(int center_x, int center_y, int radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%s", center_x, center_y, radius, color); +} + +void Nextion::circle(int center_x, int center_y, int radius, Color color) { + this->add_no_result_to_queue_with_printf_("cir", "cir %d,%d,%d,%d", center_x, center_y, radius, + display::ColorUtil::color_to_565(color)); +} + +void Nextion::filled_circle(int center_x, int center_y, int radius, const char *color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%s", center_x, center_y, radius, color); +} + +void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) { + this->add_no_result_to_queue_with_printf_("cirs", "cirs %d,%d,%d,%d", center_x, center_y, radius, + display::ColorUtil::color_to_565(color)); +} + +#ifdef USE_TIME +void Nextion::set_nextion_rtc_time(time::ESPTime time) { + this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); + this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); + this->add_no_result_to_queue_with_printf_("rtc2", "rtc2=%u", time.day_of_month); + this->add_no_result_to_queue_with_printf_("rtc3", "rtc3=%u", time.hour); + this->add_no_result_to_queue_with_printf_("rtc4", "rtc4=%u", time.minute); + this->add_no_result_to_queue_with_printf_("rtc5", "rtc5=%u", time.second); +} +#endif + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp new file mode 100644 index 0000000000..bbb2cf6cb2 --- /dev/null +++ b/esphome/components/nextion/nextion_component.cpp @@ -0,0 +1,116 @@ +#include "nextion_component.h" + +namespace esphome { +namespace nextion { + +void NextionComponent::set_background_color(Color bco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->bco_ = bco; + this->bco_needs_update_ = true; + this->bco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_background_pressed_color(Color bco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + + this->bco2_ = bco2; + this->bco2_needs_update_ = true; + this->bco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_color(Color pco) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco_ = pco; + this->pco_needs_update_ = true; + this->pco_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_foreground_pressed_color(Color pco2) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->pco2_ = pco2; + this->pco2_needs_update_ = true; + this->pco2_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_font_id(uint8_t font_id) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->font_id_ = font_id; + this->font_id_needs_update_ = true; + this->font_id_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::set_visible(bool visible) { + if (this->variable_name_ == this->variable_name_to_send_) { + return; // This is a variable. no need to set color + } + this->visible_ = visible; + this->visible_needs_update_ = true; + this->visible_is_set_ = true; + this->update_component_settings(); +} + +void NextionComponent::update_component_settings(bool force_update) { + if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ || + (!this->visible_needs_update_ && !this->visible_)) { + this->needs_to_send_update_ = true; + return; + } + + if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) { + std::string name_to_send = this->variable_name_; + + size_t pos = name_to_send.find_last_of('.'); + if (pos != std::string::npos) { + name_to_send = name_to_send.substr(pos + 1); + } + + this->visible_needs_update_ = false; + + if (this->visible_) { + this->nextion_->show_component(name_to_send.c_str()); + this->send_state_to_nextion(); + } else { + this->nextion_->hide_component(name_to_send.c_str()); + return; + } + } + + if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); + this->bco_needs_update_ = false; + } + if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) { + this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_); + this->bco2_needs_update_ = false; + } + if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) { + this->nextion_->set_component_font_color(this->variable_name_.c_str(), this->pco_); + this->pco_needs_update_ = false; + } + if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) { + this->nextion_->set_component_pressed_font_color(this->variable_name_.c_str(), this->pco2_); + this->pco2_needs_update_ = false; + } + + if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) { + this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_); + this->font_id_needs_update_ = false; + } +} +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component.h b/esphome/components/nextion/nextion_component.h new file mode 100644 index 0000000000..2f3c4f3c16 --- /dev/null +++ b/esphome/components/nextion/nextion_component.h @@ -0,0 +1,49 @@ +#pragma once +#include "esphome/core/defines.h" +#include "esphome/core/color.h" +#include "nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionComponent; + +class NextionComponent : public NextionComponentBase { + public: + void update_component_settings() override { this->update_component_settings(false); }; + + void update_component_settings(bool force_update) override; + + void set_background_color(Color bco); + void set_background_pressed_color(Color bco2); + void set_foreground_color(Color pco); + void set_foreground_pressed_color(Color pco2); + void set_font_id(uint8_t font_id); + void set_visible(bool visible); + + protected: + NextionBase *nextion_; + + bool bco_needs_update_ = false; + bool bco_is_set_ = false; + Color bco_; + bool bco2_needs_update_ = false; + bool bco2_is_set_ = false; + Color bco2_; + bool pco_needs_update_ = false; + bool pco_is_set_ = false; + Color pco_; + bool pco2_needs_update_ = false; + bool pco2_is_set_ = false; + Color pco2_; + uint8_t font_id_ = 0; + bool font_id_needs_update_ = false; + bool font_id_is_set_ = false; + + bool visible_ = true; + bool visible_needs_update_ = false; + bool visible_is_set_ = false; + + // void send_state_to_nextion() = 0; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h new file mode 100644 index 0000000000..71ad803bc4 --- /dev/null +++ b/esphome/components/nextion/nextion_component_base.h @@ -0,0 +1,95 @@ +#pragma once +#include +#include "esphome/core/defines.h" + +namespace esphome { +namespace nextion { + +enum NextionQueueType { + NO_RESULT = 0, + SENSOR = 1, + BINARY_SENSOR = 2, + SWITCH = 3, + TEXT_SENSOR = 4, + WAVEFORM_SENSOR = 5, +}; + +static const char *const NEXTION_QUEUE_TYPE_STRINGS[] = {"NO_RESULT", "SENSOR", "BINARY_SENSOR", + "SWITCH", "TEXT_SENSOR", "WAVEFORM_SENSOR"}; + +class NextionComponentBase; + +class NextionQueue { + public: + virtual ~NextionQueue() = default; + NextionComponentBase *component; + uint32_t queue_time = 0; +}; + +class NextionComponentBase { + public: + virtual ~NextionComponentBase() = default; + + void set_variable_name(const std::string &variable_name, const std::string &variable_name_to_send = "") { + variable_name_ = variable_name; + if (variable_name_to_send.empty()) { + variable_name_to_send_ = variable_name; + } else { + variable_name_to_send_ = variable_name_to_send; + } + } + + virtual void update_component_settings(){}; + virtual void update_component_settings(bool force_update){}; + + virtual void update_component(){}; + virtual void process_sensor(const std::string &variable_name, int state){}; + virtual void process_touch(uint8_t page_id, uint8_t component_id, bool on){}; + virtual void process_text(const std::string &variable_name, const std::string &text_value){}; + virtual void process_bool(const std::string &variable_name, bool on){}; + + virtual void set_state(float state){}; + virtual void set_state(float state, bool publish){}; + virtual void set_state(float state, bool publish, bool send_to_nextion){}; + + virtual void set_state(bool state){}; + virtual void set_state(bool state, bool publish){}; + virtual void set_state(bool state, bool publish, bool send_to_nextion){}; + + virtual void set_state(const std::string &state) {} + virtual void set_state(const std::string &state, bool publish) {} + virtual void set_state(const std::string &state, bool publish, bool send_to_nextion){}; + + uint8_t get_component_id() { return this->component_id_; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + + uint8_t get_wave_channel_id() { return this->wave_chan_id_; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + + std::vector get_wave_buffer() { return this->wave_buffer_; } + size_t get_wave_buffer_size() { return this->wave_buffer_.size(); } + + std::string get_variable_name() { return this->variable_name_; } + std::string get_variable_name_to_send() { return this->variable_name_to_send_; } + virtual NextionQueueType get_queue_type() { return NextionQueueType::NO_RESULT; } + virtual std::string get_queue_type_string() { return NEXTION_QUEUE_TYPE_STRINGS[this->get_queue_type()]; } + virtual void set_state_from_int(int state_value, bool publish, bool send_to_nextion){}; + virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; + virtual void send_state_to_nextion(){}; + bool get_needs_to_send_update() { return this->needs_to_send_update_; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + + protected: + std::string variable_name_; + std::string variable_name_to_send_; + + uint8_t component_id_ = 0; + uint8_t wave_chan_id_ = UINT8_MAX; + std::vector wave_buffer_; + int wave_max_length_ = 255; + + bool needs_to_send_update_; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp new file mode 100644 index 0000000000..6a681af6c5 --- /dev/null +++ b/esphome/components/nextion/nextion_upload.cpp @@ -0,0 +1,343 @@ + +#include "nextion.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion_upload"; + +#if defined(USE_TFT_UPLOAD) && (defined(USE_ETHERNET) || defined(USE_WIFI)) + +// Followed guide +// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2 + +int Nextion::upload_by_chunks_(HTTPClient *http, int range_start) { + int range_end = 0; + + if (range_start == 0 && this->transfer_buffer_size_ > 16384) { // Start small at the first run in case of a big skip + range_end = 16384 - 1; + } else { + range_end = range_start + this->transfer_buffer_size_ - 1; + } + + if (range_end > this->tft_size_) + range_end = this->tft_size_; + + bool begin_status = false; +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http->begin(this->tft_url_.c_str()); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + http->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http->setRedirectLimit(3); + begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + char range_header[64]; + sprintf(range_header, "bytes=%d-%d", range_start, range_end); + + ESP_LOGD(TAG, "Requesting range: %s", range_header); + + int tries = 1; + int code = 0; + while (tries <= 5) { +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http->begin(this->tft_url_.c_str()); +#endif +#ifndef CLANG_TIDY +#ifdef ARDUINO_ARCH_ESP8266 + begin_status = http->begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + ++tries; + if (!begin_status) { + ESP_LOGD(TAG, "upload_by_chunks_: connection failed"); + continue; + } + + http->addHeader("Range", range_header); + + code = http->GET(); + if (code == 200 || code == 206) { + break; + } + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retries(%d/5)", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + http->end(); + App.feed_wdt(); + delay(500); // NOLINT + } + + if (tries > 5) { + return -1; + } + + std::string recv_string; + size_t size = 0; + int sent = 0; + int range = range_end - range_start; + + while (sent < range) { + size = http->getStreamPtr()->available(); + if (!size) { + App.feed_wdt(); + delay(0); + continue; + } + int c = http->getStreamPtr()->readBytes( + &this->transfer_buffer_[sent], ((size > this->transfer_buffer_size_) ? this->transfer_buffer_size_ : size)); + sent += c; + } + http->end(); + ESP_LOGN(TAG, "this->content_length_ %d sent %d", this->content_length_, sent); + for (uint32_t i = 0; i < range; i += 4096) { + this->write_array(&this->transfer_buffer_[i], 4096); + this->content_length_ -= 4096; + ESP_LOGN(TAG, "this->content_length_ %d range %d range_end %d range_start %d", this->content_length_, range, + range_end, range_start); + + if (!this->upload_first_chunk_sent_) { + this->upload_first_chunk_sent_ = true; + delay(500); // NOLINT + App.feed_wdt(); + } + + this->recv_ret_string_(recv_string, 2048, true); + if (recv_string[0] == 0x08) { + uint32_t result = 0; + for (int i = 0; i < 4; ++i) { + result += static_cast(recv_string[i + 1]) << (8 * i); + } + if (result > 0) { + ESP_LOGD(TAG, "Nextion reported new range %d", result); + this->content_length_ = this->tft_size_ - result; + return result; + } + } + recv_string.clear(); + } + return range_end + 1; +} + +void Nextion::upload_tft() { + if (this->is_updating_) { + ESP_LOGD(TAG, "Currently updating"); + return; + } + + if (!network_is_connected()) { + ESP_LOGD(TAG, "network is not connected"); + return; + } + + this->is_updating_ = true; + + HTTPClient http; + http.setTimeout(15000); // Yes 15 seconds.... Helps 8266s along + bool begin_status = false; +#ifdef ARDUINO_ARCH_ESP32 + begin_status = http.begin(this->tft_url_.c_str()); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + http.setRedirectLimit(3); + begin_status = http.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); +#endif +#endif + + if (!begin_status) { + this->is_updating_ = false; + ESP_LOGD(TAG, "connection failed"); +#ifdef ARDUINO_ARCH_ESP32 + if (psramFound()) + free(this->transfer_buffer_); + else +#endif + delete this->transfer_buffer_; + return; + } else { + ESP_LOGD(TAG, "Connected"); + } + + http.addHeader("Range", "bytes=0-255"); + const char *header_names[] = {"Content-Range"}; + http.collectHeaders(header_names, 1); + ESP_LOGD(TAG, "Requesting URL: %s", this->tft_url_.c_str()); + + http.setReuse(true); + // try up to 5 times. DNS sometimes needs a second try or so + int tries = 1; + int code = http.GET(); + delay(100); // NOLINT + + App.feed_wdt(); + while (code != 200 && code != 206 && tries <= 5) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s, retrying (%d/5)", this->tft_url_.c_str(), + HTTPClient::errorToString(code).c_str(), tries); + + delay(250); // NOLINT + App.feed_wdt(); + code = http.GET(); + ++tries; + } + + if ((code != 200 && code != 206) || tries > 5) { + this->upload_end_(); + } + + String content_range_string = http.header("Content-Range"); + content_range_string.remove(0, 12); + this->content_length_ = content_range_string.toInt(); + this->tft_size_ = content_length_; + http.end(); + + if (this->content_length_ < 4096) { + ESP_LOGE(TAG, "Failed to get file size"); + this->upload_end_(); + } + + ESP_LOGD(TAG, "Updating Nextion %s...", this->device_model_.c_str()); + // The Nextion will ignore the update command if it is sleeping + + this->send_command_("sleep=0"); + this->set_backlight_brightness(1.0); + delay(250); // NOLINT + + App.feed_wdt(); + + char command[128]; + // Tells the Nextion the content length of the tft file and baud rate it will be sent at + // Once the Nextion accepts the command it will wait until the file is successfully uploaded + // If it fails for any reason a power cycle of the display will be needed + sprintf(command, "whmi-wris %d,%d,1", this->content_length_, this->parent_->get_baud_rate()); + + // Clear serial receive buffer + uint8_t d; + while (this->available()) { + this->read_byte(&d); + }; + + this->send_command_(command); + + App.feed_wdt(); + + std::string response; + ESP_LOGD(TAG, "Waiting for upgrade response"); + this->recv_ret_string_(response, 2000, true); // This can take some time to return + + // The Nextion display will, if it's ready to accept data, send a 0x05 byte. + ESP_LOGD(TAG, "Upgrade response is %s %zu", response.c_str(), response.length()); + + for (int i = 0; i < response.length(); i++) { + ESP_LOGD(TAG, "Available %d : 0x%02X", i, response[i]); + } + + if (response.find(0x05) != std::string::npos) { + ESP_LOGD(TAG, "preparation for tft update done"); + } else { + ESP_LOGD(TAG, "preparation for tft update failed %d \"%s\"", response[0], response.c_str()); + this->upload_end_(); + } + + // Nextion wants 4096 bytes at a time. Make chunk_size a multiple of 4096 +#ifdef ARDUINO_ARCH_ESP32 + uint32_t chunk_size = 8192; + if (psramFound()) { + chunk_size = this->content_length_; + } else { + if (ESP.getFreeHeap() > 40960) { // 32K to keep on hand + int chunk = int((ESP.getFreeHeap() - 32768) / 4096); + chunk_size = chunk * 4096; + chunk_size = chunk_size > 65536 ? 65536 : chunk_size; + } else if (ESP.getFreeHeap() < 10240) { + chunk_size = 4096; + } + } +#else + uint32_t chunk_size = ESP.getFreeHeap() < 10240 ? 4096 : 8192; +#endif + + if (this->transfer_buffer_ == nullptr) { +#ifdef ARDUINO_ARCH_ESP32 + if (psramFound()) { + ESP_LOGD(TAG, "Allocating PSRAM buffer size %d, Free PSRAM size is %u", chunk_size, ESP.getFreePsram()); + this->transfer_buffer_ = (uint8_t *) ps_malloc(chunk_size); + if (this->transfer_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer size %d!", chunk_size); + this->upload_end_(); + } + } else { +#endif + ESP_LOGD(TAG, "Allocating buffer size %d, Heap size is %u", chunk_size, ESP.getFreeHeap()); + this->transfer_buffer_ = new uint8_t[chunk_size]; + if (!this->transfer_buffer_) { // Try a smaller size + ESP_LOGD(TAG, "Could not allocate buffer size: %d trying 4096 instead", chunk_size); + chunk_size = 4096; + ESP_LOGD(TAG, "Allocating %d buffer", chunk_size); + this->transfer_buffer_ = new uint8_t[chunk_size]; + + if (!this->transfer_buffer_) + this->upload_end_(); +#ifdef ARDUINO_ARCH_ESP32 + } +#endif + } + + this->transfer_buffer_size_ = chunk_size; + } + + ESP_LOGD(TAG, "Updating tft from \"%s\" with a file size of %d using %zu chunksize, Heap Size %d", + this->tft_url_.c_str(), this->content_length_, this->transfer_buffer_size_, ESP.getFreeHeap()); + + int result = 0; + while (this->content_length_ > 0) { + result = this->upload_by_chunks_(&http, result); + if (result < 0) { + ESP_LOGD(TAG, "Error updating Nextion!"); + this->upload_end_(); + } + App.feed_wdt(); + ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_); + } + ESP_LOGD(TAG, "Succesfully updated Nextion!"); + + this->upload_end_(); +} + +void Nextion::upload_end_() { + ESP_LOGD(TAG, "Restarting Nextion"); + this->soft_reset(); + delay(1500); // NOLINT + ESP_LOGD(TAG, "Restarting esphome"); + ESP.restart(); +} + +#ifdef ARDUINO_ARCH_ESP8266 +WiFiClient *Nextion::get_wifi_client_() { + if (this->tft_url_.compare(0, 6, "https:") == 0) { + if (this->wifi_client_secure_ == nullptr) { + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + this->wifi_client_ = new WiFiClient(); + } + return this->wifi_client_; +} +#endif + +#else +void Nextion::upload_tft() { ESP_LOGW(TAG, "tft_url, WIFI or Ethernet components are needed. Cannot upload."); } +#endif +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py new file mode 100644 index 0000000000..f8a383e3ac --- /dev/null +++ b/esphome/components/nextion/sensor/__init__.py @@ -0,0 +1,99 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor + +from esphome.const import ( + CONF_ID, + UNIT_EMPTY, + ICON_EMPTY, + CONF_COMPONENT_ID, + DEVICE_CLASS_EMPTY, +) +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_SENSOR_COMPONENT_SCHEMA, + CONF_VARIABLE_NAME, + CONF_COMPONENT_NAME, + CONF_PRECISION, + CONF_WAVE_CHANNEL_ID, + CONF_WAVE_MAX_VALUE, + CONF_WAVEFORM_SEND_LAST_VALUE, + CONF_WAVE_MAX_LENGTH, +) + + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSensor = nextion_ns.class_("NextionSensor", sensor.Sensor, cg.PollingComponent) + + +def CheckWaveID(value): + value = cv.int_(value) + if value < 0 or value > 3: + raise cv.Invalid(f"Valid range for {CONF_WAVE_CHANNEL_ID} is 0-3") + return value + + +def _validate(config): + if CONF_WAVE_CHANNEL_ID in config and CONF_COMPONENT_ID not in config: + raise cv.Invalid( + f"{CONF_COMPONENT_ID} is required when {CONF_WAVE_CHANNEL_ID} is set" + ) + + return config + + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(NextionSensor), + cv.Optional(CONF_PRECISION, default=0): cv.int_range(min=0, max=8), + cv.Optional(CONF_WAVE_CHANNEL_ID): CheckWaveID, + cv.Optional(CONF_COMPONENT_ID): cv.uint8_t, + cv.Optional(CONF_WAVE_MAX_LENGTH, default=255): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVE_MAX_VALUE, default=100): cv.int_range( + min=1, max=1024 + ), + cv.Optional(CONF_WAVEFORM_SEND_LAST_VALUE, default=True): cv.boolean, + } + ) + .extend(CONFIG_SENSOR_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_ID, CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), + _validate, +) + + +async def to_code(config): + + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + cg.add(hub.register_sensor_component(var)) + + await setup_component_core_(var, config, ".val") + + if CONF_PRECISION in config: + cg.add(var.set_precision(config[CONF_PRECISION])) + + if CONF_COMPONENT_ID in config: + cg.add(var.set_component_id(config[CONF_COMPONENT_ID])) + + if CONF_WAVE_CHANNEL_ID in config: + cg.add(var.set_wave_channel_id(config[CONF_WAVE_CHANNEL_ID])) + + if CONF_WAVEFORM_SEND_LAST_VALUE in config: + cg.add(var.set_waveform_send_last_value(config[CONF_WAVEFORM_SEND_LAST_VALUE])) + + if CONF_WAVE_MAX_VALUE in config: + cg.add(var.set_wave_max_value(config[CONF_WAVE_MAX_VALUE])) + + if CONF_WAVE_MAX_LENGTH in config: + cg.add(var.set_wave_max_length(config[CONF_WAVE_MAX_LENGTH])) diff --git a/esphome/components/nextion/sensor/nextion_sensor.cpp b/esphome/components/nextion/sensor/nextion_sensor.cpp new file mode 100644 index 0000000000..bbcb465d85 --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.cpp @@ -0,0 +1,110 @@ +#include "nextion_sensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_sensor"; + +void NextionSensor::process_sensor(const std::string &variable_name, int state) { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX && this->variable_name_ == variable_name) { + this->publish_state(state); + ESP_LOGD(TAG, "Processed sensor \"%s\" state %d", variable_name.c_str(), state); + } +} + +void NextionSensor::add_to_wave_buffer(float state) { + this->needs_to_send_update_ = true; + + int wave_state = (int) ((state / (float) this->wave_maxvalue_) * 100); + + wave_buffer_.push_back(wave_state); + + if (this->wave_buffer_.size() > this->wave_max_length_) { + this->wave_buffer_.erase(this->wave_buffer_.begin()); + } +} + +void NextionSensor::update() { + if (!this->nextion_->is_setup()) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + this->nextion_->add_to_get_queue(this); + } else { + if (this->send_last_value_) { + this->add_to_wave_buffer(this->last_value_); + } + + this->wave_update_(); + } +} + +void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (isnan(state)) + return; + + if (this->wave_chan_id_ == UINT8_MAX) { + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + + if (this->precision_ > 0) { + double to_multiply = pow(10, this->precision_); + int state_value = (int) (state * to_multiply); + + this->nextion_->add_no_result_to_queue_with_set(this, (int) state_value); + } else { + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + } + } else { + if (this->send_last_value_) { + this->last_value_ = state; // Update will handle setting the buffer + } else { + this->add_to_wave_buffer(state); + } + } + + if (this->wave_chan_id_ == UINT8_MAX) { + if (publish) { + this->publish_state(state); + } else { + this->raw_state = state; + this->state = state; + this->has_state_ = true; + } + } + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for sensor \"%s\" state %lf", this->variable_name_.c_str(), state); +} + +void NextionSensor::wave_update_() { + if (this->nextion_->is_sleeping() || this->wave_buffer_.empty()) { + return; + } + +#ifdef NEXTION_PROTOCOL_LOG + size_t buffer_to_send = + this->wave_buffer_.size() < 255 ? this->wave_buffer_.size() : 255; // ADDT command can only send 255 + + ESP_LOGN(TAG, "wave_update send %zu of %zu value(s) to wave nextion component id %d and wave channel id %d", + buffer_to_send, this->wave_buffer_.size(), this->component_id_, this->wave_chan_id_); +#endif + + this->nextion_->add_addt_command_to_queue(this); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/sensor/nextion_sensor.h b/esphome/components/nextion/sensor/nextion_sensor.h new file mode 100644 index 0000000000..e4dde9a513 --- /dev/null +++ b/esphome/components/nextion/sensor/nextion_sensor.h @@ -0,0 +1,49 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSensor; + +class NextionSensor : public NextionComponent, public sensor::Sensor, public PollingComponent { + public: + NextionSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + + void update_component() override { this->update(); } + void update() override; + void add_to_wave_buffer(float state); + void set_precision(uint8_t precision) { this->precision_ = precision; } + void set_component_id(uint8_t component_id) { component_id_ = component_id; } + void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_value(uint32_t wave_maxvalue) { this->wave_maxvalue_ = wave_maxvalue; } + void process_sensor(const std::string &variable_name, int state) override; + + void set_state(float state) override { this->set_state(state, true, true); } + void set_state(float state, bool publish) override { this->set_state(state, publish, true); } + void set_state(float state, bool publish, bool send_to_nextion) override; + + void set_waveform_send_last_value(bool send_last_value) { this->send_last_value_ = send_last_value; } + uint8_t get_wave_chan_id() { return this->wave_chan_id_; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } + NextionQueueType get_queue_type() override { + return this->wave_chan_id_ == UINT8_MAX ? NextionQueueType::SENSOR : NextionQueueType::WAVEFORM_SENSOR; + } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } + + protected: + uint8_t precision_ = 0; + uint32_t wave_maxvalue_ = 255; + + float last_value_ = 0; + bool send_last_value_ = true; + void wave_update_(); +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/__init__.py b/esphome/components/nextion/switch/__init__.py new file mode 100644 index 0000000000..068681fa14 --- /dev/null +++ b/esphome/components/nextion/switch/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch + +from esphome.const import CONF_ID +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONF_COMPONENT_NAME, + CONF_VARIABLE_NAME, + CONFIG_SWITCH_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionSwitch = nextion_ns.class_("NextionSwitch", switch.Switch, cg.PollingComponent) + +CONFIG_SCHEMA = cv.All( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionSwitch), + } + ) + .extend(CONFIG_SWITCH_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")), + cv.has_exactly_one_key(CONF_COMPONENT_NAME, CONF_VARIABLE_NAME), +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await switch.register_switch(var, config) + + cg.add(hub.register_switch_component(var)) + + await setup_component_core_(var, config, ".val") diff --git a/esphome/components/nextion/switch/nextion_switch.cpp b/esphome/components/nextion/switch/nextion_switch.cpp new file mode 100644 index 0000000000..1f32ad3425 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.cpp @@ -0,0 +1,52 @@ +#include "nextion_switch.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { + +static const char *const TAG = "nextion_switch"; + +void NextionSwitch::process_bool(const std::string &variable_name, bool on) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(on); + + ESP_LOGD(TAG, "Processed switch \"%s\" state %s", variable_name.c_str(), state ? "ON" : "OFF"); + } +} + +void NextionSwitch::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(this); +} + +void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->needs_to_send_update_ = false; + this->nextion_->add_no_result_to_queue_with_set(this, (int) state); + } + } + if (publish) { + this->publish_state(state); + } else { + this->state = state; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Updated switch \"%s\" state %s", this->variable_name_.c_str(), ONOFF(state)); +} + +void NextionSwitch::write_state(bool state) { this->set_state(state); } + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/switch/nextion_switch.h b/esphome/components/nextion/switch/nextion_switch.h new file mode 100644 index 0000000000..1548287473 --- /dev/null +++ b/esphome/components/nextion/switch/nextion_switch.h @@ -0,0 +1,34 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/switch/switch.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionSwitch; + +class NextionSwitch : public NextionComponent, public switch_::Switch, public PollingComponent { + public: + NextionSwitch(NextionBase *nextion) { this->nextion_ = nextion; } + + void update() override; + void update_component() override { this->update(); } + void process_bool(const std::string &variable_name, bool on) override; + + void set_state(bool state) override { this->set_state(state, true, true); } + void set_state(bool state, bool publish) override { this->set_state(state, publish, true); } + void set_state(bool state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::SWITCH; } + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value != 0, publish, send_to_nextion); + } + + protected: + void write_state(bool state) override; +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/__init__.py b/esphome/components/nextion/text_sensor/__init__.py new file mode 100644 index 0000000000..9c170dd807 --- /dev/null +++ b/esphome/components/nextion/text_sensor/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID + +from .. import nextion_ns, CONF_NEXTION_ID + +from ..base_component import ( + setup_component_core_, + CONFIG_TEXT_COMPONENT_SCHEMA, +) + +CODEOWNERS = ["@senexcrenshaw"] + +NextionTextSensor = nextion_ns.class_( + "NextionTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + +CONFIG_SCHEMA = ( + text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(NextionTextSensor), + } + ) + .extend(CONFIG_TEXT_COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("never")) +) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_NEXTION_ID]) + var = cg.new_Pvariable(config[CONF_ID], hub) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + + cg.add(hub.register_textsensor_component(var)) + + await setup_component_core_(var, config, ".txt") diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.cpp b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp new file mode 100644 index 0000000000..08f032df74 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.cpp @@ -0,0 +1,49 @@ +#include "nextion_textsensor.h" +#include "esphome/core/util.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nextion { +static const char *const TAG = "nextion_textsensor"; + +void NextionTextSensor::process_text(const std::string &variable_name, const std::string &text_value) { + if (!this->nextion_->is_setup()) + return; + if (this->variable_name_ == variable_name) { + this->publish_state(text_value); + ESP_LOGD(TAG, "Processed text_sensor \"%s\" state \"%s\"", variable_name.c_str(), text_value.c_str()); + } +} + +void NextionTextSensor::update() { + if (!this->nextion_->is_setup()) + return; + this->nextion_->add_to_get_queue(this); +} + +void NextionTextSensor::set_state(const std::string &state, bool publish, bool send_to_nextion) { + if (!this->nextion_->is_setup()) + return; + + if (send_to_nextion) { + if (this->nextion_->is_sleeping() || !this->visible_) { + this->needs_to_send_update_ = true; + } else { + this->nextion_->add_no_result_to_queue_with_set(this, state); + } + } + + if (publish) { + this->publish_state(state); + } else { + this->state = state; + this->has_state_ = true; + } + + this->update_component_settings(); + + ESP_LOGN(TAG, "Wrote state for text_sensor \"%s\" state \"%s\"", this->variable_name_.c_str(), state.c_str()); +} + +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/nextion/text_sensor/nextion_textsensor.h b/esphome/components/nextion/text_sensor/nextion_textsensor.h new file mode 100644 index 0000000000..5716d0a008 --- /dev/null +++ b/esphome/components/nextion/text_sensor/nextion_textsensor.h @@ -0,0 +1,32 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "../nextion_component.h" +#include "../nextion_base.h" + +namespace esphome { +namespace nextion { +class NextionTextSensor; + +class NextionTextSensor : public NextionComponent, public text_sensor::TextSensor, public PollingComponent { + public: + NextionTextSensor(NextionBase *nextion) { this->nextion_ = nextion; } + void update() override; + void update_component() override { this->update(); } + void on_state_changed(const std::string &state); + + void process_text(const std::string &variable_name, const std::string &text_value) override; + + void set_state(const std::string &state, bool publish) override { this->set_state(state, publish, true); } + void set_state(const std::string &state) override { this->set_state(state, true, true); } + void set_state(const std::string &state, bool publish, bool send_to_nextion) override; + + void send_state_to_nextion() override { this->set_state(this->state, false, true); }; + NextionQueueType get_queue_type() override { return NextionQueueType::TEXT_SENSOR; } + void set_state_from_int(int state_value, bool publish, bool send_to_nextion) override {} + void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion) override { + this->set_state(state_value, publish, send_to_nextion); + } +}; +} // namespace nextion +} // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 6dd62070da..cd54290c73 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -56,6 +56,7 @@ class ESP8266SoftwareSerial { class UARTComponent : public Component, public Stream { public: void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + uint32_t get_baud_rate() const { return baud_rate_; } uint32_t get_config(); diff --git a/script/ci-custom.py b/script/ci-custom.py index 02f193c6e0..d79e5b5e2f 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +from helpers import git_ls_files, filter_changed import codecs import collections import fnmatch @@ -12,7 +13,6 @@ import functools import argparse sys.path.append(os.path.dirname(__file__)) -from helpers import git_ls_files, filter_changed def find_all(a_str, sub): @@ -562,6 +562,7 @@ def lint_inclusive_language(fname, match): "esphome/components/number/number.h", "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", + "esphome/components/nextion/nextion_base.h", "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", diff --git a/tests/test1.yaml b/tests/test1.yaml index 1c522a23d4..f9fe7d106d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1055,10 +1055,6 @@ binary_sensor: pin: GPIO27 threshold: 1000 id: btn_left - - platform: nextion - page_id: 0 - component_id: 2 - name: 'Nextion Component 2 Touch' - platform: template name: 'Garage Door Open' id: garage_door @@ -1882,11 +1878,6 @@ display: intensity: 3 lambda: |- it.print("1234"); - - platform: nextion - uart_id: uart0 - lambda: |- - it.set_component_value("gauge", 50); - it.set_component_text("textview", "Hello World!"); - platform: pcd8544 cs_pin: GPIO23 dc_pin: GPIO23 diff --git a/tests/test3.yaml b/tests/test3.yaml index c709539309..acd975d794 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -269,6 +269,7 @@ wled: adalight: + sensor: - platform: apds9960 type: proximity @@ -534,6 +535,15 @@ sensor: export_reactive_energy: name: 'Export Reactive Energy' + - platform: nextion + id: testnumber + name: 'testnumber' + variable_name: testnumber + - platform: nextion + id: testwave + name: 'testwave' + component_id: 2 + wave_channel_id: 1 time: - platform: homeassistant @@ -605,7 +615,14 @@ binary_sensor: binary_sensors: - id: custom_binary_sensor name: Custom Binary Sensor - + - platform: nextion + page_id: 0 + component_id: 2 + name: 'Nextion Component 2 Touch' + - platform: nextion + id: r0_sensor + name: 'R0 Sensor' + component_name: page0.r0 globals: - id: my_global_string type: std::string @@ -653,6 +670,11 @@ text_sensor: text_sensors: - id: custom_text_sensor name: Custom Text Sensor + - platform: nextion + name: text0 + id: text0 + update_interval: 4s + component_name: text0 script: - id: my_script @@ -704,6 +726,10 @@ switch: switches: - id: custom_switch name: Custom Switch + - platform: nextion + id: r0 + name: 'R0 Switch' + component_name: page0.r0 custom_component: lambda: |- @@ -1086,6 +1112,16 @@ display: id: my_matrix lambda: |- it.printdigit("hello"); + - platform: nextion + uart_id: uart1 + tft_url: 'http://esphome.io/default35.tft' + update_interval: 5s + on_sleep: + then: + lambda: 'ESP_LOGD("display","Display went to sleep");' + on_wake: + then: + lambda: 'ESP_LOGD("display","Display woke up");' http_request: useragent: esphome/device From 628a94bad3e5964ef64101a61407077f6ab9842e Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Wed, 14 Jul 2021 23:45:41 -0400 Subject: [PATCH 569/643] Fix ethernet component hostname handling (#2010) Co-authored-by: Otto Winter --- .gitignore | 3 + .../ethernet/ethernet_component.cpp | 213 +++++++++--------- .../components/ethernet/ethernet_component.h | 10 +- 3 files changed, 120 insertions(+), 106 deletions(-) diff --git a/.gitignore b/.gitignore index a24550ad54..954ecb2cb8 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,9 @@ __pycache__/ # Intellij Idea .idea +# Vim +*.swp + # Hide some OS X stuff .DS_Store .AppleDouble diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 22007caafa..52c184eef6 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -25,6 +25,13 @@ static const char *const TAG = "ethernet"; EthernetComponent *global_eth_component; +#define ESPHL_ERROR_CHECK(err, message) \ + if (err != ESP_OK) { \ + ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \ + this->mark_failed(); \ + return; \ + } + EthernetComponent::EthernetComponent() { global_eth_component = this; } void EthernetComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Ethernet..."); @@ -36,103 +43,6 @@ void EthernetComponent::setup() { this->power_pin_->setup(); } - this->start_connect_(); - -#ifdef USE_MDNS - network_setup_mdns(); -#endif -} -void EthernetComponent::loop() { - const uint32_t now = millis(); - if (!this->connected_ && !this->last_connected_ && now - this->connect_begin_ > 15000) { - ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); - this->start_connect_(); - return; - } - - if (this->connected_ == this->last_connected_) - // nothing changed - return; - - if (this->connected_) { - // connection established - ESP_LOGI(TAG, "Connected via Ethernet!"); - this->dump_connect_params_(); - this->status_clear_warning(); - } else { - // connection lost - ESP_LOGW(TAG, "Connection via Ethernet lost! Re-connecting..."); - this->start_connect_(); - } - - this->last_connected_ = this->connected_; - - network_tick_mdns(); -} -void EthernetComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Ethernet:"); - this->dump_connect_params_(); - LOG_PIN(" Power Pin: ", this->power_pin_); - ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); - ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); - ESP_LOGCONFIG(TAG, " Type: %s", this->type_ == ETHERNET_TYPE_LAN8720 ? "LAN8720" : "TLK110"); -} -float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } -bool EthernetComponent::can_proceed() { return this->is_connected(); } -IPAddress EthernetComponent::get_ip_address() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); - return IPAddress(ip.ip.addr); -} - -void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_info_t info) { - const char *event_name; - - switch (event) { - case SYSTEM_EVENT_ETH_START: - event_name = "ETH started"; - break; - case SYSTEM_EVENT_ETH_STOP: - event_name = "ETH stopped"; - this->connected_ = false; - break; - case SYSTEM_EVENT_ETH_CONNECTED: - event_name = "ETH connected"; - break; - case SYSTEM_EVENT_ETH_DISCONNECTED: - event_name = "ETH disconnected"; - this->connected_ = false; - break; - case SYSTEM_EVENT_ETH_GOT_IP: - event_name = "ETH Got IP"; - this->connected_ = true; - break; - default: - return; - } - - ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); -} - -#define ESPHL_ERROR_CHECK(err, message) \ - if (err != ESP_OK) { \ - ESP_LOGE(TAG, message ": %d", err); \ - this->mark_failed(); \ - return; \ - } - -void EthernetComponent::start_connect_() { - this->connect_begin_ = millis(); - this->status_set_warning(); - - esp_err_t err; - if (this->initialized_) { - // already initialized - err = esp_eth_enable(); - ESPHL_ERROR_CHECK(err, "ETH enable error"); - return; - } - switch (this->type_) { case ETHERNET_TYPE_LAN8720: { memcpy(&this->eth_config, &phy_lan8720_default_ethernet_config, sizeof(eth_config_t)); @@ -160,16 +70,111 @@ void EthernetComponent::start_connect_() { tcpipInit(); + esp_err_t err; err = esp_eth_init(&this->eth_config); - if (err != ESP_OK) { - ESP_LOGE(TAG, "ETH init error: %d", err); - this->mark_failed(); - return; + ESPHL_ERROR_CHECK(err, "ETH init error"); + err = esp_eth_enable(); + ESPHL_ERROR_CHECK(err, "ETH enable error"); + +#ifdef USE_MDNS + network_setup_mdns(); +#endif +} +void EthernetComponent::loop() { + const uint32_t now = millis(); + + switch (this->state_) { + case EthernetComponentState::STOPPED: + if (this->started_) { + ESP_LOGI(TAG, "Starting ethernet connection"); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTING: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped ethernet connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (this->connected_) { + // connection established + ESP_LOGI(TAG, "Connected via Ethernet!"); + this->state_ = EthernetComponentState::CONNECTED; + + this->dump_connect_params_(); + this->status_clear_warning(); + + network_tick_mdns(); + } else if (now - this->connect_begin_ > 15000) { + ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); + this->start_connect_(); + } + break; + case EthernetComponentState::CONNECTED: + if (!this->started_) { + ESP_LOGI(TAG, "Stopped ethernet connection"); + this->state_ = EthernetComponentState::STOPPED; + } else if (!this->connected_) { + ESP_LOGW(TAG, "Connection via Ethernet lost! Re-connecting..."); + this->state_ = EthernetComponentState::CONNECTING; + this->start_connect_(); + } + break; + } +} +void EthernetComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Ethernet:"); + this->dump_connect_params_(); + LOG_PIN(" Power Pin: ", this->power_pin_); + ESP_LOGCONFIG(TAG, " MDC Pin: %u", this->mdc_pin_); + ESP_LOGCONFIG(TAG, " MDIO Pin: %u", this->mdio_pin_); + ESP_LOGCONFIG(TAG, " Type: %s", this->type_ == ETHERNET_TYPE_LAN8720 ? "LAN8720" : "TLK110"); +} +float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } +bool EthernetComponent::can_proceed() { return this->is_connected(); } +IPAddress EthernetComponent::get_ip_address() { + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); + return IPAddress(ip.ip.addr); +} + +void EthernetComponent::on_wifi_event_(system_event_id_t event, system_event_info_t info) { + const char *event_name; + + switch (event) { + case SYSTEM_EVENT_ETH_START: + event_name = "ETH started"; + this->started_ = true; + break; + case SYSTEM_EVENT_ETH_STOP: + event_name = "ETH stopped"; + this->started_ = false; + this->connected_ = false; + break; + case SYSTEM_EVENT_ETH_CONNECTED: + event_name = "ETH connected"; + break; + case SYSTEM_EVENT_ETH_DISCONNECTED: + event_name = "ETH disconnected"; + this->connected_ = false; + break; + case SYSTEM_EVENT_ETH_GOT_IP: + event_name = "ETH Got IP"; + this->connected_ = true; + break; + default: + return; } - this->initialized_ = true; + ESP_LOGV(TAG, "[Ethernet event] %s (num=%d)", event_name, event); +} - tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); +void EthernetComponent::start_connect_() { + this->connect_begin_ = millis(); + this->status_set_warning(); + + esp_err_t err; + err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_ETH, App.get_name().c_str()); + ESPHL_ERROR_CHECK(err, "ETH set hostname error"); tcpip_adapter_ip_info_t info; if (this->manual_ip_.has_value()) { @@ -220,7 +225,7 @@ void EthernetComponent::eth_phy_power_enable_(bool enable) { delay(1); global_eth_component->orig_power_enable_fun_(enable); } -bool EthernetComponent::is_connected() { return this->connected_ && this->last_connected_; } +bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } void EthernetComponent::dump_connect_params_() { tcpip_adapter_ip_info_t ip; tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 2dbd8ccd9d..326cd1edea 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -26,6 +26,12 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +enum class EthernetComponentState { + STOPPED, + CONNECTING, + CONNECTED, +}; + class EthernetComponent : public Component { public: EthernetComponent(); @@ -65,9 +71,9 @@ class EthernetComponent : public Component { eth_clock_mode_t clk_mode_{ETH_CLOCK_GPIO0_IN}; optional manual_ip_{}; - bool initialized_{false}; + bool started_{false}; bool connected_{false}; - bool last_connected_{false}; + EthernetComponentState state_{EthernetComponentState::STOPPED}; uint32_t connect_begin_; eth_config_t eth_config; eth_phy_power_enable_func orig_power_enable_fun_; From 45d368e3a199dfd4e995d67a51c6173b02ce6428 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Thu, 15 Jul 2021 00:12:48 -0400 Subject: [PATCH 570/643] Always tick mdns in ethernet component (#2018) --- esphome/components/ethernet/ethernet_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 52c184eef6..963ee267b3 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -102,8 +102,6 @@ void EthernetComponent::loop() { this->dump_connect_params_(); this->status_clear_warning(); - - network_tick_mdns(); } else if (now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); this->start_connect_(); @@ -120,6 +118,8 @@ void EthernetComponent::loop() { } break; } + + network_tick_mdns(); } void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, "Ethernet:"); From cc7dbeada689a0017cc719d01735a31c5f2ac883 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 15 Jul 2021 21:30:04 +0200 Subject: [PATCH 571/643] Refactor docker build system and workflows (#2023) --- .github/workflows/ci-docker.yml | 47 ++-- .github/workflows/ci.yml | 178 ++++++------- .github/workflows/docker-lint-build.yml | 108 ++++++-- .github/workflows/matchers/pytest.json | 19 ++ .github/workflows/release-dev.yml | 247 ------------------- .github/workflows/release.yml | 315 ++++++------------------ docker/Dockerfile | 2 +- docker/Dockerfile.hassio | 2 +- docker/Dockerfile.lint | 3 +- docker/build.py | 177 +++++++++++++ esphome/const.py | 6 +- script/bump-docker-base-version.py | 50 ---- script/bump-version.py | 10 +- 13 files changed, 456 insertions(+), 708 deletions(-) create mode 100644 .github/workflows/matchers/pytest.json delete mode 100644 .github/workflows/release-dev.yml create mode 100755 docker/build.py delete mode 100755 script/bump-docker-base-version.py diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 91ec88aeb3..45fd3e141b 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -18,38 +18,23 @@ jobs: name: Build docker containers runs-on: ubuntu-latest strategy: - fail-fast: false matrix: arch: [amd64, armv7, aarch64] - build_type: ["hassio", "docker"] + build_type: ["ha-addon", "docker", "lint"] steps: - - uses: actions/checkout@v2 - - name: Set up env variables - run: | - base_version="3.4.0" + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Set TAG + run: | + echo "TAG=check" >> $GITHUB_ENV - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi - - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:dev" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=ci" \ - --cache-from "${BUILD_TO}:dev" \ - --file "${DOCKERFILE}" \ - . + - name: Run build + run: | + docker/build.py \ + --tag "${TAG}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + build diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 121b3f1339..de777d5a3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,40 +4,36 @@ name: CI on: push: - # On dev branch release-dev already performs CI checks - # On other branches the `pull_request` trigger will be used - branches: [beta, release] + branches: [dev, beta, release] pull_request: jobs: - lint-clang-format: + ci-with-container: + name: ${{ matrix.name }} runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files strategy: fail-fast: false matrix: - split: [1, 2, 3, 4] + include: + - id: clang-format + name: Run script/clang-format + - id: clang-tidy + name: Run script/clang-tidy 1/4 + split: 1 + - id: clang-tidy + name: Run script/clang-tidy 2/4 + split: 2 + - id: clang-tidy + name: Run script/clang-tidy 3/4 + split: 3 + - id: clang-tidy + name: Run script/clang-tidy 4/4 + split: 4 + + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:1.1 steps: - uses: actions/checkout@v2 # Set up the pio project so that the cpp checks know how files are compiled @@ -45,26 +41,57 @@ jobs: - name: Set up platformio environment run: pio init --ide atom - - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" echo "::add-matcher::.github/workflows/matchers/gcc.json" + + - name: Run clang-format + run: script/clang-format -i + if: ${{ matrix.id == 'clang-format' }} + - name: Run clang-tidy run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + if: ${{ matrix.id == 'clang-tidy' }} + - name: Suggest changes run: script/ci-suggest-changes - lint-python: + ci: # Don't use the esphome-lint docker image because it may contain outdated requirements. # This way, all dependencies are cached via the cache action. + name: ${{ matrix.name }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - id: ci-custom + name: Run script/ci-custom + - id: lint-python + name: Run script/lint-python + - id: test + file: tests/test1.yaml + name: Test tests/test1.yaml + - id: test + file: tests/test2.yaml + name: Test tests/test2.yaml + - id: test + file: tests/test3.yaml + name: Test tests/test3.yaml + - id: test + file: tests/test4.yaml + name: Test tests/test4.yaml + - id: pytest + name: Run pytest + steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.7' + - name: Cache pip modules uses: actions/cache@v1 with: @@ -72,6 +99,17 @@ jobs: key: esphome-pip-3.7-${{ hashFiles('setup.py') }} restore-keys: | esphome-pip-3.7- + + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.file }}-${{ hashFiles('esphome/core/config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.file }}- + if: ${{ matrix.id == 'test' }} + - name: Set up python environment run: script/setup @@ -80,82 +118,22 @@ jobs: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" echo "::add-matcher::.github/workflows/matchers/lint-python.json" echo "::add-matcher::.github/workflows/matchers/python.json" + echo "::add-matcher::.github/workflows/matchers/pytest.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Lint Custom - run: script/ci-custom.py + run: | + script/ci-custom.py + script/build_codeowners.py --check + if: ${{ matrix.id == 'ci-custom' }} - name: Lint Python run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check + if: ${{ matrix.id == 'lint-python' }} - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup + - run: esphome compile ${{ matrix.file }} + if: ${{ matrix.id == 'test' }} - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - name: Run pytest run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests + pytest -vv --tb=native tests + if: ${{ matrix.id == 'pytest' }} diff --git a/.github/workflows/docker-lint-build.yml b/.github/workflows/docker-lint-build.yml index d254ac332a..32aec87cdd 100644 --- a/.github/workflows/docker-lint-build.yml +++ b/.github/workflows/docker-lint-build.yml @@ -13,30 +13,88 @@ on: - '.github/workflows/docker-lint-build.yml' jobs: - publish-docker-lint-iage: - name: Build docker containers + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["lint"] steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - echo "TAG=1.1" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "esphome/esphome-lint:latest" || true - - name: Build - run: | - docker build \ - --cache-from "esphome/esphome-lint:latest" \ - --file "docker/Dockerfile.lint" \ - --tag "esphome/esphome-lint:latest" \ - --tag "esphome/esphome-lint:${TAG}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: | - docker push "esphome/esphome-lint:${TAG}" - docker push "esphome/esphome-lint:latest" + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Set TAG + run: | + echo "TAG=1.1" >> $GITHUB_ENV + + - name: Run build + run: | + docker/build.py \ + --tag "${TAG}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + build + + - name: Log in to docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run push + run: | + docker/build.py \ + --tag "${TAG}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + push + + deploy-docker-manifest: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + strategy: + matrix: + build_type: ["lint"] + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + - name: Set TAG + run: | + echo "TAG=1.1" >> $GITHUB_ENV + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + + - name: Log in to docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Run manifest + run: | + docker/build.py \ + --tag "${TAG}" \ + --build-type "${{ matrix.build_type }}" \ + manifest diff --git a/.github/workflows/matchers/pytest.json b/.github/workflows/matchers/pytest.json new file mode 100644 index 0000000000..0eb8f050e6 --- /dev/null +++ b/.github/workflows/matchers/pytest.json @@ -0,0 +1,19 @@ +{ + "problemMatcher": [ + { + "owner": "pytest", + "fileLocation": "absolute", + "pattern": [ + { + "regexp": "^\\s+File \"(.*)\", line (\\d+), in (.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s+(.*)$", + "message": 1 + } + ] + } + ] +} diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml deleted file mode 100644 index f8b90d524f..0000000000 --- a/.github/workflows/release-dev.yml +++ /dev/null @@ -1,247 +0,0 @@ -name: Publish dev releases to docker hub - -on: - push: - branches: - - dev - -jobs: - # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - - lint-clang-format: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files - strategy: - fail-fast: false - matrix: - split: [1, 2, 3, 4] - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - - name: Run clang-tidy - run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} - - name: Suggest changes - run: script/ci-suggest-changes - - lint-python: - # Don't use the esphome-lint docker image because it may contain outdated requirements. - # This way, all dependencies are cached via the cache action. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up python environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Lint Custom - run: script/ci-custom.py - - name: Lint Python - run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup - - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Run pytest - run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests - - deploy-docker: - name: Build and publish docker containers - if: github.repository == 'esphome/esphome' - runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] - strategy: - matrix: - arch: [amd64, armv7, aarch64] - # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly - build_type: ["docker"] - steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - TAG="${GITHUB_SHA:0:7}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Set up env variables - run: | - base_version="3.4.0" - - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi - - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:dev" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --tag "${BUILD_TO}:dev" \ - --cache-from "${BUILD_TO}:dev" \ - --file "${DOCKERFILE}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: | - docker push "${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:dev" - - - deploy-docker-manifest: - if: github.repository == 'esphome/esphome' - runs-on: ubuntu-latest - needs: [deploy-docker] - steps: - - name: Enable experimental manifest support - run: | - mkdir -p ~/.docker - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - name: Set TAG - run: | - TAG="${GITHUB_SHA:0:7}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - name: "Create the manifest" - run: | - docker manifest create esphome/esphome:${TAG} \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:${TAG} - - docker manifest create esphome/esphome:dev \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 383f2878e5..1b15b540f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,164 +1,35 @@ name: Publish Release on: + workflow_dispatch: release: types: [published] + schedule: + - cron: "0 2 * * *" jobs: - # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - - lint-clang-format: + init: + name: Initialize build runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 + outputs: + tag: ${{ steps.tag.outputs.tag }} steps: - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - name: Run clang-format - run: script/clang-format -i - - name: Suggest changes - run: script/ci-suggest-changes - - lint-clang-tidy: - runs-on: ubuntu-latest - # cpp lint job runs with esphome-lint docker image so that clang-format-* - # doesn't have to be installed - container: esphome/esphome-lint:1.1 - # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files - strategy: - fail-fast: false - matrix: - split: [1, 2, 3, 4] - steps: - - uses: actions/checkout@v2 - # Set up the pio project so that the cpp checks know how files are compiled - # (build flags, libraries etc) - - name: Set up platformio environment - run: pio init --ide atom - - - - name: Register problem matchers + - name: Get tag + id: tag run: | - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - - name: Run clang-tidy - run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} - - name: Suggest changes - run: script/ci-suggest-changes - - lint-python: - # Don't use the esphome-lint docker image because it may contain outdated requirements. - # This way, all dependencies are cached via the cache action. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up python environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Lint Custom - run: script/ci-custom.py - - name: Lint Python - run: script/lint-python - - name: Lint CODEOWNERS - run: script/build_codeowners.py --check - - test: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - test: - - test1 - - test2 - - test3 - - test4 - - test5 - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - # Use per test platformio cache because tests have different platform versions - - name: Cache ~/.platformio - uses: actions/cache@v1 - with: - path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} - restore-keys: | - test-home-platformio-${{ matrix.test }}- - - name: Set up environment - run: script/setup - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - run: esphome compile tests/${{ matrix.test }}.yaml - - pytest: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.7' - - name: Cache pip modules - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: esphome-pip-3.7-${{ hashFiles('setup.py') }} - restore-keys: | - esphome-pip-3.7- - - name: Set up environment - run: script/setup - - name: Install Github Actions annotator - run: pip install pytest-github-actions-annotate-failures - - - name: Register problem matchers - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Run pytest - run: | - pytest \ - -qq \ - --durations=10 \ - -o console_output_style=count \ - tests + if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then + TAG="${GITHUB_REF#refs/tags/v}" + else + TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") + today="$(date --utc '+%Y%m%d')" + TAG="${TAG}${today}" + fi + echo "::set-output name=tag::${TAG}" deploy-pypi: name: Build and publish to PyPi - if: github.repository == 'esphome/esphome' - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -182,119 +53,85 @@ jobs: name: Build and publish docker containers if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + needs: [init] strategy: matrix: arch: [amd64, armv7, aarch64] - build_type: ["hassio", "docker"] + build_type: ["ha-addon", "docker"] steps: - - uses: actions/checkout@v2 - - name: Set TAG - run: | - TAG="${GITHUB_REF#refs/tags/v}" - echo "TAG=${TAG}" >> $GITHUB_ENV - - name: Set up env variables - run: | - base_version="3.4.0" + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' - if [[ "${{ matrix.build_type }}" == "hassio" ]]; then - build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" - dockerfile="docker/Dockerfile.hassio" - else - build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" - dockerfile="docker/Dockerfile" - fi + - name: Run build + run: | + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + build - if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then - cache_tag="beta" - else - cache_tag="latest" - fi + - name: Log in to docker hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - # Set env variables so these values don't need to be calculated again - echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV - echo "BUILD_TO=${build_to}" >> $GITHUB_ENV - echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV - echo "CACHE_TAG=${cache_tag}" >> $GITHUB_ENV - - name: Pull for cache - run: | - docker pull "${BUILD_TO}:${CACHE_TAG}" || true - - name: Register QEMU binfmt - run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes - - run: | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --cache-from "${BUILD_TO}:${CACHE_TAG}" \ - --file "${DOCKERFILE}" \ - . - - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: docker push "${BUILD_TO}:${TAG}" - - # Always publish to beta tag (also full releases) - - name: Publish docker beta tag - run: | - docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" - docker push "${BUILD_TO}:beta" - - - if: ${{ !github.event.release.prerelease }} - name: Publish docker latest tag - run: | - docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" - docker push "${BUILD_TO}:latest" + - name: Run push + run: | + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --arch "${{ matrix.arch }}" \ + --build-type "${{ matrix.build_type }}" \ + push deploy-docker-manifest: if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest - needs: [deploy-docker] + needs: [init, deploy-docker] + strategy: + matrix: + build_type: ["ha-addon", "docker"] steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' - name: Enable experimental manifest support run: | mkdir -p ~/.docker echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - name: Set TAG - run: | - TAG="${GITHUB_REF#refs/tags/v}" - echo "TAG=${TAG}" >> $GITHUB_ENV + - name: Log in to docker hub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - name: "Create the manifest" - run: | - docker manifest create esphome/esphome:${TAG} \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:${TAG} + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASSWORD }} + - name: Log in to the GitHub container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - - name: Publish docker beta tag + - name: Run manifest run: | - docker manifest create esphome/esphome:beta \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:beta - - - name: Publish docker latest tag - if: ${{ !github.event.release.prerelease }} - run: | - docker manifest create esphome/esphome:latest \ - esphome/esphome-aarch64:${TAG} \ - esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} - docker manifest push esphome/esphome:latest + docker/build.py \ + --tag "${{ needs.init.outputs.tag }}" \ + --build-type "${{ matrix.build_type }}" \ + manifest deploy-hassio-repo: - if: github.repository == 'esphome/esphome' + if: github.repository == 'esphome/esphome' && github.event_name == 'release' runs-on: ubuntu-latest needs: [deploy-docker] steps: diff --git a/docker/Dockerfile b/docker/Dockerfile index 0d126a2944..907c041119 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:3.4.0 +ARG BUILD_FROM=esphome/esphome-base:latest FROM ${BUILD_FROM} # First install requirements to leverage caching when requirements don't change diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index 5dd9339b18..ad80074ada 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -1,4 +1,4 @@ -ARG BUILD_FROM +ARG BUILD_FROM=esphome/esphome-hassio-base:latest FROM ${BUILD_FROM} # First install requirements to leverage caching when requirements don't change diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 60d63152a0..3a090c3b41 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,5 @@ -FROM esphome/esphome-lint-base:3.4.0 +ARG BUILD_FROM=esphome/esphome-lint-base:latest +FROM ${BUILD_FROM} COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / RUN \ diff --git a/docker/build.py b/docker/build.py new file mode 100755 index 0000000000..97222df8d1 --- /dev/null +++ b/docker/build.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +from dataclasses import dataclass +import subprocess +import argparse +import platform +import shlex +import re +import sys + + +CHANNEL_DEV = 'dev' +CHANNEL_BETA = 'beta' +CHANNEL_RELEASE = 'release' +CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] + +ARCH_AMD64 = 'amd64' +ARCH_ARMV7 = 'armv7' +ARCH_AARCH64 = 'aarch64' +ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] + +TYPE_DOCKER = 'docker' +TYPE_HA_ADDON = 'ha-addon' +TYPE_LINT = 'lint' +TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] + + +BASE_VERSION = "3.6.0" + + +parser = argparse.ArgumentParser() +parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") +parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") +parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") +parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") +subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) +build_parser = subparsers.add_parser("build", help="Build the image") +push_parser = subparsers.add_parser("push", help="Tag the already built image and push it to docker hub") +manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") + + + +# only lists some possibilities, doesn't have to be perfect +# https://stackoverflow.com/a/45125525 +UNAME_TO_ARCH = { + "x86_64": ARCH_AMD64, + "aarch64": ARCH_AARCH64, + "aarch64_be": ARCH_AARCH64, + "arm": ARCH_ARMV7, +} + + +@dataclass(frozen=True) +class DockerParams: + build_from: str + build_to: str + manifest_to: str + dockerfile: str + + @classmethod + def for_type_arch(cls, build_type, arch): + prefix = { + TYPE_DOCKER: "esphome/esphome", + TYPE_HA_ADDON: "esphome/esphome-hassio", + TYPE_LINT: "esphome/esphome-lint" + }[build_type] + build_from = f"ghcr.io/{prefix}-base-{arch}:{BASE_VERSION}" + build_to = f"{prefix}-{arch}" + dockerfile = { + TYPE_DOCKER: "docker/Dockerfile", + TYPE_HA_ADDON: "docker/Dockerfile.hassio", + TYPE_LINT: "docker/Dockerfile.lint", + }[build_type] + return cls( + build_from=build_from, + build_to=build_to, + manifest_to=prefix, + dockerfile=dockerfile + ) + + +def main(): + args = parser.parse_args() + + def run_command(*cmd, ignore_error: bool = False): + print(f"$ {shlex.join(list(cmd))}") + if not args.dry_run: + rc = subprocess.call(list(cmd)) + if rc != 0 and not ignore_error: + print("Command failed") + sys.exit(1) + + # detect channel from tag + match = re.match(r'\d+\.\d+(?:\.\d+)?(b\d+)?', args.tag) + if match is None: + channel = CHANNEL_DEV + elif match.group(1) is None: + channel = CHANNEL_RELEASE + else: + channel = CHANNEL_BETA + + tags_to_push = [args.tag] + if channel == CHANNEL_DEV: + tags_to_push.append("dev") + elif channel == CHANNEL_BETA: + tags_to_push.append("beta") + elif channel == CHANNEL_RELEASE: + # Additionally push to beta + tags_to_push.append("beta") + tags_to_push.append("latest") + + if args.command == "build": + # 1. pull cache image + params = DockerParams.for_type_arch(args.build_type, args.arch) + cache_tag = { + CHANNEL_DEV: "dev", + CHANNEL_BETA: "beta", + CHANNEL_RELEASE: "latest", + }[channel] + cache_img = f"ghcr.io/{params.build_to}:{cache_tag}" + run_command("docker", "pull", cache_img, ignore_error=True) + + # 2. register QEMU binfmt (if not host arch) + is_native = UNAME_TO_ARCH.get(platform.machine()) == args.arch + if not is_native: + run_command( + "docker", "run", "--rm", "--privileged", "multiarch/qemu-user-static:5.2.0-2", + "--reset", "-p", "yes" + ) + + # 3. build + run_command( + "docker", "build", + "--build-arg", f"BUILD_FROM={params.build_from}", + "--build-arg", f"BUILD_VERSION={args.tag}", + "--tag", f"{params.build_to}:{args.tag}", + "--cache-from", cache_img, + "--file", params.dockerfile, + "." + ) + elif args.command == "push": + params = DockerParams.for_type_arch(args.build_type, args.arch) + imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push] + imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push] + src = imgs[0] + # 1. tag images + for img in imgs[1:]: + run_command( + "docker", "tag", src, img + ) + # 2. push images + for img in imgs: + run_command( + "docker", "push", img + ) + elif args.command == "manifest": + manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to + + targets = [f"{manifest}:{tag}" for tag in tags_to_push] + targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push] + # 1. Create manifests + for target in targets: + cmd = ["docker", "manifest", "create", target] + for arch in ARCHS: + src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}" + if target.startswith("ghcr.io"): + src = f"ghcr.io/{src}" + cmd.append(src) + run_command(*cmd) + # 2. Push manifests + for target in targets: + run_command( + "docker", "manifest", "push", target + ) + + +if __name__ == "__main__": + main() diff --git a/esphome/const.py b/esphome/const.py index 7f1e3a2c58..1ea4285747 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,10 +1,6 @@ """Constants used by esphome.""" -MAJOR_VERSION = 1 -MINOR_VERSION = 21 -PATCH_VERSION = "0-dev" -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" +__version__ = "1.21.0-dev" ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" diff --git a/script/bump-docker-base-version.py b/script/bump-docker-base-version.py deleted file mode 100755 index f5f4399fd2..0000000000 --- a/script/bump-docker-base-version.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import re -import sys - - -def sub(path, pattern, repl, expected_count=1): - with open(path) as fh: - content = fh.read() - content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) - if expected_count is not None: - assert count == expected_count, f"Pattern {pattern} replacement failed!" - with open(path, "wt") as fh: - fh.write(content) - - -def write_version(version: str): - for p in [ - ".github/workflows/ci-docker.yml", - ".github/workflows/release-dev.yml", - ".github/workflows/release.yml", - ]: - sub(p, r'base_version=".*"', f'base_version="{version}"') - - sub( - "docker/Dockerfile", - r"ARG BUILD_FROM=esphome/esphome-base-amd64:.*", - f"ARG BUILD_FROM=esphome/esphome-base-amd64:{version}", - ) - sub( - "docker/Dockerfile.lint", - r"FROM esphome/esphome-lint-base:.*", - f"FROM esphome/esphome-lint-base:{version}", - ) - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument("new_version", type=str) - args = parser.parse_args() - - version = args.new_version - print(f"Bumping to {version}") - write_version(version) - return 0 - - -if __name__ == "__main__": - sys.exit(main() or 0) diff --git a/script/bump-version.py b/script/bump-version.py index b7b048eb22..1f034344f9 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -50,16 +50,10 @@ def sub(path, pattern, repl, expected_count=1): def write_version(version: Version): - sub( - "esphome/const.py", r"^MAJOR_VERSION = \d+$", f"MAJOR_VERSION = {version.major}" - ) - sub( - "esphome/const.py", r"^MINOR_VERSION = \d+$", f"MINOR_VERSION = {version.minor}" - ) sub( "esphome/const.py", - r"^PATCH_VERSION = .*$", - f'PATCH_VERSION = "{version.full_patch}"', + r"^__version__ = .*$", + f'__version__ = "{version}"', ) From 799f04efc090837d82aa314559897d865c798cb1 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 15 Jul 2021 21:51:52 +0200 Subject: [PATCH 572/643] GH Actions CI use GHCR (#2027) --- .github/workflows/ci.yml | 2 +- docker/build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de777d5a3b..4ccaaa47b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:1.1 + container: ghcr.io/esphome/esphome-lint:1.1 steps: - uses: actions/checkout@v2 # Set up the pio project so that the cpp checks know how files are compiled diff --git a/docker/build.py b/docker/build.py index 97222df8d1..54a279f845 100755 --- a/docker/build.py +++ b/docker/build.py @@ -90,7 +90,7 @@ def main(): sys.exit(1) # detect channel from tag - match = re.match(r'\d+\.\d+(?:\.\d+)?(b\d+)?', args.tag) + match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag) if match is None: channel = CHANNEL_DEV elif match.group(1) is None: From 442e58b07aba1f59f37b7433c9c89f2c7e7b56ec Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 16 Jul 2021 10:22:42 +0200 Subject: [PATCH 573/643] Dashboard disable assets caching (#2025) --- esphome/dashboard/dashboard.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 00b12199c0..66f72bfc00 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -782,10 +782,9 @@ def make_app(debug=get_bool_env(ENV_DEV)): class StaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): - if debug: - self.set_header( - "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0" - ) + self.set_header( + "Cache-Control", "no-store, no-cache, must-revalidate, max-age=0" + ) app_settings = { "debug": debug, From 06912b492f23d0af42ddad7b4add5d4347266eef Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 16 Jul 2021 10:23:08 +0200 Subject: [PATCH 574/643] Improve external components error messages (#2026) --- .../external_components/__init__.py | 95 +++++++++++-------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index 1602ac3b07..bf44dc1929 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -109,7 +109,15 @@ def _compute_destination_path(key: str) -> Path: return base_dir / h.hexdigest()[:8] -def _handle_git_response(ret): +def _run_git_command(cmd): + try: + ret = subprocess.run(cmd, capture_output=True, check=False) + except FileNotFoundError as err: + raise cv.Invalid( + "git is not installed but required for external_components.\n" + "Please see https://git-scm.com/book/en/v2/Getting-Started-Installing-Git for installing git" + ) from err + if ret.returncode != 0 and ret.stderr: err_str = ret.stderr.decode("utf-8") lines = [x.strip() for x in err_str.splitlines()] @@ -118,46 +126,59 @@ def _handle_git_response(ret): raise cv.Invalid(err_str) +def _process_git_config(config: dict, refresh) -> str: + key = f"{config[CONF_URL]}@{config.get(CONF_REF)}" + repo_dir = _compute_destination_path(key) + if not repo_dir.is_dir(): + _LOGGER.info("Cloning %s", key) + _LOGGER.debug("Location: %s", repo_dir) + cmd = ["git", "clone", "--depth=1"] + if CONF_REF in config: + cmd += ["--branch", config[CONF_REF]] + cmd += ["--", config[CONF_URL], str(repo_dir)] + _run_git_command(cmd) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.datetime.now() - datetime.datetime.fromtimestamp( + file_timestamp.stat().st_mtime + ) + if age.seconds > refresh.total_seconds: + _LOGGER.info("Updating %s", key) + _LOGGER.debug("Location: %s", repo_dir) + # Stash local changes (if any) + _run_git_command(["git", "stash", "push", "--include-untracked"]) + # Fetch remote ref + cmd = ["git", "fetch", "--", "origin"] + if CONF_REF in config: + cmd.append(config[CONF_REF]) + _run_git_command(cmd) + # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) + _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"]) + + if (repo_dir / "esphome" / "components").is_dir(): + components_dir = repo_dir / "esphome" / "components" + elif (repo_dir / "components").is_dir(): + components_dir = repo_dir / "components" + else: + raise cv.Invalid( + "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder" + ) + + return components_dir + + def _process_single_config(config: dict): conf = config[CONF_SOURCE] if conf[CONF_TYPE] == TYPE_GIT: - key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}" - repo_dir = _compute_destination_path(key) - if not repo_dir.is_dir(): - cmd = ["git", "clone", "--depth=1"] - if CONF_REF in conf: - cmd += ["--branch", conf[CONF_REF]] - cmd += [conf[CONF_URL], str(repo_dir)] - ret = subprocess.run(cmd, capture_output=True, check=False) - _handle_git_response(ret) - - else: - # Check refresh needed - file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") - # On first clone, FETCH_HEAD does not exists - if not file_timestamp.exists(): - file_timestamp = Path(repo_dir / ".git" / "HEAD") - age = datetime.datetime.now() - datetime.datetime.fromtimestamp( - file_timestamp.stat().st_mtime + with cv.prepend_path([CONF_SOURCE]): + components_dir = _process_git_config( + config[CONF_SOURCE], config[CONF_REFRESH] ) - if age.seconds > config[CONF_REFRESH].total_seconds: - _LOGGER.info("Executing git pull %s", key) - cmd = ["git", "pull"] - ret = subprocess.run( - cmd, cwd=repo_dir, capture_output=True, check=False - ) - _handle_git_response(ret) - - if (repo_dir / "esphome" / "components").is_dir(): - components_dir = repo_dir / "esphome" / "components" - elif (repo_dir / "components").is_dir(): - components_dir = repo_dir / "components" - else: - raise cv.Invalid( - "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder", - [CONF_SOURCE], - ) - elif conf[CONF_TYPE] == TYPE_LOCAL: components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) else: From 2e49fd7b48e471a5f5c6c4d5010a409a8441f6ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jul 2021 08:09:00 +1200 Subject: [PATCH 575/643] Bump black from 21.6b0 to 21.7b0 (#2031) Bumps [black](https://github.com/psf/black) from 21.6b0 to 21.7b0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/commits) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f9566d5adc..dc46b6f3bf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==2.8.2 flake8==3.9.2 -black==21.6b0 +black==21.7b0 pexpect==4.8.0 pre-commit From 71d9d64a020db671aa7b2ee7c3193160017ae9d6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Jul 2021 08:22:49 +1200 Subject: [PATCH 576/643] Number and Template Number updates (#2036) Co-authored-by: Otto winter --- esphome/components/api/api_connection.cpp | 8 +- esphome/components/mqtt/mqtt_number.cpp | 22 +++-- esphome/components/number/__init__.py | 27 ++++-- esphome/components/number/number.cpp | 69 ++++---------- esphome/components/number/number.h | 93 +++++++------------ .../components/template/number/__init__.py | 25 +++-- .../template/number/template_number.cpp | 33 ++++--- .../template/number/template_number.h | 19 ++-- esphome/components/web_server/web_server.cpp | 5 +- 9 files changed, 148 insertions(+), 153 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8c76583fc7..79ffcfa69e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -570,11 +570,11 @@ bool APIConnection::send_number_info(number::Number *number) { msg.object_id = number->get_object_id(); msg.name = number->get_name(); msg.unique_id = get_default_unique_id("number", number); - msg.icon = number->get_icon(); + msg.icon = number->traits.get_icon(); - msg.min_value = number->get_min_value(); - msg.max_value = number->get_max_value(); - msg.step = number->get_step(); + msg.min_value = number->traits.get_min_value(); + msg.max_value = number->traits.get_max_value(); + msg.step = number->traits.get_step(); return this->send_list_entities_number_response(msg); } diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index bb67a225fd..0311526340 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -3,10 +3,6 @@ #ifdef USE_NUMBER -#ifdef USE_DEEP_SLEEP -#include "esphome/components/deep_sleep/deep_sleep_component.h" -#endif - namespace esphome { namespace mqtt { @@ -20,7 +16,7 @@ void MQTTNumberComponent::setup() { this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { auto val = parse_float(state); if (!val.has_value()) { - ESP_LOGE(TAG, "Can't convert '%s' to number!", state.c_str()); + ESP_LOGW(TAG, "Can't convert '%s' to number!", state.c_str()); return; } auto call = this->number_->make_call(); @@ -39,8 +35,15 @@ std::string MQTTNumberComponent::component_type() const { return "number"; } std::string MQTTNumberComponent::friendly_name() const { return this->number_->get_name(); } void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { - if (!this->number_->get_icon().empty()) - root["icon"] = this->number_->get_icon(); + const auto &traits = number_->traits; + // https://www.home-assistant.io/integrations/number.mqtt/ + if (!traits.get_icon().empty()) + root["icon"] = traits.get_icon(); + root["min_value"] = traits.get_min_value(); + root["max_value"] = traits.get_max_value(); + root["step"] = traits.get_step(); + + config.command_topic = true; } bool MQTTNumberComponent::send_initial_state() { if (this->number_->has_state()) { @@ -51,8 +54,9 @@ bool MQTTNumberComponent::send_initial_state() { } bool MQTTNumberComponent::is_internal() { return this->number_->is_internal(); } bool MQTTNumberComponent::publish_state(float value) { - int8_t accuracy = this->number_->get_accuracy_decimals(); - return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + return this->publish(this->get_state_topic_(), buffer); } } // namespace mqtt diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ed33931d8b..bf95cb1b31 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,3 +1,4 @@ +from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -66,12 +67,18 @@ NUMBER_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( ) -async def setup_number_core_(var, config): +async def setup_number_core_( + var, config, *, min_value: float, max_value: float, step: Optional[float] +): cg.add(var.set_name(config[CONF_NAME])) if CONF_INTERNAL in config: cg.add(var.set_internal(config[CONF_INTERNAL])) - cg.add(var.set_icon(config[CONF_ICON])) + cg.add(var.traits.set_icon(config[CONF_ICON])) + cg.add(var.traits.set_min_value(min_value)) + cg.add(var.traits.set_max_value(max_value)) + if step is not None: + cg.add(var.traits.set_step(step)) for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) @@ -92,16 +99,24 @@ async def setup_number_core_(var, config): await mqtt.register_mqtt_component(mqtt_, config) -async def register_number(var, config): +async def register_number( + var, config, *, min_value: float, max_value: float, step: Optional[float] = None +): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_number(var)) - await setup_number_core_(var, config) + await setup_number_core_( + var, config, min_value=min_value, max_value=max_value, step=step + ) -async def new_number(config): +async def new_number( + config, *, min_value: float, max_value: float, step: Optional[float] = None +): var = cg.new_Pvariable(config[CONF_ID]) - await register_number(var, config) + await register_number( + var, config, min_value=min_value, max_value=max_value, step=step + ) return var diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index eaee5d4e69..dbc1c88a5d 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -8,67 +8,38 @@ static const char *const TAG = "number"; void NumberCall::perform() { ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); - if (this->value_.has_value()) { - auto value = *this->value_; - uint8_t accuracy = this->parent_->get_accuracy_decimals(); - float min_value = this->parent_->get_min_value(); - if (value < min_value) { - ESP_LOGW(TAG, " Value %s must not be less than minimum %s", value_accuracy_to_string(value, accuracy).c_str(), - value_accuracy_to_string(min_value, accuracy).c_str()); - this->value_.reset(); - return; - } - float max_value = this->parent_->get_max_value(); - if (value > max_value) { - ESP_LOGW(TAG, " Value %s must not be larger than maximum %s", value_accuracy_to_string(value, accuracy).c_str(), - value_accuracy_to_string(max_value, accuracy).c_str()); - this->value_.reset(); - return; - } - ESP_LOGD(TAG, " Value: %s", value_accuracy_to_string(*this->value_, accuracy).c_str()); - this->parent_->set(*this->value_); + if (!this->value_.has_value() || isnan(*this->value_)) { + ESP_LOGW(TAG, "No value set for NumberCall"); + return; } + + const auto &traits = this->parent_->traits; + auto value = *this->value_; + + float min_value = traits.get_min_value(); + if (value < min_value) { + ESP_LOGW(TAG, " Value %f must not be less than minimum %f", value, min_value); + return; + } + float max_value = traits.get_max_value(); + if (value > max_value) { + ESP_LOGW(TAG, " Value %f must not be greater than maximum %f", value, max_value); + return; + } + ESP_LOGD(TAG, " Value: %f", *this->value_); + this->parent_->control(*this->value_); } -NumberCall &NumberCall::set_value(float value) { - this->value_ = value; - return *this; -} - -const optional &NumberCall::get_value() const { return this->value_; } - -NumberCall Number::make_call() { return NumberCall(this); } - void Number::publish_state(float state) { this->has_state_ = true; this->state = state; - ESP_LOGD(TAG, "'%s': Sending state %.5f", this->get_name().c_str(), state); + ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); this->state_callback_.call(state); } -uint32_t Number::update_interval() { return 0; } -Number::Number(const std::string &name) : Nameable(name), state(NAN) {} -Number::Number() : Number("") {} - void Number::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } -void Number::set_icon(const std::string &icon) { this->icon_ = icon; } -std::string Number::get_icon() { return *this->icon_; } -int8_t Number::get_accuracy_decimals() { - // use printf %g to find number of digits based on step - char buf[32]; - sprintf(buf, "%.5g", this->step_); - std::string str{buf}; - size_t dot_pos = str.find('.'); - if (dot_pos == std::string::npos) - return 0; - - return str.length() - dot_pos - 1; -} -float Number::get_state() const { return this->state; } - -bool Number::has_state() const { return this->has_state_; } uint32_t Number::hash_base() { return 2282307003UL; } diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 4fe9692a6b..e32b53187b 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -9,8 +9,8 @@ namespace number { #define LOG_NUMBER(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->traits.get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->traits.get_icon().c_str()); \ } \ } @@ -19,72 +19,56 @@ class Number; class NumberCall { public: explicit NumberCall(Number *parent) : parent_(parent) {} - NumberCall &set_value(float value); void perform(); - const optional &get_value() const; + NumberCall &set_value(float value) { + value_ = value; + return *this; + } + const optional &get_value() const { return value_; } protected: Number *const parent_; optional value_; }; +class NumberTraits { + public: + void set_min_value(float min_value) { min_value_ = min_value; } + float get_min_value() const { return min_value_; } + void set_max_value(float max_value) { max_value_ = max_value; } + float get_max_value() const { return max_value_; } + void set_step(float step) { step_ = step; } + float get_step() const { return step_; } + void set_icon(std::string icon) { icon_ = std::move(icon); } + const std::string &get_icon() const { return icon_; } + + protected: + float min_value_ = NAN; + float max_value_ = NAN; + float step_ = NAN; + std::string icon_; +}; + /** Base-class for all numbers. * * A number can use publish_state to send out a new value. */ class Number : public Nameable { public: - explicit Number(); - explicit Number(const std::string &name); - - /** Manually set the icon of this number. By default the number's default defined by icon() is used. - * - * @param icon The icon, for example "mdi:flash". "" to disable. - */ - void set_icon(const std::string &icon); - /// Get the Home Assistant Icon. Uses the manual override if specified or the default value instead. - std::string get_icon(); - - /// Getter-syntax for .state. - float get_state() const; - - /// Get the accuracy in decimals. Based on the step value. - int8_t get_accuracy_decimals(); - - /** Publish the current state to the front-end. - */ - void publish_state(float state); - - NumberCall make_call(); - - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) - /// Add a callback that will be called every time the state changes. - void add_on_state_callback(std::function &&callback); - - /** This member variable stores the last state. - * - * On startup, when no state is available yet, this is NAN (not-a-number) and the validity - * can be checked using has_state(). - * - * This is exposed through a member variable for ease of use in esphome lambdas. - */ float state; + void publish_state(float state); + + NumberCall make_call() { return NumberCall(this); } + void set(float value) { make_call().set_value(value).perform(); } + + void add_on_state_callback(std::function &&callback); + + NumberTraits traits; + /// Return whether this number has gotten a full state yet. - bool has_state() const; - - /// Return with which interval the number is polled. Return 0 for non-polling mode. - virtual uint32_t update_interval(); - - void set_min_value(float min_value) { this->min_value_ = min_value; } - void set_max_value(float max_value) { this->max_value_ = max_value; } - void set_step(float step) { this->step_ = step; } - - float get_min_value() const { return this->min_value_; } - float get_max_value() const { return this->max_value_; } - float get_step() const { return this->step_; } + bool has_state() const { return has_state_; } protected: friend class NumberCall; @@ -95,17 +79,12 @@ class Number : public Nameable { * * @param value The value as validated by the NumberCall. */ - virtual void set(float value) = 0; + virtual void control(float value) = 0; uint32_t hash_base() override; CallbackManager state_callback_; - /// Override the icon advertised to Home Assistant, otherwise number's icon will be used. - optional icon_; bool has_state_{false}; - float step_{1.0}; - float min_value_{0}; - float max_value_{100}; }; } // namespace number diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index cf70a48c4d..557a01c6fa 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -4,6 +4,7 @@ import esphome.config_validation as cv from esphome.components import number from esphome.const import ( CONF_ID, + CONF_INITIAL_VALUE, CONF_LAMBDA, CONF_MAX_VALUE, CONF_MIN_VALUE, @@ -32,9 +33,10 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, - cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, + cv.Exclusive(CONF_LAMBDA, "lambda-optimistic"): cv.returning_lambda, + cv.Exclusive(CONF_OPTIMISTIC, "lambda-optimistic"): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_VALUE): cv.float_, } ).extend(cv.polling_component_schema("60s")), validate_min_max, @@ -44,20 +46,27 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await number.register_number(var, config) + await number.register_number( + var, + config, + min_value=config[CONF_MIN_VALUE], + max_value=config[CONF_MAX_VALUE], + step=config[CONF_STEP], + ) if CONF_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(float) ) cg.add(var.set_template(template_)) + + elif CONF_OPTIMISTIC in config: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if CONF_SET_ACTION in config: await automation.build_automation( var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] ) - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - - cg.add(var.set_min_value(config[CONF_MIN_VALUE])) - cg.add(var.set_max_value(config[CONF_MAX_VALUE])) - cg.add(var.set_step(config[CONF_STEP])) + if CONF_INITIAL_VALUE in config: + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 69c5d62684..500f9f2272 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -6,34 +6,45 @@ namespace template_ { static const char *const TAG = "template.number"; -TemplateNumber::TemplateNumber() : set_trigger_(new Trigger()) {} +void TemplateNumber::setup() { + if (this->f_.has_value() || !this->optimistic_) + return; + + this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + float value; + if (!this->pref_.load(&value)) { + if (!isnan(this->initial_value_)) + value = this->initial_value_; + else + value = this->traits.get_min_value(); + } + this->publish_state(value); +} void TemplateNumber::update() { if (!this->f_.has_value()) return; auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); - } + if (!val.has_value()) + return; + + this->publish_state(*val); } -void TemplateNumber::set(float value) { +void TemplateNumber::control(float value) { this->set_trigger_->trigger(value); - if (this->optimistic_) + if (this->optimistic_) { this->publish_state(value); + this->pref_.save(&value); + } } -float TemplateNumber::get_setup_priority() const { return setup_priority::HARDWARE; } -void TemplateNumber::set_template(std::function()> &&f) { this->f_ = f; } void TemplateNumber::dump_config() { LOG_NUMBER("", "Template Number", this); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); LOG_UPDATE_INTERVAL(this); } -void TemplateNumber::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } -Trigger *TemplateNumber::get_set_trigger() const { return this->set_trigger_; }; - } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 4c633e3b53..50cd256b7f 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -3,27 +3,32 @@ #include "esphome/components/number/number.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/preferences.h" namespace esphome { namespace template_ { class TemplateNumber : public number::Number, public PollingComponent { public: - TemplateNumber(); - void set_template(std::function()> &&f); + void set_template(std::function()> &&f) { this->f_ = f; } + void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const; - void set_optimistic(bool optimistic); + Trigger *get_set_trigger() const { return set_trigger_; } + void set_optimistic(bool optimistic) { optimistic_ = optimistic; } + void set_initial_value(float initial_value) { initial_value_ = initial_value; } protected: - void set(float value) override; + void control(float value) override; bool optimistic_{false}; - Trigger *set_trigger_; + float initial_value_{NAN}; + Trigger *set_trigger_ = new Trigger(); optional()>> f_; + + ESPPreferenceObject pref_; }; } // namespace template_ diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 57eef7a946..b775d44211 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -614,8 +614,9 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM std::string WebServer::number_json(number::Number *obj, float value) { return json::build_json([obj, value](JsonObject &root) { root["id"] = "number-" + obj->get_object_id(); - std::string state = value_accuracy_to_string(value, obj->get_accuracy_decimals()); - root["state"] = state; + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%f", value); + root["state"] = buffer; root["value"] = value; }); } From 7619507e6c93251b147d258958d854cbc1921a82 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Jul 2021 15:31:54 +1200 Subject: [PATCH 577/643] Convert Arduino boolean to bool (#2042) --- esphome/components/dfplayer/dfplayer.h | 4 ++-- esphome/components/st7735/st7735.cpp | 3 +-- esphome/components/st7735/st7735.h | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 5cd49c311d..ae47cb33f1 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -116,7 +116,7 @@ DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto file = this->file_.value(x...); @@ -133,7 +133,7 @@ template class PlayFolderAction : public Action, public P public: TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) - TEMPLATABLE_VALUE(boolean, loop) + TEMPLATABLE_VALUE(bool, loop) void play(Ts... x) override { auto folder = this->folder_.value(x...); diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp index f329ef4620..0467ed83db 100644 --- a/esphome/components/st7735/st7735.cpp +++ b/esphome/components/st7735/st7735.cpp @@ -220,8 +220,7 @@ static const uint8_t PROGMEM // clang-format on static const char *const TAG = "st7735"; -ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, - boolean usebgr) { +ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr) { model_ = model; this->width_ = width; this->height_ = height; diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h index 11bcc746f0..737170e99b 100644 --- a/esphome/components/st7735/st7735.h +++ b/esphome/components/st7735/st7735.h @@ -37,7 +37,7 @@ class ST7735 : public PollingComponent, public spi::SPIDevice { public: - ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, boolean usebgr); + ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, bool eightbitcolor, bool usebgr); void dump_config() override; void setup() override; @@ -75,8 +75,8 @@ class ST7735 : public PollingComponent, ST7735Model model_{ST7735_INITR_18BLACKTAB}; uint8_t colstart_ = 0, rowstart_ = 0; - boolean eightbitcolor_ = false; - boolean usebgr_ = false; + bool eightbitcolor_ = false; + bool usebgr_ = false; int16_t width_ = 80, height_ = 80; // Watch heap size GPIOPin *reset_pin_{nullptr}; From 99d2db42cd6336f0bc6a5e8ad7425eb2f008473c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 20 Jul 2021 15:40:42 +1200 Subject: [PATCH 578/643] Add restore_value to template number (#2041) --- .../components/template/number/__init__.py | 30 ++++++++++++++----- .../template/number/template_number.cpp | 23 ++++++++------ .../template/number/template_number.h | 2 ++ 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 557a01c6fa..22bbaacc15 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, CONF_STEP, ) from .. import template_ns @@ -26,6 +27,17 @@ def validate_min_max(config): return config +def validate(config): + if CONF_LAMBDA in config: + if CONF_OPTIMISTIC in config: + raise cv.Invalid("optimistic cannot be used with lambda") + if CONF_INITIAL_VALUE in config: + raise cv.Invalid("initial_value cannot be used with lambda") + if CONF_RESTORE_VALUE in config: + raise cv.Invalid("restore_value cannot be used with lambda") + return config + + CONFIG_SCHEMA = cv.All( number.NUMBER_SCHEMA.extend( { @@ -33,13 +45,15 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_MAX_VALUE): cv.float_, cv.Required(CONF_MIN_VALUE): cv.float_, cv.Required(CONF_STEP): cv.positive_float, - cv.Exclusive(CONF_LAMBDA, "lambda-optimistic"): cv.returning_lambda, - cv.Exclusive(CONF_OPTIMISTIC, "lambda-optimistic"): cv.boolean, + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC): cv.boolean, cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_INITIAL_VALUE): cv.float_, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, } ).extend(cv.polling_component_schema("60s")), validate_min_max, + validate, ) @@ -60,13 +74,15 @@ async def to_code(config): ) cg.add(var.set_template(template_)) - elif CONF_OPTIMISTIC in config: - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + else: + if CONF_OPTIMISTIC in config: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + if CONF_INITIAL_VALUE in config: + cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) if CONF_SET_ACTION in config: await automation.build_automation( var.get_set_trigger(), [(float, "x")], config[CONF_SET_ACTION] ) - - if CONF_INITIAL_VALUE in config: - cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 500f9f2272..eb9b17b976 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -7,16 +7,20 @@ namespace template_ { static const char *const TAG = "template.number"; void TemplateNumber::setup() { - if (this->f_.has_value() || !this->optimistic_) + if (this->f_.has_value()) return; - this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); float value; - if (!this->pref_.load(&value)) { - if (!isnan(this->initial_value_)) - value = this->initial_value_; - else - value = this->traits.get_min_value(); + if (!this->restore_value_) { + value = this->initial_value_; + } else { + this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + if (!isnan(this->initial_value_)) + value = this->initial_value_; + else + value = this->traits.get_min_value(); + } } this->publish_state(value); } @@ -35,10 +39,11 @@ void TemplateNumber::update() { void TemplateNumber::control(float value) { this->set_trigger_->trigger(value); - if (this->optimistic_) { + if (this->optimistic_) this->publish_state(value); + + if (this->restore_value_) this->pref_.save(&value); - } } void TemplateNumber::dump_config() { LOG_NUMBER("", "Template Number", this); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 50cd256b7f..9a82e44339 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -20,11 +20,13 @@ class TemplateNumber : public number::Number, public PollingComponent { Trigger *get_set_trigger() const { return set_trigger_; } void set_optimistic(bool optimistic) { optimistic_ = optimistic; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } protected: void control(float value) override; bool optimistic_{false}; float initial_value_{NAN}; + bool restore_value_{false}; Trigger *set_trigger_ = new Trigger(); optional()>> f_; From d2ed3b9becaedca26953d8976a29fdc51e087192 Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Tue, 20 Jul 2021 08:26:07 +0400 Subject: [PATCH 579/643] midea_ac: Fix turbo mode. Preset BOOST. (#2029) --- esphome/components/midea_ac/midea_frame.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h index a84161b4af..3777f6fd77 100644 --- a/esphome/components/midea_ac/midea_frame.h +++ b/esphome/components/midea_ac/midea_frame.h @@ -102,8 +102,11 @@ class PropertiesFrame : public midea_dongle::BaseFrame { void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); } /* TURBO MODE */ - bool get_turbo_mode() const { return this->pbuf_[18] & 0x20; } - void set_turbo_mode(bool state) { this->set_bytemask_(18, 0x20, state); } + bool get_turbo_mode() const { return this->pbuf_[18] & 0x20 || this->pbuf_[20] & 0x02; } + void set_turbo_mode(bool state) { + this->set_bytemask_(18, 0x20, state); + this->set_bytemask_(20, 0x02, state); + } /* FREEZE PROTECTION */ bool get_freeze_protection_mode() const { return this->pbuf_[31] & 0x80; } From 9b5a3cbcd30ef1b3b638bf2baeeb554900176dc9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 19 Jul 2021 21:44:39 -0700 Subject: [PATCH 580/643] Bump dashboard to 20210719.0 (#2043) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c3562f492d..ab54828194 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210623.0 +esphome-dashboard==20210719.0 From 766866197bd0d217655c2976aa45323395465fec Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Tue, 20 Jul 2021 01:05:25 -0400 Subject: [PATCH 581/643] Correct ADS1115 handling of multiple sensors in continuous mode (#2016) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ads1115/ads1115.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index d33ac83813..8cac897a15 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -107,17 +107,22 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { } this->prev_config_ = config; - // about 1.6 ms with 860 samples per second + // about 1.2 ms with 860 samples per second delay(2); - uint32_t start = millis(); - while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { - if (millis() - start > 100) { - ESP_LOGW(TAG, "Reading ADS1115 timed out"); - this->status_set_warning(); - return NAN; + // in continuous mode, conversion will always be running, rely on the delay + // to ensure conversion is taking place with the correct settings + // can we use the rdy pin to trigger when a conversion is done? + if (!this->continuous_mode_) { + uint32_t start = millis(); + while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { + if (millis() - start > 100) { + ESP_LOGW(TAG, "Reading ADS1115 timed out"); + this->status_set_warning(); + return NAN; + } + yield(); } - yield(); } } From 01a4b4e82f9037b25565f694207b2be329d7f40d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 20 Jul 2021 07:05:56 +0200 Subject: [PATCH 582/643] ESP32 ADC use esp-idf (#2024) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/adc/adc_sensor.cpp | 52 +++++++++++++++++++++------ esphome/components/adc/adc_sensor.h | 8 +++-- esphome/components/adc/sensor.py | 8 ++--- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 960d9ed8e2..d6469ab785 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -11,7 +11,30 @@ namespace adc { static const char *const TAG = "adc"; #ifdef ARDUINO_ARCH_ESP32 -void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; } +void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } + +inline adc1_channel_t gpio_to_adc1(uint8_t pin) { + switch (pin) { + case 36: + return ADC1_CHANNEL_0; + case 37: + return ADC1_CHANNEL_1; + case 38: + return ADC1_CHANNEL_2; + case 39: + return ADC1_CHANNEL_3; + case 32: + return ADC1_CHANNEL_4; + case 33: + return ADC1_CHANNEL_5; + case 34: + return ADC1_CHANNEL_6; + case 35: + return ADC1_CHANNEL_7; + default: + return ADC1_CHANNEL_MAX; + } +} #endif void ADCSensor::setup() { @@ -21,7 +44,9 @@ void ADCSensor::setup() { #endif #ifdef ARDUINO_ARCH_ESP32 - analogSetPinAttenuation(this->pin_, this->attenuation_); + adc1_config_channel_atten(gpio_to_adc1(pin_), attenuation_); + adc1_config_width(ADC_WIDTH_BIT_12); + adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_)); #endif } void ADCSensor::dump_config() { @@ -36,18 +61,20 @@ void ADCSensor::dump_config() { #ifdef ARDUINO_ARCH_ESP32 ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); switch (this->attenuation_) { - case ADC_0db: + case ADC_ATTEN_DB_0: ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); break; - case ADC_2_5db: + case ADC_ATTEN_DB_2_5: ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); break; - case ADC_6db: + case ADC_ATTEN_DB_6: ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); break; - case ADC_11db: + case ADC_ATTEN_DB_11: ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; } #endif LOG_UPDATE_INTERVAL(this); @@ -60,20 +87,23 @@ void ADCSensor::update() { } float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; // NOLINT + int raw = adc1_get_raw(gpio_to_adc1(pin_)); + float value_v = raw / 4095.0f; switch (this->attenuation_) { - case ADC_0db: + case ADC_ATTEN_DB_0: value_v *= 1.1; break; - case ADC_2_5db: + case ADC_ATTEN_DB_2_5: value_v *= 1.5; break; - case ADC_6db: + case ADC_ATTEN_DB_6: value_v *= 2.2; break; - case ADC_11db: + case ADC_ATTEN_DB_11: value_v *= 3.9; break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; } return value_v; #endif diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 3a08ff6be4..4591ed758d 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -6,6 +6,10 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/voltage_sampler/voltage_sampler.h" +#ifdef ARDUINO_ARCH_ESP32 +#include "driver/adc.h" +#endif + namespace esphome { namespace adc { @@ -13,7 +17,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage public: #ifdef ARDUINO_ARCH_ESP32 /// Set the attenuation for this pin. Only available on the ESP32. - void set_attenuation(adc_attenuation_t attenuation); + void set_attenuation(adc_atten_t attenuation); #endif /// Update adc values. @@ -34,7 +38,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage uint8_t pin_; #ifdef ARDUINO_ARCH_ESP32 - adc_attenuation_t attenuation_{ADC_0db}; + adc_atten_t attenuation_{ADC_ATTEN_DB_0}; #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 90561679b7..7a944a7260 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -16,10 +16,10 @@ from esphome.const import ( AUTO_LOAD = ["voltage_sampler"] ATTENUATION_MODES = { - "0db": cg.global_ns.ADC_0db, - "2.5db": cg.global_ns.ADC_2_5db, - "6db": cg.global_ns.ADC_6db, - "11db": cg.global_ns.ADC_11db, + "0db": cg.global_ns.ADC_ATTEN_DB_0, + "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, + "6db": cg.global_ns.ADC_ATTEN_DB_6, + "11db": cg.global_ns.ADC_ATTEN_DB_11, } From 9f2b2f51ffbdc455e2ebbe6edde0f93041f73c2b Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Tue, 20 Jul 2021 11:12:22 +0200 Subject: [PATCH 583/643] Esp32 c3 support (#2035) --- esphome/components/api/api_server.h | 1 - esphome/components/api/user_services.cpp | 2 +- esphome/core/esphal.cpp | 15 ++++++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index add22e121e..68d1df2c1f 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -9,7 +9,6 @@ #include "util.h" #include "list_entities.h" #include "subscribe_state.h" -#include "homeassistant_service.h" #include "user_services.h" #ifdef ARDUINO_ARCH_ESP32 diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 39e42bcc02..49618f5467 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -15,7 +15,7 @@ template<> std::string get_execute_arg_value(const ExecuteServiceAr template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.bool_array; } -template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { +template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { return arg.int_array; } template<> std::vector get_execute_arg_value>(const ExecuteServiceArgument &arg) { diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index d7adc8dbf1..6b8350991e 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -27,11 +27,16 @@ GPIOPin::GPIOPin(uint8_t pin, uint8_t mode, bool inverted) #ifdef ARDUINO_ARCH_ESP8266 gpio_read_(pin < 16 ? &GPI : &GP16I), gpio_mask_(pin < 16 ? (1UL << pin) : 1) -#endif -#ifdef ARDUINO_ARCH_ESP32 - gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), +#elif ARDUINO_ARCH_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32C3 + gpio_set_(&GPIO.out_w1ts.val), + gpio_clear_(&GPIO.out_w1tc.val), + gpio_read_(&GPIO.in.val), +#else + gpio_set_(pin < 32 ? &GPIO.out_w1ts : &GPIO.out1_w1ts.val), gpio_clear_(pin < 32 ? &GPIO.out_w1tc : &GPIO.out1_w1tc.val), gpio_read_(pin < 32 ? &GPIO.in : &GPIO.in1.val), +#endif gpio_mask_(pin < 32 ? (1UL << pin) : (1UL << (pin - 32))) #endif { @@ -194,12 +199,16 @@ void ICACHE_RAM_ATTR ISRInternalGPIOPin::clear_interrupt() { GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, this->gpio_mask_); #endif #ifdef ARDUINO_ARCH_ESP32 +#ifdef CONFIG_IDF_TARGET_ESP32C3 + GPIO.status_w1tc.val = this->gpio_mask_; +#else if (this->pin_ < 32) { GPIO.status_w1tc = this->gpio_mask_; } else { GPIO.status1_w1tc.intr_st = this->gpio_mask_; } #endif +#endif } void ICACHE_RAM_ATTR HOT GPIOPin::pin_mode(uint8_t mode) { From fc0deb642a0f5328e5866c7d585e40da27221117 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 20 Jul 2021 22:42:03 +0200 Subject: [PATCH 584/643] Fix white value transition for addressable lights (#2045) --- esphome/components/light/addressable_light.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index ea24736c63..4ef293cd03 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -68,10 +68,7 @@ void AddressableLight::write_state(LightState *state) { // our transition will handle brightness, disable brightness in correction. this->correction_.set_local_brightness(255); - uint8_t orig_w = target_color.w; target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); - // w is not scaled by brightness - target_color.w = orig_w; float denom = (1.0f - new_smoothed); float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; From 3b3297d2695d831b062e935d2879455a775ad2a0 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 21 Jul 2021 09:20:20 +1200 Subject: [PATCH 585/643] Adding last_reset_type to sensors that should support it. (#2039) --- esphome/components/api/api.proto | 7 ++++++ esphome/components/api/api_connection.cpp | 1 + esphome/components/api/api_pb2.cpp | 21 ++++++++++++++++++ esphome/components/api/api_pb2.h | 6 ++++++ esphome/components/atm90e32/sensor.py | 15 +++++++++++-- esphome/components/havells_solar/sensor.py | 7 ++++-- esphome/components/hlw8012/sensor.py | 9 ++++++-- esphome/components/pzem004t/sensor.py | 5 +++-- esphome/components/pzemac/sensor.py | 5 +++-- esphome/components/sdm_meter/sensor.py | 22 ++++++++++++++----- esphome/components/sensor/__init__.py | 25 ++++++++++++++++++++++ esphome/components/sensor/sensor.cpp | 13 +++++++++++ esphome/components/sensor/sensor.h | 24 +++++++++++++++++++++ esphome/const.py | 8 +++++++ 14 files changed, 153 insertions(+), 15 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 40be1fd0db..073775ed2e 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -422,6 +422,12 @@ enum SensorStateClass { STATE_CLASS_MEASUREMENT = 1; } +enum SensorLastResetType { + LAST_RESET_NONE = 0; + LAST_RESET_NEVER = 1; + LAST_RESET_AUTO = 2; +} + message ListEntitiesSensorResponse { option (id) = 16; option (source) = SOURCE_SERVER; @@ -438,6 +444,7 @@ message ListEntitiesSensorResponse { bool force_update = 8; string device_class = 9; SensorStateClass state_class = 10; + SensorLastResetType last_reset_type = 11; } message SensorStateResponse { option (id) = 25; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 79ffcfa69e..fb05772e5e 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -399,6 +399,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class(); msg.state_class = static_cast(sensor->state_class); + msg.last_reset_type = static_cast(sensor->last_reset_type); return this->send_list_entities_sensor_response(msg); } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c3cfc8cd76..057d71324f 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -72,6 +72,18 @@ template<> const char *proto_enum_to_string(enums::Sens return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::SensorLastResetType value) { + switch (value) { + case enums::LAST_RESET_NONE: + return "LAST_RESET_NONE"; + case enums::LAST_RESET_NEVER: + return "LAST_RESET_NEVER"; + case enums::LAST_RESET_AUTO: + return "LAST_RESET_AUTO"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -1592,6 +1604,10 @@ bool ListEntitiesSensorResponse::decode_varint(uint32_t field_id, ProtoVarInt va this->state_class = value.as_enum(); return true; } + case 11: { + this->last_reset_type = value.as_enum(); + return true; + } default: return false; } @@ -1647,6 +1663,7 @@ void ListEntitiesSensorResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(8, this->force_update); buffer.encode_string(9, this->device_class); buffer.encode_enum(10, this->state_class); + buffer.encode_enum(11, this->last_reset_type); } void ListEntitiesSensorResponse::dump_to(std::string &out) const { char buffer[64]; @@ -1692,6 +1709,10 @@ void ListEntitiesSensorResponse::dump_to(std::string &out) const { out.append(" state_class: "); out.append(proto_enum_to_string(this->state_class)); out.append("\n"); + + out.append(" last_reset_type: "); + out.append(proto_enum_to_string(this->last_reset_type)); + out.append("\n"); out.append("}"); } bool SensorStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e3bb1d9106..0551508b4b 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -36,6 +36,11 @@ enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, }; +enum SensorLastResetType : uint32_t { + LAST_RESET_NONE = 0, + LAST_RESET_NEVER = 1, + LAST_RESET_AUTO = 2, +}; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -429,6 +434,7 @@ class ListEntitiesSensorResponse : public ProtoMessage { bool force_update{false}; std::string device_class{}; enums::SensorStateClass state_class{}; + enums::SensorLastResetType last_reset_type{}; void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 68ec199bff..2c34d76b52 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -21,6 +21,7 @@ from esphome.const import ( ICON_EMPTY, ICON_LIGHTBULB, ICON_CURRENT_AC, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_HERTZ, UNIT_VOLT, @@ -91,10 +92,20 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + UNIT_WATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + UNIT_WATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 7d1e2be581..1926d4d68a 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -15,6 +15,7 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_EMPTY, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_AMPERE, @@ -121,14 +122,16 @@ CONFIG_SCHEMA = ( ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( UNIT_KILOWATT_HOURS, ICON_EMPTY, 0, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( UNIT_HOURS, diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index e24e995eba..face32872e 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -19,8 +19,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_EMPTY, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -67,7 +67,12 @@ CONFIG_SCHEMA = cv.Schema( UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 1, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_WATT_HOURS, + ICON_EMPTY, + 1, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index e3859f090c..b358b8c650 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -12,8 +12,8 @@ from esphome.const import ( DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_EMPTY, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, @@ -47,7 +47,8 @@ CONFIG_SCHEMA = ( ICON_EMPTY, 0, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 778c5054a0..1dd77a0371 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -17,8 +17,8 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, ICON_EMPTY, ICON_CURRENT_AC, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, @@ -54,7 +54,8 @@ CONFIG_SCHEMA = ( ICON_EMPTY, 0, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( UNIT_HERTZ, diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 39ef280fef..ce560b9d4b 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -25,8 +25,8 @@ from esphome.const import ( ICON_CURRENT_AC, ICON_EMPTY, ICON_FLASH, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_AMPERE, UNIT_DEGREES, UNIT_EMPTY, @@ -88,24 +88,36 @@ CONFIG_SCHEMA = ( STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_WATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + UNIT_WATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( UNIT_VOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( UNIT_VOLT_AMPS_REACTIVE_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY, - STATE_CLASS_NONE, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 89bde9476a..0a0c3a9214 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -17,6 +17,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_INTERNAL, + CONF_LAST_RESET_TYPE, CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, @@ -30,6 +31,9 @@ from esphome.const import ( CONF_NAME, CONF_MQTT_ID, CONF_FORCE_UPDATE, + LAST_RESET_TYPE_AUTO, + LAST_RESET_TYPE_NEVER, + LAST_RESET_TYPE_NONE, UNIT_EMPTY, ICON_EMPTY, DEVICE_CLASS_EMPTY, @@ -79,6 +83,15 @@ STATE_CLASSES = { } validate_state_class = cv.enum(STATE_CLASSES, lower=True, space="_") +LastResetTypes = sensor_ns.enum("LastResetType") +LAST_RESET_TYPES = { + LAST_RESET_TYPE_NONE: LastResetTypes.LAST_RESET_TYPE_NONE, + LAST_RESET_TYPE_NEVER: LastResetTypes.LAST_RESET_TYPE_NEVER, + LAST_RESET_TYPE_AUTO: LastResetTypes.LAST_RESET_TYPE_AUTO, +} +validate_last_reset_type = cv.enum(LAST_RESET_TYPES, lower=True, space="_") + + IS_PLATFORM_COMPONENT = True @@ -168,6 +181,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, cv.Optional(CONF_DEVICE_CLASS): device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, + cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type, cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All( cv.requires_component("mqtt"), @@ -202,6 +216,7 @@ def sensor_schema( accuracy_decimals_: int, device_class_: Optional[str] = DEVICE_CLASS_EMPTY, state_class_: Optional[str] = STATE_CLASS_NONE, + last_reset_type_: Optional[str] = LAST_RESET_TYPE_NONE, ) -> cv.Schema: schema = SENSOR_SCHEMA if unit_of_measurement_ != UNIT_EMPTY: @@ -230,6 +245,14 @@ def sensor_schema( schema = schema.extend( {cv.Optional(CONF_STATE_CLASS, default=state_class_): validate_state_class} ) + if last_reset_type_ != LAST_RESET_TYPE_NONE: + schema = schema.extend( + { + cv.Optional( + CONF_LAST_RESET_TYPE, default=last_reset_type_ + ): validate_last_reset_type + } + ) return schema @@ -479,6 +502,8 @@ async def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) + if CONF_LAST_RESET_TYPE in config: + cg.add(var.set_last_reset_type(config[CONF_LAST_RESET_TYPE])) cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) if config.get(CONF_FILTERS): # must exist and not be empty filters = await build_filters(config[CONF_FILTERS]) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index fe92f88308..6e8765a8df 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -16,6 +16,18 @@ const char *state_class_to_string(StateClass state_class) { } } +const char *last_reset_type_to_string(LastResetType last_reset_type) { + switch (last_reset_type) { + case LAST_RESET_TYPE_NEVER: + return "never"; + case LAST_RESET_TYPE_AUTO: + return "auto"; + case LAST_RESET_TYPE_NONE: + default: + return ""; + } +} + void Sensor::publish_state(float state) { this->raw_state = state; this->raw_callback_.call(state); @@ -64,6 +76,7 @@ void Sensor::set_state_class(const std::string &state_class) { ESP_LOGW(TAG, "'%s' - Unrecognized state class %s", this->get_name().c_str(), state_class.c_str()); } } +void Sensor::set_last_reset_type(LastResetType last_reset_type) { this->last_reset_type = last_reset_type; } std::string Sensor::get_unit_of_measurement() { if (this->unit_of_measurement_.has_value()) return *this->unit_of_measurement_; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 123e7eddb3..b9908b6cbe 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -14,6 +14,10 @@ namespace sensor { ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ ESP_LOGCONFIG(TAG, "%s State Class: '%s'", prefix, state_class_to_string((obj)->state_class)); \ + if ((obj)->state_class == sensor::STATE_CLASS_MEASUREMENT && \ + (obj)->last_reset_type != sensor::LAST_RESET_TYPE_NONE) { \ + ESP_LOGCONFIG(TAG, "%s Last Reset Type: '%s'", prefix, last_reset_type_to_string((obj)->last_reset_type)); \ + } \ ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->get_unit_of_measurement().c_str()); \ ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, (obj)->get_accuracy_decimals()); \ if (!(obj)->get_icon().empty()) { \ @@ -37,6 +41,20 @@ enum StateClass : uint8_t { const char *state_class_to_string(StateClass state_class); +/** + * Sensor last reset types + */ +enum LastResetType : uint8_t { + /// This sensor does not support resetting. ie, it is not accumulative + LAST_RESET_TYPE_NONE = 0, + /// This sensor is expected to never reset its value + LAST_RESET_TYPE_NEVER = 1, + /// This sensor may reset and Home Assistant will watch for this + LAST_RESET_TYPE_AUTO = 2, +}; + +const char *last_reset_type_to_string(LastResetType last_reset_type); + /** Base-class for all sensors. * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. @@ -155,6 +173,12 @@ class Sensor : public Nameable { */ virtual std::string device_class(); + // The Last reset type of this sensor + LastResetType last_reset_type{LAST_RESET_TYPE_NONE}; + + /// Manually set the Home Assistant last reset type for this sensor. + void set_last_reset_type(LastResetType last_reset_type); + /** A unique ID for this sensor, empty for no unique id. See unique ID requirements: * https://developers.home-assistant.io/docs/en/entity_registry_index.html#unique-id-requirements * diff --git a/esphome/const.py b/esphome/const.py index 1ea4285747..8baca4d34f 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -297,6 +297,7 @@ CONF_KEY = "key" CONF_LAMBDA = "lambda" CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_FINGER_ID = "last_finger_id" +CONF_LAST_RESET_TYPE = "last_reset_type" CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" @@ -785,3 +786,10 @@ STATE_CLASS_NONE = "" # The state represents a measurement in present time STATE_CLASS_MEASUREMENT = "measurement" + +# This sensor does not support resetting. ie, it is not accumulative +LAST_RESET_TYPE_NONE = "" +# This sensor is expected to never reset its value +LAST_RESET_TYPE_NEVER = "never" +# This sensor may reset and Home Assistant will watch for this +LAST_RESET_TYPE_AUTO = "auto" From 3e65e6c69aaed96ea302240bd64282f4db360530 Mon Sep 17 00:00:00 2001 From: Sean Vig Date: Tue, 20 Jul 2021 17:35:45 -0400 Subject: [PATCH 586/643] Remove superfluous polling on ADS1115 (#2015) --- esphome/components/ads1115/ads1115.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/ads1115/ads1115.cpp b/esphome/components/ads1115/ads1115.cpp index 8cac897a15..92f0ffbe3a 100644 --- a/esphome/components/ads1115/ads1115.cpp +++ b/esphome/components/ads1115/ads1115.cpp @@ -64,11 +64,6 @@ void ADS1115Component::setup() { return; } this->prev_config_ = config; - - for (auto *sensor : this->sensors_) { - this->set_interval(sensor->get_name(), sensor->update_interval(), - [this, sensor] { this->request_measurement(sensor); }); - } } void ADS1115Component::dump_config() { ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); From fc42f14448458b3791ea0584872bf246e7ce0705 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jul 2021 14:40:09 +1200 Subject: [PATCH 587/643] Bump pylint from 2.8.2 to 2.9.4 (#2047) * Bump pylint from 2.8.2 to 2.9.4 Bumps [pylint](https://github.com/PyCQA/pylint) from 2.8.2 to 2.9.4. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/v2.8.2...v2.9.4) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Fix up functionality needed for latest pylint (#2049) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sean Vig --- esphome/final_validate.py | 5 +++-- esphome/yaml_util.py | 4 +++- requirements_test.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/final_validate.py b/esphome/final_validate.py index 47071b5391..199c68210e 100644 --- a/esphome/final_validate.py +++ b/esphome/final_validate.py @@ -1,4 +1,4 @@ -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from typing import Dict, Any import contextvars @@ -14,7 +14,8 @@ from esphome.core import CORE class FinalValidateConfig(ABC): - @abstractproperty + @property + @abstractmethod def data(self) -> Dict[str, Any]: """A dictionary that can be used by post validation functions to store global data during the validation phase. Each component should store its diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index f98bb272b8..10417e6de4 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -411,17 +411,19 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:str", value=str(value)) - # pylint: disable=arguments-differ + # pylint: disable=arguments-renamed def represent_bool(self, value): return self.represent_scalar( "tag:yaml.org,2002:bool", "true" if value else "false" ) + # pylint: disable=arguments-renamed def represent_int(self, value): if is_secret(value): return self.represent_secret(value) return self.represent_scalar(tag="tag:yaml.org,2002:int", value=str(value)) + # pylint: disable=arguments-renamed def represent_float(self, value): if is_secret(value): return self.represent_secret(value) diff --git a/requirements_test.txt b/requirements_test.txt index dc46b6f3bf..985f7d0932 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.8.2 +pylint==2.9.4 flake8==3.9.2 black==21.7b0 pexpect==4.8.0 From c9062599df0b4b275fa734cdeadc872320791dd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 22 Jul 2021 08:58:41 +1200 Subject: [PATCH 588/643] Bump pylint from 2.9.4 to 2.9.5 (#2050) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.9.4 to 2.9.5. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/main/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/v2.9.4...v2.9.5) --- updated-dependencies: - dependency-name: pylint dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 985f7d0932..b0e24b5b2f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.9.4 +pylint==2.9.5 flake8==3.9.2 black==21.7b0 pexpect==4.8.0 From 0a32321c8543924b4222a987594b67372b4c409d Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 21 Jul 2021 16:09:31 -0500 Subject: [PATCH 589/643] Thermostat fixes+updates 1 (#2032) Co-authored-by: Otto Winter --- esphome/components/thermostat/climate.py | 42 ++++++++++++ .../thermostat/thermostat_climate.cpp | 64 ++++++++++++++----- .../thermostat/thermostat_climate.h | 16 +++++ esphome/const.py | 2 + 4 files changed, 107 insertions(+), 17 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 07a94fd184..bf3195a5dd 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -7,6 +7,7 @@ from esphome.const import ( CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, + CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, @@ -33,10 +34,12 @@ from esphome.const import ( CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION, + CONF_TARGET_TEMPERATURE_CHANGE_ACTION, ) CODEOWNERS = ["@kbx81"] +climate_ns = cg.esphome_ns.namespace("climate") thermostat_ns = cg.esphome_ns.namespace("thermostat") ThermostatClimate = thermostat_ns.class_( "ThermostatClimate", climate.Climate, cg.Component @@ -44,6 +47,17 @@ ThermostatClimate = thermostat_ns.class_( ThermostatClimateTargetTempConfig = thermostat_ns.struct( "ThermostatClimateTargetTempConfig" ) +ClimateMode = climate_ns.enum("ClimateMode") +CLIMATE_MODES = { + "OFF": ClimateMode.CLIMATE_MODE_OFF, + "HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL, + "COOL": ClimateMode.CLIMATE_MODE_COOL, + "HEAT": ClimateMode.CLIMATE_MODE_HEAT, + "DRY": ClimateMode.CLIMATE_MODE_DRY, + "FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY, + "AUTO": ClimateMode.CLIMATE_MODE_AUTO, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) def validate_thermostat(config): @@ -141,6 +155,21 @@ def validate_thermostat(config): CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION ) ) + # verify default climate mode is valid given above configuration + default_mode = config[CONF_DEFAULT_MODE] + requirements = { + "HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + "COOL": [CONF_COOL_ACTION], + "HEAT": [CONF_HEAT_ACTION], + "DRY": [CONF_DRY_ACTION], + "FAN_ONLY": [CONF_FAN_ONLY_ACTION], + "AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION], + }.get(default_mode, []) + for req in requirements: + if req not in config: + raise cv.Invalid( + f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration" + ) return config @@ -204,6 +233,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation( single=True ), + cv.Optional( + CONF_TARGET_TEMPERATURE_CHANGE_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable( + validate_climate_mode + ), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, @@ -233,6 +268,7 @@ async def to_code(config): ) sens = await cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) cg.add(var.set_sensor(sens)) cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) @@ -380,6 +416,12 @@ async def to_code(config): config[CONF_SWING_VERTICAL_ACTION], ) cg.add(var.set_supports_swing_mode_vertical(True)) + if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config: + await automation.build_automation( + var.get_temperature_change_trigger(), + [], + config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION], + ) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 305db66f16..4610d5caad 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -21,7 +21,7 @@ void ThermostatClimate::setup() { restore->to_call(this).perform(); } else { // restore from defaults, change_away handles temps for us - this->mode = climate::CLIMATE_MODE_HEAT_COOL; + this->mode = this->default_mode_; this->change_away_(false); } // refresh the climate action based on the restored settings @@ -35,9 +35,18 @@ void ThermostatClimate::refresh() { this->switch_to_action_(compute_action_()); this->switch_to_fan_mode_(this->fan_mode.value()); this->switch_to_swing_mode_(this->swing_mode); + this->check_temperature_change_trigger_(); this->publish_state(); } void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_preset().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); + } else { + this->preset = *call.get_preset(); + } + } if (call.get_mode().has_value()) this->mode = *call.get_mode(); if (call.get_fan_mode().has_value()) @@ -50,15 +59,6 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_preset().has_value()) { - // setup_complete_ blocks modifying/resetting the temps immediately after boot - if (this->setup_complete_) { - this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); - } else { - this->preset = *call.get_preset(); - ; - } - } // set point validation if (this->supports_two_points_) { if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) @@ -128,7 +128,16 @@ climate::ClimateTraits ThermostatClimate::traits() { return traits; } climate::ClimateAction ThermostatClimate::compute_action_() { + // we need to know the current climate action before anything else happens here climate::ClimateAction target_action = this->action; + // if the climate mode is OFF then the climate action must be OFF + if (this->mode == climate::CLIMATE_MODE_OFF) { + return climate::CLIMATE_ACTION_OFF; + } else if (this->action == climate::CLIMATE_ACTION_OFF) { + // ...but if the climate mode is NOT OFF then the climate action must not be OFF + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_two_points_) { if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high) || isnan(this->hysteresis_)) @@ -153,9 +162,6 @@ climate::ClimateAction ThermostatClimate::compute_action_() { case climate::CLIMATE_MODE_DRY: target_action = climate::CLIMATE_ACTION_DRYING; break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; case climate::CLIMATE_MODE_HEAT_COOL: case climate::CLIMATE_MODE_COOL: case climate::CLIMATE_MODE_HEAT: @@ -200,9 +206,6 @@ climate::ClimateAction ThermostatClimate::compute_action_() { case climate::CLIMATE_MODE_DRY: target_action = climate::CLIMATE_ACTION_DRYING; break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; case climate::CLIMATE_MODE_COOL: if (this->supports_cool_) { if (this->current_temperature > this->target_temperature + this->hysteresis_) @@ -410,6 +413,30 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; } +void ThermostatClimate::check_temperature_change_trigger_() { + if (this->supports_two_points_) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((this->prev_target_temperature_low_ == this->target_temperature_low) && + (this->prev_target_temperature_high_ == this->target_temperature_high) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperatures so we can check them again later; the trigger will fire below + this->prev_target_temperature_low_ = this->target_temperature_low; + this->prev_target_temperature_high_ = this->target_temperature_high; + } + } else { + if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) { + return; // nothing changed, no reason to trigger + } else { + // save the new temperature so we can check it again later; the trigger will fire below + this->prev_target_temperature_ = this->target_temperature; + } + } + // trigger the action + Trigger<> *trig = this->temperature_change_trigger_; + assert(trig != nullptr); + trig->trigger(); +} void ThermostatClimate::change_away_(bool away) { if (!away) { if (this->supports_two_points_) { @@ -457,7 +484,9 @@ ThermostatClimate::ThermostatClimate() swing_mode_both_trigger_(new Trigger<>()), swing_mode_off_trigger_(new Trigger<>()), swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()) {} + swing_mode_vertical_trigger_(new Trigger<>()), + temperature_change_trigger_(new Trigger<>()) {} +void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { @@ -534,6 +563,7 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this- Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); if (this->supports_heat_) { diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 3fd482da53..bff9e9bdc1 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -26,6 +26,7 @@ class ThermostatClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; + void set_default_mode(climate::ClimateMode default_mode); void set_hysteresis(float hysteresis); void set_sensor(sensor::Sensor *sensor); void set_supports_auto(bool supports_auto); @@ -76,6 +77,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_horizontal_trigger() const; Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; + Trigger<> *get_temperature_change_trigger() const; /// Get current hysteresis value float hysteresis(); /// Call triggers based on updated climate states (modes/actions) @@ -106,6 +108,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// Switch the climate device to the given climate swing mode. void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + /// Check if the temperature change trigger should be called. + void check_temperature_change_trigger_(); + /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -242,6 +247,9 @@ class ThermostatClimate : public climate::Climate, public Component { /// The trigger to call when the controller should switch the swing mode to "vertical". Trigger<> *swing_mode_vertical_trigger_{nullptr}; + /// The trigger to call when the target temperature(s) change(es). + Trigger<> *temperature_change_trigger_{nullptr}; + /// A reference to the trigger that was previously active. /// /// This is so that the previous trigger can be stopped before enabling a new one @@ -256,8 +264,16 @@ class ThermostatClimate : public climate::Climate, public Component { /// These are used to determine when a trigger/action needs to be called climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF}; climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + /// Store previously-known temperatures + /// + /// These are used to determine when the temperature change trigger/action needs to be called + float prev_target_temperature_{NAN}; + float prev_target_temperature_low_{NAN}; + float prev_target_temperature_high_{NAN}; + /// Temperature data for normal/home and away modes ThermostatClimateTargetTempConfig normal_config_{}; ThermostatClimateTargetTempConfig away_config_{}; diff --git a/esphome/const.py b/esphome/const.py index 8baca4d34f..3eb56c7cf6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -159,6 +159,7 @@ CONF_DAYS_OF_WEEK = "days_of_week" CONF_DC_PIN = "dc_pin" CONF_DEBOUNCE = "debounce" CONF_DECELERATION = "deceleration" +CONF_DEFAULT_MODE = "default_mode" CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high" CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low" CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length" @@ -572,6 +573,7 @@ CONF_TABLET = "tablet" CONF_TAG = "tag" CONF_TARGET = "target" CONF_TARGET_TEMPERATURE = "target_temperature" +CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action" CONF_TARGET_TEMPERATURE_HIGH = "target_temperature_high" CONF_TARGET_TEMPERATURE_LOW = "target_temperature_low" CONF_TEMPERATURE = "temperature" From 5379794f16b9c2c6fb6e721272850c428c759889 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 22 Jul 2021 13:24:01 +1200 Subject: [PATCH 590/643] Add test5 back to CI (#2052) --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ccaaa47b5..a0be9be903 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,6 +82,9 @@ jobs: - id: test file: tests/test4.yaml name: Test tests/test4.yaml + - id: test + file: tests/test5.yaml + name: Test tests/test5.yaml - id: pytest name: Run pytest From 90394a50df8bcc0ede3af16a3d7ebbf0257a95c8 Mon Sep 17 00:00:00 2001 From: Pasi Suominen Date: Thu, 22 Jul 2021 06:21:08 +0300 Subject: [PATCH 591/643] Added support for pvvx_mithermometer sensor (#1546) Co-authored-by: Pasi Suominen Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/pvvx_mithermometer/__init__.py | 0 .../pvvx_mithermometer/pvvx_mithermometer.cpp | 145 ++++++++++++++++++ .../pvvx_mithermometer/pvvx_mithermometer.h | 48 ++++++ .../components/pvvx_mithermometer/sensor.py | 72 +++++++++ tests/test2.yaml | 10 ++ 6 files changed, 276 insertions(+) create mode 100644 esphome/components/pvvx_mithermometer/__init__.py create mode 100644 esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp create mode 100644 esphome/components/pvvx_mithermometer/pvvx_mithermometer.h create mode 100644 esphome/components/pvvx_mithermometer/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 557fe7cc08..1decc5c330 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,6 +88,7 @@ esphome/components/pn532_i2c/* @OttoWinter @jesserockz esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core esphome/components/pulse_meter/* @stevebaxter +esphome/components/pvvx_mithermometer/* @pasiz esphome/components/rc522/* @glmnet esphome/components/rc522_i2c/* @glmnet esphome/components/rc522_spi/* @glmnet diff --git a/esphome/components/pvvx_mithermometer/__init__.py b/esphome/components/pvvx_mithermometer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp new file mode 100644 index 0000000000..34f190e45e --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -0,0 +1,145 @@ +#include "pvvx_mithermometer.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace pvvx_mithermometer { + +static const char *TAG = "pvvx_mithermometer"; + +void PVVXMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "PVVX MiThermometer"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); +} + +bool PVVXMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = parse_header(service_data); + if (res->is_duplicate) { + continue; + } + if (!(parse_message(service_data.data, *res))) { + continue; + } + if (!(report_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) + this->battery_voltage_->publish_state(*res->battery_voltage); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +optional PVVXMiThermometer::parse_header(const esp32_ble_tracker::ServiceData &service_data) { + ParseResult result; + if (!service_data.uuid.contains(0x1A, 0x18)) { + ESP_LOGVV(TAG, "parse_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[13]; + result.is_duplicate = false; + + return result; +} + +bool PVVXMiThermometer::parse_message(const std::vector &message, ParseResult &result) { + /* + All data little endian + uint8_t size; // = 19 + uint8_t uid; // = 0x16, 16-bit UUID + uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing + uint8_t MAC[6]; // [0] - lo, .. [5] - hi digits + int16_t temperature; // x 0.01 degree [6,7] + uint16_t humidity; // x 0.01 % [8,9] + uint16_t battery_mv; // mV [10,11] + uint8_t battery_level; // 0..100 % [12] + uint8_t counter; // measurement count [13] + uint8_t flags; [14] + */ + + const uint8_t *data = message.data(); + const int data_length = 15; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + // int16_t temperature; // x 0.01 degree [6,7] + const int16_t temperature = int16_t(data[6]) | (int16_t(data[7]) << 8); + result.temperature = temperature / 1.0e2f; + + // uint16_t humidity; // x 0.01 % [8,9] + const int16_t humidity = uint16_t(data[8]) | (uint16_t(data[9]) << 8); + result.humidity = humidity / 1.0e2f; + + // uint16_t battery_mv; // mV [10,11] + const int16_t battery_voltage = uint16_t(data[10]) | (uint16_t(data[11]) << 8); + result.battery_voltage = battery_voltage / 1.0e3f; + + // uint8_t battery_level; // 0..100 % [12] + result.battery_level = uint8_t(data[12]); + + return true; +} + +bool PVVXMiThermometer::report_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got PVVX MiThermometer (%s):", address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.2f °C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.2f %%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f %%", *result->battery_level); + } + if (result->battery_voltage.has_value()) { + ESP_LOGD(TAG, " Battery Voltage: %.3f V", *result->battery_voltage); + } + + return true; +} + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h new file mode 100644 index 0000000000..42a40d4200 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace pvvx_mithermometer { + +struct ParseResult { + optional temperature; + optional humidity; + optional battery_level; + optional battery_voltage; + bool is_duplicate; + int raw_offset; +}; + +class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + + optional parse_header(const esp32_ble_tracker::ServiceData &service_data); + bool parse_message(const std::vector &message, ParseResult &result); + bool report_results(const optional &result, const std::string &address); +}; + +} // namespace pvvx_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py new file mode 100644 index 0000000000..cccf92b277 --- /dev/null +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -0,0 +1,72 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import ( + CONF_BATTERY_LEVEL, + CONF_BATTERY_VOLTAGE, + CONF_MAC_ADDRESS, + CONF_HUMIDITY, + CONF_TEMPERATURE, + CONF_ID, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@pasiz"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +pvvx_mithermometer_ns = cg.esphome_ns.namespace("pvvx_mithermometer") +PVVXMiThermometer = pvvx_mithermometer_ns.class_( + "PVVXMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PVVXMiThermometer), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 2, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE + ), + } + ) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_BATTERY_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + cg.add(var.set_battery_voltage(sens)) diff --git a/tests/test2.yaml b/tests/test2.yaml index faa76300cc..1746804061 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -218,6 +218,16 @@ sensor: name: 'ATC Battery-Level' battery_voltage: name: 'ATC Battery-Voltage' + - platform: pvvx_mithermometer + mac_address: 'A4:C1:38:4E:16:78' + temperature: + name: 'PVVX Temperature' + humidity: + name: 'PVVX Humidity' + battery_level: + name: 'PVVX Battery-Level' + battery_voltage: + name: 'PVVX Battery-Voltage' - platform: inkbird_ibsth1_mini mac_address: 38:81:D7:0A:9C:11 temperature: From acbb8e9fd028339ac143a9dd9df1a7060a79a4b1 Mon Sep 17 00:00:00 2001 From: Sourabh Jaiswal Date: Thu, 22 Jul 2021 09:01:28 +0530 Subject: [PATCH 592/643] Added support for Selec Energy Meter (#1993) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/selec_meter/__init__.py | 0 .../components/selec_meter/selec_meter.cpp | 108 +++++++++++ esphome/components/selec_meter/selec_meter.h | 45 +++++ .../selec_meter/selec_meter_registers.h | 32 ++++ esphome/components/selec_meter/sensor.py | 169 ++++++++++++++++++ tests/test5.yaml | 44 +++++ 7 files changed, 399 insertions(+) create mode 100644 esphome/components/selec_meter/__init__.py create mode 100644 esphome/components/selec_meter/selec_meter.cpp create mode 100644 esphome/components/selec_meter/selec_meter.h create mode 100644 esphome/components/selec_meter/selec_meter_registers.h create mode 100644 esphome/components/selec_meter/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1decc5c330..844bea763e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -97,6 +97,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces +esphome/components/selec_meter/* @sourabhjaiswal esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny diff --git a/esphome/components/selec_meter/__init__.py b/esphome/components/selec_meter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp new file mode 100644 index 0000000000..8bcf91f3c0 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -0,0 +1,108 @@ +#include "selec_meter.h" +#include "selec_meter_registers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace selec_meter { + +static const char *const TAG = "selec_meter"; + +static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers + +void SelecMeter::on_modbus_data(const std::vector &data) { + if (data.size() < MODBUS_REGISTER_COUNT * 2) { + ESP_LOGW(TAG, "Invalid size for SelecMeter!"); + return; + } + + auto selec_meter_get_float = [&](size_t i, float unit) -> float { + uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]); + + float f; + memcpy(&f, &temp, sizeof(f)); + return (f * unit); + }; + + float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); + float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); + float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT); + float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT); + float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT); + float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT); + float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT); + float maximum_demand_active_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_reactive_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); + float maximum_demand_apparent_power = + selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); + + if (this->total_active_energy_sensor_ != nullptr) + this->total_active_energy_sensor_->publish_state(total_active_energy); + if (this->import_active_energy_sensor_ != nullptr) + this->import_active_energy_sensor_->publish_state(import_active_energy); + if (this->export_active_energy_sensor_ != nullptr) + this->export_active_energy_sensor_->publish_state(export_active_energy); + if (this->total_reactive_energy_sensor_ != nullptr) + this->total_reactive_energy_sensor_->publish_state(total_reactive_energy); + if (this->import_reactive_energy_sensor_ != nullptr) + this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); + if (this->export_reactive_energy_sensor_ != nullptr) + this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); + if (this->apparent_energy_sensor_ != nullptr) + this->apparent_energy_sensor_->publish_state(apparent_energy); + if (this->active_power_sensor_ != nullptr) + this->active_power_sensor_->publish_state(active_power); + if (this->reactive_power_sensor_ != nullptr) + this->reactive_power_sensor_->publish_state(reactive_power); + if (this->apparent_power_sensor_ != nullptr) + this->apparent_power_sensor_->publish_state(apparent_power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->maximum_demand_active_power_sensor_ != nullptr) + this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power); + if (this->maximum_demand_reactive_power_sensor_ != nullptr) + this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power); + if (this->maximum_demand_apparent_power_sensor_ != nullptr) + this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); +} + +void SelecMeter::update() { this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT); } +void SelecMeter::dump_config() { + ESP_LOGCONFIG(TAG, "SELEC Meter:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR(" ", "Total Active Energy", this->total_active_energy_sensor_); + LOG_SENSOR(" ", "Import Active Energy", this->import_active_energy_sensor_); + LOG_SENSOR(" ", "Export Active Energy", this->export_active_energy_sensor_); + LOG_SENSOR(" ", "Total Reactive Energy", this->total_reactive_energy_sensor_); + LOG_SENSOR(" ", "Import Reactive Energy", this->import_reactive_energy_sensor_); + LOG_SENSOR(" ", "Export Reactive Energy", this->export_reactive_energy_sensor_); + LOG_SENSOR(" ", "Apparent Energy", this->apparent_energy_sensor_); + LOG_SENSOR(" ", "Active Power", this->active_power_sensor_); + LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); + LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->frequency_sensor_); + LOG_SENSOR(" ", "Maximum Demand Active Power", this->maximum_demand_active_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Reactive Power", this->maximum_demand_reactive_power_sensor_); + LOG_SENSOR(" ", "Maximum Demand Apparent Power", this->maximum_demand_apparent_power_sensor_); +} + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h new file mode 100644 index 0000000000..0477cd2a62 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace selec_meter { + +#define SELEC_METER_SENSOR(name) \ + protected: \ + sensor::Sensor *name##_sensor_{nullptr}; \ +\ + public: \ + void set_##name##_sensor(sensor::Sensor *(name)) { this->name##_sensor_ = name; } + +class SelecMeter : public PollingComponent, public modbus::ModbusDevice { + public: + SELEC_METER_SENSOR(total_active_energy) + SELEC_METER_SENSOR(import_active_energy) + SELEC_METER_SENSOR(export_active_energy) + SELEC_METER_SENSOR(total_reactive_energy) + SELEC_METER_SENSOR(import_reactive_energy) + SELEC_METER_SENSOR(export_reactive_energy) + SELEC_METER_SENSOR(apparent_energy) + SELEC_METER_SENSOR(active_power) + SELEC_METER_SENSOR(reactive_power) + SELEC_METER_SENSOR(apparent_power) + SELEC_METER_SENSOR(voltage) + SELEC_METER_SENSOR(current) + SELEC_METER_SENSOR(power_factor) + SELEC_METER_SENSOR(frequency) + SELEC_METER_SENSOR(maximum_demand_active_power) + SELEC_METER_SENSOR(maximum_demand_reactive_power) + SELEC_METER_SENSOR(maximum_demand_apparent_power) + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; +}; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/selec_meter_registers.h b/esphome/components/selec_meter/selec_meter_registers.h new file mode 100644 index 0000000000..dfaf65ff08 --- /dev/null +++ b/esphome/components/selec_meter/selec_meter_registers.h @@ -0,0 +1,32 @@ +#pragma once + +namespace esphome { +namespace selec_meter { + +static const float TWO_DEC_UNIT = 0.01; +static const float ONE_DEC_UNIT = 0.1; +static const float NO_DEC_UNIT = 1; +static const float MULTIPLY_TEN_UNIT = 10; +static const float MULTIPLY_THOUSAND_UNIT = 1000; + +/* PHASE STATUS REGISTERS */ +static const uint16_t SELEC_TOTAL_ACTIVE_ENERGY = 0x0000; +static const uint16_t SELEC_IMPORT_ACTIVE_ENERGY = 0x0002; +static const uint16_t SELEC_EXPORT_ACTIVE_ENERGY = 0x0004; +static const uint16_t SELEC_TOTAL_REACTIVE_ENERGY = 0x0006; +static const uint16_t SELEC_IMPORT_REACTIVE_ENERGY = 0x0008; +static const uint16_t SELEC_EXPORT_REACTIVE_ENERGY = 0x000A; +static const uint16_t SELEC_APPARENT_ENERGY = 0x000C; +static const uint16_t SELEC_ACTIVE_POWER = 0x000E; +static const uint16_t SELEC_REACTIVE_POWER = 0x0010; +static const uint16_t SELEC_APPARENT_POWER = 0x0012; +static const uint16_t SELEC_VOLTAGE = 0x0014; +static const uint16_t SELEC_CURRENT = 0x0016; +static const uint16_t SELEC_POWER_FACTOR = 0x0018; +static const uint16_t SELEC_FREQUENCY = 0x001A; +static const uint16_t SELEC_MAXIMUM_DEMAND_ACTIVE_POWER = 0x001C; +static const uint16_t SELEC_MAXIMUM_DEMAND_REACTIVE_POWER = 0x001E; +static const uint16_t SELEC_MAXIMUM_DEMAND_APPARENT_POWER = 0x0020; + +} // namespace selec_meter +} // namespace esphome diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py new file mode 100644 index 0000000000..7eb526aec7 --- /dev/null +++ b/esphome/components/selec_meter/sensor.py @@ -0,0 +1,169 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import ( + CONF_ACTIVE_POWER, + CONF_APPARENT_POWER, + CONF_CURRENT, + CONF_EXPORT_ACTIVE_ENERGY, + CONF_EXPORT_REACTIVE_ENERGY, + CONF_FREQUENCY, + CONF_ID, + CONF_IMPORT_ACTIVE_ENERGY, + CONF_IMPORT_REACTIVE_ENERGY, + CONF_POWER_FACTOR, + CONF_REACTIVE_POWER, + CONF_VOLTAGE, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + ICON_CURRENT_AC, + ICON_EMPTY, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_AMPERE, + UNIT_EMPTY, + UNIT_HERTZ, + UNIT_VOLT, + UNIT_VOLT_AMPS, + UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT, +) + +AUTO_LOAD = ["modbus"] +CODEOWNERS = ["@sourabhjaiswal"] + +CONF_TOTAL_ACTIVE_ENERGY = "total_active_energy" +CONF_TOTAL_REACTIVE_ENERGY = "total_reactive_energy" +CONF_APPARENT_ENERGY = "apparent_energy" +CONF_MAXIMUM_DEMAND_ACTIVE_POWER = "maximum_demand_active_power" +CONF_MAXIMUM_DEMAND_REACTIVE_POWER = "maximum_demand_reactive_power" +CONF_MAXIMUM_DEMAND_APPARENT_POWER = "maximum_demand_apparent_power" + +UNIT_KILOWATT_HOURS = "kWh" +UNIT_KILOVOLT_AMPS_HOURS = "kVAh" +UNIT_KILOVOLT_AMPS_REACTIVE_HOURS = "kVARh" + +selec_meter_ns = cg.esphome_ns.namespace("selec_meter") +SelecMeter = selec_meter_ns.class_( + "SelecMeter", cg.PollingComponent, modbus.ModbusDevice +) + +SENSORS = { + CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOWATT_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_APPARENT_ENERGY: sensor.sensor_schema( + UNIT_KILOVOLT_AMPS_HOURS, + ICON_EMPTY, + 2, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ), + CONF_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_REACTIVE_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + CONF_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_VOLTAGE: sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + ), + CONF_CURRENT: sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + CONF_POWER_FACTOR: sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + ), + CONF_FREQUENCY: sensor.sensor_schema( + UNIT_HERTZ, ICON_CURRENT_AC, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS_REACTIVE, + ICON_EMPTY, + 3, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + ), + CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( + UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), +} + +CONFIG_SCHEMA = ( + cv.Schema({cv.GenerateID(): cv.declare_id(SelecMeter)}) + .extend( + {cv.Optional(sensor_name): schema for sensor_name, schema in SENSORS.items()} + ) + .extend(cv.polling_component_schema("10s")) + .extend(modbus.modbus_device_schema(0x01)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await modbus.register_modbus_device(var, config) + for name in SENSORS: + if name in config: + sens = await sensor.new_sensor(config[name]) + cg.add(getattr(var, f"set_{name}_sensor")(sens)) diff --git a/tests/test5.yaml b/tests/test5.yaml index 35225402a3..5d1c99d777 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -18,6 +18,13 @@ ota: logger: +uart: + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + +modbus: + binary_sensor: - platform: gpio pin: GPIO0 @@ -55,3 +62,40 @@ number: max_value: 100 min_value: 0 step: 5 + +sensor: + - platform: selec_meter + total_active_energy: + name: "SelecEM2M Total Active Energy" + import_active_energy: + name: "SelecEM2M Import Active Energy" + export_active_energy: + name: "SelecEM2M Export Active Energy" + total_reactive_energy: + name: "SelecEM2M Total Reactive Energy" + import_reactive_energy: + name: "SelecEM2M Import Reactive Energy" + export_reactive_energy: + name: "SelecEM2M Export Reactive Energy" + apparent_energy: + name: "SelecEM2M Apparent Energy" + active_power: + name: "SelecEM2M Active Power" + reactive_power: + name: "SelecEM2M Reactive Power" + apparent_power: + name: "SelecEM2M Apparent Power" + voltage: + name: "SelecEM2M Voltage" + current: + name: "SelecEM2M Current" + power_factor: + name: "SelecEM2M Power Factor" + frequency: + name: "SelecEM2M Frequency" + maximum_demand_active_power: + name: "SelecEM2M Maximum Demand Active Power" + maximum_demand_reactive_power: + name: "SelecEM2M Maximum Demand Reactive Power" + maximum_demand_apparent_power: + name: "SelecEM2M Maximum Demand Apparent Power" From 80949521b6937d8e82435460a052436b12d71c59 Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Thu, 22 Jul 2021 14:37:42 +0200 Subject: [PATCH 593/643] Accept change as proposed by black. (#2055) --- esphome/components/external_components/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py index bf44dc1929..3e833d0b66 100644 --- a/esphome/components/external_components/__init__.py +++ b/esphome/components/external_components/__init__.py @@ -109,9 +109,9 @@ def _compute_destination_path(key: str) -> Path: return base_dir / h.hexdigest()[:8] -def _run_git_command(cmd): +def _run_git_command(cmd, cwd=None): try: - ret = subprocess.run(cmd, capture_output=True, check=False) + ret = subprocess.run(cmd, cwd=cwd, capture_output=True, check=False) except FileNotFoundError as err: raise cv.Invalid( "git is not installed but required for external_components.\n" @@ -151,14 +151,16 @@ def _process_git_config(config: dict, refresh) -> str: _LOGGER.info("Updating %s", key) _LOGGER.debug("Location: %s", repo_dir) # Stash local changes (if any) - _run_git_command(["git", "stash", "push", "--include-untracked"]) + _run_git_command( + ["git", "stash", "push", "--include-untracked"], str(repo_dir) + ) # Fetch remote ref cmd = ["git", "fetch", "--", "origin"] if CONF_REF in config: cmd.append(config[CONF_REF]) - _run_git_command(cmd) + _run_git_command(cmd, str(repo_dir)) # Hard reset to FETCH_HEAD (short-lived git ref corresponding to most recent fetch) - _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"]) + _run_git_command(["git", "reset", "--hard", "FETCH_HEAD"], str(repo_dir)) if (repo_dir / "esphome" / "components").is_dir(): components_dir = repo_dir / "esphome" / "components" From ba461e51a83e1d589d23b246ccdd136369d9a1dc Mon Sep 17 00:00:00 2001 From: "Sergey V. DUDANOV" Date: Thu, 22 Jul 2021 16:39:21 +0400 Subject: [PATCH 594/643] midea_ac: fix presets implementation (#2054) --- esphome/components/midea_ac/midea_climate.cpp | 4 ++-- esphome/components/midea_ac/midea_frame.cpp | 22 ++++++++++++------- esphome/components/midea_ac/midea_frame.h | 1 + 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/esphome/components/midea_ac/midea_climate.cpp b/esphome/components/midea_ac/midea_climate.cpp index 9fe5df7de3..72f7d23404 100644 --- a/esphome/components/midea_ac/midea_climate.cpp +++ b/esphome/components/midea_ac/midea_climate.cpp @@ -100,7 +100,7 @@ bool MideaAC::allow_preset(climate::ClimatePreset preset) const { ESP_LOGD(TAG, "BOOST preset is only available in HEAT or COOL mode"); } break; - case climate::CLIMATE_PRESET_HOME: + case climate::CLIMATE_PRESET_NONE: return true; default: break; @@ -191,7 +191,7 @@ climate::ClimateTraits MideaAC::traits() { if (traits_swing_both_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); traits.set_supported_presets({ - climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_NONE, }); if (traits_preset_eco_) traits.add_supported_preset(climate::CLIMATE_PRESET_ECO); diff --git a/esphome/components/midea_ac/midea_frame.cpp b/esphome/components/midea_ac/midea_frame.cpp index 5f09f4314f..c0a5ce4b55 100644 --- a/esphome/components/midea_ac/midea_frame.cpp +++ b/esphome/components/midea_ac/midea_frame.cpp @@ -86,18 +86,17 @@ void PropertiesFrame::set_mode(climate::ClimateMode mode) { } optional PropertiesFrame::get_preset() const { - if (this->get_eco_mode()) { + if (this->get_eco_mode()) return climate::CLIMATE_PRESET_ECO; - } else if (this->get_sleep_mode()) { + if (this->get_sleep_mode()) return climate::CLIMATE_PRESET_SLEEP; - } else if (this->get_turbo_mode()) { + if (this->get_turbo_mode()) return climate::CLIMATE_PRESET_BOOST; - } else { - return climate::CLIMATE_PRESET_HOME; - } + return climate::CLIMATE_PRESET_NONE; } void PropertiesFrame::set_preset(climate::ClimatePreset preset) { + this->clear_presets(); switch (preset) { case climate::CLIMATE_PRESET_ECO: this->set_eco_mode(true); @@ -113,14 +112,21 @@ void PropertiesFrame::set_preset(climate::ClimatePreset preset) { } } +void PropertiesFrame::clear_presets() { + this->set_eco_mode(false); + this->set_sleep_mode(false); + this->set_turbo_mode(false); + this->set_freeze_protection_mode(false); +} + bool PropertiesFrame::is_custom_preset() const { return this->get_freeze_protection_mode(); } const std::string &PropertiesFrame::get_custom_preset() const { return midea_ac::MIDEA_FREEZE_PROTECTION_PRESET; }; void PropertiesFrame::set_custom_preset(const std::string &preset) { - if (preset == MIDEA_FREEZE_PROTECTION_PRESET) { + this->clear_presets(); + if (preset == MIDEA_FREEZE_PROTECTION_PRESET) this->set_freeze_protection_mode(true); - } } bool PropertiesFrame::is_custom_fan_mode() const { diff --git a/esphome/components/midea_ac/midea_frame.h b/esphome/components/midea_ac/midea_frame.h index 3777f6fd77..e1d6fed49d 100644 --- a/esphome/components/midea_ac/midea_frame.h +++ b/esphome/components/midea_ac/midea_frame.h @@ -115,6 +115,7 @@ class PropertiesFrame : public midea_dongle::BaseFrame { /* PRESET */ optional get_preset() const; void set_preset(climate::ClimatePreset preset); + void clear_presets(); bool is_custom_preset() const; const std::string &get_custom_preset() const; From 03e317d052e2f3fe7cb6114956218f2d6978b48e Mon Sep 17 00:00:00 2001 From: carstenschroeder Date: Thu, 22 Jul 2021 14:39:57 +0200 Subject: [PATCH 595/643] Fixes new auto mode COOL and HEAT after #1994 (#2053) --- esphome/components/pid/pid_climate.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index dac4426698..ef8a4df962 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -35,8 +35,8 @@ void PIDClimate::control(const climate::ClimateCall &call) { if (call.get_target_temperature().has_value()) this->target_temperature = *call.get_target_temperature(); - // If switching to non-auto mode, set output immediately - if (this->mode != climate::CLIMATE_MODE_HEAT_COOL) + // If switching to off mode, set output immediately + if (this->mode == climate::CLIMATE_MODE_OFF) this->handle_non_auto_mode_(); this->publish_state(); From f0d9ad6a4e35c8cf3abac8fbc9f4875cb1859a3d Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 23 Jul 2021 17:53:59 +0200 Subject: [PATCH 596/643] Add TAG to all compile units (#2060) When using static TAG is only valid in the current compile unit. For some reason it seems that the current ESP8266/ESP32 compiler use the instance from ble.cpp, but it seems that this causes issues with newer compiler leading to compile time errors like this: In file included from /root/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-log.h:164, from /root/.platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal.h:71, from /root/.platformio/packages/framework-arduinoespressif32/cores/esp32/Arduino.h:36, from src/esphome/core/esphal.h:3, from src/esphome/core/helpers.h:10, from src/esphome/components/esp32_ble/ble_uuid.h:3, from src/esphome/components/esp32_ble/ble_advertising.cpp:5: src/esphome/components/esp32_ble/ble_advertising.cpp: In member function 'void esphome::esp32_ble::BLEAdvertising::start()': src/esphome/components/esp32_ble/ble_advertising.cpp:64:14: error: 'TAG' was not declared in this scope ESP_LOGE(TAG, "esp_ble_gap_config_adv_data failed (Advertising): %d", err); ^~~ --- esphome/components/esp32_ble/ble_advertising.cpp | 2 ++ esphome/components/esp32_ble/ble_uuid.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/esphome/components/esp32_ble/ble_advertising.cpp b/esphome/components/esp32_ble/ble_advertising.cpp index f215fb48a3..92270124dd 100644 --- a/esphome/components/esp32_ble/ble_advertising.cpp +++ b/esphome/components/esp32_ble/ble_advertising.cpp @@ -7,6 +7,8 @@ namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + BLEAdvertising::BLEAdvertising() { this->advertising_data_.set_scan_rsp = false; this->advertising_data_.include_name = true; diff --git a/esphome/components/esp32_ble/ble_uuid.cpp b/esphome/components/esp32_ble/ble_uuid.cpp index cb0b99c62b..0de938d9a8 100644 --- a/esphome/components/esp32_ble/ble_uuid.cpp +++ b/esphome/components/esp32_ble/ble_uuid.cpp @@ -5,6 +5,8 @@ namespace esphome { namespace esp32_ble { +static const char *const TAG = "esp32_ble"; + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; From 66cdb761dc26d3c76fd54705aefc1ef7212e38d7 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Sat, 24 Jul 2021 11:55:25 +0200 Subject: [PATCH 597/643] Fix minor build issues with Arduino ESP32 2.0.0-rc1 (#2057) --- esphome/components/uart/uart.h | 1 + esphome/components/uart/uart_esp32.cpp | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index cd54290c73..1dc1e18412 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/core/esphal.h" #include "esphome/core/component.h" diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index 89de4c0cc1..16d683e4a6 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -12,7 +12,7 @@ uint8_t next_uart_num = 1; static const uint32_t UART_PARITY_EVEN = 0 << 0; static const uint32_t UART_PARITY_ODD = 1 << 0; -static const uint32_t UART_PARITY_EN = 1 << 1; +static const uint32_t UART_PARITY_ENABLE = 1 << 1; static const uint32_t UART_NB_BIT_5 = 0 << 2; static const uint32_t UART_NB_BIT_6 = 1 << 2; static const uint32_t UART_NB_BIT_7 = 2 << 2; @@ -39,9 +39,9 @@ uint32_t UARTComponent::get_config() { */ if (this->parity_ == UART_CONFIG_PARITY_EVEN) - config |= UART_PARITY_EVEN | UART_PARITY_EN; + config |= UART_PARITY_EVEN | UART_PARITY_ENABLE; else if (this->parity_ == UART_CONFIG_PARITY_ODD) - config |= UART_PARITY_ODD | UART_PARITY_EN; + config |= UART_PARITY_ODD | UART_PARITY_ENABLE; switch (this->data_bits_) { case 5: From 3749c11f21ca5648b1cc17309c2b0b6459cfd120 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Sun, 25 Jul 2021 23:54:32 +0200 Subject: [PATCH 598/643] Fix clang-format script behaviour without -i + code cleanup (#2002) Co-authored-by: Stefan Agner --- .github/workflows/ci.yml | 9 +++-- script/clang-format | 75 ++++++++++++++++------------------------ script/clang-tidy | 46 +++++++++--------------- script/helpers.py | 4 ++- 4 files changed, 57 insertions(+), 77 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0be9be903..433c7fb872 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,16 +46,21 @@ jobs: echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" echo "::add-matcher::.github/workflows/matchers/gcc.json" + # Also run git-diff-index so that the step is marked as failed on formatting errors, + # since clang-format doesn't do anything but change files if -i is passed. - name: Run clang-format - run: script/clang-format -i + run: | + script/clang-format -i + git diff-index --quiet HEAD -- if: ${{ matrix.id == 'clang-format' }} - name: Run clang-tidy run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} if: ${{ matrix.id == 'clang-tidy' }} - - name: Suggest changes + - name: Suggested changes run: script/ci-suggest-changes + if: always() ci: # Don't use the esphome-lint docker image because it may contain outdated requirements. diff --git a/script/clang-format b/script/clang-format index bb2b722e1c..d6588f1ccb 100755 --- a/script/clang-format +++ b/script/clang-format @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse import multiprocessing import os +import queue import re import subprocess import sys @@ -13,59 +12,47 @@ import threading import click sys.path.append(os.path.dirname(__file__)) -from helpers import basepath, get_output, git_ls_files, filter_changed - -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue - -root_path = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..'))) -basepath = os.path.join(root_path, 'esphome') -rel_basepath = os.path.relpath(basepath, os.getcwd()) +from helpers import get_output, git_ls_files, filter_changed -def run_format(args, queue, lock): - """Takes filenames out of queue and runs clang-tidy on them.""" +def run_format(args, queue, lock, failed_files): + """Takes filenames out of queue and runs clang-format on them.""" while True: path = queue.get() invocation = ['clang-format-11'] if args.inplace: invocation.append('-i') + else: + invocation.extend(['--dry-run', '-Werror']) invocation.append(path) - proc = subprocess.Popen(invocation, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - output, err = proc.communicate() - with lock: - if proc.returncode != 0: - print(' '.join(invocation)) - print(output.decode('utf-8')) - print(err.decode('utf-8')) + proc = subprocess.run(invocation, capture_output=True, encoding='utf-8') + if proc.returncode != 0: + with lock: + print() + print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) + print(proc.stdout) + print(proc.stderr) + print() + failed_files.append(path) queue.task_done() def progress_bar_show(value): - if value is None: - return '' - return value + return value if value is not None else '' def main(): parser = argparse.ArgumentParser() parser.add_argument('-j', '--jobs', type=int, default=multiprocessing.cpu_count(), - help='number of tidy instances to be run in parallel.') + help='number of format instances to be run in parallel.') parser.add_argument('files', nargs='*', default=[], help='files to be processed (regex on path)') parser.add_argument('-i', '--inplace', action='store_true', - help='apply fix-its') - parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='reformat files in-place') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') + help='only run on changed files') args = parser.parse_args() try: @@ -75,7 +62,7 @@ def main(): Oops. It looks like clang-format is not installed. Please check you can run "clang-format-11 -version" in your terminal and install - clang-format (v7) if necessary. + clang-format (v11) if necessary. Note you can also upload your code as a pull request on GitHub and see the CI check output to apply clang-format. @@ -83,28 +70,26 @@ def main(): return 1 files = [] - for path in git_ls_files(): - filetypes = ('.cpp', '.h', '.tcc') - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp', '*.h', '*.tcc']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] if args.changed: files = filter_changed(files) files.sort() - return_code = 0 + failed_files = [] try: task_queue = queue.Queue(args.jobs) lock = threading.Lock() for _ in range(args.jobs): t = threading.Thread(target=run_format, - args=(args, task_queue, lock)) + args=(args, task_queue, lock, failed_files)) t.daemon = True t.start() @@ -122,7 +107,7 @@ def main(): print('Ctrl-C detected, goodbye.') os.kill(0, 9) - sys.exit(return_code) + sys.exit(len(failed_files)) if __name__ == '__main__': diff --git a/script/clang-tidy b/script/clang-tidy index 0bf17f9076..11b9818016 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -1,10 +1,9 @@ #!/usr/bin/env python3 -from __future__ import print_function - import argparse import multiprocessing import os +import queue import re import shutil import subprocess @@ -19,13 +18,6 @@ sys.path.append(os.path.dirname(__file__)) from helpers import basepath, shlex_quote, get_output, build_compile_commands, \ build_all_include, temp_header_file, git_ls_files, filter_changed -is_py2 = sys.version[0] == '2' - -if is_py2: - import Queue as queue -else: - import queue as queue - def run_tidy(args, tmpdir, queue, lock, failed_files): while True: @@ -49,8 +41,8 @@ def run_tidy(args, tmpdir, queue, lock, failed_files): # Use pexpect for a pseudy-TTY with colored output output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8', timeout=15 * 60) - with lock: - if rc != 0: + if rc != 0: + with lock: print() print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) print(output) @@ -78,15 +70,15 @@ def main(): help='files to be processed (regex on path)') parser.add_argument('--fix', action='store_true', help='apply fix-its') parser.add_argument('-q', '--quiet', action='store_false', - help='Run clang-tidy in quiet mode') + help='run clang-tidy in quiet mode') parser.add_argument('-c', '--changed', action='store_true', - help='Only run on changed files') - parser.add_argument('--split-num', type=int, help='Split the files into X jobs.', + help='only run on changed files') + parser.add_argument('--split-num', type=int, help='split the files into X jobs.', default=None) - parser.add_argument('--split-at', type=int, help='Which split is this? Starts at 1', + parser.add_argument('--split-at', type=int, help='which split is this? starts at 1', default=None) parser.add_argument('--all-headers', action='store_true', - help='Create a dummy file that checks all headers') + help='create a dummy file that checks all headers') args = parser.parse_args() try: @@ -107,15 +99,13 @@ def main(): build_compile_commands() files = [] - for path in git_ls_files(): - filetypes = ('.cpp',) - ext = os.path.splitext(path)[1] - if ext in filetypes: - path = os.path.relpath(path, os.getcwd()) - files.append(path) - # Match against re - file_name_re = re.compile('|'.join(args.files)) - files = [p for p in files if file_name_re.search(p)] + for path in git_ls_files(['*.cpp']): + files.append(os.path.relpath(path, os.getcwd())) + + if args.files: + # Match against files specified on command-line + file_name_re = re.compile('|'.join(args.files)) + files = [p for p in files if file_name_re.search(p)] if args.changed: files = filter_changed(files) @@ -133,7 +123,6 @@ def main(): tmpdir = tempfile.mkdtemp() failed_files = [] - return_code = 0 try: task_queue = queue.Queue(args.jobs) lock = threading.Lock() @@ -151,7 +140,6 @@ def main(): # Wait for all threads to be done. task_queue.join() - return_code = len(failed_files) except KeyboardInterrupt: print() @@ -168,8 +156,8 @@ def main(): print('Error applying fixes.\n', file=sys.stderr) raise - return return_code + sys.exit(len(failed_files)) if __name__ == '__main__': - sys.exit(main()) + main() diff --git a/script/helpers.py b/script/helpers.py index 1a4402aa1d..e0ec2d169e 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -145,8 +145,10 @@ def filter_changed(files): return files -def git_ls_files(): +def git_ls_files(patterns=None): command = ["git", "ls-files", "-s"] + if patterns is not None: + command.extend(patterns) proc = subprocess.Popen(command, stdout=subprocess.PIPE) output, err = proc.communicate() lines = [x.split() for x in output.decode("utf-8").splitlines()] From 1f5cbca5092c716c9f1dfa2d3a314515e8705537 Mon Sep 17 00:00:00 2001 From: Trevor North Date: Mon, 26 Jul 2021 07:59:18 +0100 Subject: [PATCH 599/643] Merge build flags from platformio_options (#1651) --- esphome/writer.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 57698f8c25..572e976025 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -72,9 +72,7 @@ upload_flags = """, ) -UPLOAD_SPEED_OVERRIDE = { - "esp210": 57600, -} +UPLOAD_SPEED_OVERRIDE = {"esp210": 57600} def get_flags(key): @@ -210,11 +208,12 @@ def gather_lib_deps(): return [x.as_lib_dep for x in CORE.libraries] -def gather_build_flags(): - build_flags = CORE.build_flags +def gather_build_flags(overrides): + build_flags = list(CORE.build_flags) + build_flags += [overrides] if isinstance(overrides, str) else overrides # avoid changing build flags order - return list(sorted(list(build_flags))) + return list(sorted(build_flags)) ESP32_LARGE_PARTITIONS_CSV = """\ @@ -228,8 +227,10 @@ spiffs, data, spiffs, 0x391000, 0x00F000 def get_ini_content(): + overrides = CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {}) + lib_deps = gather_lib_deps() - build_flags = gather_build_flags() + build_flags = gather_build_flags(overrides.pop("build_flags", [])) data = { "platform": CORE.arduino_version, @@ -275,7 +276,7 @@ def get_ini_content(): # Ignore libraries that are not explicitly used, but may # be added by LDF # data['lib_ldf_mode'] = 'chain' - data.update(CORE.config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS, {})) + data.update(overrides) content = f"[env:{CORE.name}]\n" content += format_ini(data) From 1dd43a75f222507ddaeea343732484e0cd328003 Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Mon, 26 Jul 2021 17:20:02 +1000 Subject: [PATCH 600/643] Correctly invert esp32 RMT TX (#2022) --- esphome/components/remote_transmitter/remote_transmitter.h | 1 + .../remote_transmitter/remote_transmitter_esp32.cpp | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 000fbabfee..853b5b6289 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -41,6 +41,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, bool initialized_{false}; std::vector rmt_temp_; esp_err_t error_code_{ESP_OK}; + bool inverted_{false}; #endif uint8_t carrier_duty_percent_{50}; }; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 3d3e26160a..7b366fa52b 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -50,6 +50,7 @@ void RemoteTransmitterComponent::configure_rmt() { } else { c.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; c.tx_config.idle_level = RMT_IDLE_LEVEL_HIGH; + this->inverted_ = true; } esp_err_t error = rmt_config(&c); @@ -95,10 +96,10 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen val -= item; if (rmt_i % 2 == 0) { - rmt_item.level0 = static_cast(level); + rmt_item.level0 = static_cast(level ^ this->inverted_); rmt_item.duration0 = static_cast(item); } else { - rmt_item.level1 = static_cast(level); + rmt_item.level1 = static_cast(level ^ this->inverted_); rmt_item.duration1 = static_cast(item); this->rmt_temp_.push_back(rmt_item); } From a34d5e3901bba6ffc0a499368b8040b907669434 Mon Sep 17 00:00:00 2001 From: WeekendWarrior1 Date: Mon, 26 Jul 2021 17:32:08 +1000 Subject: [PATCH 601/643] Move configure_rmt() into setup() (#2028) --- .../components/remote_transmitter/remote_transmitter_esp32.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 7b366fa52b..90166d2741 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -9,7 +9,7 @@ namespace remote_transmitter { static const char *const TAG = "remote_transmitter"; -void RemoteTransmitterComponent::setup() {} +void RemoteTransmitterComponent::setup() { this->configure_rmt(); } void RemoteTransmitterComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Transmitter..."); From 237edd75d18c2465da9117148855a3efc24851c8 Mon Sep 17 00:00:00 2001 From: buxtronix Date: Mon, 26 Jul 2021 18:41:54 +1000 Subject: [PATCH 602/643] Log warning about lack of support for Anova nano (#2063) Co-authored-by: Ben Buxton --- esphome/components/anova/anova.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index c4b08ca6b5..63e0710936 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -60,6 +60,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); if (chr == nullptr) { ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); + ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); break; } this->char_handle_ = chr->handle; From c2637a76f7264f8876ea19b02b0a93df70aebe39 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 26 Jul 2021 10:49:38 +0200 Subject: [PATCH 603/643] Print BLE 128-bit UUIDs according to spec (#2061) Since the iterator integer counts the bytes backwards we need to use the complement to 15. --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 2a3403f88d..b3db651655 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -372,7 +372,7 @@ std::string ESPBTUUID::to_string() { for (int8_t i = 15; i >= 0; i--) { sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); bpos += 2; - if (i == 3 || i == 5 || i == 7 || i == 9) + if (i == 6 || i == 8 || i == 10 || i == 12) sprintf(bpos++, "-"); } sbuf[47] = '\0'; From 159744e09ea3eac9e7bb4893089e35c86efb31cd Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 26 Jul 2021 10:50:45 +0200 Subject: [PATCH 604/643] Support library override using named library with repository (#2056) --- esphome/core/__init__.py | 30 +++++++++++++++++++++++++++--- esphome/core/config.py | 8 ++++++++ esphome/cpp_generator.py | 4 ++-- tests/unit_tests/test_core.py | 23 ++++++++++++++++------- 4 files changed, 53 insertions(+), 12 deletions(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index df98e1b150..743e6938d7 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -408,19 +408,28 @@ class Define: class Library: - def __init__(self, name, version): + def __init__(self, name, version, repository=None): self.name = name self.version = version + self.repository = repository + + def __str__(self): + return self.as_lib_dep @property def as_lib_dep(self): + if self.repository is not None: + if self.name is not None: + return f"{self.name}={self.repository}" + return self.repository + if self.version is None: return self.name return f"{self.name}@{self.version}" @property def as_tuple(self): - return self.name, self.version + return self.name, self.version, self.repository def __hash__(self): return hash(self.as_tuple) @@ -632,10 +641,24 @@ class EsphomeCore: "Library {} must be instance of Library, not {}" "".format(library, type(library)) ) - _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: if other.name != library.name: continue + if other.repository is not None: + if library.repository is None or other.repository == library.repository: + # Other is using a/the same repository, takes precendence + break + raise ValueError( + "Adding named Library with repository failed! Libraries {} and {} " + "requested with conflicting repositories!" + "".format(library, other) + ) + + if library.repository is not None: + # This is more specific since its using a repository + self.libraries.remove(other) + continue + if library.version is None: # Other requirement is more specific break @@ -652,6 +675,7 @@ class EsphomeCore: "".format(library, other) ) else: + _LOGGER.debug("Adding library: %s", library) self.libraries.append(library) return library diff --git a/esphome/core/config.py b/esphome/core/config.py index 9475225f4d..eb748d89ac 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -332,6 +332,14 @@ async def to_code(config): if "@" in lib: name, vers = lib.split("@", 1) cg.add_library(name, vers) + elif "://" in lib: + # Repository... + if "=" in lib: + name, repo = lib.split("=", 1) + cg.add_library(name, None, repo) + else: + cg.add_library(None, None, lib) + else: cg.add_library(lib, None) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 802e9a9d38..eda378e5eb 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -543,13 +543,13 @@ def add_global(expression: Union[SafeExpType, Statement]): CORE.add_global(expression) -def add_library(name: str, version: Optional[str]): +def add_library(name: str, version: Optional[str], repository: Optional[str] = None): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') :param version: The version of the library, may be None. """ - CORE.add_library(Library(name, version)) + CORE.add_library(Library(name, version, repository)) def add_build_flag(build_flag: str): diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 4e60880033..9e4ad3d79d 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -444,16 +444,24 @@ class TestDefine: class TestLibrary: @pytest.mark.parametrize( - "name, value, prop, expected", + "name, version, repository, prop, expected", ( - ("mylib", None, "as_lib_dep", "mylib"), - ("mylib", None, "as_tuple", ("mylib", None)), - ("mylib", "1.2.3", "as_lib_dep", "mylib@1.2.3"), - ("mylib", "1.2.3", "as_tuple", ("mylib", "1.2.3")), + ("mylib", None, None, "as_lib_dep", "mylib"), + ("mylib", None, None, "as_tuple", ("mylib", None, None)), + ("mylib", "1.2.3", None, "as_lib_dep", "mylib@1.2.3"), + ("mylib", "1.2.3", None, "as_tuple", ("mylib", "1.2.3", None)), + ("mylib", None, "file:///test", "as_lib_dep", "mylib=file:///test"), + ( + "mylib", + None, + "file:///test", + "as_tuple", + ("mylib", None, "file:///test"), + ), ), ) - def test_properties(self, name, value, prop, expected): - target = core.Library(name, value) + def test_properties(self, name, version, repository, prop, expected): + target = core.Library(name, version, repository) actual = getattr(target, prop) @@ -465,6 +473,7 @@ class TestLibrary: ("__eq__", core.Library(name="libfoo", version="1.2.3"), True), ("__eq__", core.Library(name="libfoo", version="1.2.4"), False), ("__eq__", core.Library(name="libbar", version="1.2.3"), False), + ("__eq__", core.Library(name="libbar", version=None, repository="file:///test"), False), ("__eq__", 1000, NotImplemented), ("__eq__", "1000", NotImplemented), ("__eq__", True, NotImplemented), From d9f09a7523e4a31b7498fdd63283987f42aff090 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Mon, 26 Jul 2021 11:10:56 +0200 Subject: [PATCH 605/643] Initial ESP32-C3-DevKitM-1 board support (#2062) Co-authored-by: Stijn Tintel --- esphome/boards.py | 875 +++++++++++++++++++++++++ esphome/components/adc/adc_sensor.cpp | 38 ++ esphome/core/__init__.py | 11 + esphome/core/config.py | 13 +- esphome/pins.py | 895 +------------------------- esphome/platformio_api.py | 5 + esphome/wizard.py | 2 +- esphome/writer.py | 2 +- tests/unit_tests/test_pins.py | 28 +- tests/unit_tests/test_wizard.py | 2 +- 10 files changed, 984 insertions(+), 887 deletions(-) create mode 100644 esphome/boards.py diff --git a/esphome/boards.py b/esphome/boards.py new file mode 100644 index 0000000000..220d440a37 --- /dev/null +++ b/esphome/boards.py @@ -0,0 +1,875 @@ +ESP8266_BASE_PINS = { + "A0": 17, + "SS": 15, + "MOSI": 13, + "MISO": 12, + "SCK": 14, + "SDA": 4, + "SCL": 5, + "RX": 3, + "TX": 1, +} + +ESP8266_BOARD_PINS = { + "d1": { + "D0": 3, + "D1": 1, + "D2": 16, + "D3": 5, + "D4": 4, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 0, + "D9": 2, + "D10": 15, + "D11": 13, + "D12": 14, + "D13": 14, + "D14": 4, + "D15": 5, + "LED": 2, + }, + "d1_mini": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "LED": 2, + }, + "d1_mini_lite": "d1_mini", + "d1_mini_pro": "d1_mini", + "esp01": {}, + "esp01_1m": {}, + "esp07": {}, + "esp12e": {}, + "esp210": {}, + "esp8285": {}, + "esp_wroom_02": {}, + "espduino": {"LED": 16}, + "espectro": {"LED": 15, "BUTTON": 2}, + "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, + "espinotee": {"LED": 16}, + "espresso_lite_v1": {"LED": 16}, + "espresso_lite_v2": {"LED": 2}, + "gen4iod": {}, + "heltec_wifi_kit_8": "d1_mini", + "huzzah": { + "LED": 0, + "LED_RED": 0, + "LED_BLUE": 2, + "D4": 4, + "D5": 5, + "D12": 12, + "D13": 13, + "D14": 14, + "D15": 15, + "D16": 16, + }, + "inventone": {}, + "modwifi": {}, + "nodemcu": { + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + "LED": 16, + }, + "nodemcuv2": "nodemcu", + "oak": { + "P0": 2, + "P1": 5, + "P2": 0, + "P3": 3, + "P4": 1, + "P5": 4, + "P6": 15, + "P7": 13, + "P8": 12, + "P9": 14, + "P10": 16, + "P11": 17, + "LED": 5, + }, + "phoenix_v1": {"LED": 16}, + "phoenix_v2": {"LED": 2}, + "sparkfunBlynk": "thing", + "thing": {"LED": 5, "SDA": 2, "SCL": 14}, + "thingdev": "thing", + "wifi_slot": {"LED": 2}, + "wifiduino": { + "D0": 3, + "D1": 1, + "D2": 2, + "D3": 0, + "D4": 4, + "D5": 5, + "D6": 16, + "D7": 14, + "D8": 12, + "D9": 13, + "D10": 15, + "D11": 13, + "D12": 12, + "D13": 14, + }, + "wifinfo": { + "LED": 12, + "D0": 16, + "D1": 5, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "D10": 1, + }, + "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, + "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, + "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, +} + +FLASH_SIZE_1_MB = 2 ** 20 +FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 +FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB +FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB +FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB + +ESP8266_FLASH_SIZES = { + "d1": FLASH_SIZE_4_MB, + "d1_mini": FLASH_SIZE_4_MB, + "d1_mini_lite": FLASH_SIZE_1_MB, + "d1_mini_pro": FLASH_SIZE_16_MB, + "esp01": FLASH_SIZE_512_KB, + "esp01_1m": FLASH_SIZE_1_MB, + "esp07": FLASH_SIZE_4_MB, + "esp12e": FLASH_SIZE_4_MB, + "esp210": FLASH_SIZE_4_MB, + "esp8285": FLASH_SIZE_1_MB, + "esp_wroom_02": FLASH_SIZE_2_MB, + "espduino": FLASH_SIZE_4_MB, + "espectro": FLASH_SIZE_4_MB, + "espino": FLASH_SIZE_4_MB, + "espinotee": FLASH_SIZE_4_MB, + "espresso_lite_v1": FLASH_SIZE_4_MB, + "espresso_lite_v2": FLASH_SIZE_4_MB, + "gen4iod": FLASH_SIZE_512_KB, + "heltec_wifi_kit_8": FLASH_SIZE_4_MB, + "huzzah": FLASH_SIZE_4_MB, + "inventone": FLASH_SIZE_4_MB, + "modwifi": FLASH_SIZE_2_MB, + "nodemcu": FLASH_SIZE_4_MB, + "nodemcuv2": FLASH_SIZE_4_MB, + "oak": FLASH_SIZE_4_MB, + "phoenix_v1": FLASH_SIZE_4_MB, + "phoenix_v2": FLASH_SIZE_4_MB, + "sparkfunBlynk": FLASH_SIZE_4_MB, + "thing": FLASH_SIZE_512_KB, + "thingdev": FLASH_SIZE_512_KB, + "wifi_slot": FLASH_SIZE_1_MB, + "wifiduino": FLASH_SIZE_4_MB, + "wifinfo": FLASH_SIZE_1_MB, + "wio_link": FLASH_SIZE_4_MB, + "wio_node": FLASH_SIZE_4_MB, + "xinabox_cw01": FLASH_SIZE_4_MB, +} + +ESP8266_LD_SCRIPTS = { + FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), + FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), + FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), + FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), + FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), +} + +ESP32_BASE_PINS = { + "TX": 1, + "RX": 3, + "SDA": 21, + "SCL": 22, + "SS": 5, + "MOSI": 23, + "MISO": 19, + "SCK": 18, + "A0": 36, + "A3": 39, + "A4": 32, + "A5": 33, + "A6": 34, + "A7": 35, + "A10": 4, + "A11": 0, + "A12": 2, + "A13": 15, + "A14": 13, + "A15": 12, + "A16": 14, + "A17": 27, + "A18": 25, + "A19": 26, + "T0": 4, + "T1": 0, + "T2": 2, + "T3": 15, + "T4": 13, + "T5": 12, + "T6": 14, + "T7": 27, + "T8": 33, + "T9": 32, + "DAC1": 25, + "DAC2": 26, + "SVP": 36, + "SVN": 39, +} + +ESP32_BOARD_PINS = { + "alksesp32": { + "A0": 32, + "A1": 33, + "A2": 25, + "A3": 26, + "A4": 27, + "A5": 14, + "A6": 12, + "A7": 15, + "D0": 40, + "D1": 41, + "D10": 19, + "D11": 21, + "D12": 22, + "D13": 23, + "D2": 15, + "D3": 2, + "D4": 0, + "D5": 4, + "D6": 16, + "D7": 17, + "D8": 5, + "D9": 18, + "DHT_PIN": 26, + "LED": 23, + "L_B": 5, + "L_G": 17, + "L_R": 22, + "L_RGB_B": 16, + "L_RGB_G": 21, + "L_RGB_R": 4, + "L_Y": 23, + "MISO": 22, + "MOSI": 21, + "PHOTO": 25, + "PIEZO1": 19, + "PIEZO2": 18, + "POT1": 32, + "POT2": 33, + "S1": 4, + "S2": 16, + "S3": 18, + "S4": 19, + "S5": 21, + "SCK": 23, + "SCL": 14, + "SDA": 27, + "SS": 19, + "SW1": 15, + "SW2": 2, + "SW3": 0, + }, + "bpi-bit": { + "BUTTON_A": 35, + "BUTTON_B": 27, + "BUZZER": 25, + "LIGHT_SENSOR1": 36, + "LIGHT_SENSOR2": 39, + "MPU9250_INT": 0, + "P0": 25, + "P1": 32, + "P10": 26, + "P11": 27, + "P12": 2, + "P13": 18, + "P14": 19, + "P15": 23, + "P16": 5, + "P19": 22, + "P2": 33, + "P20": 21, + "P3": 13, + "P4": 15, + "P5": 35, + "P6": 12, + "P7": 14, + "P8": 16, + "P9": 17, + "RGB_LED": 4, + "TEMPERATURE_SENSOR": 34, + }, + "d-duino-32": { + "D1": 5, + "D10": 1, + "D2": 4, + "D3": 0, + "D4": 2, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 3, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 4, + "SDA": 5, + "SS": 15, + }, + "esp-wrover-kit": {}, + "esp32-devkitlipo": {}, + "esp32-evb": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, + "esp32-poe-iso": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + }, + "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, + "esp32-pro": { + "BUTTON": 34, + "MISO": 15, + "MOSI": 2, + "SCK": 14, + "SCL": 16, + "SDA": 13, + "SS": 17, + }, + "esp320": { + "LED": 5, + "MISO": 12, + "MOSI": 13, + "SCK": 14, + "SCL": 14, + "SDA": 2, + "SS": 15, + }, + "esp32cam": {}, + "esp32dev": {}, + "esp32doit-devkit-v1": {"LED": 2}, + "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, + "esp32vn-iot-uno": {}, + "espea32": {"BUTTON": 0, "LED": 5}, + "espectro32": {"LED": 15, "SD_SS": 33}, + "espino32": {"BUTTON": 0, "LED": 16}, + "featheresp32": { + "A0": 26, + "A1": 25, + "A10": 27, + "A11": 12, + "A12": 13, + "A13": 35, + "A2": 34, + "A4": 36, + "A5": 4, + "A6": 14, + "A7": 32, + "A8": 15, + "A9": 33, + "Ax": 2, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 33, + "TX": 17, + }, + "firebeetle32": {"LED": 2}, + "fm-devkit": { + "D0": 34, + "D1": 35, + "D10": 0, + "D2": 32, + "D3": 33, + "D4": 27, + "D5": 14, + "D6": 12, + "D7": 13, + "D8": 15, + "D9": 23, + "I2S_DOUT": 22, + "I2S_LRCLK": 25, + "I2S_MCLK": 2, + "I2S_SCLK": 26, + "LED": 5, + "SCL": 17, + "SDA": 16, + "SW1": 4, + "SW2": 18, + "SW3": 19, + "SW4": 21, + }, + "frogboard": {}, + "heltec_wifi_kit_32": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 25, + "RST_OLED": 16, + "SCL_OLED": 15, + "SDA_OLED": 4, + "Vext": 21, + }, + "heltec_wifi_lora_32": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 33, + "DIO2": 32, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wifi_lora_32_V2": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "heltec_wireless_stick": { + "BUTTON": 0, + "DIO0": 26, + "DIO1": 35, + "DIO2": 34, + "LED": 25, + "MOSI": 27, + "RST_LoRa": 14, + "RST_OLED": 16, + "SCK": 5, + "SCL_OLED": 15, + "SDA_OLED": 4, + "SS": 18, + "Vext": 21, + }, + "hornbill32dev": {"BUTTON": 0, "LED": 13}, + "hornbill32minima": {"SS": 2}, + "intorobot": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 4, + "MISO": 17, + "MOSI": 16, + "RGB_B_BUILTIN": 22, + "RGB_G_BUILTIN": 21, + "RGB_R_BUILTIN": 27, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "iotaap_magnolia": {}, + "iotbusio": {}, + "iotbusproteus": {}, + "lolin32": {"LED": 5}, + "lolin32_lite": {"LED": 22}, + "lolin_d32": {"LED": 5, "_VBAT": 35}, + "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, + "lopy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "lopy4": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 18, + }, + "m5stack-core-esp32": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stack-fire": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + }, + "m5stack-grey": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G1": 1, + "G12": 12, + "G13": 13, + "G15": 15, + "G16": 16, + "G17": 17, + "G18": 18, + "G19": 19, + "G2": 2, + "G21": 21, + "G22": 22, + "G23": 23, + "G25": 25, + "G26": 26, + "G3": 3, + "G34": 34, + "G35": 35, + "G36": 36, + "G5": 5, + "RXD2": 16, + "TXD2": 17, + }, + "m5stick-c": { + "ADC1": 35, + "ADC2": 36, + "G0": 0, + "G10": 10, + "G26": 26, + "G32": 32, + "G33": 33, + "G36": 36, + "G37": 37, + "G39": 39, + "G9": 9, + "MISO": 36, + "MOSI": 15, + "SCK": 13, + "SCL": 33, + "SDA": 32, + }, + "magicbit": { + "BLUE_LED": 17, + "BUZZER": 25, + "GREEN_LED": 16, + "LDR": 36, + "LED": 16, + "LEFT_BUTTON": 35, + "MOTOR1A": 27, + "MOTOR1B": 18, + "MOTOR2A": 16, + "MOTOR2B": 17, + "POT": 39, + "RED_LED": 27, + "RIGHT_PUTTON": 34, + "YELLOW_LED": 18, + }, + "mhetesp32devkit": {"LED": 2}, + "mhetesp32minikit": {"LED": 2}, + "microduino-core-esp32": { + "A0": 12, + "A1": 13, + "A10": 25, + "A11": 26, + "A12": 27, + "A13": 14, + "A2": 15, + "A3": 4, + "A6": 38, + "A7": 37, + "A8": 32, + "A9": 33, + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 23, + "D12": 19, + "D13": 18, + "D14": 12, + "D15": 13, + "D16": 15, + "D17": 4, + "D18": 22, + "D19": 21, + "D2": 16, + "D20": 38, + "D21": 37, + "D3": 17, + "D4": 32, + "D5": 33, + "D6": 25, + "D7": 26, + "D8": 27, + "D9": 14, + "SCL": 21, + "SCL1": 13, + "SDA": 22, + "SDA1": 12, + }, + "nano32": {"BUTTON": 0, "LED": 16}, + "nina_w10": { + "D0": 3, + "D1": 1, + "D10": 5, + "D11": 19, + "D12": 23, + "D13": 18, + "D14": 13, + "D15": 12, + "D16": 32, + "D17": 33, + "D18": 21, + "D19": 34, + "D2": 26, + "D20": 36, + "D21": 39, + "D3": 25, + "D4": 35, + "D5": 27, + "D6": 22, + "D7": 0, + "D8": 15, + "D9": 14, + "LED_BLUE": 21, + "LED_GREEN": 33, + "LED_RED": 23, + "SCL": 13, + "SDA": 12, + "SW1": 33, + "SW2": 27, + }, + "node32s": {}, + "nodemcu-32s": {"BUTTON": 0, "LED": 2}, + "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, + "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, + "oroca_edubot": { + "A0": 34, + "A1": 39, + "A2": 36, + "A3": 33, + "D0": 4, + "D1": 16, + "D2": 17, + "D3": 22, + "D4": 23, + "D5": 5, + "D6": 18, + "D7": 19, + "D8": 33, + "LED": 13, + "MOSI": 18, + "RX": 16, + "SCK": 5, + "SDA": 23, + "SS": 2, + "TX": 17, + "VBAT": 35, + }, + "pico32": {}, + "pocket_32": {"LED": 16}, + "pycom_gpy": { + "A1": 37, + "A2": 38, + "LED": 0, + "MISO": 37, + "MOSI": 22, + "SCK": 13, + "SCL": 13, + "SDA": 12, + "SS": 17, + }, + "quantum": {}, + "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, + "tinypico": {}, + "ttgo-lora32-v1": { + "A1": 37, + "A2": 38, + "BUTTON": 0, + "LED": 2, + "MOSI": 27, + "SCK": 5, + "SS": 18, + }, + "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, + "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, + "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, + "ttgo-t7-v13-mini32": {"LED": 22}, + "ttgo-t7-v14-mini32": {"LED": 19}, + "turta_iot_node": {}, + "vintlabs-devkit-v1": { + "LED": 2, + "PWM0": 12, + "PWM1": 13, + "PWM2": 14, + "PWM3": 15, + "PWM4": 16, + "PWM5": 17, + "PWM6": 18, + "PWM7": 19, + }, + "wemos_d1_mini32": { + "D0": 26, + "D1": 22, + "D2": 21, + "D3": 17, + "D4": 16, + "D5": 18, + "D6": 19, + "D7": 23, + "D8": 5, + "LED": 2, + "RXD": 3, + "TXD": 1, + "_VBAT": 35, + }, + "wemosbat": {"LED": 16}, + "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, + "widora-air": { + "A1": 39, + "A2": 35, + "A3": 25, + "A4": 26, + "A5": 14, + "A6": 12, + "A7": 15, + "A8": 13, + "A9": 2, + "BUTTON": 0, + "D0": 19, + "D1": 23, + "D2": 18, + "D3": 17, + "D4": 16, + "D5": 5, + "D6": 4, + "LED": 25, + "MISO": 17, + "MOSI": 16, + "SCL": 19, + "SDA": 23, + "T0": 19, + "T1": 23, + "T2": 18, + "T3": 17, + "T4": 16, + "T5": 5, + "T6": 4, + }, + "xinabox_cw02": {"LED": 27}, +} + +ESP32_C3_BASE_PINS = { + "TX": 21, + "RX": 20, + "ADC1_0": 0, + "ADC1_1": 1, + "ADC1_2": 2, + "ADC1_3": 3, + "ADC1_4": 4, + "ADC2_0": 5, +} + +ESP32_C3_BOARD_PINS = { + "esp32-c3-devkitm-1": {"LED": 8}, + "esp32-c3-devkitc-02": "esp32-c3-devkitm-1", +} diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index d6469ab785..78f12a6b9e 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -14,6 +14,7 @@ static const char *const TAG = "adc"; void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } inline adc1_channel_t gpio_to_adc1(uint8_t pin) { +#if CONFIG_IDF_TARGET_ESP32 switch (pin) { case 36: return ADC1_CHANNEL_0; @@ -34,6 +35,22 @@ inline adc1_channel_t gpio_to_adc1(uint8_t pin) { default: return ADC1_CHANNEL_MAX; } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (pin) { + case 0: + return ADC1_CHANNEL_0; + case 1: + return ADC1_CHANNEL_1; + case 2: + return ADC1_CHANNEL_2; + case 3: + return ADC1_CHANNEL_3; + case 4: + return ADC1_CHANNEL_4; + default: + return ADC1_CHANNEL_MAX; + } +#endif } #endif @@ -46,8 +63,10 @@ void ADCSensor::setup() { #ifdef ARDUINO_ARCH_ESP32 adc1_config_channel_atten(gpio_to_adc1(pin_), attenuation_); adc1_config_width(ADC_WIDTH_BIT_12); +#if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_)); #endif +#endif } void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); @@ -89,6 +108,7 @@ float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 int raw = adc1_get_raw(gpio_to_adc1(pin_)); float value_v = raw / 4095.0f; +#if CONFIG_IDF_TARGET_ESP32 switch (this->attenuation_) { case ADC_ATTEN_DB_0: value_v *= 1.1; @@ -105,6 +125,24 @@ float ADCSensor::sample() { default: // This is to satisfy the unused ADC_ATTEN_MAX break; } +#elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 + switch (this->attenuation_) { + case ADC_ATTEN_DB_0: + value_v *= 0.84; + break; + case ADC_ATTEN_DB_2_5: + value_v *= 1.13; + break; + case ADC_ATTEN_DB_6: + value_v *= 1.56; + break; + case ADC_ATTEN_DB_11: + value_v *= 3.0; + break; + default: // This is to satisfy the unused ADC_ATTEN_MAX + break; + } +#endif return value_v; #endif diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 743e6938d7..c61b288d09 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -20,6 +20,7 @@ from esphome.coroutine import FakeEventLoop as _FakeEventLoop from esphome.coroutine import coroutine, coroutine_with_priority # noqa from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict +from esphome import boards if TYPE_CHECKING: from ..cpp_generator import MockObj, MockObjClass, Statement @@ -593,10 +594,20 @@ class EsphomeCore: @property def is_esp32(self): + """Check if the ESP32 platform is used. + + This checks if the ESP32 platform is in use, which + support ESP32 as well as other chips such as ESP32-C3 + """ if self.esp_platform is None: raise ValueError("No platform specified") return self.esp_platform == "ESP32" + @property + def is_esp32_c3(self): + """Check if the ESP32-C3 SoC is being used.""" + return self.is_esp32 and self.board in boards.ESP32_C3_BOARD_PINS + def add_job(self, func, *args, **kwargs): self.event_loop.add_job(func, *args, **kwargs) diff --git a/esphome/core/config.py b/esphome/core/config.py index eb748d89ac..23e6e34625 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -4,7 +4,7 @@ import re import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation, pins +from esphome import automation, boards from esphome.const import ( CONF_ARDUINO_VERSION, CONF_BOARD, @@ -50,18 +50,19 @@ VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:[ab]\d+)?$") CONF_NAME_ADD_MAC_SUFFIX = "name_add_mac_suffix" -def validate_board(value): +def validate_board(value: str): if CORE.is_esp8266: - board_pins = pins.ESP8266_BOARD_PINS + boardlist = boards.ESP8266_BOARD_PINS.keys() elif CORE.is_esp32: - board_pins = pins.ESP32_BOARD_PINS + boardlist = list(boards.ESP32_BOARD_PINS.keys()) + boardlist += list(boards.ESP32_C3_BOARD_PINS.keys()) else: raise NotImplementedError - if value not in board_pins: + if value not in boardlist: raise cv.Invalid( "Could not find board '{}'. Valid boards are {}".format( - value, ", ".join(sorted(board_pins.keys())) + value, ", ".join(sorted(boardlist)) ) ) return value diff --git a/esphome/pins.py b/esphome/pins.py index 6356ae9bd0..5eef60e15d 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -4,877 +4,22 @@ import esphome.config_validation as cv from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER from esphome.core import CORE from esphome.util import SimpleRegistry +from esphome import boards _LOGGER = logging.getLogger(__name__) -ESP8266_BASE_PINS = { - "A0": 17, - "SS": 15, - "MOSI": 13, - "MISO": 12, - "SCK": 14, - "SDA": 4, - "SCL": 5, - "RX": 3, - "TX": 1, -} - -ESP8266_BOARD_PINS = { - "d1": { - "D0": 3, - "D1": 1, - "D2": 16, - "D3": 5, - "D4": 4, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 0, - "D9": 2, - "D10": 15, - "D11": 13, - "D12": 14, - "D13": 14, - "D14": 4, - "D15": 5, - "LED": 2, - }, - "d1_mini": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "LED": 2, - }, - "d1_mini_lite": "d1_mini", - "d1_mini_pro": "d1_mini", - "esp01": {}, - "esp01_1m": {}, - "esp07": {}, - "esp12e": {}, - "esp210": {}, - "esp8285": {}, - "esp_wroom_02": {}, - "espduino": {"LED": 16}, - "espectro": {"LED": 15, "BUTTON": 2}, - "espino": {"LED": 2, "LED_RED": 2, "LED_GREEN": 4, "LED_BLUE": 5, "BUTTON": 0}, - "espinotee": {"LED": 16}, - "espresso_lite_v1": {"LED": 16}, - "espresso_lite_v2": {"LED": 2}, - "gen4iod": {}, - "heltec_wifi_kit_8": "d1_mini", - "huzzah": { - "LED": 0, - "LED_RED": 0, - "LED_BLUE": 2, - "D4": 4, - "D5": 5, - "D12": 12, - "D13": 13, - "D14": 14, - "D15": 15, - "D16": 16, - }, - "inventone": {}, - "modwifi": {}, - "nodemcu": { - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - "LED": 16, - }, - "nodemcuv2": "nodemcu", - "oak": { - "P0": 2, - "P1": 5, - "P2": 0, - "P3": 3, - "P4": 1, - "P5": 4, - "P6": 15, - "P7": 13, - "P8": 12, - "P9": 14, - "P10": 16, - "P11": 17, - "LED": 5, - }, - "phoenix_v1": {"LED": 16}, - "phoenix_v2": {"LED": 2}, - "sparkfunBlynk": "thing", - "thing": {"LED": 5, "SDA": 2, "SCL": 14}, - "thingdev": "thing", - "wifi_slot": {"LED": 2}, - "wifiduino": { - "D0": 3, - "D1": 1, - "D2": 2, - "D3": 0, - "D4": 4, - "D5": 5, - "D6": 16, - "D7": 14, - "D8": 12, - "D9": 13, - "D10": 15, - "D11": 13, - "D12": 12, - "D13": 14, - }, - "wifinfo": { - "LED": 12, - "D0": 16, - "D1": 5, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "D10": 1, - }, - "wio_link": {"LED": 2, "GROVE": 15, "D0": 14, "D1": 12, "D2": 13, "BUTTON": 0}, - "wio_node": {"LED": 2, "GROVE": 15, "D0": 3, "D1": 5, "BUTTON": 0}, - "xinabox_cw01": {"SDA": 2, "SCL": 14, "LED": 5, "LED_RED": 12, "LED_GREEN": 13}, -} - -FLASH_SIZE_1_MB = 2 ** 20 -FLASH_SIZE_512_KB = FLASH_SIZE_1_MB // 2 -FLASH_SIZE_2_MB = 2 * FLASH_SIZE_1_MB -FLASH_SIZE_4_MB = 4 * FLASH_SIZE_1_MB -FLASH_SIZE_16_MB = 16 * FLASH_SIZE_1_MB - -ESP8266_FLASH_SIZES = { - "d1": FLASH_SIZE_4_MB, - "d1_mini": FLASH_SIZE_4_MB, - "d1_mini_lite": FLASH_SIZE_1_MB, - "d1_mini_pro": FLASH_SIZE_16_MB, - "esp01": FLASH_SIZE_512_KB, - "esp01_1m": FLASH_SIZE_1_MB, - "esp07": FLASH_SIZE_4_MB, - "esp12e": FLASH_SIZE_4_MB, - "esp210": FLASH_SIZE_4_MB, - "esp8285": FLASH_SIZE_1_MB, - "esp_wroom_02": FLASH_SIZE_2_MB, - "espduino": FLASH_SIZE_4_MB, - "espectro": FLASH_SIZE_4_MB, - "espino": FLASH_SIZE_4_MB, - "espinotee": FLASH_SIZE_4_MB, - "espresso_lite_v1": FLASH_SIZE_4_MB, - "espresso_lite_v2": FLASH_SIZE_4_MB, - "gen4iod": FLASH_SIZE_512_KB, - "heltec_wifi_kit_8": FLASH_SIZE_4_MB, - "huzzah": FLASH_SIZE_4_MB, - "inventone": FLASH_SIZE_4_MB, - "modwifi": FLASH_SIZE_2_MB, - "nodemcu": FLASH_SIZE_4_MB, - "nodemcuv2": FLASH_SIZE_4_MB, - "oak": FLASH_SIZE_4_MB, - "phoenix_v1": FLASH_SIZE_4_MB, - "phoenix_v2": FLASH_SIZE_4_MB, - "sparkfunBlynk": FLASH_SIZE_4_MB, - "thing": FLASH_SIZE_512_KB, - "thingdev": FLASH_SIZE_512_KB, - "wifi_slot": FLASH_SIZE_1_MB, - "wifiduino": FLASH_SIZE_4_MB, - "wifinfo": FLASH_SIZE_1_MB, - "wio_link": FLASH_SIZE_4_MB, - "wio_node": FLASH_SIZE_4_MB, - "xinabox_cw01": FLASH_SIZE_4_MB, -} - -ESP8266_LD_SCRIPTS = { - FLASH_SIZE_512_KB: ("eagle.flash.512k0.ld", "eagle.flash.512k.ld"), - FLASH_SIZE_1_MB: ("eagle.flash.1m0.ld", "eagle.flash.1m.ld"), - FLASH_SIZE_2_MB: ("eagle.flash.2m.ld", "eagle.flash.2m.ld"), - FLASH_SIZE_4_MB: ("eagle.flash.4m.ld", "eagle.flash.4m.ld"), - FLASH_SIZE_16_MB: ("eagle.flash.16m.ld", "eagle.flash.16m14m.ld"), -} - -ESP32_BASE_PINS = { - "TX": 1, - "RX": 3, - "SDA": 21, - "SCL": 22, - "SS": 5, - "MOSI": 23, - "MISO": 19, - "SCK": 18, - "A0": 36, - "A3": 39, - "A4": 32, - "A5": 33, - "A6": 34, - "A7": 35, - "A10": 4, - "A11": 0, - "A12": 2, - "A13": 15, - "A14": 13, - "A15": 12, - "A16": 14, - "A17": 27, - "A18": 25, - "A19": 26, - "T0": 4, - "T1": 0, - "T2": 2, - "T3": 15, - "T4": 13, - "T5": 12, - "T6": 14, - "T7": 27, - "T8": 33, - "T9": 32, - "DAC1": 25, - "DAC2": 26, - "SVP": 36, - "SVN": 39, -} - -ESP32_BOARD_PINS = { - "alksesp32": { - "A0": 32, - "A1": 33, - "A2": 25, - "A3": 26, - "A4": 27, - "A5": 14, - "A6": 12, - "A7": 15, - "D0": 40, - "D1": 41, - "D10": 19, - "D11": 21, - "D12": 22, - "D13": 23, - "D2": 15, - "D3": 2, - "D4": 0, - "D5": 4, - "D6": 16, - "D7": 17, - "D8": 5, - "D9": 18, - "DHT_PIN": 26, - "LED": 23, - "L_B": 5, - "L_G": 17, - "L_R": 22, - "L_RGB_B": 16, - "L_RGB_G": 21, - "L_RGB_R": 4, - "L_Y": 23, - "MISO": 22, - "MOSI": 21, - "PHOTO": 25, - "PIEZO1": 19, - "PIEZO2": 18, - "POT1": 32, - "POT2": 33, - "S1": 4, - "S2": 16, - "S3": 18, - "S4": 19, - "S5": 21, - "SCK": 23, - "SCL": 14, - "SDA": 27, - "SS": 19, - "SW1": 15, - "SW2": 2, - "SW3": 0, - }, - "bpi-bit": { - "BUTTON_A": 35, - "BUTTON_B": 27, - "BUZZER": 25, - "LIGHT_SENSOR1": 36, - "LIGHT_SENSOR2": 39, - "MPU9250_INT": 0, - "P0": 25, - "P1": 32, - "P10": 26, - "P11": 27, - "P12": 2, - "P13": 18, - "P14": 19, - "P15": 23, - "P16": 5, - "P19": 22, - "P2": 33, - "P20": 21, - "P3": 13, - "P4": 15, - "P5": 35, - "P6": 12, - "P7": 14, - "P8": 16, - "P9": 17, - "RGB_LED": 4, - "TEMPERATURE_SENSOR": 34, - }, - "d-duino-32": { - "D1": 5, - "D10": 1, - "D2": 4, - "D3": 0, - "D4": 2, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 3, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 4, - "SDA": 5, - "SS": 15, - }, - "esp-wrover-kit": {}, - "esp32-devkitlipo": {}, - "esp32-evb": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp32-gateway": {"BUTTON": 34, "LED": 33, "SCL": 16, "SDA": 32}, - "esp32-poe-iso": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - }, - "esp32-poe": {"BUTTON": 34, "MISO": 15, "MOSI": 2, "SCK": 14, "SCL": 16, "SDA": 13}, - "esp32-pro": { - "BUTTON": 34, - "MISO": 15, - "MOSI": 2, - "SCK": 14, - "SCL": 16, - "SDA": 13, - "SS": 17, - }, - "esp320": { - "LED": 5, - "MISO": 12, - "MOSI": 13, - "SCK": 14, - "SCL": 14, - "SDA": 2, - "SS": 15, - }, - "esp32cam": {}, - "esp32dev": {}, - "esp32doit-devkit-v1": {"LED": 2}, - "esp32thing": {"BUTTON": 0, "LED": 5, "SS": 2}, - "esp32vn-iot-uno": {}, - "espea32": {"BUTTON": 0, "LED": 5}, - "espectro32": {"LED": 15, "SD_SS": 33}, - "espino32": {"BUTTON": 0, "LED": 16}, - "featheresp32": { - "A0": 26, - "A1": 25, - "A10": 27, - "A11": 12, - "A12": 13, - "A13": 35, - "A2": 34, - "A4": 36, - "A5": 4, - "A6": 14, - "A7": 32, - "A8": 15, - "A9": 33, - "Ax": 2, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 33, - "TX": 17, - }, - "firebeetle32": {"LED": 2}, - "fm-devkit": { - "D0": 34, - "D1": 35, - "D10": 0, - "D2": 32, - "D3": 33, - "D4": 27, - "D5": 14, - "D6": 12, - "D7": 13, - "D8": 15, - "D9": 23, - "I2S_DOUT": 22, - "I2S_LRCLK": 25, - "I2S_MCLK": 2, - "I2S_SCLK": 26, - "LED": 5, - "SCL": 17, - "SDA": 16, - "SW1": 4, - "SW2": 18, - "SW3": 19, - "SW4": 21, - }, - "frogboard": {}, - "heltec_wifi_kit_32": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 25, - "RST_OLED": 16, - "SCL_OLED": 15, - "SDA_OLED": 4, - "Vext": 21, - }, - "heltec_wifi_lora_32": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 33, - "DIO2": 32, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wifi_lora_32_V2": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "heltec_wireless_stick": { - "BUTTON": 0, - "DIO0": 26, - "DIO1": 35, - "DIO2": 34, - "LED": 25, - "MOSI": 27, - "RST_LoRa": 14, - "RST_OLED": 16, - "SCK": 5, - "SCL_OLED": 15, - "SDA_OLED": 4, - "SS": 18, - "Vext": 21, - }, - "hornbill32dev": {"BUTTON": 0, "LED": 13}, - "hornbill32minima": {"SS": 2}, - "intorobot": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 4, - "MISO": 17, - "MOSI": 16, - "RGB_B_BUILTIN": 22, - "RGB_G_BUILTIN": 21, - "RGB_R_BUILTIN": 27, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "iotaap_magnolia": {}, - "iotbusio": {}, - "iotbusproteus": {}, - "lolin32": {"LED": 5}, - "lolin32_lite": {"LED": 22}, - "lolin_d32": {"LED": 5, "_VBAT": 35}, - "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, - "lopy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "lopy4": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 18, - }, - "m5stack-core-esp32": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stack-fire": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - }, - "m5stack-grey": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G1": 1, - "G12": 12, - "G13": 13, - "G15": 15, - "G16": 16, - "G17": 17, - "G18": 18, - "G19": 19, - "G2": 2, - "G21": 21, - "G22": 22, - "G23": 23, - "G25": 25, - "G26": 26, - "G3": 3, - "G34": 34, - "G35": 35, - "G36": 36, - "G5": 5, - "RXD2": 16, - "TXD2": 17, - }, - "m5stick-c": { - "ADC1": 35, - "ADC2": 36, - "G0": 0, - "G10": 10, - "G26": 26, - "G32": 32, - "G33": 33, - "G36": 36, - "G37": 37, - "G39": 39, - "G9": 9, - "MISO": 36, - "MOSI": 15, - "SCK": 13, - "SCL": 33, - "SDA": 32, - }, - "magicbit": { - "BLUE_LED": 17, - "BUZZER": 25, - "GREEN_LED": 16, - "LDR": 36, - "LED": 16, - "LEFT_BUTTON": 35, - "MOTOR1A": 27, - "MOTOR1B": 18, - "MOTOR2A": 16, - "MOTOR2B": 17, - "POT": 39, - "RED_LED": 27, - "RIGHT_PUTTON": 34, - "YELLOW_LED": 18, - }, - "mhetesp32devkit": {"LED": 2}, - "mhetesp32minikit": {"LED": 2}, - "microduino-core-esp32": { - "A0": 12, - "A1": 13, - "A10": 25, - "A11": 26, - "A12": 27, - "A13": 14, - "A2": 15, - "A3": 4, - "A6": 38, - "A7": 37, - "A8": 32, - "A9": 33, - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 23, - "D12": 19, - "D13": 18, - "D14": 12, - "D15": 13, - "D16": 15, - "D17": 4, - "D18": 22, - "D19": 21, - "D2": 16, - "D20": 38, - "D21": 37, - "D3": 17, - "D4": 32, - "D5": 33, - "D6": 25, - "D7": 26, - "D8": 27, - "D9": 14, - "SCL": 21, - "SCL1": 13, - "SDA": 22, - "SDA1": 12, - }, - "nano32": {"BUTTON": 0, "LED": 16}, - "nina_w10": { - "D0": 3, - "D1": 1, - "D10": 5, - "D11": 19, - "D12": 23, - "D13": 18, - "D14": 13, - "D15": 12, - "D16": 32, - "D17": 33, - "D18": 21, - "D19": 34, - "D2": 26, - "D20": 36, - "D21": 39, - "D3": 25, - "D4": 35, - "D5": 27, - "D6": 22, - "D7": 0, - "D8": 15, - "D9": 14, - "LED_BLUE": 21, - "LED_GREEN": 33, - "LED_RED": 23, - "SCL": 13, - "SDA": 12, - "SW1": 33, - "SW2": 27, - }, - "node32s": {}, - "nodemcu-32s": {"BUTTON": 0, "LED": 2}, - "odroid_esp32": {"ADC1": 35, "ADC2": 36, "LED": 2, "SCL": 4, "SDA": 15, "SS": 22}, - "onehorse32dev": {"A1": 37, "A2": 38, "BUTTON": 0, "LED": 5}, - "oroca_edubot": { - "A0": 34, - "A1": 39, - "A2": 36, - "A3": 33, - "D0": 4, - "D1": 16, - "D2": 17, - "D3": 22, - "D4": 23, - "D5": 5, - "D6": 18, - "D7": 19, - "D8": 33, - "LED": 13, - "MOSI": 18, - "RX": 16, - "SCK": 5, - "SDA": 23, - "SS": 2, - "TX": 17, - "VBAT": 35, - }, - "pico32": {}, - "pocket_32": {"LED": 16}, - "pycom_gpy": { - "A1": 37, - "A2": 38, - "LED": 0, - "MISO": 37, - "MOSI": 22, - "SCK": 13, - "SCL": 13, - "SDA": 12, - "SS": 17, - }, - "quantum": {}, - "sparkfun_lora_gateway_1-channel": {"MISO": 12, "MOSI": 13, "SCK": 14, "SS": 16}, - "tinypico": {}, - "ttgo-lora32-v1": { - "A1": 37, - "A2": 38, - "BUTTON": 0, - "LED": 2, - "MOSI": 27, - "SCK": 5, - "SS": 18, - }, - "ttgo-t-beam": {"BUTTON": 39, "LED": 14, "MOSI": 27, "SCK": 5, "SS": 18}, - "ttgo-t-watch": {"BUTTON": 36, "MISO": 2, "MOSI": 15, "SCK": 14, "SS": 13}, - "ttgo-t1": {"LED": 22, "MISO": 2, "MOSI": 15, "SCK": 14, "SCL": 23, "SS": 13}, - "ttgo-t7-v13-mini32": {"LED": 22}, - "ttgo-t7-v14-mini32": {"LED": 19}, - "turta_iot_node": {}, - "vintlabs-devkit-v1": { - "LED": 2, - "PWM0": 12, - "PWM1": 13, - "PWM2": 14, - "PWM3": 15, - "PWM4": 16, - "PWM5": 17, - "PWM6": 18, - "PWM7": 19, - }, - "wemos_d1_mini32": { - "D0": 26, - "D1": 22, - "D2": 21, - "D3": 17, - "D4": 16, - "D5": 18, - "D6": 19, - "D7": 23, - "D8": 5, - "LED": 2, - "RXD": 3, - "TXD": 1, - "_VBAT": 35, - }, - "wemosbat": {"LED": 16}, - "wesp32": {"MISO": 32, "SCL": 4, "SDA": 15}, - "widora-air": { - "A1": 39, - "A2": 35, - "A3": 25, - "A4": 26, - "A5": 14, - "A6": 12, - "A7": 15, - "A8": 13, - "A9": 2, - "BUTTON": 0, - "D0": 19, - "D1": 23, - "D2": 18, - "D3": 17, - "D4": 16, - "D5": 5, - "D6": 4, - "LED": 25, - "MISO": 17, - "MOSI": 16, - "SCL": 19, - "SDA": 23, - "T0": 19, - "T1": 23, - "T2": 18, - "T3": 17, - "T4": 16, - "T5": 5, - "T6": 4, - }, - "xinabox_cw02": {"LED": 27}, -} - def _lookup_pin(value): if CORE.is_esp8266: - board_pins_dict = ESP8266_BOARD_PINS - base_pins = ESP8266_BASE_PINS + board_pins_dict = boards.ESP8266_BOARD_PINS + base_pins = boards.ESP8266_BASE_PINS elif CORE.is_esp32: - board_pins_dict = ESP32_BOARD_PINS - base_pins = ESP32_BASE_PINS + if CORE.board in boards.ESP32_C3_BOARD_PINS: + board_pins_dict = boards.ESP32_C3_BOARD_PINS + base_pins = boards.ESP32_C3_BASE_PINS + else: + board_pins_dict = boards.ESP32_BOARD_PINS + base_pins = boards.ESP32_BASE_PINS else: raise NotImplementedError @@ -915,9 +60,27 @@ _ESP_SDIO_PINS = { 11: "Flash Command", } +_ESP32C3_SDIO_PINS = { + 12: "Flash IO3/HOLD#", + 13: "Flash IO2/WP#", + 14: "Flash CS#", + 15: "Flash CLK", + 16: "Flash IO0/DI", + 17: "Flash IO1/DO", +} + def validate_gpio_pin(value): value = _translate_pin(value) + if CORE.is_esp32_c3: + if value < 0 or value > 22: + raise cv.Invalid(f"ESP32-C3: Invalid pin number: {value}") + if value in _ESP32C3_SDIO_PINS: + raise cv.Invalid( + "This pin cannot be used on ESP32-C3s and is already used by " + "the flash interface (function: {})".format(_ESP_SDIO_PINS[value]) + ) + return value if CORE.is_esp32: if value < 0 or value > 39: raise cv.Invalid(f"ESP32: Invalid pin number: {value}") @@ -995,6 +158,10 @@ def output_pin(value): def analog_pin(value): value = validate_gpio_pin(value) if CORE.is_esp32: + if CORE.is_esp32_c3: + if 0 <= value <= 4: # ADC1 + return value + raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") if 32 <= value <= 39: # ADC1 return value raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 87ca12c9a8..3d7e9a6d48 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -202,6 +202,8 @@ STACKTRACE_ESP8266_PC_RE = re.compile(r"epc1=0x(4[0-9a-fA-F]{7})") STACKTRACE_ESP8266_EXCVADDR_RE = re.compile(r"excvaddr=0x(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_PC_RE = re.compile(r"PC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_ESP32_EXCVADDR_RE = re.compile(r"EXCVADDR\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_PC_RE = re.compile(r"MEPC\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") +STACKTRACE_ESP32_C3_RA_RE = re.compile(r"RA\s*:\s*(?:0x)?(4[0-9a-fA-F]{7})") STACKTRACE_BAD_ALLOC_RE = re.compile( r"^last failed alloc call: (4[0-9a-fA-F]{7})\((\d+)\)$" ) @@ -228,6 +230,9 @@ def process_stacktrace(config, line, backtrace_state): # ESP32 PC/EXCVADDR _parse_register(config, STACKTRACE_ESP32_PC_RE, line) _parse_register(config, STACKTRACE_ESP32_EXCVADDR_RE, line) + # ESP32-C3 PC/RA + _parse_register(config, STACKTRACE_ESP32_C3_PC_RE, line) + _parse_register(config, STACKTRACE_ESP32_C3_RA_RE, line) # bad alloc match = re.match(STACKTRACE_BAD_ALLOC_RE, line) diff --git a/esphome/wizard.py b/esphome/wizard.py index 0d912e4bbf..3f989dd93d 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -10,7 +10,7 @@ from esphome.helpers import get_bool_env, write_file from esphome.log import color, Fore # pylint: disable=anomalous-backslash-in-string -from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS +from esphome.boards import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.storage_json import StorageJSON, ext_storage_path from esphome.util import safe_print from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD diff --git a/esphome/writer.py b/esphome/writer.py index 572e976025..641ae9b3cc 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -25,7 +25,7 @@ from esphome.helpers import ( get_bool_env, ) from esphome.storage_json import StorageJSON, storage_path -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS +from esphome.boards import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS from esphome import loader _LOGGER = logging.getLogger(__name__) diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py index 6bc6f4d766..d2ffd5f7cd 100644 --- a/tests/unit_tests/test_pins.py +++ b/tests/unit_tests/test_pins.py @@ -11,13 +11,13 @@ import pytest from esphome.config_validation import Invalid from esphome.core import EsphomeCore -from esphome import pins +from esphome import boards, pins MOCK_ESP8266_BOARD_ID = "_mock_esp8266" MOCK_ESP8266_PINS = {"X0": 16, "X1": 5, "X2": 4, "LED": 2} MOCK_ESP8266_BOARD_ALIAS_ID = "_mock_esp8266_alias" -MOCK_ESP8266_FLASH_SIZE = pins.FLASH_SIZE_2_MB +MOCK_ESP8266_FLASH_SIZE = boards.FLASH_SIZE_2_MB MOCK_ESP32_BOARD_ID = "_mock_esp32" MOCK_ESP32_PINS = {"Y0": 12, "Y1": 8, "Y2": 3, "LED": 9, "A0": 8} @@ -31,19 +31,19 @@ def mock_mcu(monkeypatch): """ Add a mock MCU into the lists as a stable fixture """ - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID - pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS - pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID + boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_PINS + boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] = MOCK_ESP8266_FLASH_SIZE + boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_BOARD_ID + boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] = MOCK_ESP8266_FLASH_SIZE + boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] = MOCK_ESP32_PINS + boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] = MOCK_ESP32_BOARD_ID yield - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] - del pins.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] - del pins.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] + del boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ID] + del boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ID] + del boards.ESP8266_BOARD_PINS[MOCK_ESP8266_BOARD_ALIAS_ID] + del boards.ESP8266_FLASH_SIZES[MOCK_ESP8266_BOARD_ALIAS_ID] + del boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ID] + del boards.ESP32_BOARD_PINS[MOCK_ESP32_BOARD_ALIAS_ID] @pytest.fixture diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 0ca7c83e1b..56bd5119b5 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -2,7 +2,7 @@ import esphome.wizard as wz import pytest -from esphome.pins import ESP8266_BOARD_PINS +from esphome.boards import ESP8266_BOARD_PINS from mock import MagicMock From 6b535b11f8eae61713dd279e8d7934ad98137020 Mon Sep 17 00:00:00 2001 From: Chris Nussbaum Date: Mon, 26 Jul 2021 04:39:03 -0500 Subject: [PATCH 606/643] Couple more updates for the Tuya component (#2065) Co-authored-by: Chris Nussbaum --- esphome/components/tuya/tuya.cpp | 40 ++++++++++++++++++++++++++++---- esphome/components/tuya/tuya.h | 1 + 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index d9a0a9932a..916a550675 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -8,9 +8,10 @@ namespace tuya { static const char *const TAG = "tuya"; static const int COMMAND_DELAY = 50; +static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { - this->set_interval("heartbeat", 10000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); } void Tuya::loop() { @@ -117,7 +118,13 @@ void Tuya::handle_char_(uint8_t c) { } void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { - switch ((TuyaCommandType) command) { + TuyaCommandType command_type = (TuyaCommandType) command; + + if (this->expected_response_.has_value() && this->expected_response_ == command_type) { + this->expected_response_.reset(); + } + + switch (command_type) { case TuyaCommandType::HEARTBEAT: ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); this->protocol_version_ = version; @@ -316,6 +323,25 @@ void Tuya::send_raw_command_(TuyaCommand command) { uint8_t version = 0; this->last_command_timestamp_ = millis(); + switch (command.cmd) { + case TuyaCommandType::HEARTBEAT: + this->expected_response_ = TuyaCommandType::HEARTBEAT; + break; + case TuyaCommandType::PRODUCT_QUERY: + this->expected_response_ = TuyaCommandType::PRODUCT_QUERY; + break; + case TuyaCommandType::CONF_QUERY: + this->expected_response_ = TuyaCommandType::CONF_QUERY; + break; + case TuyaCommandType::DATAPOINT_DELIVER: + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + break; + case TuyaCommandType::DATAPOINT_QUERY: + this->expected_response_ = TuyaCommandType::DATAPOINT_REPORT; + break; + default: + break; + } ESP_LOGV(TAG, "Sending Tuya: CMD=0x%02X VERSION=%u DATA=[%s] INIT_STATE=%u", static_cast(command.cmd), version, hexencode(command.payload).c_str(), static_cast(this->init_state_)); @@ -332,8 +358,14 @@ void Tuya::send_raw_command_(TuyaCommand command) { void Tuya::process_command_queue_() { uint32_t delay = millis() - this->last_command_timestamp_; + + if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { + this->expected_response_.reset(); + } + // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly - if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty()) { + if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && + !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); this->command_queue_.erase(command_queue_.begin()); } @@ -345,7 +377,7 @@ void Tuya::send_command_(const TuyaCommand &command) { } void Tuya::send_empty_command_(TuyaCommandType command) { - send_command_(TuyaCommand{.cmd = command, .payload = std::vector{0x04}}); + send_command_(TuyaCommand{.cmd = command, .payload = std::vector{}}); } void Tuya::send_wifi_status_() { diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 4ca4f56366..68decf7e9e 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -113,6 +113,7 @@ class Tuya : public Component, public uart::UARTDevice { std::vector rx_message_; std::vector ignore_mcu_update_on_datapoints_{}; std::vector command_queue_; + optional expected_response_{}; uint8_t wifi_status_ = -1; }; From a3dcac62f92cea8d9c23d92ab99dfe40ac35f06d Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Mon, 26 Jul 2021 14:48:57 +0200 Subject: [PATCH 607/643] Fix a bunch of typos (#2058) Co-authored-by: Stefan Agner Co-authored-by: Otto Winter Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ac_dimmer/ac_dimmer.cpp | 2 +- esphome/components/aht10/aht10.cpp | 8 ++--- esphome/components/atm90e32/atm90e32_reg.h | 2 +- esphome/components/b_parasite/b_parasite.cpp | 2 +- .../components/dallas/dallas_component.cpp | 2 +- esphome/components/dht/dht.cpp | 2 +- esphome/components/e131/e131.cpp | 2 +- esphome/components/esp32_ble/queue.h | 2 +- esphome/components/esp32_ble_tracker/queue.h | 2 +- .../ethernet/ethernet_component.cpp | 2 +- .../fujitsu_general/fujitsu_general.h | 2 +- .../inkbird_ibsth1_mini.cpp | 8 ++--- .../components/max7219digit/max7219digit.cpp | 6 ++-- esphome/components/nextion/nextion.cpp | 2 +- esphome/components/nextion/nextion.h | 8 ++--- esphome/components/nextion/nextion_upload.cpp | 2 +- esphome/components/qmc5883l/qmc5883l.cpp | 2 +- esphome/components/rc522/rc522.cpp | 4 +-- esphome/components/rc522/rc522.h | 2 +- .../components/remote_base/remote_base.cpp | 2 +- esphome/components/scd30/scd30.cpp | 2 +- esphome/components/script/__init__.py | 2 +- esphome/components/script/script.h | 2 +- .../components/ssd1331_base/ssd1331_base.cpp | 2 +- esphome/components/st7789v/st7789v.h | 32 +++++++++---------- esphome/components/sx1509/sx1509_registers.h | 2 +- esphome/components/tuya/tuya.cpp | 4 +-- esphome/components/vl53l0x/LICENSE.txt | 2 +- .../waveshare_epaper/waveshare_epaper.cpp | 2 +- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 4 +-- .../xiaomi_miscale/xiaomi_miscale.cpp | 2 +- esphome/core/component.h | 2 +- script/build_jsonschema.py | 4 +-- 33 files changed, 63 insertions(+), 63 deletions(-) diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp index ad6018268e..7e9251ddf3 100644 --- a/esphome/components/ac_dimmer/ac_dimmer.cpp +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -125,7 +125,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { } void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { - // Attaching pin interrupts on the same pin will override the previous interupt + // Attaching pin interrupts on the same pin will override the previous interrupt // However, the user expects that multiple dimmers sharing the same ZC pin will work. // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 4688440d80..da8c6e844e 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -10,7 +10,7 @@ // // According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost // immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best -// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. +// results making successive requests; the current implementation makes 3 attempts with a delay of 30ms each time. #include "aht10.h" #include "esphome/core/log.h" @@ -23,7 +23,7 @@ static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms -static const uint8_t AHT10_ATTEMPS = 3; // safety margin, normally 3 attemps are enough: 3*30=90ms +static const uint8_t AHT10_ATTEMPTS = 3; // safety margin, normally 3 attempts are enough: 3*30=90ms void AHT10Component::setup() { ESP_LOGCONFIG(TAG, "Setting up AHT10..."); @@ -58,8 +58,8 @@ void AHT10Component::update() { uint8_t delay = AHT10_DEFAULT_DELAY; if (this->humidity_sensor_ != nullptr) delay = AHT10_HUMIDITY_DELAY; - for (int i = 0; i < AHT10_ATTEMPS; ++i) { - ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); + for (int i = 0; i < AHT10_ATTEMPTS; ++i) { + ESP_LOGVV(TAG, "Attempt %u at %6ld", i, millis()); delay_microseconds_accurate(4); if (!this->read_bytes(0, data, 6, delay)) { ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index dc2048fbc2..7927a7fdfb 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -39,7 +39,7 @@ static const uint16_t ATM90E32_STATUS_S0_OVPHASEBST = 1 << 11; // Over voltage static const uint16_t ATM90E32_STATUS_S0_OVPHASECST = 1 << 10; // Over voltage on phase C static const uint16_t ATM90E32_STATUS_S0_UREVWNST = 1 << 9; // Voltage Phase Sequence Error status static const uint16_t ATM90E32_STATUS_S0_IREVWNST = 1 << 8; // Current Phase Sequence Error status -static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater tha INWarnTh reg +static const uint16_t ATM90E32_STATUS_S0_INOV0ST = 1 << 7; // Calculated N line current greater than INWarnTh reg static const uint16_t ATM90E32_STATUS_S0_TQNOLOADST = 1 << 6; // All phase sum reactive power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TPNOLOADST = 1 << 5; // All phase sum active power no-load condition status static const uint16_t ATM90E32_STATUS_S0_TASNOLOADST = 1 << 4; // All phase sum apparent power no-load status diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp index 81c9243bdb..3098d462d2 100644 --- a/esphome/components/b_parasite/b_parasite.cpp +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -47,7 +47,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { uint16_t battery_millivolt = data[2] << 8 | data[3]; float battery_voltage = battery_millivolt / 1000.0f; - // Temperature in 1000 * Celcius. + // Temperature in 1000 * Celsius. uint16_t temp_millicelcius = data[4] << 8 | data[5]; float temp_celcius = temp_millicelcius / 1000.0f; diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 7fc5e424f0..7e34546078 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -137,7 +137,7 @@ void DallasComponent::update() { } if (!res) { - ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); + ESP_LOGW(TAG, "'%s' - Resetting bus for read failed!", sensor->get_name().c_str()); sensor->publish_state(NAN); this->status_set_warning(); return; diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 4a5c418e0a..a7f5747d68 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -203,7 +203,7 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, const uint16_t raw_humidity = uint16_t(data[0]) * 10 + data[1]; *humidity = raw_humidity / 10.0f; } else { - // For compatibily with DHT11 models which might only use 2 bytes checksums, only use the data from these two + // For compatibility with DHT11 models which might only use 2 bytes checksums, only use the data from these two // bytes *temperature = data[2]; *humidity = data[0]; diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index fb72e5b470..bd749683c5 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -50,7 +50,7 @@ void E131Component::loop() { } if (!packet_(payload, universe, packet)) { - ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); continue; } diff --git a/esphome/components/esp32_ble/queue.h b/esphome/components/esp32_ble/queue.h index 3c87a90f22..cd123d5469 100644 --- a/esphome/components/esp32_ble/queue.h +++ b/esphome/components/esp32_ble/queue.h @@ -13,7 +13,7 @@ /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h index 17adb98034..f0f6ab9f17 100644 --- a/esphome/components/esp32_ble_tracker/queue.h +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -12,7 +12,7 @@ /* * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather - * than trying to deal wth various locking strategies, all incoming GAP and GATT + * than trying to deal with various locking strategies, all incoming GAP and GATT * events will simply be placed on a semaphore guarded queue. The next time the * component runs loop(), these events are popped off the queue and handed at * this safer time. diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 963ee267b3..b22f8c97d1 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -9,7 +9,7 @@ #include #include -/// Macro for IDF version comparision +/// Macro for IDF version comparison #ifndef ESP_IDF_VERSION_VAL #define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) #endif diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 7a26cd7b6b..8dc7a3e484 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -62,7 +62,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { /// Transmit via IR power off command. void transmit_off_(); - /// Parse incomming message + /// Parse incoming message bool on_receive(remote_base::RemoteReceiveData data) override; /// Transmit message as IR pulses diff --git a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp index 45be7c1acf..a940a77148 100644 --- a/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp +++ b/esphome/components/inkbird_ibsth1_mini/inkbird_ibsth1_mini.cpp @@ -23,9 +23,9 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // for Inkbird IBS-TH1 Mini device we expect // 1) expected mac address // 2) device address type == PUBLIC - // 3) no service datas - // 4) one manufacturer datas - // 5) the manufacturer datas should contain a 16-bit uuid amd a 7-byte data vector + // 3) no service data + // 4) one manufacturer data + // 5) the manufacturer data should contain a 16-bit uuid amd a 7-byte data vector // 6) the 7-byte data component should have data[2] == 0 and data[6] == 8 // the address should match the address we declared @@ -63,7 +63,7 @@ bool InkbirdIBSTH1_MINI::parse_device(const esp32_ble_tracker::ESPBTDevice &devi // sensor output encoding // data[5] is a battery level // data[0] and data[1] is humidity * 100 (in pct) - // uuid is a temperature * 100 (in Celcius) + // uuid is a temperature * 100 (in Celsius) // when data[2] == 0 temperature is from internal sensor (IBS-TH1 or IBS-TH1 Mini) // when data[2] == 1 temperature is from external sensor (IBS-TH1 only) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index b130823c12..520694af9d 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -104,7 +104,7 @@ void MAX7219Component::display() { uint8_t pixels[8]; // Run this loop for every MAX CHIP (GRID OF 64 leds) // Run this routine for the rows of every chip 8x row 0 top to 7 bottom - // Fill the pixel parameter with diplay data + // Fill the pixel parameter with display data // Send the data to the chip for (uint8_t i = 0; i < this->num_chips_; i++) { for (uint8_t j = 0; j < 8; j++) { @@ -119,7 +119,7 @@ void MAX7219Component::display() { } int MAX7219Component::get_height_internal() { - return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHER // TO BE DONE -> CREATE Virtual size of screen and scroll } @@ -238,7 +238,7 @@ void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { } else { b = pixels[7 - col]; } - // send this byte to dispay at selected chip + // send this byte to display at selected chip if (this->invert_) { this->send_byte_(col + 1, ~b); } else { diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 9a5424917f..b6b0d83885 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1015,7 +1015,7 @@ void Nextion::add_no_result_to_queue_with_set_internal_(const std::string &varia * * @param variable_name Variable name for the queue * @param variable_name_to_send Variable name for the left of the command - * @param state_value Sting value to set + * @param state_value String value to set * @param is_sleep_safe The command is safe to send when the Nextion is sleeping */ void Nextion::add_no_result_to_queue_with_set(NextionComponentBase *component, const std::string &state_value) { diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2389cc6235..3a43a51975 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -419,7 +419,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * fill_area(50, 50, 100, 100, "RED"); * ``` * - * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to * convert color codes to Nextion HMI colors */ @@ -437,7 +437,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * fill_area(50, 50, 100, 100, color); * ``` * - * Fills an area that starts at x coordiante `50` and y coordinate `50` with a height of `100` and width of `100` with + * Fills an area that starts at x coordinate `50` and y coordinate `50` with a height of `100` and width of `100` with * the color of blue. Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to * convert color codes to Nextion HMI colors */ @@ -546,7 +546,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.filled_cricle(25, 25, 10, "17013"); * ``` * - * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to * Nextion HMI colors. */ @@ -563,7 +563,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe * it.filled_cricle(25, 25, 10, color); * ``` * - * Makes a filled circle at the x cordinates `25` and y coordinate `25` with a radius of `10` with a color of blue. + * Makes a filled circle at the x coordinate `25` and y coordinate `25` with a radius of `10` with a color of blue. * Use this [color picker](https://nodtem66.github.io/nextion-hmi-color-convert/index.html) to convert color codes to * Nextion HMI colors. */ diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index 6a681af6c5..1a6e6d1066 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -305,7 +305,7 @@ void Nextion::upload_tft() { App.feed_wdt(); ESP_LOGD(TAG, "Heap Size %d, Bytes left %d", ESP.getFreeHeap(), this->content_length_); } - ESP_LOGD(TAG, "Succesfully updated Nextion!"); + ESP_LOGD(TAG, "Successfully updated Nextion!"); this->upload_end_(); } diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 43e7939cf1..9e80cdbd88 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -116,7 +116,7 @@ void QMC5883LComponent::update() { bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { bool success = this->read_byte_16(a_register, data); - *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte oder, LSB first; + *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte order, LSB first; return success; } diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 789ab6197b..6261f63132 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -162,7 +162,7 @@ void RC522::loop() { ESP_LOGW(TAG, "CMD_REQA -> Not OK %d", status); state_ = STATE_DONE; } else if (back_length_ != 2) { // || *valid_bits_ != 0) { // ATQA must be exactly 16 bits. - ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpacted back_length_ of %d", back_length_); + ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpected back_length_ of %d", back_length_); state_ = STATE_DONE; } else { state_ = STATE_READ_SERIAL; @@ -470,7 +470,7 @@ RC522::StatusCode RC522::await_crc_() { return STATUS_WAITING; ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT"); - // 89ms passed and nothing happend. Communication with the MFRC522 might be down. + // 89ms passed and nothing happened. Communication with the MFRC522 might be down. return STATUS_TIMEOUT; } diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index 7fb49e97fd..6880651ac4 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -32,7 +32,7 @@ class RC522 : public PollingComponent { STATUS_OK, // Success STATUS_WAITING, // Waiting result from RC522 chip STATUS_ERROR, // Error in communication - STATUS_COLLISION, // Collission detected + STATUS_COLLISION, // Collision detected STATUS_TIMEOUT, // Timeout in communication. STATUS_NO_ROOM, // A buffer is not big enough. STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-) diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 7198f2d917..d36c0d7ebe 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -16,7 +16,7 @@ RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_b void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_7) { this->mem_block_num_ = int(RMT_CHANNEL_7) - int(this->channel_) + 1; - ESP_LOGW(TAG, "Not enough RMT memory blocks avaiable, reduced to %i blocks.", this->mem_block_num_); + ESP_LOGW(TAG, "Not enough RMT memory blocks available, reduced to %i blocks.", this->mem_block_num_); } rmt.channel = this->channel_; rmt.clk_div = this->clock_divider_; diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3eda98d41d..838c92f04e 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -55,7 +55,7 @@ void SCD30Component::setup() { // According ESP32 clock stretching is typically 30ms and up to 150ms "due to // internal calibration processes". The I2C peripheral only supports 13ms (at // least when running at 80MHz). - // In practise it seems that clock stretching occures during this calibration + // In practise it seems that clock stretching occurs during this calibration // calls. It also seems that delays in between calls makes them // disappear/shorter. Hence work around with delays for ESP32. // diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 43356c0036..9702878475 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -50,7 +50,7 @@ def assign_declare_id(value): CONFIG_SCHEMA = automation.validate_automation( { # Don't declare id as cv.declare_id yet, because the ID type - # dpeends on the mode. Will be checked later with assign_declare_id + # depends on the mode. Will be checked later with assign_declare_id cv.Required(CONF_ID): cv.string_strict, cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of( *SCRIPT_MODES, lower=True diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 64db6b80e7..5663d32ce8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -65,7 +65,7 @@ class QueueingScript : public Script, public Component { /** A script type that executes new instances in parallel. * * If a new instance is started while previous ones haven't finished yet, - * the new one is exeucted in parallel to the other instances. + * the new one is executed in parallel to the other instances. */ class ParallelScript : public Script { public: diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp index f25ef50075..7f761b4b55 100644 --- a/esphome/components/ssd1331_base/ssd1331_base.cpp +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -19,7 +19,7 @@ static const uint8_t SSD1331_DRAWLINE = 0x21; // Draw line static const uint8_t SSD1331_DRAWRECT = 0x22; // Draw rectangle static const uint8_t SSD1331_FILL = 0x26; // Fill enable/disable static const uint8_t SSD1331_SETCOLUMN = 0x15; // Set column address -static const uint8_t SSD1331_SETROW = 0x75; // Set row adress +static const uint8_t SSD1331_SETROW = 0x75; // Set row address static const uint8_t SSD1331_CONTRASTA = 0x81; // Set contrast for color A static const uint8_t SSD1331_CONTRASTB = 0x82; // Set contrast for color B static const uint8_t SSD1331_CONTRASTC = 0x83; // Set contrast for color C diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 0e17e65fd7..2aef043ba0 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -19,13 +19,13 @@ static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod -static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Result static const uint8_t ST7789_SLPIN = 0x10; // Sleep in static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out -static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O -static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode On +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode On static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off -static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_INVON = 0x21; // Display Inversion On static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set static const uint8_t ST7789_DISPOFF = 0x28; // Display Off static const uint8_t ST7789_DISPON = 0x29; // Display On @@ -34,18 +34,18 @@ static const uint8_t ST7789_RASET = 0x2B; // Row Address Set static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read static const uint8_t ST7789_PTLAR = 0x30; // Partial Area -static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio -static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definition +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line Off static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off -static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_IDMON = 0x39; // Idle Mode On static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline -static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanline static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display @@ -59,17 +59,17 @@ static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control -static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Control static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) -static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro -static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro -static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Control +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Control +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustment static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control -static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin -static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Setting +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enable static const uint8_t ST7789_VRHS = 0xC3; // VRH Set static const uint8_t ST7789_VDVS = 0xC4; // VDV Set static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set @@ -89,8 +89,8 @@ static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control -static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro -static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Control +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enable static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting static const uint8_t ST7789_PROMACT = 0xFE; // Program action diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h index d73f397f16..b97b85993f 100644 --- a/esphome/components/sx1509/sx1509_registers.h +++ b/esphome/components/sx1509/sx1509_registers.h @@ -9,7 +9,7 @@ Here you'll find the Arduino code used to interface with the SX1509 I2C 16 I/O expander. There are functions to take advantage of everything the SX1509 provides - input/output setting, writing pins high/low, reading the input value of pins, LED driver utilities (blink, breath, pwm), and -keypad engine utilites. +keypad engine utilities. Development environment specifics: IDE: Arduino 1.6.5 diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 916a550675..42ecd2477b 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -163,7 +163,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->gpio_reset_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { - // If mcu returned status gpio, then we can ommit sending wifi state + // If mcu returned status gpio, then we can omit sending wifi state if (this->gpio_status_ != -1) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); @@ -363,7 +363,7 @@ void Tuya::process_command_queue_() { this->expected_response_.reset(); } - // Left check of delay since last command in case theres ever a command sent by calling send_raw_command_ directly + // Left check of delay since last command in case there's ever a command sent by calling send_raw_command_ directly if (delay > COMMAND_DELAY && !this->command_queue_.empty() && this->rx_message_.empty() && !this->expected_response_.has_value()) { this->send_raw_command_(command_queue_.front()); diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt index fe33583414..f7a234d023 100644 --- a/esphome/components/vl53l0x/LICENSE.txt +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -3,7 +3,7 @@ by Pololu (Pololu Corporation), which in turn is based on the VL53L0X API from ST. The code has been adapted to work with ESPHome's i2c APIs. Please see the top-level LICENSE.txt for information about ESPHome's license. The licenses for Pololu's and ST's software are included below. -Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). +Originally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). ================================================================= diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 4518dd60df..458f04c674 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -598,7 +598,7 @@ void WaveshareEPaper2P9InB::initialize() { this->data(0x9F); // COMMAND RESOLUTION SETTING - // set to 128x296 by COMMAND PANNEL SETTING + // set to 128x296 by COMMAND PANEL SETTING // COMMAND VCOM AND DATA INTERVAL SETTING // use defaults for white border and ESPHome image polarity diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index c736a236a1..c4434801b4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -344,9 +344,9 @@ bool report_xiaomi_results(const optional &result, const std: bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() // and then again by the respective device class's parse_device() function. Parsing the header - // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // here and then for each device seems to be unnecessary and complicates the duplicate packet filtering. // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely - // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // in the respective device instance. The XiaomiListener class is defined in __init__.py and I was not // able to remove it entirely. return false; // with true it's not showing device scans diff --git a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp index 78464da6e3..36b4c8cc00 100644 --- a/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp +++ b/esphome/components/xiaomi_miscale/xiaomi_miscale.cpp @@ -51,7 +51,7 @@ optional XiaomiMiscale::parse_header(const esp32_ble_tracker::Servi } bool XiaomiMiscale::parse_message(const std::vector &message, ParseResult &result) { - // exemple 1d18 a2 6036 e307 07 11 0f1f11 + // example 1d18 a2 6036 e307 07 11 0f1f11 // 1-2 Weight (MISCALE 181D) // 3-4 Years (MISCALE 181D) // 5 month (MISCALE 181D) diff --git a/esphome/core/component.h b/esphome/core/component.h index 001620fe4a..433259f627 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -22,7 +22,7 @@ extern const float IO; extern const float HARDWARE; /// For components that import data from directly connected sensors like DHT. extern const float DATA; -/// Alias for DATA (here for compatability reasons) +/// Alias for DATA (here for compatibility reasons) extern const float HARDWARE_LATE; /// For components that use data from sensors like displays extern const float PROCESSOR; diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 89d621fd5a..1ab0ffa015 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -419,7 +419,7 @@ def get_jschema(path, vschema, create_return_ref=True): def get_schema_str(vschema): - # Hack on cs.use_id, in the future this can be improved by trackign which type is required by + # Hack on cs.use_id, in the future this can be improved by tracking which type is required by # the id, this information can be added somehow to schema (not supported by jsonschema) and # completion can be improved listing valid ids only Meanwhile it's a problem because it makes # all partial schemas with cv.use_id different, e.g. i2c @@ -675,7 +675,7 @@ def dump_schema(): # The root directory of the repo root = Path(__file__).parent.parent - # Fake some diretory so that get_component works + # Fake some directory so that get_component works CORE.config_path = str(root) file_path = args.output From 2a9e3d84fd83e28620d6a2ca67e108624d8c9bc6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 26 Jul 2021 15:19:50 +0200 Subject: [PATCH 608/643] Fix climate restore schema changed resulting in invalid restore (#2068) Co-authored-by: Stefan Agner --- esphome/components/climate/climate.cpp | 6 +++++- esphome/components/climate/climate.h | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 07347e4eee..8da2206f37 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -312,8 +312,12 @@ void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); } +// Random 32bit value; If this changes existing restore preferences are invalidated +static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; + optional Climate::restore_state_() { - this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_ = + global_preferences.make_preference(this->get_object_id_hash() ^ RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index ed5c5069b7..690e81c250 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -120,6 +120,7 @@ class ClimateCall { }; /// Struct used to save the state of the climate device in restore memory. +/// Make sure to update RESTORE_STATE_VERSION when changing the struct entries. struct ClimateDeviceRestoreState { ClimateMode mode; bool uses_custom_fan_mode{false}; From b0a38914987b4df5c45901c6b29c844046d96c8b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 26 Jul 2021 21:46:13 +0200 Subject: [PATCH 609/643] Fix MQTT climate custom fan modes without regular ones (#2071) --- esphome/components/mqtt/mqtt_climate.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index ab8354e66c..5809b6616c 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -72,7 +72,7 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC root["act_t"] = this->get_action_state_topic(); } - if (traits.get_supports_fan_modes()) { + if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { // fan_mode_command_topic root["fan_mode_cmd_t"] = this->get_fan_mode_command_topic(); // fan_mode_state_topic From 5abbe385c55740138b89c224dd0a2ebbc1f30e29 Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Tue, 27 Jul 2021 21:01:15 -0500 Subject: [PATCH 610/643] More Tuya MCU robustness (#2080) --- esphome/components/tuya/tuya.cpp | 11 +++++++++-- esphome/components/tuya/tuya.h | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 42ecd2477b..9e036feda9 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -7,7 +7,7 @@ namespace esphome { namespace tuya { static const char *const TAG = "tuya"; -static const int COMMAND_DELAY = 50; +static const int COMMAND_DELAY = 10; static const int RECEIVE_TIMEOUT = 300; void Tuya::setup() { @@ -114,6 +114,8 @@ void Tuya::handle_char_(uint8_t c) { this->rx_message_.push_back(c); if (!this->validate_message_()) { this->rx_message_.clear(); + } else { + this->last_rx_char_timestamp_ = millis(); } } @@ -357,7 +359,12 @@ void Tuya::send_raw_command_(TuyaCommand command) { } void Tuya::process_command_queue_() { - uint32_t delay = millis() - this->last_command_timestamp_; + uint32_t now = millis(); + uint32_t delay = now - this->last_command_timestamp_; + + if (now - this->last_rx_char_timestamp_ > RECEIVE_TIMEOUT) { + this->rx_message_.clear(); + } if (this->expected_response_.has_value() && delay > RECEIVE_TIMEOUT) { this->expected_response_.reset(); diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 68decf7e9e..7ce4be4315 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -107,6 +107,7 @@ class Tuya : public Component, public uart::UARTDevice { int gpio_status_ = -1; int gpio_reset_ = -1; uint32_t last_command_timestamp_ = 0; + uint32_t last_rx_char_timestamp_ = 0; std::string product_ = ""; std::vector listeners_; std::vector datapoints_; From b9259a0238b731f6e487a93f39bd7254dd6087ae Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 28 Jul 2021 15:18:31 +1200 Subject: [PATCH 611/643] Bump esphome dashboard to 20210728.0 (#2081) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ab54828194..561bf0f4d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ ifaddr==0.1.7 platformio==5.1.1 esptool==2.8 click==7.1.2 -esphome-dashboard==20210719.0 +esphome-dashboard==20210728.0 From f97cfe99167da2a37bd68237e97654ee2fffede9 Mon Sep 17 00:00:00 2001 From: Peter van Dijk Date: Wed, 28 Jul 2021 10:41:21 +0200 Subject: [PATCH 612/643] pm1006: add rx-only support (#2038) --- esphome/components/pm1006/__init__.py | 0 esphome/components/pm1006/pm1006.cpp | 96 +++++++++++++++++++++++++++ esphome/components/pm1006/pm1006.h | 35 ++++++++++ esphome/components/pm1006/sensor.py | 44 ++++++++++++ esphome/const.py | 1 + 5 files changed, 176 insertions(+) create mode 100644 esphome/components/pm1006/__init__.py create mode 100644 esphome/components/pm1006/pm1006.cpp create mode 100644 esphome/components/pm1006/pm1006.h create mode 100644 esphome/components/pm1006/sensor.py diff --git a/esphome/components/pm1006/__init__.py b/esphome/components/pm1006/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp new file mode 100644 index 0000000000..9bedb3cfc0 --- /dev/null +++ b/esphome/components/pm1006/pm1006.cpp @@ -0,0 +1,96 @@ +#include "pm1006.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pm1006 { + +static const char *const TAG = "pm1006"; + +static const uint8_t PM1006_RESPONSE_HEADER[] = {0x16, 0x11, 0x0B}; + +void PM1006Component::setup() { + // because this implementation is currently rx-only, there is nothing to setup +} + +void PM1006Component::dump_config() { + ESP_LOGCONFIG(TAG, "PM1006:"); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + this->check_uart_settings(9600); +} + +void PM1006Component::loop() { + while (this->available() != 0) { + this->read_byte(&this->data_[this->data_index_]); + auto check = this->check_byte_(); + if (!check.has_value()) { + // finished + this->parse_data_(); + this->data_index_ = 0; + } else if (!*check) { + // wrong data + ESP_LOGV(TAG, "Byte %i of received data frame is invalid.", this->data_index_); + this->data_index_ = 0; + } else { + // next byte + this->data_index_++; + } + } +} + +float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } + +uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { + uint8_t sum = 0; + for (uint8_t i = 0; i < length; i++) { + sum += command_data[i]; + } + return sum; +} + +optional PM1006Component::check_byte_() const { + uint8_t index = this->data_index_; + uint8_t byte = this->data_[index]; + + // index 0..2 are the fixed header + if (index < sizeof(PM1006_RESPONSE_HEADER)) { + return byte == PM1006_RESPONSE_HEADER[index]; + } + + // just some additional notes here: + // index 3..4 is unused + // index 5..6 is our PM2.5 reading (3..6 is called DF1-DF4 in the datasheet at + // http://www.jdscompany.co.kr/download.asp?gubun=07&filename=PM1006_LED_PARTICLE_SENSOR_MODULE_SPECIFICATIONS.pdf + // that datasheet goes on up to DF16, which is unused for PM1006 but used in PM1006K + // so this code should be trivially extensible to support that one later + if (index < (sizeof(PM1006_RESPONSE_HEADER) + 16)) + return true; + + // checksum + if (index == (sizeof(PM1006_RESPONSE_HEADER) + 16)) { + uint8_t checksum = pm1006_checksum_(this->data_, sizeof(PM1006_RESPONSE_HEADER) + 17); + if (checksum != 0) { + ESP_LOGW(TAG, "PM1006 checksum is wrong: %02x, expected zero", checksum); + return false; + } + return {}; + } + + return false; +} + +void PM1006Component::parse_data_() { + const int pm_2_5_concentration = this->get_16_bit_uint_(5); + + ESP_LOGD(TAG, "Got PM2.5 Concentration: %d µg/m³", pm_2_5_concentration); + + if (this->pm_2_5_sensor_ != nullptr) { + this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); + } +} + +uint16_t PM1006Component::get_16_bit_uint_(uint8_t start_index) const { + return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); +} + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h new file mode 100644 index 0000000000..66f4cf0311 --- /dev/null +++ b/esphome/components/pm1006/pm1006.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace pm1006 { + +class PM1006Component : public Component, public uart::UARTDevice { + public: + PM1006Component() = default; + + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { this->pm_2_5_sensor_ = pm_2_5_sensor; } + void setup() override; + void dump_config() override; + void loop() override; + + float get_setup_priority() const override; + + protected: + optional check_byte_() const; + void parse_data_(); + uint16_t get_16_bit_uint_(uint8_t start_index) const; + uint8_t pm1006_checksum_(const uint8_t *command_data, uint8_t length) const; + + sensor::Sensor *pm_2_5_sensor_{nullptr}; + + uint8_t data_[20]; + uint8_t data_index_{0}; + uint32_t last_transmission_{0}; +}; + +} // namespace pm1006 +} // namespace esphome diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py new file mode 100644 index 0000000000..18e1b0d87c --- /dev/null +++ b/esphome/components/pm1006/sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + CONF_PM_2_5, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_BLUR, +) + +DEPENDENCIES = ["uart"] + +pm1006_ns = cg.esphome_ns.namespace("pm1006") +PM1006Component = pm1006_ns.class_("PM1006Component", uart.UARTDevice, cg.Component) + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(PM1006Component), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_BLUR, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA), +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_PM_2_5 in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) diff --git a/esphome/const.py b/esphome/const.py index 3eb56c7cf6..b22bc5e1de 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -654,6 +654,7 @@ ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" ICON_BLUETOOTH = "mdi:bluetooth" +ICON_BLUR = "mdi:blur" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BUG = "mdi:bug" From 618cfd9ec524f8b5f15341de04afeb3585f67b44 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 28 Jul 2021 15:34:18 +0200 Subject: [PATCH 613/643] Add sensor monetary device_class (#2083) --- esphome/components/sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 0a0c3a9214..62ed07b408 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -44,6 +44,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_POWER, @@ -66,6 +67,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_MONETARY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TIMESTAMP, diff --git a/esphome/const.py b/esphome/const.py index b22bc5e1de..6702e773c6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -777,6 +777,7 @@ DEVICE_CLASS_CURRENT = "current" DEVICE_CLASS_ENERGY = "energy" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_ILLUMINANCE = "illuminance" +DEVICE_CLASS_MONETARY = "monetary" DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" DEVICE_CLASS_TEMPERATURE = "temperature" DEVICE_CLASS_POWER_FACTOR = "power_factor" From 1652914d39f34fb31fe96e5f6d02db9bd1a51479 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Wed, 28 Jul 2021 19:49:43 +0200 Subject: [PATCH 614/643] Make light.addressable_set color parameters behave as documented & consistent with elsewhere (#2009) --- .../components/light/addressable_light.cpp | 12 +++---- esphome/components/light/addressable_light.h | 4 +-- .../light/addressable_light_effect.h | 2 +- esphome/components/light/automation.cpp | 15 ++++++++ esphome/components/light/automation.h | 36 ++++++++++++------- esphome/components/light/automation.py | 23 +++++------- .../components/light/esp_color_correction.cpp | 5 +-- esphome/components/light/light_color_values.h | 2 ++ 8 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 esphome/components/light/automation.cpp diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 4ef293cd03..4eb9124a7b 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -25,16 +25,16 @@ void AddressableLight::call_setup() { } Color esp_color_from_light_color_values(LightColorValues val) { - auto r = static_cast(roundf(val.get_color_brightness() * val.get_red() * 255.0f)); - auto g = static_cast(roundf(val.get_color_brightness() * val.get_green() * 255.0f)); - auto b = static_cast(roundf(val.get_color_brightness() * val.get_blue() * 255.0f)); - auto w = static_cast(roundf(val.get_white() * 255.0f)); + auto r = to_uint8_scale(val.get_color_brightness() * val.get_red()); + auto g = to_uint8_scale(val.get_color_brightness() * val.get_green()); + auto b = to_uint8_scale(val.get_color_brightness() * val.get_blue()); + auto w = to_uint8_scale(val.get_white()); return Color(r, g, b, w); } void AddressableLight::write_state(LightState *state) { auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); + auto max_brightness = to_uint8_scale(val.get_brightness() * val.get_state()); this->correction_.set_local_brightness(max_brightness); this->last_transition_progress_ = 0.0f; @@ -68,7 +68,7 @@ void AddressableLight::write_state(LightState *state) { // our transition will handle brightness, disable brightness in correction. this->correction_.set_local_brightness(255); - target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); + target_color *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); float denom = (1.0f - new_smoothed); float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index 460cb88935..f64abb3eec 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -54,8 +54,8 @@ class AddressableLight : public LightOutput, public Component { void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } void write_state(LightState *state) override; void set_correction(float red, float green, float blue, float white = 1.0f) { - this->correction_.set_max_brightness(Color(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), - uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); + this->correction_.set_max_brightness( + Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { this->correction_.calculate_gamma_table(state->get_gamma_correct()); diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index d1ea9e3ff0..25493a30ea 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -337,7 +337,7 @@ class AddressableFlickerEffect : public AddressableLightEffect { } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } - void set_intensity(float intensity) { this->intensity_ = static_cast(roundf(intensity * 255.0f)); } + void set_intensity(float intensity) { this->intensity_ = to_uint8_scale(intensity); } protected: uint32_t update_interval_{16}; diff --git a/esphome/components/light/automation.cpp b/esphome/components/light/automation.cpp new file mode 100644 index 0000000000..8c1785f061 --- /dev/null +++ b/esphome/components/light/automation.cpp @@ -0,0 +1,15 @@ +#include "automation.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +static const char *const TAG = "light.automation"; + +void addressableset_warn_about_scale(const char *field) { + ESP_LOGW(TAG, "Lambda for parameter %s of light.addressable_set should return values in range 0-1 instead of 0-255.", + field); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index e99d5c58bd..02c6163555 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -135,39 +135,51 @@ class LightTurnOffTrigger : public Trigger<> { } }; +// This is slightly ugly, but we can't log in headers, and can't make this a static method on AddressableSet +// due to the template. It's just a temporary warning anyway. +void addressableset_warn_about_scale(const char *field); + template class AddressableSet : public Action { public: explicit AddressableSet(LightState *parent) : parent_(parent) {} TEMPLATABLE_VALUE(int32_t, range_from) TEMPLATABLE_VALUE(int32_t, range_to) - TEMPLATABLE_VALUE(uint8_t, color_brightness) - TEMPLATABLE_VALUE(uint8_t, red) - TEMPLATABLE_VALUE(uint8_t, green) - TEMPLATABLE_VALUE(uint8_t, blue) - TEMPLATABLE_VALUE(uint8_t, white) + TEMPLATABLE_VALUE(float, color_brightness) + TEMPLATABLE_VALUE(float, red) + TEMPLATABLE_VALUE(float, green) + TEMPLATABLE_VALUE(float, blue) + TEMPLATABLE_VALUE(float, white) void play(Ts... x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = this->range_from_.value_or(x..., 0); int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; - uint8_t remote_color_brightness = - static_cast(roundf(this->parent_->remote_values.get_color_brightness() * 255.0f)); - uint8_t color_brightness = this->color_brightness_.value_or(x..., remote_color_brightness); + uint8_t color_brightness = + to_uint8_scale(this->color_brightness_.value_or(x..., this->parent_->remote_values.get_color_brightness())); auto range = out->range(range_from, range_to); if (this->red_.has_value()) - range.set_red(esp_scale8(this->red_.value(x...), color_brightness)); + range.set_red(esp_scale8(to_uint8_compat(this->red_.value(x...), "red"), color_brightness)); if (this->green_.has_value()) - range.set_green(esp_scale8(this->green_.value(x...), color_brightness)); + range.set_green(esp_scale8(to_uint8_compat(this->green_.value(x...), "green"), color_brightness)); if (this->blue_.has_value()) - range.set_blue(esp_scale8(this->blue_.value(x...), color_brightness)); + range.set_blue(esp_scale8(to_uint8_compat(this->blue_.value(x...), "blue"), color_brightness)); if (this->white_.has_value()) - range.set_white(this->white_.value(x...)); + range.set_white(to_uint8_compat(this->white_.value(x...), "white")); out->schedule_show(); } protected: LightState *parent_; + + // Historically, this action required uint8_t (0-255) for RGBW values from lambdas. Keep compatibility. + static inline uint8_t to_uint8_compat(float value, const char *field) { + if (value > 1.0f) { + addressableset_warn_about_scale(field); + return static_cast(value); + } + return to_uint8_scale(value); + } }; } // namespace light diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index dd1148131a..85741f1ffd 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -173,6 +173,7 @@ LIGHT_ADDRESSABLE_SET_ACTION_SCHEMA = cv.Schema( cv.Required(CONF_ID): cv.use_id(AddressableLightState), cv.Optional(CONF_RANGE_FROM): cv.templatable(cv.positive_int), cv.Optional(CONF_RANGE_TO): cv.templatable(cv.positive_int), + cv.Optional(CONF_COLOR_BRIGHTNESS): cv.templatable(cv.percentage), cv.Optional(CONF_RED): cv.templatable(cv.percentage), cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), @@ -194,28 +195,20 @@ async def light_addressable_set_to_code(config, action_id, template_arg, args): templ = await cg.templatable(config[CONF_RANGE_TO], args, cg.int32) cg.add(var.set_range_to(templ)) - def rgbw_to_exp(x): - return int(round(x * 255)) - + if CONF_COLOR_BRIGHTNESS in config: + templ = await cg.templatable(config[CONF_COLOR_BRIGHTNESS], args, cg.float_) + cg.add(var.set_color_brightness(templ)) if CONF_RED in config: - templ = await cg.templatable( - config[CONF_RED], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_RED], args, cg.float_) cg.add(var.set_red(templ)) if CONF_GREEN in config: - templ = await cg.templatable( - config[CONF_GREEN], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_GREEN], args, cg.float_) cg.add(var.set_green(templ)) if CONF_BLUE in config: - templ = await cg.templatable( - config[CONF_BLUE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_BLUE], args, cg.float_) cg.add(var.set_blue(templ)) if CONF_WHITE in config: - templ = await cg.templatable( - config[CONF_WHITE], args, cg.uint8, to_exp=rgbw_to_exp - ) + templ = await cg.templatable(config[CONF_WHITE], args, cg.float_) cg.add(var.set_white(templ)) return var diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 19a2af3da1..e5e68264cc 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -1,4 +1,5 @@ #include "esp_color_correction.h" +#include "light_color_values.h" #include "esphome/core/log.h" namespace esphome { @@ -7,7 +8,7 @@ namespace light { void ESPColorCorrection::calculate_gamma_table(float gamma) { for (uint16_t i = 0; i < 256; i++) { // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); this->gamma_table_[i] = corrected; } if (gamma == 0.0f) { @@ -17,7 +18,7 @@ void ESPColorCorrection::calculate_gamma_table(float gamma) { } for (uint16_t i = 0; i < 256; i++) { // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); this->gamma_reverse_table_[i] = uncorrected; } } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 54dcaea5a3..344411b75e 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -11,6 +11,8 @@ namespace esphome { namespace light { +inline static uint8_t to_uint8_scale(float x) { return static_cast(roundf(x * 255.0f)); } + /** This class represents the color state for a light object. * * All values in this class (except color temperature) are represented using floats in the range From 5c3a6164bb39a0c83a04aca09976e7f11ffdf848 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Jul 2021 21:23:24 +0200 Subject: [PATCH 615/643] Bump pylint from 2.9.5 to 2.9.6 (#2087) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index b0e24b5b2f..684582bd4c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.9.5 +pylint==2.9.6 flake8==3.9.2 black==21.7b0 pexpect==4.8.0 From 31d6a54b06c6f10c06b87fb4ef66b01c58ab0e87 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 28 Jul 2021 21:23:41 +0200 Subject: [PATCH 616/643] Fix PID climate breaks when restoring old modes (#2086) --- esphome/components/pid/pid_climate.cpp | 13 ++----------- esphome/components/pid/pid_climate.h | 1 - 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index ef8a4df962..4c7d92e26d 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -37,7 +37,7 @@ void PIDClimate::control(const climate::ClimateCall &call) { // If switching to off mode, set output immediately if (this->mode == climate::CLIMATE_MODE_OFF) - this->handle_non_auto_mode_(); + this->write_output_(0.0f); this->publish_state(); } @@ -98,15 +98,6 @@ void PIDClimate::write_output_(float value) { } this->pid_computed_callback_.call(); } -void PIDClimate::handle_non_auto_mode_() { - // in non-auto mode, switch directly to appropriate action - // - OFF mode -> Output at 0% - if (this->mode == climate::CLIMATE_MODE_OFF) { - this->write_output_(0.0); - } else { - assert(false); - } -} void PIDClimate::update_pid_() { float value; if (isnan(this->current_temperature) || isnan(this->target_temperature)) { @@ -135,7 +126,7 @@ void PIDClimate::update_pid_() { } if (this->mode == climate::CLIMATE_MODE_OFF) { - this->handle_non_auto_mode_(); + this->write_output_(0.0); } else { this->write_output_(value); } diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index f11d768867..ff301386b6 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -56,7 +56,6 @@ class PIDClimate : public climate::Climate, public Component { bool supports_heat_() const { return this->heat_output_ != nullptr; } void write_output_(float value); - void handle_non_auto_mode_(); /// The sensor used for getting the current temperature sensor::Sensor *sensor_; From 246950159d3e5782730a328ac28b77aaa1e9ccdb Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 28 Jul 2021 21:24:10 +0200 Subject: [PATCH 617/643] Bump ESPAsyncWebServer-esphome to 1.3.0 (#2075) --- esphome/components/web_server_base/__init__.py | 4 ++-- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 37f3c989e4..8f64c473f3 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -25,5 +25,5 @@ async def to_code(config): if CORE.is_esp32: cg.add_library("FS", None) - # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library("ESPAsyncWebServer-esphome", "1.2.7") + # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json + cg.add_library("esphome/ESPAsyncWebServer-esphome", "1.3.0") diff --git a/platformio.ini b/platformio.ini index ba7724ad24..e1fb845358 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,7 +12,7 @@ include_dir = include lib_deps = AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 - ESPAsyncWebServer-esphome@1.2.7 + esphome/ESPAsyncWebServer-esphome@1.3.0 FastLED@3.3.2 NeoPixelBus-esphome@2.6.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) From 316777f757b087553dc3595ed726824c9a0a6255 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 29 Jul 2021 07:53:54 +1200 Subject: [PATCH 618/643] HLW8012 - Dump energy sensor config (#2082) --- esphome/components/hlw8012/hlw8012.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index 79c25a45b0..ecdaa07ab2 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -45,6 +45,7 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_) LOG_SENSOR(" ", "Current", this->current_sensor_) LOG_SENSOR(" ", "Power", this->power_sensor_) + LOG_SENSOR(" ", "Energy", this->energy_sensor_) } float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { From 513066ba520ac698b7a88e0cefccc93950cc6fdf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 29 Jul 2021 19:46:15 +1200 Subject: [PATCH 619/643] Use sensor_schema for total_daily_energy (#2090) Co-authored-by: Otto Winter --- .../components/total_daily_energy/sensor.py | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index b70a0cece9..7fdd176d42 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -1,7 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, time -from esphome.const import CONF_ID, CONF_TIME_ID +from esphome.const import ( + CONF_ID, + CONF_TIME_ID, + DEVICE_CLASS_ENERGY, + ICON_EMPTY, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_EMPTY, +) DEPENDENCIES = ["time"] @@ -11,13 +19,24 @@ TotalDailyEnergy = total_daily_energy_ns.class_( "TotalDailyEnergy", sensor.Sensor, cg.Component ) -CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(TotalDailyEnergy), - cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = ( + sensor.sensor_schema( + UNIT_EMPTY, + ICON_EMPTY, + 0, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(TotalDailyEnergy), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_POWER_ID): cv.use_id(sensor.Sensor), + } + ) + .extend(cv.COMPONENT_SCHEMA) +) async def to_code(config): From 8dbac20f8b50e0cf81026739efb14cf570cf68d3 Mon Sep 17 00:00:00 2001 From: Nicholas Peters Date: Thu, 29 Jul 2021 04:57:52 -0400 Subject: [PATCH 620/643] Add SDP3x sensor (#2064) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Otto Winter --- CODEOWNERS | 2 + esphome/components/sdp3x/__init__.py | 0 esphome/components/sdp3x/sdp3x.cpp | 124 +++++++++++++++++++++++++++ esphome/components/sdp3x/sdp3x.h | 30 +++++++ esphome/components/sdp3x/sensor.py | 40 +++++++++ esphome/components/tmp117/sensor.py | 1 + tests/test1.yaml | 5 ++ 7 files changed, 202 insertions(+) create mode 100644 esphome/components/sdp3x/__init__.py create mode 100644 esphome/components/sdp3x/sdp3x.cpp create mode 100644 esphome/components/sdp3x/sdp3x.h create mode 100644 esphome/components/sdp3x/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 844bea763e..21f464863d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -97,6 +97,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces +esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw @@ -128,6 +129,7 @@ esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage +esphome/components/tmp117/* @Azimath esphome/components/tof10120/* @wstrzalka esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz diff --git a/esphome/components/sdp3x/__init__.py b/esphome/components/sdp3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp new file mode 100644 index 0000000000..5e6c5887ac --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -0,0 +1,124 @@ +#include "sdp3x.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace sdp3x { + +static const char *const TAG = "sdp3x.sensor"; +static const uint8_t SDP3X_SOFT_RESET[2] = {0x00, 0x06}; +static const uint8_t SDP3X_READ_ID1[2] = {0x36, 0x7C}; +static const uint8_t SDP3X_READ_ID2[2] = {0xE1, 0x02}; +static const uint8_t SDP3X_START_DP_AVG[2] = {0x36, 0x15}; +static const uint8_t SDP3X_STOP_MEAS[2] = {0x3F, 0xF9}; + +void SDP3XComponent::update() { this->read_pressure_(); } + +void SDP3XComponent::setup() { + ESP_LOGD(TAG, "Setting up SDP3X..."); + + if (!this->write_bytes_raw(SDP3X_STOP_MEAS, 2)) { + ESP_LOGW(TAG, "Stop SDP3X failed!"); // This sometimes fails for no good reason + } + + if (!this->write_bytes_raw(SDP3X_SOFT_RESET, 2)) { + ESP_LOGW(TAG, "Soft Reset SDP3X failed!"); // This sometimes fails for no good reason + } + + delay_microseconds_accurate(20000); + + if (!this->write_bytes_raw(SDP3X_READ_ID1, 2)) { + ESP_LOGE(TAG, "Read ID1 SDP3X failed!"); + this->mark_failed(); + return; + } + if (!this->write_bytes_raw(SDP3X_READ_ID2, 2)) { + ESP_LOGE(TAG, "Read ID2 SDP3X failed!"); + this->mark_failed(); + return; + } + + uint8_t data[18]; + if (!this->read_bytes_raw(data, 18)) { + ESP_LOGE(TAG, "Read ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]))) { + ESP_LOGE(TAG, "CRC ID SDP3X failed!"); + this->mark_failed(); + return; + } + + if (data[3] == 0x01) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP31"); + pressure_scale_factor_ = 60.0f * 100.0f; // Scale factors converted to hPa per count + } else if (data[3] == 0x02) { + ESP_LOGCONFIG(TAG, "SDP3X is SDP32"); + pressure_scale_factor_ = 240.0f * 100.0f; + } + + if (!this->write_bytes_raw(SDP3X_START_DP_AVG, 2)) { + ESP_LOGE(TAG, "Start Measurements SDP3X failed!"); + this->mark_failed(); + return; + } + ESP_LOGCONFIG(TAG, "SDP3X started!"); +} +void SDP3XComponent::dump_config() { + LOG_SENSOR(" ", "SDP3X", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, " Connection with SDP3X failed!"); + } + LOG_UPDATE_INTERVAL(this); +} + +void SDP3XComponent::read_pressure_() { + uint8_t data[9]; + if (!this->read_bytes_raw(data, 9)) { + ESP_LOGW(TAG, "Couldn't read SDP3X data!"); + this->status_set_warning(); + return; + } + + if (!(check_crc_(&data[0], 2, data[2]) && check_crc_(&data[3], 2, data[5]) && check_crc_(&data[6], 2, data[8]))) { + ESP_LOGW(TAG, "Invalid SDP3X data!"); + this->status_set_warning(); + return; + } + + int16_t pressure_raw = encode_uint16(data[0], data[1]); + float pressure = pressure_raw / pressure_scale_factor_; + ESP_LOGV(TAG, "Got raw pressure=%d, scale factor =%.3f ", pressure_raw, pressure_scale_factor_); + ESP_LOGD(TAG, "Got Pressure=%.3f hPa", pressure); + + this->publish_state(pressure); + this->status_clear_warning(); +} + +float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } + +// Check CRC function from SDP3X sample code provided by sensirion +// Returns true if a checksum is OK +bool SDP3XComponent::check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum) { + uint8_t crc = 0xFF; + + // calculates 8-Bit checksum with given polynomial 0x31 (x^8 + x^5 + x^4 + 1) + for (int i = 0; i < size; i++) { + crc ^= (data[i]); + for (uint8_t bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x31; + else + crc = (crc << 1); + } + } + + // verify checksum + return (crc == checksum); +} + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h new file mode 100644 index 0000000000..51c9973c61 --- /dev/null +++ b/esphome/components/sdp3x/sdp3x.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sdp3x { + +class SDP3XComponent : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Schedule temperature+pressure readings. + void update() override; + /// Setup the sensor and test for a connection. + void setup() override; + void dump_config() override; + + float get_setup_priority() const override; + + protected: + /// Internal method to read the pressure from the component after it has been scheduled. + void read_pressure_(); + + bool check_crc_(const uint8_t data[], uint8_t size, uint8_t checksum); + + float pressure_scale_factor_ = 0.0f; // hPa per count +}; + +} // namespace sdp3x +} // namespace esphome diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py new file mode 100644 index 0000000000..68e3d2812c --- /dev/null +++ b/esphome/components/sdp3x/sensor.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_PRESSURE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_HECTOPASCAL, +) + +DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] + +sdp3x_ns = cg.esphome_ns.namespace("sdp3x") +SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + UNIT_HECTOPASCAL, + ICON_EMPTY, + 3, + DEVICE_CLASS_PRESSURE, + STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(SDP3XComponent), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x21)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + await sensor.register_sensor(var, config) diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index a5fc027b20..c3fed06953 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -11,6 +11,7 @@ from esphome.const import ( ) DEPENDENCIES = ["i2c"] +CODEOWNERS = ["@Azimath"] tmp117_ns = cg.esphome_ns.namespace("tmp117") TMP117Component = tmp117_ns.class_( diff --git a/tests/test1.yaml b/tests/test1.yaml index f9fe7d106d..b25852c93d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -922,6 +922,11 @@ sensor: id: ph_ezo address: 99 unit_of_measurement: 'pH' + - platform: sdp3x + name: "HVAC Filter Pressure drop" + id: filter_pressure + update_interval: 5s + accuracy_decimals: 3 - platform: cs5460a id: cs5460a1 current: From c6c857dfff1b52b7925ba6a87f272a7890ae6dfb Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Thu, 29 Jul 2021 11:16:04 +0200 Subject: [PATCH 621/643] Add support for the TLC5947 24-Channel, 12-Bit PWM LED Driver (#2066) Co-authored-by: Richard Nauber --- CODEOWNERS | 1 + esphome/components/tlc5947/__init__.py | 50 +++++++++++++++++++ esphome/components/tlc5947/output.py | 28 +++++++++++ esphome/components/tlc5947/tlc5947.cpp | 65 ++++++++++++++++++++++++ esphome/components/tlc5947/tlc5947.h | 69 ++++++++++++++++++++++++++ tests/test5.yaml | 10 ++++ 6 files changed, 223 insertions(+) create mode 100644 esphome/components/tlc5947/__init__.py create mode 100644 esphome/components/tlc5947/output.py create mode 100644 esphome/components/tlc5947/tlc5947.cpp create mode 100644 esphome/components/tlc5947/tlc5947.h diff --git a/CODEOWNERS b/CODEOWNERS index 21f464863d..40883d39b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -127,6 +127,7 @@ esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter +esphome/components/tlc5947/* @rnauber esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage esphome/components/tmp117/* @Azimath diff --git a/esphome/components/tlc5947/__init__.py b/esphome/components/tlc5947/__init__.py new file mode 100644 index 0000000000..84380bdace --- /dev/null +++ b/esphome/components/tlc5947/__init__.py @@ -0,0 +1,50 @@ +# this component is for the "TLC5947 24-Channel, 12-Bit PWM LED Driver" [https://www.ti.com/lit/ds/symlink/tlc5947.pdf], +# which is used e.g. on [https://www.adafruit.com/product/1429]. The code is based on the components sm2135 and sm26716. + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import ( + CONF_CLOCK_PIN, + CONF_DATA_PIN, + CONF_ID, + CONF_NUM_CHIPS, +) + +CONF_LAT_PIN = "lat_pin" +CONF_OE_PIN = "oe_pin" + +AUTO_LOAD = ["output"] +CODEOWNERS = ["@rnauber"] + +tlc5947_ns = cg.esphome_ns.namespace("tlc5947") +TLC5947 = tlc5947_ns.class_("TLC5947", cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TLC5947), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LAT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + data = await cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + lat = await cg.gpio_pin_expression(config[CONF_LAT_PIN]) + cg.add(var.set_lat_pin(lat)) + if CONF_OE_PIN in config: + outenable = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_outenable_pin(outenable)) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/tlc5947/output.py b/esphome/components/tlc5947/output.py new file mode 100644 index 0000000000..ece47fa63d --- /dev/null +++ b/esphome/components/tlc5947/output.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import TLC5947 + +DEPENDENCIES = ["tlc5947"] +CODEOWNERS = ["@rnauber"] + +Channel = TLC5947.class_("Channel", output.FloatOutput) + +CONF_TLC5947_ID = "tlc5947_id" +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.GenerateID(CONF_TLC5947_ID): cv.use_id(TLC5947), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await output.register_output(var, config) + + parent = await cg.get_variable(config[CONF_TLC5947_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/tlc5947/tlc5947.cpp b/esphome/components/tlc5947/tlc5947.cpp new file mode 100644 index 0000000000..a7e08c8341 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.cpp @@ -0,0 +1,65 @@ +#include "tlc5947.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tlc5947 { + +static const char *const TAG = "tlc5947"; + +void TLC5947::setup() { + this->data_pin_->setup(); + this->data_pin_->digital_write(true); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(true); + this->lat_pin_->setup(); + this->lat_pin_->digital_write(true); + if (this->outenable_pin_ != nullptr) { + this->outenable_pin_->setup(); + this->outenable_pin_->digital_write(false); + } + + this->pwm_amounts_.resize(this->num_chips_ * N_CHANNELS_PER_CHIP, 0); + + ESP_LOGCONFIG(TAG, "Done setting up TLC5947 output component."); +} +void TLC5947::dump_config() { + ESP_LOGCONFIG(TAG, "TLC5947:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + LOG_PIN(" LAT Pin: ", this->lat_pin_); + if (this->outenable_pin_ != nullptr) + LOG_PIN(" OE Pin: ", this->outenable_pin_); + ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); +} + +void TLC5947::loop() { + if (!this->update_) + return; + + this->lat_pin_->digital_write(false); + + // push the data out, MSB first, 12 bit word per channel, 24 channels per chip + for (int32_t ch = N_CHANNELS_PER_CHIP * num_chips_ - 1; ch >= 0; ch--) { + uint16_t word = pwm_amounts_[ch]; + for (uint8_t bit = 0; bit < 12; bit++) { + this->clock_pin_->digital_write(false); + this->data_pin_->digital_write(word & 0x800); + word <<= 1; + + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(true); // TWH0>12ns, so we should be fine using this as delay + } + } + + this->clock_pin_->digital_write(false); + + // latch the values, so they will be applied + this->lat_pin_->digital_write(true); + delayMicroseconds(1); // TWH1 > 30ns + this->lat_pin_->digital_write(false); + + this->update_ = false; +} + +} // namespace tlc5947 +} // namespace esphome diff --git a/esphome/components/tlc5947/tlc5947.h b/esphome/components/tlc5947/tlc5947.h new file mode 100644 index 0000000000..b608b861e7 --- /dev/null +++ b/esphome/components/tlc5947/tlc5947.h @@ -0,0 +1,69 @@ +#pragma once +// TLC5947 24-Channel, 12-Bit PWM LED Driver +// https://www.ti.com/lit/ds/symlink/tlc5947.pdf + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace tlc5947 { + +class TLC5947 : public Component { + public: + class Channel; + + const uint8_t N_CHANNELS_PER_CHIP = 24; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_lat_pin(GPIOPin *lat_pin) { lat_pin_ = lat_pin; } + void set_outenable_pin(GPIOPin *outenable_pin) { outenable_pin_ = outenable_pin; } + void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(TLC5947 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = static_cast(state * 0xfff); + this->parent_->set_channel_value_(this->channel_, amount); + } + + TLC5947 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint16_t channel, uint16_t value) { + if (channel >= this->num_chips_ * N_CHANNELS_PER_CHIP) + return; + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + } + this->pwm_amounts_[channel] = value; + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *lat_pin_; + GPIOPin *outenable_pin_{nullptr}; + uint8_t num_chips_; + + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace tlc5947 +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index 5d1c99d777..e117b3244f 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -30,11 +30,21 @@ binary_sensor: pin: GPIO0 id: io0_button +tlc5947: + data_pin: GPIO12 + clock_pin: GPIO14 + lat_pin: GPIO15 + output: - platform: gpio pin: GPIO2 id: built_in_led + - platform: tlc5947 + id: output_red + channel: 0 + max_power: 0.8 + esp32_ble: esp32_ble_server: From 5e2d4e332aaa40559e1120577cdec1c3fb990e0b Mon Sep 17 00:00:00 2001 From: Tyler Menezes Date: Thu, 29 Jul 2021 05:24:36 -0400 Subject: [PATCH 622/643] Add T6615 (#1170) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/t6615/__init__.py | 0 esphome/components/t6615/sensor.py | 48 +++++++++++++++++ esphome/components/t6615/t6615.cpp | 81 ++++++++++++++++++++++++++++ esphome/components/t6615/t6615.h | 42 +++++++++++++++ tests/test5.yaml | 17 ++++-- 6 files changed, 186 insertions(+), 3 deletions(-) create mode 100644 esphome/components/t6615/__init__.py create mode 100644 esphome/components/t6615/sensor.py create mode 100644 esphome/components/t6615/t6615.cpp create mode 100644 esphome/components/t6615/t6615.h diff --git a/CODEOWNERS b/CODEOWNERS index 40883d39b0..b3a4668f3a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -122,6 +122,7 @@ esphome/components/st7789v/* @kbx81 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/t6615/* @tylermenezes esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax diff --git a/esphome/components/t6615/__init__.py b/esphome/components/t6615/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py new file mode 100644 index 0000000000..efe4994a97 --- /dev/null +++ b/esphome/components/t6615/sensor.py @@ -0,0 +1,48 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_CO2, + CONF_ID, + DEVICE_CLASS_CARBON_DIOXIDE, + ICON_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_PARTS_PER_MILLION, +) + +CODEOWNERS = ["@tylermenezes"] +DEPENDENCIES = ["uart"] + +t6615_ns = cg.esphome_ns.namespace("t6615") +T6615Component = t6615_ns.class_("T6615Component", cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(T6615Component), + cv.Required(CONF_CO2): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, + ICON_EMPTY, + 0, + DEVICE_CLASS_CARBON_DIOXIDE, + STATE_CLASS_MEASUREMENT, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "t6615", baud_rate=19200, require_rx=True, require_tx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = await sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp new file mode 100644 index 0000000000..09ff61827c --- /dev/null +++ b/esphome/components/t6615/t6615.cpp @@ -0,0 +1,81 @@ +#include "t6615.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace t6615 { + +static const char *const TAG = "t6615"; + +static const uint8_t T6615_RESPONSE_BUFFER_LENGTH = 32; +static const uint8_t T6615_MAGIC = 0xFF; +static const uint8_t T6615_ADDR_HOST = 0xFA; +static const uint8_t T6615_ADDR_SENSOR = 0xFE; +static const uint8_t T6615_COMMAND_GET_PPM[] = {0x02, 0x03}; +static const uint8_t T6615_COMMAND_GET_SERIAL[] = {0x02, 0x01}; +static const uint8_t T6615_COMMAND_GET_VERSION[] = {0x02, 0x0D}; +static const uint8_t T6615_COMMAND_GET_ELEVATION[] = {0x02, 0x0F}; +static const uint8_t T6615_COMMAND_GET_ABC[] = {0xB7, 0x00}; +static const uint8_t T6615_COMMAND_ENABLE_ABC[] = {0xB7, 0x01}; +static const uint8_t T6615_COMMAND_DISABLE_ABC[] = {0xB7, 0x02}; +static const uint8_t T6615_COMMAND_SET_ELEVATION[] = {0x03, 0x0F}; + +void T6615Component::loop() { + if (!this->available()) + return; + + // Read header + uint8_t header[3]; + this->read_array(header, 3); + if (header[0] != T6615_MAGIC || header[1] != T6615_ADDR_HOST) { + ESP_LOGW(TAG, "Reading data from T6615 failed!"); + while (this->available()) + this->read(); // Clear the incoming buffer + this->status_set_warning(); + return; + } + + // Read body + uint8_t length = header[2]; + uint8_t response[T6615_RESPONSE_BUFFER_LENGTH]; + this->read_array(response, length); + + this->status_clear_warning(); + + switch (this->command_) { + case T6615Command::GET_PPM: { + const uint16_t ppm = encode_uint16(response[0], response[1]); + ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm); + this->co2_sensor_->publish_state(ppm); + break; + } + default: + break; + } + + this->command_ = T6615Command::NONE; +} + +void T6615Component::update() { this->query_ppm_(); } + +void T6615Component::query_ppm_() { + if (this->co2_sensor_ == nullptr || this->command_ != T6615Command::NONE) { + return; + } + + this->command_ = T6615Command::GET_PPM; + + this->write_byte(T6615_MAGIC); + this->write_byte(T6615_ADDR_SENSOR); + this->write_byte(sizeof(T6615_COMMAND_GET_PPM)); + this->write_array(T6615_COMMAND_GET_PPM, sizeof(T6615_COMMAND_GET_PPM)); +} + +float T6615Component::get_setup_priority() const { return setup_priority::DATA; } +void T6615Component::dump_config() { + ESP_LOGCONFIG(TAG, "T6615:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(19200); +} + +} // namespace t6615 +} // namespace esphome diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h new file mode 100644 index 0000000000..a7da3b4cf6 --- /dev/null +++ b/esphome/components/t6615/t6615.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace t6615 { + +enum class T6615Command : uint8_t { + NONE = 0, + GET_PPM, + GET_SERIAL, + GET_VERSION, + GET_ELEVATION, + GET_ABC, + ENABLE_ABC, + DISABLE_ABC, + SET_ELEVATION, +}; + +class T6615Component : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override; + + void loop() override; + void update() override; + void dump_config() override; + + void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; } + + protected: + void query_ppm_(); + + T6615Command command_ = T6615Command::NONE; + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace t6615 +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index e117b3244f..23f8ae6c4d 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -19,11 +19,18 @@ ota: logger: uart: - tx_pin: 1 - rx_pin: 3 - baud_rate: 9600 + - id: uart1 + tx_pin: 1 + rx_pin: 3 + baud_rate: 9600 + - id: uart2 + tx_pin: 17 + rx_pin: 16 + baud_rate: 19200 + modbus: + uart_id: uart1 binary_sensor: - platform: gpio @@ -109,3 +116,7 @@ sensor: name: "SelecEM2M Maximum Demand Reactive Power" maximum_demand_apparent_power: name: "SelecEM2M Maximum Demand Apparent Power" + - platform: t6615 + uart_id: uart2 + co2: + name: CO2 Sensor From ee19ef1aaca41cd4d8434a3b75b62751a9aab280 Mon Sep 17 00:00:00 2001 From: Mike Meessen Date: Thu, 29 Jul 2021 11:37:31 +0200 Subject: [PATCH 623/643] Add support for the HRXL MaxSonar WR series sensors (#2020) --- CODEOWNERS | 1 + .../components/hrxl_maxsonar_wr/__init__.py | 0 .../hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp | 66 +++++++++++++++++++ .../hrxl_maxsonar_wr/hrxl_maxsonar_wr.h | 25 +++++++ esphome/components/hrxl_maxsonar_wr/sensor.py | 41 ++++++++++++ tests/test4.yaml | 9 +++ 6 files changed, 142 insertions(+) create mode 100644 esphome/components/hrxl_maxsonar_wr/__init__.py create mode 100644 esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp create mode 100644 esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h create mode 100644 esphome/components/hrxl_maxsonar_wr/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index b3a4668f3a..8806489884 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,6 +49,7 @@ esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle esphome/components/havells_solar/* @sourabhjaiswal esphome/components/homeassistant/* @OttoWinter +esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/i2c/* @esphome/core esphome/components/improv/* @jesserockz esphome/components/inkbird_ibsth1_mini/* @fkirill diff --git a/esphome/components/hrxl_maxsonar_wr/__init__.py b/esphome/components/hrxl_maxsonar_wr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp new file mode 100644 index 0000000000..cf6c9eea65 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -0,0 +1,66 @@ +// Official Datasheet: +// https://www.maxbotix.com/documents/HRXL-MaxSonar-WR_Datasheet.pdf +// +// This implementation is designed to work with the TTL Versions of the +// MaxBotix HRXL MaxSonar WR sensor series. The sensor's TTL Pin (5) should be +// wired to one of the ESP's input pins and configured as uart rx_pin. + +#include "hrxl_maxsonar_wr.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +static const char *const TAG = "hrxl.maxsonar.wr.sensor"; +static const uint8_t ASCII_CR = 0x0D; +static const uint8_t ASCII_NBSP = 0xFF; +static const int MAX_DATA_LENGTH_BYTES = 6; + +/** + * The sensor outputs something like "R1234\r" at a fixed rate of 6 Hz. Where + * 1234 means a distance of 1,234 m. + */ +void HrxlMaxsonarWrComponent::loop() { + uint8_t data; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_ += (char) data; + this->check_buffer_(); + } + } +} + +void HrxlMaxsonarWrComponent::check_buffer_() { + // The sensor seems to inject a rogue ASCII 255 byte from time to time. Get rid of that. + if (this->buffer_.back() == static_cast(ASCII_NBSP)) { + this->buffer_.pop_back(); + return; + } + + // Stop reading at ASCII_CR. Also prevent the buffer from growing + // indefinitely if no ASCII_CR is received after MAX_DATA_LENGTH_BYTES. + if (this->buffer_.back() == static_cast(ASCII_CR) || this->buffer_.length() >= MAX_DATA_LENGTH_BYTES) { + ESP_LOGV(TAG, "Read from serial: %s", this->buffer_.c_str()); + + if (this->buffer_.length() == MAX_DATA_LENGTH_BYTES && this->buffer_[0] == 'R' && + this->buffer_.back() == static_cast(ASCII_CR)) { + int millimeters = strtol(this->buffer_.substr(1, MAX_DATA_LENGTH_BYTES - 2).c_str(), nullptr, 10); + float meters = float(millimeters) / 1000.0; + ESP_LOGV(TAG, "Distance from sensor: %d mm, %f m", millimeters, meters); + this->publish_state(meters); + } else { + ESP_LOGW(TAG, "Invalid data read from sensor: %s", this->buffer_.c_str()); + } + this->buffer_.clear(); + } +} + +void HrxlMaxsonarWrComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:"); + LOG_SENSOR(" ", "Distance", this); + // As specified in the sensor's data sheet + this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); +} + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h new file mode 100644 index 0000000000..efb8bc5f4b --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace hrxl_maxsonar_wr { + +class HrxlMaxsonarWrComponent : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + // Nothing really public. + + // ========== INTERNAL METHODS ========== + void loop() override; + void dump_config() override; + + protected: + void check_buffer_(); + + std::string buffer_; +}; + +} // namespace hrxl_maxsonar_wr +} // namespace esphome diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py new file mode 100644 index 0000000000..370ee04b98 --- /dev/null +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, +) + +CODEOWNERS = ["@netmikey"] +DEPENDENCIES = ["uart"] + +hrxlmaxsonarwr_ns = cg.esphome_ns.namespace("hrxl_maxsonar_wr") +HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( + "HrxlMaxsonarWrComponent", sensor.Sensor, cg.Component, uart.UARTDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + UNIT_METER, + ICON_ARROW_EXPAND_VERTICAL, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + ) + .extend( + { + cv.GenerateID(): cv.declare_id(HrxlMaxsonarWrComponent), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/test4.yaml b/tests/test4.yaml index 7868fd4968..2857e8ca5f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -63,6 +63,15 @@ sensor: - platform: tuya id: tuya_sensor sensor_datapoint: 1 + - platform: "hrxl_maxsonar_wr" + name: "Rainwater Tank Level" + filters: + - sliding_window_moving_average: + window_size: 12 + send_every: 12 + - or: + - throttle: "20min" + - delta: 0.02 # # platform sensor.apds9960 requires component apds9960 # From af8d04818db1ed1f060f54022c93413d1823fa5c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 29 Jul 2021 11:44:19 +0200 Subject: [PATCH 624/643] Pull ESP32 Wifi fixes from arduino-esp32 (#2069) --- .../components/wifi/wifi_component_esp32.cpp | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index c51c17d60c..1bccf08a7f 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -142,11 +142,7 @@ IPAddress WiFiComponent::wifi_sta_ip_() { } bool WiFiComponent::wifi_apply_hostname_() { - esp_err_t err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "Setting hostname failed: %d", err); - return false; - } + // setting is done in SYSTEM_EVENT_STA_START callback return true; } bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { @@ -154,11 +150,25 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { if (!this->wifi_mode_(true, {})) return false; + // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t wifi_config_t conf; memset(&conf, 0, sizeof(conf)); strcpy(reinterpret_cast(conf.sta.ssid), ap.get_ssid().c_str()); strcpy(reinterpret_cast(conf.sta.password), ap.get_password().c_str()); + // The weakest authmode to accept in the fast scan mode + if (ap.get_password().empty()) { + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; + } else { + conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK; + } + +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE; + } +#endif + if (ap.get_bssid().has_value()) { conf.sta.bssid_set = 1; memcpy(conf.sta.bssid, ap.get_bssid()->data(), 6); @@ -167,7 +177,26 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } if (ap.get_channel().has_value()) { conf.sta.channel = *ap.get_channel(); + conf.sta.scan_method = WIFI_FAST_SCAN; + } else { + conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; } + // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set. + // Units: AP beacon intervals. Defaults to 3 if set to 0. + conf.sta.listen_interval = 0; + +#if ESP_IDF_VERSION_MAJOR >= 4 + // Protected Management Frame + // Device will prefer to connect in PMF mode if other device also advertizes PMF capability. + conf.sta.pmf_cfg.capable = true; + conf.sta.pmf_cfg.required = false; +#endif + + // note, we do our own filtering + // The minimum rssi to accept in the fast scan mode + conf.sta.threshold.rssi = -127; + + conf.sta.threshold.authmode = WIFI_AUTH_OPEN; wifi_config_t current_conf; esp_err_t err; @@ -348,6 +377,8 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association Failed"; case WIFI_REASON_HANDSHAKE_TIMEOUT: return "Handshake Failed"; + case WIFI_REASON_CONNECTION_FAIL: + return "Connection Failed"; case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; @@ -374,6 +405,7 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i } case SYSTEM_EVENT_STA_START: { ESP_LOGV(TAG, "Event: WiFi STA start"); + tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); break; } case SYSTEM_EVENT_STA_STOP: { @@ -636,6 +668,11 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { strcpy(reinterpret_cast(conf.ap.password), ap.get_password().c_str()); } +#if ESP_IDF_VERSION_MAJOR >= 4 + // pairwise cipher of SoftAP, group cipher will be derived using this. + conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP; +#endif + esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf); if (err != ESP_OK) { ESP_LOGV(TAG, "esp_wifi_set_config failed! %d", err); From 16dbbfabc61809c8df1a79249af0bfe4ec80c2ee Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 29 Jul 2021 11:50:55 +0200 Subject: [PATCH 625/643] Add demo integration (#2085) --- esphome/components/demo/__init__.py | 451 +++++++++++++++++++ esphome/components/demo/demo_binary_sensor.h | 22 + esphome/components/demo/demo_climate.h | 157 +++++++ esphome/components/demo/demo_cover.h | 86 ++++ esphome/components/demo/demo_fan.h | 54 +++ esphome/components/demo/demo_light.h | 82 ++++ esphome/components/demo/demo_number.h | 39 ++ esphome/components/demo/demo_sensor.h | 28 ++ esphome/components/demo/demo_switch.h | 22 + esphome/components/demo/demo_text_sensor.h | 25 + tests/test5.yaml | 3 + 11 files changed, 969 insertions(+) create mode 100644 esphome/components/demo/__init__.py create mode 100644 esphome/components/demo/demo_binary_sensor.h create mode 100644 esphome/components/demo/demo_climate.h create mode 100644 esphome/components/demo/demo_cover.h create mode 100644 esphome/components/demo/demo_fan.h create mode 100644 esphome/components/demo/demo_light.h create mode 100644 esphome/components/demo/demo_number.h create mode 100644 esphome/components/demo/demo_sensor.h create mode 100644 esphome/components/demo/demo_switch.h create mode 100644 esphome/components/demo/demo_text_sensor.h diff --git a/esphome/components/demo/__init__.py b/esphome/components/demo/__init__.py new file mode 100644 index 0000000000..b3ea47d869 --- /dev/null +++ b/esphome/components/demo/__init__.py @@ -0,0 +1,451 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ( + binary_sensor, + climate, + cover, + fan, + light, + number, + sensor, + switch, + text_sensor, +) +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_BINARY_SENSORS, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_ICON, + CONF_ID, + CONF_INVERTED, + CONF_LAST_RESET_TYPE, + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_NAME, + CONF_OUTPUT_ID, + CONF_SENSORS, + CONF_STATE_CLASS, + CONF_STEP, + CONF_SWITCHES, + CONF_TEXT_SENSORS, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_TEMPERATURE, + ICON_BLUETOOTH, + ICON_BLUR, + ICON_EMPTY, + ICON_THERMOMETER, + LAST_RESET_TYPE_AUTO, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_PERCENT, + UNIT_WATT_HOURS, +) + +AUTO_LOAD = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "number", + "sensor", + "switch", + "text_sensor", +] + +demo_ns = cg.esphome_ns.namespace("demo") +DemoBinarySensor = demo_ns.class_( + "DemoBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent +) +DemoClimate = demo_ns.class_("DemoClimate", climate.Climate, cg.Component) +DemoClimateType = demo_ns.enum("DemoClimateType", is_class=True) +DemoCover = demo_ns.class_("DemoCover", cover.Cover, cg.Component) +DemoCoverType = demo_ns.enum("DemoCoverType", is_class=True) +DemoFan = demo_ns.class_("DemoFan", cg.Component) +DemoFanType = demo_ns.enum("DemoFanType", is_class=True) +DemoLight = demo_ns.class_("DemoLight", light.LightOutput, cg.Component) +DemoLightType = demo_ns.enum("DemoLightType", is_class=True) +DemoNumber = demo_ns.class_("DemoNumber", number.Number, cg.Component) +DemoNumberType = demo_ns.enum("DemoNumberType", is_class=True) +DemoSensor = demo_ns.class_("DemoSensor", sensor.Sensor, cg.PollingComponent) +DemoSwitch = demo_ns.class_("DemoSwitch", switch.Switch, cg.Component) +DemoTextSensor = demo_ns.class_( + "DemoTextSensor", text_sensor.TextSensor, cg.PollingComponent +) + + +CLIMATE_TYPES = { + 1: DemoClimateType.TYPE_1, + 2: DemoClimateType.TYPE_2, + 3: DemoClimateType.TYPE_3, +} +COVER_TYPES = { + 1: DemoCoverType.TYPE_1, + 2: DemoCoverType.TYPE_2, + 3: DemoCoverType.TYPE_3, + 4: DemoCoverType.TYPE_4, +} +FAN_TYPES = { + 1: DemoFanType.TYPE_1, + 2: DemoFanType.TYPE_2, + 3: DemoFanType.TYPE_3, + 4: DemoFanType.TYPE_4, +} +LIGHT_TYPES = { + 1: DemoLightType.TYPE_1, + 2: DemoLightType.TYPE_2, + 3: DemoLightType.TYPE_3, + 4: DemoLightType.TYPE_4, + 5: DemoLightType.TYPE_5, + 6: DemoLightType.TYPE_6, + 7: DemoLightType.TYPE_7, +} +NUMBER_TYPES = { + 1: DemoNumberType.TYPE_1, + 2: DemoNumberType.TYPE_2, + 3: DemoNumberType.TYPE_3, +} + + +CONF_CLIMATES = "climates" +CONF_COVERS = "covers" +CONF_FANS = "fans" +CONF_LIGHTS = "lights" +CONF_NUMBERS = "numbers" + +CONFIG_SCHEMA = cv.Schema( + { + cv.Optional( + CONF_BINARY_SENSORS, + default=[ + { + CONF_NAME: "Demo Basement Floor Wet", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOISTURE, + }, + { + CONF_NAME: "Demo Movement Backyard", + CONF_DEVICE_CLASS: DEVICE_CLASS_MOTION, + }, + ], + ): [ + binary_sensor.BINARY_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoBinarySensor), + } + ) + ], + cv.Optional( + CONF_CLIMATES, + default=[ + { + CONF_NAME: "Demo Heatpump", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo HVAC", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Ecobee", + CONF_TYPE: 3, + }, + ], + ): [ + climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoClimate), + cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_COVERS, + default=[ + { + CONF_NAME: "Demo Kitchen Window", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Garage Door", + CONF_TYPE: 2, + CONF_DEVICE_CLASS: "garage", + }, + { + CONF_NAME: "Demo Living Room Window", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Hall Window", + CONF_TYPE: 4, + CONF_DEVICE_CLASS: "window", + }, + ], + ): [ + cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoCover), + cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_FANS, + default=[ + { + CONF_NAME: "Demo Living Room Fan", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Ceiling Fan", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo Percentage Limited Fan", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo Percentage Full Fan", + CONF_TYPE: 4, + }, + ], + ): [ + fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan), + cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_LIGHTS, + default=[ + { + CONF_NAME: "Demo Binary Light", + CONF_TYPE: 1, + }, + { + CONF_NAME: "Demo Brightness Light", + CONF_TYPE: 2, + }, + { + CONF_NAME: "Demo RGB Light", + CONF_TYPE: 3, + }, + { + CONF_NAME: "Demo RGBW Light", + CONF_TYPE: 4, + }, + { + CONF_NAME: "Demo RGBWW Light", + CONF_TYPE: 5, + }, + { + CONF_NAME: "Demo CWWW Light", + CONF_TYPE: 6, + }, + { + CONF_NAME: "Demo RGBW interlock Light", + CONF_TYPE: 7, + }, + ], + ): [ + light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), + cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), + } + ) + ], + cv.Optional( + CONF_NUMBERS, + default=[ + { + CONF_NAME: "Demo Number 0-100", + CONF_TYPE: 1, + CONF_MIN_VALUE: 0, + CONF_MAX_VALUE: 100, + CONF_STEP: 1, + }, + { + CONF_NAME: "Demo Number -50-50", + CONF_TYPE: 2, + CONF_MIN_VALUE: -50, + CONF_MAX_VALUE: 50, + CONF_STEP: 5, + }, + { + CONF_NAME: "Demo Number 40-60", + CONF_TYPE: 3, + CONF_MIN_VALUE: 40, + CONF_MAX_VALUE: 60, + CONF_STEP: 0.2, + }, + ], + ): [ + number.NUMBER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoNumber), + cv.Required(CONF_TYPE): cv.enum(NUMBER_TYPES, int=True), + cv.Required(CONF_MIN_VALUE): cv.float_, + cv.Required(CONF_MAX_VALUE): cv.float_, + cv.Required(CONF_STEP): cv.float_, + } + ) + ], + cv.Optional( + CONF_SENSORS, + default=[ + { + CONF_NAME: "Demo Plain Sensor", + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Temperature Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS, + CONF_ICON: ICON_THERMOMETER, + CONF_ACCURACY_DECIMALS: 1, + CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + }, + { + CONF_NAME: "Demo Force Update Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_PERCENT, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_FORCE_UPDATE: True, + }, + { + CONF_NAME: "Demo Energy Sensor", + CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS, + CONF_ACCURACY_DECIMALS: 0, + CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT, + CONF_LAST_RESET_TYPE: LAST_RESET_TYPE_AUTO, + }, + ], + ): [ + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0) + .extend(cv.polling_component_schema("60s")) + .extend( + { + cv.GenerateID(): cv.declare_id(DemoSensor), + } + ) + ], + cv.Optional( + CONF_SWITCHES, + default=[ + { + CONF_NAME: "Demo Switch 1", + }, + { + CONF_NAME: "Demo Switch 2", + CONF_INVERTED: True, + CONF_ICON: ICON_BLUETOOTH, + }, + ], + ): [ + switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( + { + cv.GenerateID(): cv.declare_id(DemoSwitch), + } + ) + ], + cv.Optional( + CONF_TEXT_SENSORS, + default=[ + { + CONF_NAME: "Demo Text Sensor 1", + }, + { + CONF_NAME: "Demo Text Sensor 2", + CONF_ICON: ICON_BLUR, + }, + ], + ): [ + text_sensor.TEXT_SENSOR_SCHEMA.extend( + cv.polling_component_schema("60s") + ).extend( + { + cv.GenerateID(): cv.declare_id(DemoTextSensor), + } + ) + ], + } +) + + +async def to_code(config): + for conf in config[CONF_BINARY_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await binary_sensor.register_binary_sensor(var, conf) + + for conf in config[CONF_CLIMATES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await climate.register_climate(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_COVERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await cover.register_cover(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_FANS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + fan_ = await fan.create_fan_state(conf) + cg.add(var.set_fan(fan_)) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_LIGHTS]: + var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) + await cg.register_component(var, conf) + await light.register_light(var, conf) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_NUMBERS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await number.register_number( + var, + conf, + min_value=conf[CONF_MIN_VALUE], + max_value=conf[CONF_MAX_VALUE], + step=conf[CONF_STEP], + ) + cg.add(var.set_type(conf[CONF_TYPE])) + + for conf in config[CONF_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await sensor.register_sensor(var, conf) + + for conf in config[CONF_SWITCHES]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await switch.register_switch(var, conf) + + for conf in config[CONF_TEXT_SENSORS]: + var = cg.new_Pvariable(conf[CONF_ID]) + await cg.register_component(var, conf) + await text_sensor.register_text_sensor(var, conf) diff --git a/esphome/components/demo/demo_binary_sensor.h b/esphome/components/demo/demo_binary_sensor.h new file mode 100644 index 0000000000..4dfd038761 --- /dev/null +++ b/esphome/components/demo/demo_binary_sensor.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace demo { + +class DemoBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { + public: + void setup() override { this->publish_initial_state(false); } + void update() override { + bool new_state = last_state_ = !last_state_; + this->publish_state(new_state); + } + + protected: + bool last_state_ = false; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_climate.h b/esphome/components/demo/demo_climate.h new file mode 100644 index 0000000000..0cf48dd4ee --- /dev/null +++ b/esphome/components/demo/demo_climate.h @@ -0,0 +1,157 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace demo { + +enum class DemoClimateType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoClimate : public climate::Climate, public Component { + public: + void set_type(DemoClimateType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoClimateType::TYPE_1: + this->current_temperature = 20.0; + this->target_temperature = 21.0; + this->mode = climate::CLIMATE_MODE_HEAT; + this->action = climate::CLIMATE_ACTION_HEATING; + break; + case DemoClimateType::TYPE_2: + this->target_temperature = 21.5; + this->mode = climate::CLIMATE_MODE_AUTO; + this->action = climate::CLIMATE_ACTION_COOLING; + this->fan_mode = climate::CLIMATE_FAN_HIGH; + this->custom_preset = {"My Preset"}; + break; + case DemoClimateType::TYPE_3: + this->current_temperature = 21.5; + this->target_temperature_low = 21.0; + this->target_temperature_high = 22.5; + this->mode = climate::CLIMATE_MODE_HEAT_COOL; + this->custom_fan_mode = {"Auto Low"}; + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + this->preset = climate::CLIMATE_PRESET_AWAY; + break; + } + this->publish_state(); + } + + protected: + void control(const climate::ClimateCall &call) override { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + } + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + } + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + } + if (call.get_fan_mode().has_value()) { + this->fan_mode = *call.get_fan_mode(); + this->custom_fan_mode.reset(); + } + if (call.get_swing_mode().has_value()) { + this->swing_mode = *call.get_swing_mode(); + } + if (call.get_custom_fan_mode().has_value()) { + this->custom_fan_mode = *call.get_custom_fan_mode(); + this->fan_mode.reset(); + } + if (call.get_preset().has_value()) { + this->preset = *call.get_preset(); + this->custom_preset.reset(); + } + if (call.get_custom_preset().has_value()) { + this->custom_preset = *call.get_custom_preset(); + this->preset.reset(); + } + this->publish_state(); + } + climate::ClimateTraits traits() override { + climate::ClimateTraits traits{}; + switch (type_) { + case DemoClimateType::TYPE_1: + traits.set_supports_current_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + }); + traits.set_supports_action(true); + traits.set_visual_temperature_step(0.5); + break; + case DemoClimateType::TYPE_2: + traits.set_supports_current_temperature(false); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_AUTO, + climate::CLIMATE_MODE_DRY, + climate::CLIMATE_MODE_FAN_ONLY, + }); + traits.set_supports_action(true); + traits.set_supported_fan_modes({ + climate::CLIMATE_FAN_ON, + climate::CLIMATE_FAN_OFF, + climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, + climate::CLIMATE_FAN_FOCUS, + climate::CLIMATE_FAN_DIFFUSE, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_BOTH, + climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_custom_presets({"My Preset"}); + break; + case DemoClimateType::TYPE_3: + traits.set_supports_current_temperature(true); + traits.set_supports_two_point_target_temperature(true); + traits.set_supported_modes({ + climate::CLIMATE_MODE_OFF, + climate::CLIMATE_MODE_COOL, + climate::CLIMATE_MODE_HEAT, + climate::CLIMATE_MODE_HEAT_COOL, + }); + traits.set_supported_custom_fan_modes({"Auto Low", "Auto High"}); + traits.set_supported_swing_modes({ + climate::CLIMATE_SWING_OFF, + climate::CLIMATE_SWING_HORIZONTAL, + }); + traits.set_supported_presets({ + climate::CLIMATE_PRESET_NONE, + climate::CLIMATE_PRESET_HOME, + climate::CLIMATE_PRESET_AWAY, + climate::CLIMATE_PRESET_BOOST, + climate::CLIMATE_PRESET_COMFORT, + climate::CLIMATE_PRESET_ECO, + climate::CLIMATE_PRESET_SLEEP, + climate::CLIMATE_PRESET_ACTIVITY, + }); + break; + } + return traits; + } + + DemoClimateType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h new file mode 100644 index 0000000000..ab039736fb --- /dev/null +++ b/esphome/components/demo/demo_cover.h @@ -0,0 +1,86 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/cover/cover.h" + +namespace esphome { +namespace demo { + +enum class DemoCoverType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoCover : public cover::Cover, public Component { + public: + void set_type(DemoCoverType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoCoverType::TYPE_1: + this->position = cover::COVER_OPEN; + break; + case DemoCoverType::TYPE_2: + this->position = 0.7; + break; + case DemoCoverType::TYPE_3: + this->position = 0.1; + this->tilt = 0.8; + break; + case DemoCoverType::TYPE_4: + this->position = cover::COVER_CLOSED; + this->tilt = 1.0; + break; + } + this->publish_state(); + } + + protected: + void control(const cover::CoverCall &call) override { + if (call.get_position().has_value()) { + float target = *call.get_position(); + this->current_operation = + target > this->position ? cover::COVER_OPERATION_OPENING : cover::COVER_OPERATION_CLOSING; + + this->set_timeout("move", 2000, [this, target]() { + this->current_operation = cover::COVER_OPERATION_IDLE; + this->position = target; + this->publish_state(); + }); + } + if (call.get_tilt().has_value()) { + this->tilt = *call.get_tilt(); + } + if (call.get_stop()) { + this->cancel_timeout("move"); + } + + this->publish_state(); + } + cover::CoverTraits get_traits() override { + cover::CoverTraits traits{}; + switch (type_) { + case DemoCoverType::TYPE_1: + traits.set_is_assumed_state(true); + break; + case DemoCoverType::TYPE_2: + traits.set_supports_position(true); + break; + case DemoCoverType::TYPE_3: + traits.set_supports_position(true); + traits.set_supports_tilt(true); + break; + case DemoCoverType::TYPE_4: + traits.set_is_assumed_state(true); + traits.set_supports_tilt(true); + break; + } + return traits; + } + + DemoCoverType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_fan.h b/esphome/components/demo/demo_fan.h new file mode 100644 index 0000000000..e926f68edb --- /dev/null +++ b/esphome/components/demo/demo_fan.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace demo { + +enum class DemoFanType { + TYPE_1, + TYPE_2, + TYPE_3, + TYPE_4, +}; + +class DemoFan : public Component { + public: + void set_type(DemoFanType type) { type_ = type; } + void set_fan(fan::FanState *fan) { fan_ = fan; } + void setup() override { + fan::FanTraits traits{}; + + // oscillation + // speed + // direction + // speed_count + switch (type_) { + case DemoFanType::TYPE_1: + break; + case DemoFanType::TYPE_2: + traits.set_oscillation(true); + break; + case DemoFanType::TYPE_3: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(5); + break; + case DemoFanType::TYPE_4: + traits.set_direction(true); + traits.set_speed(true); + traits.set_supported_speed_count(100); + traits.set_oscillation(true); + break; + } + + this->fan_->set_traits(traits); + } + + fan::FanState *fan_; + DemoFanType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h new file mode 100644 index 0000000000..989dbf3306 --- /dev/null +++ b/esphome/components/demo/demo_light.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace demo { + +enum class DemoLightType { + // binary + TYPE_1, + // brightness + TYPE_2, + // RGB + TYPE_3, + // RGBW + TYPE_4, + // RGBWW + TYPE_5, + // CWWW + TYPE_6, + // RGBW + color_interlock + TYPE_7, +}; + +class DemoLight : public light::LightOutput, public Component { + public: + void set_type(DemoLightType type) { type_ = type; } + light::LightTraits get_traits() override { + light::LightTraits traits{}; + // brightness + // rgb + // rgb_white_value + // color_temperature, min_mireds, max_mireds + // color_interlock + switch (type_) { + case DemoLightType::TYPE_1: + break; + case DemoLightType::TYPE_2: + traits.set_supports_brightness(true); + break; + case DemoLightType::TYPE_3: + traits.set_supports_brightness(true); + traits.set_supports_rgb(true); + break; + case DemoLightType::TYPE_4: + traits.set_supports_brightness(true); + traits.set_supports_rgb(true); + traits.set_supports_rgb_white_value(true); + break; + case DemoLightType::TYPE_5: + traits.set_supports_brightness(true); + traits.set_supports_rgb(true); + traits.set_supports_rgb_white_value(true); + traits.set_supports_color_temperature(true); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_6: + traits.set_supports_brightness(true); + traits.set_supports_color_temperature(true); + traits.set_min_mireds(153); + traits.set_max_mireds(500); + break; + case DemoLightType::TYPE_7: + traits.set_supports_brightness(true); + traits.set_supports_rgb(true); + traits.set_supports_rgb_white_value(true); + traits.set_supports_color_interlock(true); + break; + } + return traits; + } + void write_state(light::LightState *state) override { + // do nothing + } + + DemoLightType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_number.h b/esphome/components/demo/demo_number.h new file mode 100644 index 0000000000..2ce3a269bc --- /dev/null +++ b/esphome/components/demo/demo_number.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" + +namespace esphome { +namespace demo { + +enum class DemoNumberType { + TYPE_1, + TYPE_2, + TYPE_3, +}; + +class DemoNumber : public number::Number, public Component { + public: + void set_type(DemoNumberType type) { type_ = type; } + void setup() override { + switch (type_) { + case DemoNumberType::TYPE_1: + this->publish_state(50); + break; + case DemoNumberType::TYPE_2: + this->publish_state(-10); + break; + case DemoNumberType::TYPE_3: + this->publish_state(42); + break; + } + } + + protected: + void control(float value) override { this->publish_state(value); } + + DemoNumberType type_; +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_sensor.h b/esphome/components/demo/demo_sensor.h new file mode 100644 index 0000000000..117468793b --- /dev/null +++ b/esphome/components/demo/demo_sensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace demo { + +class DemoSensor : public sensor::Sensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + bool is_auto = this->last_reset_type == sensor::LAST_RESET_TYPE_AUTO; + if (is_auto) { + float base = isnan(this->state) ? 0.0f : this->state; + this->publish_state(base + val * 10); + } else { + if (val < 0.1) + this->publish_state(NAN); + else + this->publish_state(val * 100); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_switch.h b/esphome/components/demo/demo_switch.h new file mode 100644 index 0000000000..9c291318ca --- /dev/null +++ b/esphome/components/demo/demo_switch.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace demo { + +class DemoSwitch : public switch_::Switch, public Component { + public: + void setup() override { + bool initial = random_float() < 0.5; + this->publish_state(initial); + } + + protected: + void write_state(bool state) override { this->publish_state(state); } +}; + +} // namespace demo +} // namespace esphome diff --git a/esphome/components/demo/demo_text_sensor.h b/esphome/components/demo/demo_text_sensor.h new file mode 100644 index 0000000000..b4152fc248 --- /dev/null +++ b/esphome/components/demo/demo_text_sensor.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace demo { + +class DemoTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void update() override { + float val = random_float(); + if (val < 0.33) { + this->publish_state("foo"); + } else if (val < 0.66) { + this->publish_state("bar"); + } else { + this->publish_state("foobar"); + } + } +}; + +} // namespace demo +} // namespace esphome diff --git a/tests/test5.yaml b/tests/test5.yaml index 23f8ae6c4d..1a2dde1010 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -52,6 +52,8 @@ output: channel: 0 max_power: 0.8 +demo: + esp32_ble: esp32_ble_server: @@ -116,6 +118,7 @@ sensor: name: "SelecEM2M Maximum Demand Reactive Power" maximum_demand_apparent_power: name: "SelecEM2M Maximum Demand Apparent Power" + - platform: t6615 uart_id: uart2 co2: From de382b704c76d89d72a73a3ad0d12cd60663a1eb Mon Sep 17 00:00:00 2001 From: Kodey Converse Date: Thu, 29 Jul 2021 10:08:48 -0400 Subject: [PATCH 626/643] Add device class support to MQTT cover (#2092) --- esphome/components/mqtt/mqtt_cover.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 25ac430abb..b11ae1fb93 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -61,6 +61,9 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + if (!this->cover_->get_device_class().empty()) + root["device_class"] = this->cover_->get_device_class(); + auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { root["optimistic"] = true; From 5983ccc55ca722f3ddd676e6a6e59d1fcdd59f16 Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Thu, 29 Jul 2021 19:11:56 +0200 Subject: [PATCH 627/643] Color mode implementation (#2012) --- esphome/components/api/api.proto | 31 +- esphome/components/api/api_connection.cpp | 39 +- esphome/components/api/api_pb2.cpp | 156 ++++++- esphome/components/api/api_pb2.h | 30 +- .../binary/light/binary_light_output.h | 2 +- esphome/components/cwww/cwww_light_output.h | 9 +- esphome/components/cwww/light.py | 19 +- esphome/components/demo/demo_light.h | 28 +- .../components/fastled_base/fastled_light.h | 3 +- .../components/hbridge/hbridge_light_output.h | 28 +- esphome/components/light/__init__.py | 4 +- esphome/components/light/automation.h | 6 + esphome/components/light/automation.py | 17 + esphome/components/light/base_light_effects.h | 39 +- esphome/components/light/color_mode.h | 107 +++++ esphome/components/light/effects.py | 20 + esphome/components/light/light_call.cpp | 429 +++++++++++------- esphome/components/light/light_call.h | 37 +- esphome/components/light/light_color_values.h | 224 ++++----- .../components/light/light_json_schema.cpp | 165 +++++++ esphome/components/light/light_json_schema.h | 25 + esphome/components/light/light_state.cpp | 28 +- esphome/components/light/light_state.h | 5 - esphome/components/light/light_traits.h | 53 ++- esphome/components/light/light_transformer.h | 35 +- esphome/components/light/types.py | 14 + .../monochromatic_light_output.h | 2 +- esphome/components/mqtt/mqtt_light.cpp | 34 +- .../neopixelbus/neopixelbus_light.h | 7 +- esphome/components/rgb/rgb_light_output.h | 3 +- esphome/components/rgbw/rgbw_light_output.h | 8 +- esphome/components/rgbww/light.py | 18 +- esphome/components/rgbww/rgbww_light_output.h | 17 +- esphome/components/tuya/light/tuya_light.cpp | 9 +- esphome/components/web_server/web_server.cpp | 6 +- esphome/config_validation.py | 17 + esphome/const.py | 1 + esphome/core/helpers.cpp | 9 + esphome/core/helpers.h | 2 + 39 files changed, 1210 insertions(+), 476 deletions(-) create mode 100644 esphome/components/light/color_mode.h create mode 100644 esphome/components/light/light_json_schema.cpp create mode 100644 esphome/components/light/light_json_schema.h diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 073775ed2e..c04e0a32f1 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -352,6 +352,18 @@ message FanCommandRequest { } // ==================== LIGHT ==================== +enum ColorMode { + COLOR_MODE_UNKNOWN = 0; + COLOR_MODE_ON_OFF = 1; + COLOR_MODE_BRIGHTNESS = 2; + COLOR_MODE_WHITE = 7; + COLOR_MODE_COLOR_TEMPERATURE = 11; + COLOR_MODE_COLD_WARM_WHITE = 19; + COLOR_MODE_RGB = 35; + COLOR_MODE_RGB_WHITE = 39; + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; + COLOR_MODE_RGB_COLD_WARM_WHITE = 51; +} message ListEntitiesLightResponse { option (id) = 15; option (source) = SOURCE_SERVER; @@ -362,10 +374,12 @@ message ListEntitiesLightResponse { string name = 3; string unique_id = 4; - bool supports_brightness = 5; - bool supports_rgb = 6; - bool supports_white_value = 7; - bool supports_color_temperature = 8; + repeated ColorMode supported_color_modes = 12; + // next four supports_* are for legacy clients, newer clients should use color modes + bool legacy_supports_brightness = 5 [deprecated=true]; + bool legacy_supports_rgb = 6 [deprecated=true]; + bool legacy_supports_white_value = 7 [deprecated=true]; + bool legacy_supports_color_temperature = 8 [deprecated=true]; float min_mireds = 9; float max_mireds = 10; repeated string effects = 11; @@ -379,12 +393,15 @@ message LightStateResponse { fixed32 key = 1; bool state = 2; float brightness = 3; + ColorMode color_mode = 11; float color_brightness = 10; float red = 4; float green = 5; float blue = 6; float white = 7; float color_temperature = 8; + float cold_white = 12; + float warm_white = 13; string effect = 9; } message LightCommandRequest { @@ -398,6 +415,8 @@ message LightCommandRequest { bool state = 3; bool has_brightness = 4; float brightness = 5; + bool has_color_mode = 22; + ColorMode color_mode = 23; bool has_color_brightness = 20; float color_brightness = 21; bool has_rgb = 6; @@ -408,6 +427,10 @@ message LightCommandRequest { float white = 11; bool has_color_temperature = 12; float color_temperature = 13; + bool has_cold_white = 24; + float cold_white = 25; + bool has_warm_white = 26; + float warm_white = 27; bool has_transition_length = 14; uint32 transition_length = 15; bool has_flash_length = 16; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index fb05772e5e..b92c67b042 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -301,22 +301,28 @@ bool APIConnection::send_light_state(light::LightState *light) { auto traits = light->get_traits(); auto values = light->remote_values; + auto color_mode = values.get_color_mode(); LightStateResponse resp{}; resp.key = light->get_object_id_hash(); resp.state = values.is_on(); - if (traits.get_supports_brightness()) + resp.color_mode = static_cast(color_mode); + if (color_mode & light::ColorCapability::BRIGHTNESS) resp.brightness = values.get_brightness(); - if (traits.get_supports_rgb()) { + if (color_mode & light::ColorCapability::RGB) { resp.color_brightness = values.get_color_brightness(); resp.red = values.get_red(); resp.green = values.get_green(); resp.blue = values.get_blue(); } - if (traits.get_supports_rgb_white_value()) + if (color_mode & light::ColorCapability::WHITE) resp.white = values.get_white(); - if (traits.get_supports_color_temperature()) + if (color_mode & light::ColorCapability::COLOR_TEMPERATURE) resp.color_temperature = values.get_color_temperature(); + if (color_mode & light::ColorCapability::COLD_WARM_WHITE) { + resp.cold_white = values.get_cold_white(); + resp.warm_white = values.get_warm_white(); + } if (light->supports_effects()) resp.effect = light->get_effect_name(); return this->send_light_state_response(resp); @@ -328,11 +334,18 @@ bool APIConnection::send_light_info(light::LightState *light) { msg.object_id = light->get_object_id(); msg.name = light->get_name(); msg.unique_id = get_default_unique_id("light", light); - msg.supports_brightness = traits.get_supports_brightness(); - msg.supports_rgb = traits.get_supports_rgb(); - msg.supports_white_value = traits.get_supports_rgb_white_value(); - msg.supports_color_temperature = traits.get_supports_color_temperature(); - if (msg.supports_color_temperature) { + for (auto mode : traits.get_supported_color_modes()) + msg.supported_color_modes.push_back(static_cast(mode)); + + msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); + msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); + msg.legacy_supports_white_value = + msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); + msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || + traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); + + if (msg.legacy_supports_color_temperature) { msg.min_mireds = traits.get_min_mireds(); msg.max_mireds = traits.get_max_mireds(); } @@ -353,6 +366,8 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_state(msg.state); if (msg.has_brightness) call.set_brightness(msg.brightness); + if (msg.has_color_mode) + call.set_color_mode(static_cast(msg.color_mode)); if (msg.has_color_brightness) call.set_color_brightness(msg.color_brightness); if (msg.has_rgb) { @@ -364,6 +379,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { call.set_white(msg.white); if (msg.has_color_temperature) call.set_color_temperature(msg.color_temperature); + if (msg.has_cold_white) + call.set_cold_white(msg.cold_white); + if (msg.has_warm_white) + call.set_warm_white(msg.warm_white); if (msg.has_transition_length) call.set_transition_length(msg.transition_length); if (msg.has_flash_length) @@ -655,7 +674,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 5; + resp.api_version_minor = 6; resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; this->connection_state_ = ConnectionState::CONNECTED; return resp; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 057d71324f..79092cb511 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -62,6 +62,32 @@ template<> const char *proto_enum_to_string(enums::FanDirec return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::ColorMode value) { + switch (value) { + case enums::COLOR_MODE_UNKNOWN: + return "COLOR_MODE_UNKNOWN"; + case enums::COLOR_MODE_ON_OFF: + return "COLOR_MODE_ON_OFF"; + case enums::COLOR_MODE_BRIGHTNESS: + return "COLOR_MODE_BRIGHTNESS"; + case enums::COLOR_MODE_WHITE: + return "COLOR_MODE_WHITE"; + case enums::COLOR_MODE_COLOR_TEMPERATURE: + return "COLOR_MODE_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_COLD_WARM_WHITE: + return "COLOR_MODE_COLD_WARM_WHITE"; + case enums::COLOR_MODE_RGB: + return "COLOR_MODE_RGB"; + case enums::COLOR_MODE_RGB_WHITE: + return "COLOR_MODE_RGB_WHITE"; + case enums::COLOR_MODE_RGB_COLOR_TEMPERATURE: + return "COLOR_MODE_RGB_COLOR_TEMPERATURE"; + case enums::COLOR_MODE_RGB_COLD_WARM_WHITE: + return "COLOR_MODE_RGB_COLD_WARM_WHITE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::SensorStateClass value) { switch (value) { case enums::STATE_CLASS_NONE: @@ -1117,20 +1143,24 @@ void FanCommandRequest::dump_to(std::string &out) const { } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { + case 12: { + this->supported_color_modes.push_back(value.as_enum()); + return true; + } case 5: { - this->supports_brightness = value.as_bool(); + this->legacy_supports_brightness = value.as_bool(); return true; } case 6: { - this->supports_rgb = value.as_bool(); + this->legacy_supports_rgb = value.as_bool(); return true; } case 7: { - this->supports_white_value = value.as_bool(); + this->legacy_supports_white_value = value.as_bool(); return true; } case 8: { - this->supports_color_temperature = value.as_bool(); + this->legacy_supports_color_temperature = value.as_bool(); return true; } default: @@ -1182,10 +1212,13 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(2, this->key); buffer.encode_string(3, this->name); buffer.encode_string(4, this->unique_id); - buffer.encode_bool(5, this->supports_brightness); - buffer.encode_bool(6, this->supports_rgb); - buffer.encode_bool(7, this->supports_white_value); - buffer.encode_bool(8, this->supports_color_temperature); + for (auto &it : this->supported_color_modes) { + buffer.encode_enum(12, it, true); + } + buffer.encode_bool(5, this->legacy_supports_brightness); + buffer.encode_bool(6, this->legacy_supports_rgb); + buffer.encode_bool(7, this->legacy_supports_white_value); + buffer.encode_bool(8, this->legacy_supports_color_temperature); buffer.encode_float(9, this->min_mireds); buffer.encode_float(10, this->max_mireds); for (auto &it : this->effects) { @@ -1212,20 +1245,26 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const { out.append("'").append(this->unique_id).append("'"); out.append("\n"); - out.append(" supports_brightness: "); - out.append(YESNO(this->supports_brightness)); + for (const auto &it : this->supported_color_modes) { + out.append(" supported_color_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + out.append(" legacy_supports_brightness: "); + out.append(YESNO(this->legacy_supports_brightness)); out.append("\n"); - out.append(" supports_rgb: "); - out.append(YESNO(this->supports_rgb)); + out.append(" legacy_supports_rgb: "); + out.append(YESNO(this->legacy_supports_rgb)); out.append("\n"); - out.append(" supports_white_value: "); - out.append(YESNO(this->supports_white_value)); + out.append(" legacy_supports_white_value: "); + out.append(YESNO(this->legacy_supports_white_value)); out.append("\n"); - out.append(" supports_color_temperature: "); - out.append(YESNO(this->supports_color_temperature)); + out.append(" legacy_supports_color_temperature: "); + out.append(YESNO(this->legacy_supports_color_temperature)); out.append("\n"); out.append(" min_mireds: "); @@ -1251,6 +1290,10 @@ bool LightStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->state = value.as_bool(); return true; } + case 11: { + this->color_mode = value.as_enum(); + return true; + } default: return false; } @@ -1299,6 +1342,14 @@ bool LightStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 12: { + this->cold_white = value.as_float(); + return true; + } + case 13: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1307,12 +1358,15 @@ void LightStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->key); buffer.encode_bool(2, this->state); buffer.encode_float(3, this->brightness); + buffer.encode_enum(11, this->color_mode); buffer.encode_float(10, this->color_brightness); buffer.encode_float(4, this->red); buffer.encode_float(5, this->green); buffer.encode_float(6, this->blue); buffer.encode_float(7, this->white); buffer.encode_float(8, this->color_temperature); + buffer.encode_float(12, this->cold_white); + buffer.encode_float(13, this->warm_white); buffer.encode_string(9, this->effect); } void LightStateResponse::dump_to(std::string &out) const { @@ -1332,6 +1386,10 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" color_brightness: "); sprintf(buffer, "%g", this->color_brightness); out.append(buffer); @@ -1362,6 +1420,16 @@ void LightStateResponse::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" effect: "); out.append("'").append(this->effect).append("'"); out.append("\n"); @@ -1381,6 +1449,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_brightness = value.as_bool(); return true; } + case 22: { + this->has_color_mode = value.as_bool(); + return true; + } + case 23: { + this->color_mode = value.as_enum(); + return true; + } case 20: { this->has_color_brightness = value.as_bool(); return true; @@ -1397,6 +1473,14 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->has_color_temperature = value.as_bool(); return true; } + case 24: { + this->has_cold_white = value.as_bool(); + return true; + } + case 26: { + this->has_warm_white = value.as_bool(); + return true; + } case 14: { this->has_transition_length = value.as_bool(); return true; @@ -1465,6 +1549,14 @@ bool LightCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { this->color_temperature = value.as_float(); return true; } + case 25: { + this->cold_white = value.as_float(); + return true; + } + case 27: { + this->warm_white = value.as_float(); + return true; + } default: return false; } @@ -1475,6 +1567,8 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(3, this->state); buffer.encode_bool(4, this->has_brightness); buffer.encode_float(5, this->brightness); + buffer.encode_bool(22, this->has_color_mode); + buffer.encode_enum(23, this->color_mode); buffer.encode_bool(20, this->has_color_brightness); buffer.encode_float(21, this->color_brightness); buffer.encode_bool(6, this->has_rgb); @@ -1485,6 +1579,10 @@ void LightCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(11, this->white); buffer.encode_bool(12, this->has_color_temperature); buffer.encode_float(13, this->color_temperature); + buffer.encode_bool(24, this->has_cold_white); + buffer.encode_float(25, this->cold_white); + buffer.encode_bool(26, this->has_warm_white); + buffer.encode_float(27, this->warm_white); buffer.encode_bool(14, this->has_transition_length); buffer.encode_uint32(15, this->transition_length); buffer.encode_bool(16, this->has_flash_length); @@ -1517,6 +1615,14 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_color_mode: "); + out.append(YESNO(this->has_color_mode)); + out.append("\n"); + + out.append(" color_mode: "); + out.append(proto_enum_to_string(this->color_mode)); + out.append("\n"); + out.append(" has_color_brightness: "); out.append(YESNO(this->has_color_brightness)); out.append("\n"); @@ -1563,6 +1669,24 @@ void LightCommandRequest::dump_to(std::string &out) const { out.append(buffer); out.append("\n"); + out.append(" has_cold_white: "); + out.append(YESNO(this->has_cold_white)); + out.append("\n"); + + out.append(" cold_white: "); + sprintf(buffer, "%g", this->cold_white); + out.append(buffer); + out.append("\n"); + + out.append(" has_warm_white: "); + out.append(YESNO(this->has_warm_white)); + out.append("\n"); + + out.append(" warm_white: "); + sprintf(buffer, "%g", this->warm_white); + out.append(buffer); + out.append("\n"); + out.append(" has_transition_length: "); out.append(YESNO(this->has_transition_length)); out.append("\n"); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 0551508b4b..4c92b7b00d 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -32,6 +32,18 @@ enum FanDirection : uint32_t { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1, }; +enum ColorMode : uint32_t { + COLOR_MODE_UNKNOWN = 0, + COLOR_MODE_ON_OFF = 1, + COLOR_MODE_BRIGHTNESS = 2, + COLOR_MODE_WHITE = 7, + COLOR_MODE_COLOR_TEMPERATURE = 11, + COLOR_MODE_COLD_WARM_WHITE = 19, + COLOR_MODE_RGB = 35, + COLOR_MODE_RGB_WHITE = 39, + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47, + COLOR_MODE_RGB_COLD_WARM_WHITE = 51, +}; enum SensorStateClass : uint32_t { STATE_CLASS_NONE = 0, STATE_CLASS_MEASUREMENT = 1, @@ -356,10 +368,11 @@ class ListEntitiesLightResponse : public ProtoMessage { uint32_t key{0}; std::string name{}; std::string unique_id{}; - bool supports_brightness{false}; - bool supports_rgb{false}; - bool supports_white_value{false}; - bool supports_color_temperature{false}; + std::vector supported_color_modes{}; + bool legacy_supports_brightness{false}; + bool legacy_supports_rgb{false}; + bool legacy_supports_white_value{false}; + bool legacy_supports_color_temperature{false}; float min_mireds{0.0f}; float max_mireds{0.0f}; std::vector effects{}; @@ -376,12 +389,15 @@ class LightStateResponse : public ProtoMessage { uint32_t key{0}; bool state{false}; float brightness{0.0f}; + enums::ColorMode color_mode{}; float color_brightness{0.0f}; float red{0.0f}; float green{0.0f}; float blue{0.0f}; float white{0.0f}; float color_temperature{0.0f}; + float cold_white{0.0f}; + float warm_white{0.0f}; std::string effect{}; void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -398,6 +414,8 @@ class LightCommandRequest : public ProtoMessage { bool state{false}; bool has_brightness{false}; float brightness{0.0f}; + bool has_color_mode{false}; + enums::ColorMode color_mode{}; bool has_color_brightness{false}; float color_brightness{0.0f}; bool has_rgb{false}; @@ -408,6 +426,10 @@ class LightCommandRequest : public ProtoMessage { float white{0.0f}; bool has_color_temperature{false}; float color_temperature{0.0f}; + bool has_cold_white{false}; + float cold_white{0.0f}; + bool has_warm_white{false}; + float warm_white{0.0f}; bool has_transition_length{false}; uint32_t transition_length{0}; bool has_flash_length{false}; diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 731973bdad..86c83aff5c 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -12,7 +12,7 @@ class BinaryLightOutput : public light::LightOutput { void set_output(output::BinaryOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(false); + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 3351a98d24..2b7698ce5a 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -16,10 +16,7 @@ class CWWWLightOutput : public light::LightOutput { void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(false); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -34,8 +31,8 @@ class CWWWLightOutput : public light::LightOutput { protected: output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - float cold_white_temperature_; - float warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; }; diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 1a027a86b4..734f9aa1e7 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -20,11 +20,14 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -32,11 +35,19 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) + cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/demo/demo_light.h b/esphome/components/demo/demo_light.h index 989dbf3306..2007e9ff50 100644 --- a/esphome/components/demo/demo_light.h +++ b/esphome/components/demo/demo_light.h @@ -28,45 +28,31 @@ class DemoLight : public light::LightOutput, public Component { void set_type(DemoLightType type) { type_ = type; } light::LightTraits get_traits() override { light::LightTraits traits{}; - // brightness - // rgb - // rgb_white_value - // color_temperature, min_mireds, max_mireds - // color_interlock switch (type_) { case DemoLightType::TYPE_1: + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); break; case DemoLightType::TYPE_2: - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); break; case DemoLightType::TYPE_3: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); break; case DemoLightType::TYPE_4: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); break; case DemoLightType::TYPE_5: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::RGB_COLOR_TEMPERATURE}); traits.set_min_mireds(153); traits.set_max_mireds(500); break; case DemoLightType::TYPE_6: - traits.set_supports_brightness(true); - traits.set_supports_color_temperature(true); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); traits.set_min_mireds(153); traits.set_max_mireds(500); break; case DemoLightType::TYPE_7: - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_interlock(true); + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); break; } return traits; diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 59d143dbef..ac6acc95a5 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -208,8 +208,7 @@ class FastLEDLightOutput : public light::AddressableLight { // (In most use cases you won't need these) light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); + traits.set_supported_color_modes({light::ColorMode::RGB}); return traits; } void setup() override; diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/hbridge_light_output.h index 03a5b3a88c..2f4f87134c 100644 --- a/esphome/components/hbridge/hbridge_light_output.h +++ b/esphome/components/hbridge/hbridge_light_output.h @@ -18,10 +18,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); // Dimming - traits.set_supports_rgb(false); - traits.set_supports_rgb_white_value(true); // hbridge color - traits.set_supports_color_temperature(false); + traits.set_supported_color_modes({light::ColorMode::COLD_WARM_WHITE}); return traits; } @@ -31,11 +28,11 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { // This method runs around 60 times per second // We cannot do the PWM ourselves so we are reliant on the hardware PWM if (!this->forward_direction_) { // First LED Direction - this->pinb_pin_->set_level(this->duty_off_); this->pina_pin_->set_level(this->pina_duty_); + this->pinb_pin_->set_level(0); this->forward_direction_ = true; } else { // Second LED Direction - this->pina_pin_->set_level(this->duty_off_); + this->pina_pin_->set_level(0); this->pinb_pin_->set_level(this->pinb_duty_); this->forward_direction_ = false; } @@ -44,23 +41,7 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { float get_setup_priority() const override { return setup_priority::HARDWARE; } void write_state(light::LightState *state) override { - float bright; - state->current_values_as_brightness(&bright); - - state->set_gamma_correct(0); - float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); - - if ((white / bright) > 0.55) { - this->pina_duty_ = (bright * (1 - (white / bright))); - this->pinb_duty_ = bright; - } else if (white < 0.45) { - this->pina_duty_ = bright; - this->pinb_duty_ = white; - } else { - this->pina_duty_ = bright; - this->pinb_duty_ = bright; - } + state->current_values_as_cwww(&this->pina_duty_, &this->pinb_duty_, false); } protected: @@ -68,7 +49,6 @@ class HBridgeLightOutput : public PollingComponent, public light::LightOutput { output::FloatOutput *pinb_pin_; float pina_duty_ = 0; float pinb_duty_ = 0; - float duty_off_ = 0; bool forward_direction_ = false; }; diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 119ff3703c..ec7d6bcbc0 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -108,7 +108,9 @@ ADDRESSABLE_LIGHT_SCHEMA = RGB_LIGHT_SCHEMA.extend( def validate_color_temperature_channels(value): if ( - value[CONF_COLD_WHITE_COLOR_TEMPERATURE] + CONF_COLD_WHITE_COLOR_TEMPERATURE in value + and CONF_WARM_WHITE_COLOR_TEMPERATURE in value + and value[CONF_COLD_WHITE_COLOR_TEMPERATURE] >= value[CONF_WARM_WHITE_COLOR_TEMPERATURE] ): raise cv.Invalid( diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 02c6163555..a816049f08 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -27,6 +27,7 @@ template class LightControlAction : public Action { public: explicit LightControlAction(LightState *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(ColorMode, color_mode) TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(uint32_t, transition_length) TEMPLATABLE_VALUE(uint32_t, flash_length) @@ -37,10 +38,13 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, blue) TEMPLATABLE_VALUE(float, white) TEMPLATABLE_VALUE(float, color_temperature) + TEMPLATABLE_VALUE(float, cold_white) + TEMPLATABLE_VALUE(float, warm_white) TEMPLATABLE_VALUE(std::string, effect) void play(Ts... x) override { auto call = this->parent_->make_call(); + call.set_color_mode(this->color_mode_.optional_value(x...)); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); call.set_color_brightness(this->color_brightness_.optional_value(x...)); @@ -49,6 +53,8 @@ template class LightControlAction : public Action { call.set_blue(this->blue_.optional_value(x...)); call.set_white(this->white_.optional_value(x...)); call.set_color_temperature(this->color_temperature_.optional_value(x...)); + call.set_cold_white(this->cold_white_.optional_value(x...)); + call.set_warm_white(this->warm_white_.optional_value(x...)); call.set_effect(this->effect_.optional_value(x...)); call.set_flash_length(this->flash_length_.optional_value(x...)); call.set_transition_length(this->transition_length_.optional_value(x...)); diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 85741f1ffd..cfba273565 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import ( CONF_ID, + CONF_COLOR_MODE, CONF_TRANSITION_LENGTH, CONF_STATE, CONF_FLASH_LENGTH, @@ -14,10 +15,14 @@ from esphome.const import ( CONF_BLUE, CONF_WHITE, CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_RANGE_FROM, CONF_RANGE_TO, ) from .types import ( + ColorMode, + COLOR_MODES, DimRelativeAction, ToggleAction, LightState, @@ -55,6 +60,7 @@ async def light_toggle_to_code(config, action_id, template_arg, args): LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( { cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_COLOR_MODE): cv.enum(COLOR_MODES, upper=True, space="_"), cv.Optional(CONF_STATE): cv.templatable(cv.boolean), cv.Exclusive(CONF_TRANSITION_LENGTH, "transformer"): cv.templatable( cv.positive_time_period_milliseconds @@ -70,6 +76,8 @@ LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema( cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), + cv.Optional(CONF_COLD_WHITE): cv.templatable(cv.percentage), + cv.Optional(CONF_WARM_WHITE): cv.templatable(cv.percentage), } ) LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id( @@ -102,6 +110,9 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def light_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_COLOR_MODE in config: + template_ = await cg.templatable(config[CONF_COLOR_MODE], args, ColorMode) + cg.add(var.set_color_mode(template_)) if CONF_STATE in config: template_ = await cg.templatable(config[CONF_STATE], args, bool) cg.add(var.set_state(template_)) @@ -134,6 +145,12 @@ async def light_control_to_code(config, action_id, template_arg, args): if CONF_COLOR_TEMPERATURE in config: template_ = await cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) cg.add(var.set_color_temperature(template_)) + if CONF_COLD_WHITE in config: + template_ = await cg.templatable(config[CONF_COLD_WHITE], args, float) + cg.add(var.set_cold_white(template_)) + if CONF_WARM_WHITE in config: + template_ = await cg.templatable(config[CONF_WARM_WHITE], args, float) + cg.add(var.set_warm_white(template_)) if CONF_EFFECT in config: template_ = await cg.templatable(config[CONF_EFFECT], args, cg.std_string) cg.add(var.set_effect(template_)) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index eb92fec642..f66b90f665 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -57,16 +57,31 @@ class RandomLightEffect : public LightEffect { if (now - this->last_color_change_ < this->update_interval_) { return; } + + auto color_mode = this->state_->remote_values.get_color_mode(); auto call = this->state_->turn_on(); - if (this->state_->get_traits().get_supports_rgb()) { - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); - } else { - call.set_brightness_if_supported(random_float()); + bool changed = false; + if (color_mode & ColorCapability::RGB) { + call.set_red(random_float()); + call.set_green(random_float()); + call.set_blue(random_float()); + changed = true; + } + if (color_mode & ColorCapability::COLOR_TEMPERATURE) { + float min = this->state_->get_traits().get_min_mireds(); + float max = this->state_->get_traits().get_max_mireds(); + call.set_color_temperature(min + random_float() * (max - min)); + changed = true; + } + if (color_mode & ColorCapability::COLD_WARM_WHITE) { + call.set_cold_white(random_float()); + call.set_warm_white(random_float()); + changed = true; + } + if (!changed) { + // only randomize brightness if there's no colored option available + call.set_brightness(random_float()); } - call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); call.set_save(false); @@ -142,7 +157,6 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped call.set_brightness_if_supported(0.0f); - call.set_white_if_supported(0.0f); call.set_state(true); } call.set_publish(false); @@ -177,13 +191,16 @@ class FlickerLightEffect : public LightEffect { out.set_green(remote.get_green() * beta + current.get_green() * alpha + (random_cubic_float() * this->intensity_)); out.set_blue(remote.get_blue() * beta + current.get_blue() * alpha + (random_cubic_float() * this->intensity_)); out.set_white(remote.get_white() * beta + current.get_white() * alpha + (random_cubic_float() * this->intensity_)); + out.set_cold_white(remote.get_cold_white() * beta + current.get_cold_white() * alpha + + (random_cubic_float() * this->intensity_)); + out.set_warm_white(remote.get_warm_white() * beta + current.get_warm_white() * alpha + + (random_cubic_float() * this->intensity_)); auto traits = this->state_->get_traits(); auto call = this->state_->make_call(); call.set_publish(false); call.set_save(false); - if (traits.get_supports_brightness()) - call.set_transition_length(0); + call.set_transition_length_if_supported(0); call.from_light_color_values(out); call.set_state(true); call.perform(); diff --git a/esphome/components/light/color_mode.h b/esphome/components/light/color_mode.h new file mode 100644 index 0000000000..0f5b7b4b93 --- /dev/null +++ b/esphome/components/light/color_mode.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +namespace esphome { +namespace light { + +/// Color capabilities are the various outputs that a light has and that can be independently controlled by the user. +enum class ColorCapability : uint8_t { + /// Light can be turned on/off. + ON_OFF = 1 << 0, + /// Master brightness of the light can be controlled. + BRIGHTNESS = 1 << 1, + /// Brightness of white channel can be controlled separately from other channels. + WHITE = 1 << 2, + /// Color temperature can be controlled. + COLOR_TEMPERATURE = 1 << 3, + /// Brightness of cold and warm white output can be controlled. + COLD_WARM_WHITE = 1 << 4, + /// Color can be controlled using RGB format (includes a brightness control for the color). + RGB = 1 << 5 +}; + +/// Helper class to allow bitwise operations on ColorCapability +class ColorCapabilityHelper { + public: + constexpr ColorCapabilityHelper(ColorCapability val) : val_(val) {} + constexpr operator ColorCapability() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorCapability val_; +}; +constexpr ColorCapabilityHelper operator&(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator&(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapability lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorCapabilityHelper operator|(ColorCapabilityHelper lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +/// Color modes are a combination of color capabilities that can be used at the same time. +enum class ColorMode : uint8_t { + /// No color mode configured (cannot be a supported mode, only active when light is off). + UNKNOWN = 0, + /// Only on/off control. + ON_OFF = (uint8_t) ColorCapability::ON_OFF, + /// Dimmable light. + BRIGHTNESS = (uint8_t) ColorCapability::BRIGHTNESS, + /// White output only (use only if the light also has another color mode such as RGB). + WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::WHITE), + /// Controllable color temperature output. + COLOR_TEMPERATURE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLOR_TEMPERATURE), + /// Cold and warm white output with individually controllable brightness. + COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::COLD_WARM_WHITE), + /// RGB color output. + RGB = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB), + /// RGB color output and a separate white output. + RGB_WHITE = + (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | ColorCapability::WHITE), + /// RGB color output and a separate white output with controllable color temperature. + RGB_COLOR_TEMPERATURE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::WHITE | ColorCapability::COLOR_TEMPERATURE), + /// RGB color output, and separate cold and warm white outputs. + RGB_COLD_WARM_WHITE = (uint8_t)(ColorCapability::ON_OFF | ColorCapability::BRIGHTNESS | ColorCapability::RGB | + ColorCapability::COLD_WARM_WHITE), +}; + +/// Helper class to allow bitwise operations on ColorMode with ColorCapability +class ColorModeHelper { + public: + constexpr ColorModeHelper(ColorMode val) : val_(val) {} + constexpr operator ColorMode() const { return val_; } + constexpr operator uint8_t() const { return static_cast(val_); } + constexpr operator bool() const { return static_cast(val_) != 0; } + + protected: + ColorMode val_; +}; +constexpr ColorModeHelper operator&(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator&(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) & static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorMode lhs, ColorCapability rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} +constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index f6ce812c34..be558a8140 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -12,11 +12,15 @@ from esphome.const import ( CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, CONF_ALPHA, CONF_INTENSITY, CONF_SPEED, @@ -27,6 +31,8 @@ from esphome.const import ( ) from esphome.util import Registry from .types import ( + ColorMode, + COLOR_MODES, LambdaLightEffect, PulseLightEffect, RandomLightEffect, @@ -212,11 +218,17 @@ async def random_effect_to_code(config, effect_id): { cv.Optional(CONF_STATE, default=True): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_MODE): cv.enum( + COLOR_MODES, upper=True, space="_" + ), cv.Optional(CONF_COLOR_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, cv.Optional(CONF_BLUE, default=1.0): cv.percentage, cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_WARM_WHITE, default=1.0): cv.percentage, cv.Required( CONF_DURATION ): cv.positive_time_period_milliseconds, @@ -225,11 +237,15 @@ async def random_effect_to_code(config, effect_id): cv.has_at_least_one_key( CONF_STATE, CONF_BRIGHTNESS, + CONF_COLOR_MODE, CONF_COLOR_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, + CONF_COLOR_TEMPERATURE, + CONF_COLD_WHITE, + CONF_WARM_WHITE, ), ), cv.Length(min=2), @@ -246,6 +262,7 @@ async def strobe_effect_to_code(config, effect_id): ( "color", LightColorValues( + color.get(CONF_COLOR_MODE, ColorMode.UNKNOWN), color[CONF_STATE], color[CONF_BRIGHTNESS], color[CONF_COLOR_BRIGHTNESS], @@ -253,6 +270,9 @@ async def strobe_effect_to_code(config, effect_id): color[CONF_GREEN], color[CONF_BLUE], color[CONF_WHITE], + color.get(CONF_COLOR_TEMPERATURE, 0.0), + color[CONF_COLD_WHITE], + color[CONF_WARM_WHITE], ), ), ("duration", color[CONF_DURATION]), diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index d7e8ce6298..536018c33b 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -7,95 +7,42 @@ namespace light { static const char *const TAG = "light"; -#ifdef USE_JSON -LightCall &LightCall::parse_color_json(JsonObject &root) { - if (root.containsKey("state")) { - auto val = parse_on_off(root["state"]); - switch (val) { - case PARSE_ON: - this->set_state(true); - break; - case PARSE_OFF: - this->set_state(false); - break; - case PARSE_TOGGLE: - this->set_state(!this->parent_->remote_values.is_on()); - break; - case PARSE_NONE: - break; - } +static const char *color_mode_to_human(ColorMode color_mode) { + switch (color_mode) { + case ColorMode::UNKNOWN: + return "Unknown"; + case ColorMode::WHITE: + return "White"; + case ColorMode::COLOR_TEMPERATURE: + return "Color temperature"; + case ColorMode::COLD_WARM_WHITE: + return "Cold/warm white"; + case ColorMode::RGB: + return "RGB"; + case ColorMode::RGB_WHITE: + return "RGBW"; + case ColorMode::RGB_COLD_WARM_WHITE: + return "RGB + cold/warm white"; + case ColorMode::RGB_COLOR_TEMPERATURE: + return "RGB + color temperature"; + default: + return ""; } - - if (root.containsKey("brightness")) { - this->set_brightness(float(root["brightness"]) / 255.0f); - } - - if (root.containsKey("color")) { - JsonObject &color = root["color"]; - // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. - float max_rgb = 0.0f; - if (color.containsKey("r")) { - float r = float(color["r"]) / 255.0f; - max_rgb = fmaxf(max_rgb, r); - this->set_red(r); - } - if (color.containsKey("g")) { - float g = float(color["g"]) / 255.0f; - max_rgb = fmaxf(max_rgb, g); - this->set_green(g); - } - if (color.containsKey("b")) { - float b = float(color["b"]) / 255.0f; - max_rgb = fmaxf(max_rgb, b); - this->set_blue(b); - } - if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { - this->set_color_brightness(max_rgb); - } - } - - if (root.containsKey("white_value")) { - this->set_white(float(root["white_value"]) / 255.0f); - } - - if (root.containsKey("color_temp")) { - this->set_color_temperature(float(root["color_temp"])); - } - - return *this; } -LightCall &LightCall::parse_json(JsonObject &root) { - this->parse_color_json(root); - - if (root.containsKey("flash")) { - auto length = uint32_t(float(root["flash"]) * 1000); - this->set_flash_length(length); - } - - if (root.containsKey("transition")) { - auto length = uint32_t(float(root["transition"]) * 1000); - this->set_transition_length(length); - } - - if (root.containsKey("effect")) { - const char *effect = root["effect"]; - this->set_effect(effect); - } - - return *this; -} -#endif void LightCall::perform() { - // use remote values for fallback const char *name = this->parent_->get_name().c_str(); - if (this->publish_) { - ESP_LOGD(TAG, "'%s' Setting:", name); - } - LightColorValues v = this->validate_(); if (this->publish_) { + ESP_LOGD(TAG, "'%s' Setting:", name); + + // Only print color mode when it's being changed + ColorMode current_color_mode = this->parent_->remote_values.get_color_mode(); + if (this->color_mode_.value_or(current_color_mode) != current_color_mode) { + ESP_LOGD(TAG, " Color mode: %s", color_mode_to_human(v.get_color_mode())); + } + // Only print state when it's being changed bool current_state = this->parent_->remote_values.is_on(); if (this->state_.value_or(current_state) != current_state) { @@ -106,30 +53,35 @@ void LightCall::perform() { ESP_LOGD(TAG, " Brightness: %.0f%%", v.get_brightness() * 100.0f); } - if (this->color_temperature_.has_value()) { - ESP_LOGD(TAG, " Color Temperature: %.1f mireds", v.get_color_temperature()); - } - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - ESP_LOGD(TAG, " Red=%.0f%%, Green=%.0f%%, Blue=%.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, + ESP_LOGD(TAG, " Red: %.0f%%, Green: %.0f%%, Blue: %.0f%%", v.get_red() * 100.0f, v.get_green() * 100.0f, v.get_blue() * 100.0f); } + if (this->white_.has_value()) { - ESP_LOGD(TAG, " White Value: %.0f%%", v.get_white() * 100.0f); + ESP_LOGD(TAG, " White: %.0f%%", v.get_white() * 100.0f); + } + if (this->color_temperature_.has_value()) { + ESP_LOGD(TAG, " Color temperature: %.1f mireds", v.get_color_temperature()); + } + + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + ESP_LOGD(TAG, " Cold white: %.0f%%, warm white: %.0f%%", v.get_cold_white() * 100.0f, + v.get_warm_white() * 100.0f); } } if (this->has_flash_()) { // FLASH if (this->publish_) { - ESP_LOGD(TAG, " Flash Length: %.1fs", *this->flash_length_ / 1e3f); + ESP_LOGD(TAG, " Flash length: %.1fs", *this->flash_length_ / 1e3f); } this->parent_->start_flash_(v, *this->flash_length_); } else if (this->has_transition_()) { // TRANSITION if (this->publish_) { - ESP_LOGD(TAG, " Transition Length: %.1fs", *this->transition_length_ / 1e3f); + ESP_LOGD(TAG, " Transition length: %.1fs", *this->transition_length_ / 1e3f); } // Special case: Transition and effect can be set when turning off @@ -177,32 +129,48 @@ void LightCall::perform() { } LightColorValues LightCall::validate_() { - // use remote values for fallback auto *name = this->parent_->get_name().c_str(); auto traits = this->parent_->get_traits(); + // Color mode check + if (this->color_mode_.has_value() && !traits.supports_color_mode(this->color_mode_.value())) { + ESP_LOGW(TAG, "'%s' - This light does not support color mode %s!", name, + color_mode_to_human(this->color_mode_.value())); + this->color_mode_.reset(); + } + + // Ensure there is always a color mode set + if (!this->color_mode_.has_value()) { + this->color_mode_ = this->compute_color_mode_(); + } + auto color_mode = *this->color_mode_; + + // Transform calls that use non-native parameters for the current mode. + this->transform_parameters_(); + // Brightness exists check - if (this->brightness_.has_value() && !traits.get_supports_brightness()) { + if (this->brightness_.has_value() && !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support setting brightness!", name); this->brightness_.reset(); } // Transition length possible check - if (this->transition_length_.has_value() && *this->transition_length_ != 0 && !traits.get_supports_brightness()) { + if (this->transition_length_.has_value() && *this->transition_length_ != 0 && + !(color_mode & ColorCapability::BRIGHTNESS)) { ESP_LOGW(TAG, "'%s' - This light does not support transitions!", name); this->transition_length_.reset(); } // Color brightness exists check - if (this->color_brightness_.has_value() && !traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB brightness!", name); + if (this->color_brightness_.has_value() && !(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB brightness!", name); this->color_brightness_.reset(); } // RGB exists check if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!traits.get_supports_rgb()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting RGB color!", name); + if (!(color_mode & ColorCapability::RGB)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting RGB color!", name); this->red_.reset(); this->green_.reset(); this->blue_.reset(); @@ -210,68 +178,25 @@ LightColorValues LightCall::validate_() { } // White value exists check - if (this->white_.has_value() && !traits.get_supports_rgb_white_value()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting white value!", name); + if (this->white_.has_value() && + !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting white value!", name); this->white_.reset(); } // Color temperature exists check - if (this->color_temperature_.has_value() && !traits.get_supports_color_temperature()) { - ESP_LOGW(TAG, "'%s' - This light does not support setting color temperature!", name); + if (this->color_temperature_.has_value() && + !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting color temperature!", name); this->color_temperature_.reset(); } - // Set color brightness to 100% if currently zero and a color is set. This is both for compatibility with older - // clients that don't know about color brightness, and it's intuitive UX anyway: if I set a color, it should show up. - if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) - this->color_brightness_ = optional(1.0f); - } - - // Handle interaction between RGB and white for color interlock - if (traits.get_supports_color_interlock()) { - // Find out which channel (white or color) the user wanted to enable - bool output_white = this->white_.has_value() && *this->white_ > 0.0f; - bool output_color = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || - this->red_.has_value() || this->green_.has_value() || this->blue_.has_value(); - - // Interpret setting the color to white as setting the white channel. - if (output_color && *this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f) { - output_white = true; - output_color = false; - - if (!this->white_.has_value()) - this->white_ = optional(this->color_brightness_.value_or(1.0f)); - } - - // Ensure either the white value or the color brightness is always zero. - if (output_white && output_color) { - ESP_LOGW(TAG, "'%s' - Cannot enable color and white channel simultaneously with interlock!", name); - // For compatibility with historic behaviour, prefer white channel in this case. - this->color_brightness_ = optional(0.0f); - } else if (output_white) { - this->color_brightness_ = optional(0.0f); - } else if (output_color) { - this->white_ = optional(0.0f); - } - } - - // If only a color temperature is specified, change to white light - if (this->color_temperature_.has_value() && !this->white_.has_value() && !this->red_.has_value() && - !this->green_.has_value() && !this->blue_.has_value()) { - // Disable color LEDs explicitly if not already set - if (traits.get_supports_rgb() && !this->color_brightness_.has_value()) - this->color_brightness_ = optional(0.0f); - - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); - - // if setting color temperature from color (i.e. switching to white light), set White to 100% - auto cv = this->parent_->remote_values; - bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; - if (traits.get_supports_color_interlock() || was_color) { - this->white_ = optional(1.0f); + // Cold/warm white value exists check + if (this->cold_white_.has_value() || this->warm_white_.has_value()) { + if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { + ESP_LOGW(TAG, "'%s' - This color mode does not support setting cold/warm white value!", name); + this->cold_white_.reset(); + this->warm_white_.reset(); } } @@ -292,13 +217,22 @@ LightColorValues LightCall::validate_() { VALIDATE_RANGE(green, "Green") VALIDATE_RANGE(blue, "Blue") VALIDATE_RANGE(white, "White") + VALIDATE_RANGE(cold_white, "Cold white") + VALIDATE_RANGE(warm_white, "Warm white") + + // Set color brightness to 100% if currently zero and a color is set. + if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (!this->color_brightness_.has_value() && this->parent_->remote_values.get_color_brightness() == 0.0f) + this->color_brightness_ = optional(1.0f); + } auto v = this->parent_->remote_values; + if (this->color_mode_.has_value()) + v.set_color_mode(*this->color_mode_); if (this->state_.has_value()) v.set_state(*this->state_); if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); - if (this->color_brightness_.has_value()) v.set_color_brightness(*this->color_brightness_); if (this->red_.has_value()) @@ -309,9 +243,12 @@ LightColorValues LightCall::validate_() { v.set_blue(*this->blue_); if (this->white_.has_value()) v.set_white(*this->white_); - if (this->color_temperature_.has_value()) v.set_color_temperature(*this->color_temperature_); + if (this->cold_white_.has_value()) + v.set_cold_white(*this->cold_white_); + if (this->warm_white_.has_value()) + v.set_warm_white(*this->warm_white_); v.normalize_color(traits); @@ -322,7 +259,7 @@ LightColorValues LightCall::validate_() { } // validate transition length/flash length/effect not used at the same time - bool supports_transition = traits.get_supports_brightness(); + bool supports_transition = color_mode & ColorCapability::BRIGHTNESS; // If effect is already active, remove effect start if (this->has_effect_() && *this->effect_ == this->parent_->active_effect_index_) { @@ -331,7 +268,7 @@ LightColorValues LightCall::validate_() { // validate effect index if (this->has_effect_() && *this->effect_ > this->parent_->effects_.size()) { - ESP_LOGW(TAG, "'%s' Invalid effect index %u", name, *this->effect_); + ESP_LOGW(TAG, "'%s' - Invalid effect index %u!", name, *this->effect_); this->effect_.reset(); } @@ -381,6 +318,127 @@ LightColorValues LightCall::validate_() { return v; } +void LightCall::transform_parameters_() { + auto traits = this->parent_->get_traits(); + + // Allow CWWW modes to be set with a white value and/or color temperature. This is used by HA, + // which doesn't support CWWW modes (yet?), and for compatibility with the pre-colormode model, + // as CWWW and RGBWW lights used to represent their values as white + color temperature. + if (((this->white_.has_value() && *this->white_ > 0.0f) || this->color_temperature_.has_value()) && // + (*this->color_mode_ & ColorCapability::COLD_WARM_WHITE) && // + !(*this->color_mode_ & ColorCapability::WHITE) && // + !(*this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) && // + traits.get_min_mireds() > 0.0f && traits.get_max_mireds() > 0.0f) { + ESP_LOGD(TAG, "'%s' - Setting cold/warm white channels using white/color temperature values.", + this->parent_->get_name().c_str()); + auto current_values = this->parent_->remote_values; + if (this->color_temperature_.has_value()) { + const float white = + this->white_.value_or(fmaxf(current_values.get_cold_white(), current_values.get_warm_white())); + const float color_temp = clamp(*this->color_temperature_, traits.get_min_mireds(), traits.get_max_mireds()); + const float ww_fraction = + (color_temp - traits.get_min_mireds()) / (traits.get_max_mireds() - traits.get_min_mireds()); + const float cw_fraction = 1.0f - ww_fraction; + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + this->cold_white_ = white * gamma_uncorrect(cw_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + this->warm_white_ = white * gamma_uncorrect(ww_fraction / max_cw_ww, this->parent_->get_gamma_correct()); + } else { + const float max_cw_ww = std::max(current_values.get_warm_white(), current_values.get_cold_white()); + this->cold_white_ = *this->white_ * current_values.get_cold_white() / max_cw_ww; + this->warm_white_ = *this->white_ * current_values.get_warm_white() / max_cw_ww; + } + } +} +ColorMode LightCall::compute_color_mode_() { + auto supported_modes = this->parent_->get_traits().get_supported_color_modes(); + int supported_count = supported_modes.size(); + + // Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown. + if (supported_count == 0) + return ColorMode::UNKNOWN; + + // In the common case of lights supporting only a single mode, use that one. + if (supported_count == 1) + return *supported_modes.begin(); + + // Don't change if the light is being turned off. + ColorMode current_mode = this->parent_->remote_values.get_color_mode(); + if (this->state_.has_value() && !*this->state_) + return current_mode; + + // If no color mode is specified, we try to guess the color mode. This is needed for backward compatibility to + // pre-colormode clients and automations, but also for the MQTT API, where HA doesn't let us know which color mode + // was used for some reason. + std::set suitable_modes = this->get_suitable_color_modes_(); + + // Don't change if the current mode is suitable. + if (suitable_modes.count(current_mode) > 0) { + ESP_LOGI(TAG, "'%s' - Keeping current color mode %s for call without color mode.", + this->parent_->get_name().c_str(), color_mode_to_human(current_mode)); + return current_mode; + } + + // Use the preferred suitable mode. + for (auto mode : suitable_modes) { + if (supported_modes.count(mode) == 0) + continue; + + ESP_LOGI(TAG, "'%s' - Using color mode %s for call without color mode.", this->parent_->get_name().c_str(), + color_mode_to_human(mode)); + return mode; + } + + // There's no supported mode for this call, so warn, use the current more or a mode at random and let validation strip + // out whatever we don't support. + auto color_mode = current_mode != ColorMode::UNKNOWN ? current_mode : *supported_modes.begin(); + ESP_LOGW(TAG, "'%s' - No color mode suitable for this call supported, defaulting to %s!", + this->parent_->get_name().c_str(), color_mode_to_human(color_mode)); + return color_mode; +} +std::set LightCall::get_suitable_color_modes_() { + bool has_white = this->white_.has_value() && *this->white_ > 0.0f; + bool has_ct = this->color_temperature_.has_value(); + bool has_cwww = (this->cold_white_.has_value() && *this->cold_white_ > 0.0f) || + (this->warm_white_.has_value() && *this->warm_white_ > 0.0f); + bool has_rgb = (this->color_brightness_.has_value() && *this->color_brightness_ > 0.0f) || + (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()); + +#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) +#define ENTRY(white, ct, cwww, rgb, ...) \ + std::make_tuple>(KEY(white, ct, cwww, rgb), __VA_ARGS__) + + // Flag order: white, color temperature, cwww, rgb + std::array>, 10> lookup_table{ + ENTRY(true, false, false, false, + {ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, false, + {ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, false, + {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, false, {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, false, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, + ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}), + ENTRY(true, false, false, true, + {ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(true, true, false, true, {ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, true, true, {ColorMode::RGB_COLD_WARM_WHITE}), + ENTRY(false, false, false, true, + {ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}), + }; + + auto key = KEY(has_white, has_ct, has_cwww, has_rgb); + for (auto &item : lookup_table) + if (std::get<0>(item) == key) + return std::get<1>(item); + + // This happens if there are conflicting flags given. + return {}; +} + LightCall &LightCall::set_effect(const std::string &effect) { if (strcasecmp(effect.c_str(), "none") == 0) { this->set_effect(0); @@ -406,53 +464,74 @@ LightCall &LightCall::from_light_color_values(const LightColorValues &values) { this->set_state(values.is_on()); this->set_brightness_if_supported(values.get_brightness()); this->set_color_brightness_if_supported(values.get_color_brightness()); + this->set_color_mode_if_supported(values.get_color_mode()); this->set_red_if_supported(values.get_red()); this->set_green_if_supported(values.get_green()); this->set_blue_if_supported(values.get_blue()); this->set_white_if_supported(values.get_white()); this->set_color_temperature_if_supported(values.get_color_temperature()); + this->set_cold_white_if_supported(values.get_cold_white()); + this->set_warm_white_if_supported(values.get_warm_white()); return *this; } +ColorMode LightCall::get_active_color_mode_() { + return this->color_mode_.value_or(this->parent_->remote_values.get_color_mode()); +} LightCall &LightCall::set_transition_length_if_supported(uint32_t transition_length) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_transition_length(transition_length); return *this; } LightCall &LightCall::set_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_brightness()) + if (this->get_active_color_mode_() & ColorCapability::BRIGHTNESS) this->set_brightness(brightness); return *this; } +LightCall &LightCall::set_color_mode_if_supported(ColorMode color_mode) { + if (this->parent_->get_traits().supports_color_mode(color_mode)) + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness_if_supported(float brightness) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_color_brightness(brightness); return *this; } LightCall &LightCall::set_red_if_supported(float red) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_red(red); return *this; } LightCall &LightCall::set_green_if_supported(float green) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_green(green); return *this; } LightCall &LightCall::set_blue_if_supported(float blue) { - if (this->parent_->get_traits().get_supports_rgb()) + if (this->get_active_color_mode_() & ColorCapability::RGB) this->set_blue(blue); return *this; } LightCall &LightCall::set_white_if_supported(float white) { - if (this->parent_->get_traits().get_supports_rgb_white_value()) + if (this->get_active_color_mode_() & ColorCapability::WHITE) this->set_white(white); return *this; } LightCall &LightCall::set_color_temperature_if_supported(float color_temperature) { - if (this->parent_->get_traits().get_supports_color_temperature()) + if (this->get_active_color_mode_() & ColorCapability::COLOR_TEMPERATURE) this->set_color_temperature(color_temperature); return *this; } +LightCall &LightCall::set_cold_white_if_supported(float cold_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_cold_white(cold_white); + return *this; +} +LightCall &LightCall::set_warm_white_if_supported(float warm_white) { + if (this->get_active_color_mode_() & ColorCapability::COLD_WARM_WHITE) + this->set_warm_white(warm_white); + return *this; +} LightCall &LightCall::set_state(optional state) { this->state_ = state; return *this; @@ -485,6 +564,14 @@ LightCall &LightCall::set_brightness(float brightness) { this->brightness_ = brightness; return *this; } +LightCall &LightCall::set_color_mode(optional color_mode) { + this->color_mode_ = color_mode; + return *this; +} +LightCall &LightCall::set_color_mode(ColorMode color_mode) { + this->color_mode_ = color_mode; + return *this; +} LightCall &LightCall::set_color_brightness(optional brightness) { this->color_brightness_ = brightness; return *this; @@ -533,6 +620,22 @@ LightCall &LightCall::set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; return *this; } +LightCall &LightCall::set_cold_white(optional cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_cold_white(float cold_white) { + this->cold_white_ = cold_white; + return *this; +} +LightCall &LightCall::set_warm_white(optional warm_white) { + this->warm_white_ = warm_white; + return *this; +} +LightCall &LightCall::set_warm_white(float warm_white) { + this->warm_white_ = warm_white; + return *this; +} LightCall &LightCall::set_effect(optional effect) { if (effect.has_value()) this->set_effect(*effect); diff --git a/esphome/components/light/light_call.h b/esphome/components/light/light_call.h index c63b63bc54..31dc47e55d 100644 --- a/esphome/components/light/light_call.h +++ b/esphome/components/light/light_call.h @@ -44,6 +44,14 @@ class LightCall { LightCall &set_brightness(float brightness); /// Set the brightness property if the light supports brightness. LightCall &set_brightness_if_supported(float brightness); + + /// Set the color mode of the light. + LightCall &set_color_mode(optional color_mode); + /// Set the color mode of the light. + LightCall &set_color_mode(ColorMode color_mode); + /// Set the color mode of the light, if this mode is supported. + LightCall &set_color_mode_if_supported(ColorMode color_mode); + /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) LightCall &set_color_brightness(optional brightness); /// Set the color brightness of the light from 0.0 (no color) to 1.0 (fully on) @@ -98,6 +106,18 @@ class LightCall { LightCall &set_color_temperature(float color_temperature); /// Set the color_temperature property if the light supports color temperature. LightCall &set_color_temperature_if_supported(float color_temperature); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(optional cold_white); + /// Set the cold white value of the light from 0.0 to 1.0. + LightCall &set_cold_white(float cold_white); + /// Set the cold white property if the light supports cold white output. + LightCall &set_cold_white_if_supported(float cold_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(optional warm_white); + /// Set the warm white value of the light from 0.0 to 1.0. + LightCall &set_warm_white(float warm_white); + /// Set the warm white property if the light supports cold white output. + LightCall &set_warm_white_if_supported(float warm_white); /// Set the effect of the light by its name. LightCall &set_effect(optional effect); /// Set the effect of the light by its name. @@ -131,18 +151,24 @@ class LightCall { * @return The light call for chaining setters. */ LightCall &set_rgbw(float red, float green, float blue, float white); -#ifdef USE_JSON - LightCall &parse_color_json(JsonObject &root); - LightCall &parse_json(JsonObject &root); -#endif LightCall &from_light_color_values(const LightColorValues &values); void perform(); protected: + /// Get the currently targeted, or active if none set, color mode. + ColorMode get_active_color_mode_(); + /// Validate all properties and return the target light color values. LightColorValues validate_(); + //// Compute the color mode that should be used for this call. + ColorMode compute_color_mode_(); + /// Get potential color modes for this light call. + std::set get_suitable_color_modes_(); + /// Some color modes also can be set using non-native parameters, transform those calls. + void transform_parameters_(); + bool has_transition_() { return this->transition_length_.has_value(); } bool has_flash_() { return this->flash_length_.has_value(); } bool has_effect_() { return this->effect_.has_value(); } @@ -151,6 +177,7 @@ class LightCall { optional state_; optional transition_length_; optional flash_length_; + optional color_mode_; optional brightness_; optional color_brightness_; optional red_; @@ -158,6 +185,8 @@ class LightCall { optional blue_; optional white_; optional color_temperature_; + optional cold_white_; + optional warm_white_; optional effect_; bool publish_{true}; bool save_{true}; diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 344411b75e..5a5a2f6e34 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -15,40 +15,56 @@ inline static uint8_t to_uint8_scale(float x) { return static_cast(roun /** This class represents the color state for a light object. * - * All values in this class (except color temperature) are represented using floats in the range - * from 0.0 (off) to 1.0 (on). Please note that all values are automatically clamped to this range. + * The representation of the color state is dependent on the active color mode. A color mode consists of multiple + * color capabilities, and each color capability has its own representation in this class. The fields available are as + * follows: * - * This class has the following properties: - * - state: Whether the light should be on/off. Represented as a float for transitions. Used for - * all lights. - * - brightness: The master brightness of the light, applied to all channels. Used for all lights - * with brightness control. - * - color_brightness: The brightness of the color channels of the light. Used for RGB, RGBW and - * RGBWW lights. - * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of - * them is always 1.0. - * - white: The brightness of the white channel of the light. Used for RGBW and RGBWW lights. - * - color_temperature: The color temperature of the white channel in mireds. Used for RGBWW and - * CWWW lights. + * Always: + * - color_mode: The currently active color mode. * - * For lights with a color interlock (RGB lights and white light cannot be on at the same time), a - * valid state has always either color_brightness or white (or both) set to zero. + * For ON_OFF capability: + * - state: Whether the light should be on/off. Represented as a float for transitions. + * + * For BRIGHTNESS capability: + * - brightness: The master brightness of the light, should be applied to all channels. + * + * For RGB capability: + * - color_brightness: The brightness of the color channels of the light. + * - red, green, blue: The RGB values of the current color. They are normalized, so at least one of them is always 1.0. + * + * For WHITE capability: + * - white: The brightness of the white channel of the light. + * + * For COLOR_TEMPERATURE capability: + * - color_temperature: The color temperature of the white channel in mireds. Note that it is not clamped to the valid + * range as set in the traits, so the output needs to do this. + * + * For COLD_WARM_WHITE capability: + * - cold_white, warm_white: The brightness of the cald and warm white channels of the light. + * + * All values (except color temperature) are represented using floats in the range 0.0 (off) to 1.0 (on), and are + * automatically clamped to this range. Properties not used in the current color mode can still have (invalid) values + * and must not be accessed by the light output. */ class LightColorValues { public: - /// Construct the LightColorValues with all attributes enabled, but state set to 0.0 + /// Construct the LightColorValues with all attributes enabled, but state set to off. LightColorValues() - : state_(0.0f), + : color_mode_(ColorMode::UNKNOWN), + state_(0.0f), brightness_(1.0f), color_brightness_(1.0f), red_(1.0f), green_(1.0f), blue_(1.0f), white_(1.0f), - color_temperature_{1.0f} {} + color_temperature_{0.0f}, + cold_white_{1.0f}, + warm_white_{1.0f} {} - LightColorValues(float state, float brightness, float color_brightness, float red, float green, float blue, - float white, float color_temperature = 1.0f) { + LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green, + float blue, float white, float color_temperature, float cold_white, float warm_white) { + this->set_color_mode(color_mode); this->set_state(state); this->set_brightness(brightness); this->set_color_brightness(color_brightness); @@ -57,49 +73,8 @@ class LightColorValues { this->set_blue(blue); this->set_white(white); this->set_color_temperature(color_temperature); - } - - LightColorValues(bool state, float brightness, float color_brightness, float red, float green, float blue, - float white, float color_temperature = 1.0f) - : LightColorValues(state ? 1.0f : 0.0f, brightness, color_brightness, red, green, blue, white, - color_temperature) {} - - /// Create light color values from a binary true/false state. - static LightColorValues from_binary(bool state) { return {state, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; } - - /// Create light color values from a monochromatic brightness state. - static LightColorValues from_monochromatic(float brightness) { - if (brightness == 0.0f) - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - else - return {1.0f, brightness, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } - - /// Create light color values from an RGB state. - static LightColorValues from_rgb(float r, float g, float b) { - float brightness = std::max(r, std::max(g, b)); - if (brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, brightness, 1.0f, r / brightness, g / brightness, b / brightness, 1.0f}; - } - } - - /// Create light color values from an RGBW state. - static LightColorValues from_rgbw(float r, float g, float b, float w) { - float color_brightness = std::max(r, std::max(g, b)); - float master_brightness = std::max(color_brightness, w); - if (master_brightness == 0.0f) { - return {0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; - } else { - return {1.0f, - master_brightness, - color_brightness / master_brightness, - r / color_brightness, - g / color_brightness, - b / color_brightness, - w / master_brightness}; - } + this->set_cold_white(cold_white); + this->set_warm_white(warm_white); } /** Linearly interpolate between the values in start to the values in end. @@ -114,6 +89,7 @@ class LightColorValues { */ static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) { LightColorValues v; + v.set_color_mode(end.color_mode_); v.set_state(esphome::lerp(completion, start.get_state(), end.get_state())); v.set_brightness(esphome::lerp(completion, start.get_brightness(), end.get_brightness())); v.set_color_brightness(esphome::lerp(completion, start.get_color_brightness(), end.get_color_brightness())); @@ -122,33 +98,11 @@ class LightColorValues { v.set_blue(esphome::lerp(completion, start.get_blue(), end.get_blue())); v.set_white(esphome::lerp(completion, start.get_white(), end.get_white())); v.set_color_temperature(esphome::lerp(completion, start.get_color_temperature(), end.get_color_temperature())); + v.set_cold_white(esphome::lerp(completion, start.get_cold_white(), end.get_cold_white())); + v.set_warm_white(esphome::lerp(completion, start.get_warm_white(), end.get_warm_white())); return v; } -#ifdef USE_JSON - /** Dump this color into a JsonObject. Only dumps values if the corresponding traits are marked supported by traits. - * - * @param root The json root object. - * @param traits The traits object used for determining whether to include certain attributes. - */ - void dump_json(JsonObject &root, const LightTraits &traits) const { - root["state"] = (this->get_state() != 0.0f) ? "ON" : "OFF"; - if (traits.get_supports_brightness()) - root["brightness"] = uint8_t(this->get_brightness() * 255); - if (traits.get_supports_rgb()) { - JsonObject &color = root.createNestedObject("color"); - color["r"] = uint8_t(this->get_color_brightness() * this->get_red() * 255); - color["g"] = uint8_t(this->get_color_brightness() * this->get_green() * 255); - color["b"] = uint8_t(this->get_color_brightness() * this->get_blue() * 255); - } - if (traits.get_supports_rgb_white_value()) { - root["white_value"] = uint8_t(this->get_white() * 255); - } - if (traits.get_supports_color_temperature()) - root["color_temp"] = uint32_t(this->get_color_temperature()); - } -#endif - /** Normalize the color (RGB/W) component. * * Divides all color attributes by the maximum attribute, so effectively set at least one attribute to 1. @@ -159,7 +113,7 @@ class LightColorValues { * @param traits Used for determining which attributes to consider. */ void normalize_color(const LightTraits &traits) { - if (traits.get_supports_rgb()) { + if (this->color_mode_ & ColorCapability::RGB) { float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue())); if (max_value == 0.0f) { this->set_red(1.0f); @@ -172,7 +126,7 @@ class LightColorValues { } } - if (traits.get_supports_brightness() && this->get_brightness() == 0.0f) { + if (this->color_mode_ & ColorCapability::BRIGHTNESS && this->get_brightness() == 0.0f) { // 0% brightness means off this->set_state(false); // reset brightness to 100% @@ -180,6 +134,9 @@ class LightColorValues { } } + // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters + // are always used or necessary. Methods will be deprecated later. + /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } @@ -190,64 +147,68 @@ class LightColorValues { /// Convert these light color values to an RGB representation and write them to red, green, blue. void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { - float brightness = this->state_ * this->brightness_ * this->color_brightness_; - if (color_interlock && this->white_ > 0.0f) { - brightness = 0; + if (this->color_mode_ & ColorCapability::RGB) { + float brightness = this->state_ * this->brightness_ * this->color_brightness_; + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); + } else { + *red = *green = *blue = 0; } - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + this->as_rgb(red, green, blue, gamma); + if (this->color_mode_ & ColorCapability::WHITE) { + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + } else { + *white = 0; + } } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma, color_interlock); - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; - } + this->as_rgb(red, green, blue, gamma); + this->as_cwww(0, 0, cold_white, warm_white, gamma, constant_brightness); } /// Convert these light color values to an CWWW representation with the given parameters. void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { - const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); - const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - const float cw_fraction = 1.0f - ww_fraction; - const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); - *cold_white = white_level * cw_fraction; - *warm_white = white_level * ww_fraction; - if (!constant_brightness) { - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white /= max_cw_ww; - *warm_white /= max_cw_ww; + if (this->color_mode_ & ColorMode::COLD_WARM_WHITE) { + const float cw_level = gamma_correct(this->cold_white_, gamma); + const float ww_level = gamma_correct(this->warm_white_, gamma); + const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + *cold_white = white_level * cw_level; + *warm_white = white_level * ww_level; + if (constant_brightness && (cw_level > 0 || ww_level > 0)) { + const float sum = cw_level + ww_level; + *cold_white /= sum; + *warm_white /= sum; + } + } else { + *cold_white = *warm_white = 0; } } /// Compare this LightColorValues to rhs, return true if and only if all attributes match. bool operator==(const LightColorValues &rhs) const { - return state_ == rhs.state_ && brightness_ == rhs.brightness_ && color_brightness_ == rhs.color_brightness_ && - red_ == rhs.red_ && green_ == rhs.green_ && blue_ == rhs.blue_ && white_ == rhs.white_ && - color_temperature_ == rhs.color_temperature_; + return color_mode_ == rhs.color_mode_ && state_ == rhs.state_ && brightness_ == rhs.brightness_ && + color_brightness_ == rhs.color_brightness_ && red_ == rhs.red_ && green_ == rhs.green_ && + blue_ == rhs.blue_ && white_ == rhs.white_ && color_temperature_ == rhs.color_temperature_ && + cold_white_ == rhs.cold_white_ && warm_white_ == rhs.warm_white_; } bool operator!=(const LightColorValues &rhs) const { return !(rhs == *this); } + /// Get the color mode of these light color values. + ColorMode get_color_mode() const { return this->color_mode_; } + /// Set the color mode of these light color values. + void set_color_mode(ColorMode color_mode) { this->color_mode_ = color_mode; } + /// Get the state of these light color values. In range from 0.0 (off) to 1.0 (on) float get_state() const { return this->state_; } /// Get the binary true/false state of these light color values. @@ -290,11 +251,20 @@ class LightColorValues { /// Get the color temperature property of these light color values in mired. float get_color_temperature() const { return this->color_temperature_; } /// Set the color temperature property of these light color values in mired. - void set_color_temperature(float color_temperature) { - this->color_temperature_ = std::max(0.000001f, color_temperature); - } + void set_color_temperature(float color_temperature) { this->color_temperature_ = color_temperature; } + + /// Get the cold white property of these light color values. In range 0.0 to 1.0. + float get_cold_white() const { return this->cold_white_; } + /// Set the cold white property of these light color values. In range 0.0 to 1.0. + void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); } + + /// Get the warm white property of these light color values. In range 0.0 to 1.0. + float get_warm_white() const { return this->warm_white_; } + /// Set the warm white property of these light color values. In range 0.0 to 1.0. + void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); } protected: + ColorMode color_mode_; float state_; ///< ON / OFF, float for transition float brightness_; float color_brightness_; @@ -303,6 +273,8 @@ class LightColorValues { float blue_; float white_; float color_temperature_; ///< Color Temperature in Mired + float cold_white_; + float warm_white_; }; } // namespace light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp new file mode 100644 index 0000000000..2e07d91046 --- /dev/null +++ b/esphome/components/light/light_json_schema.cpp @@ -0,0 +1,165 @@ +#include "light_json_schema.h" +#include "light_output.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +// See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema + +void LightJSONSchema::dump_json(LightState &state, JsonObject &root) { + if (state.supports_effects()) + root["effect"] = state.get_effect_name(); + + auto values = state.remote_values; + auto traits = state.get_output()->get_traits(); + + switch (values.get_color_mode()) { + case ColorMode::UNKNOWN: // don't need to set color mode if we don't know it + break; + case ColorMode::ON_OFF: + root["color_mode"] = "onoff"; + break; + case ColorMode::BRIGHTNESS: + root["color_mode"] = "brightness"; + break; + case ColorMode::WHITE: // not supported by HA in MQTT + root["color_mode"] = "white"; + break; + case ColorMode::COLOR_TEMPERATURE: + root["color_mode"] = "color_temp"; + break; + case ColorMode::COLD_WARM_WHITE: // not supported by HA + root["color_mode"] = "cwww"; + break; + case ColorMode::RGB: + root["color_mode"] = "rgb"; + break; + case ColorMode::RGB_WHITE: + root["color_mode"] = "rgbw"; + break; + case ColorMode::RGB_COLOR_TEMPERATURE: // not supported by HA + root["color_mode"] = "rgbct"; + break; + case ColorMode::RGB_COLD_WARM_WHITE: + root["color_mode"] = "rgbww"; + break; + } + + if (values.get_color_mode() & ColorCapability::ON_OFF) + root["state"] = (values.get_state() != 0.0f) ? "ON" : "OFF"; + if (values.get_color_mode() & ColorCapability::BRIGHTNESS) + root["brightness"] = uint8_t(values.get_brightness() * 255); + + JsonObject &color = root.createNestedObject("color"); + if (values.get_color_mode() & ColorCapability::RGB) { + color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); + color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); + color["b"] = uint8_t(values.get_color_brightness() * values.get_blue() * 255); + } + if (values.get_color_mode() & ColorCapability::WHITE) { + color["w"] = uint8_t(values.get_white() * 255); + root["white_value"] = uint8_t(values.get_white() * 255); // legacy API + } + if (values.get_color_mode() & ColorCapability::COLOR_TEMPERATURE) { + // this one isn't under the color subkey for some reason + root["color_temp"] = uint32_t(values.get_color_temperature()); + } + if (values.get_color_mode() & ColorCapability::COLD_WARM_WHITE) { + color["c"] = uint8_t(values.get_cold_white() * 255); + color["w"] = uint8_t(values.get_warm_white() * 255); + } +} + +void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject &root) { + if (root.containsKey("state")) { + auto val = parse_on_off(root["state"]); + switch (val) { + case PARSE_ON: + call.set_state(true); + break; + case PARSE_OFF: + call.set_state(false); + break; + case PARSE_TOGGLE: + call.set_state(!state.remote_values.is_on()); + break; + case PARSE_NONE: + break; + } + } + + if (root.containsKey("brightness")) { + call.set_brightness(float(root["brightness"]) / 255.0f); + } + + if (root.containsKey("color")) { + JsonObject &color = root["color"]; + // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. + float max_rgb = 0.0f; + if (color.containsKey("r")) { + float r = float(color["r"]) / 255.0f; + max_rgb = fmaxf(max_rgb, r); + call.set_red(r); + } + if (color.containsKey("g")) { + float g = float(color["g"]) / 255.0f; + max_rgb = fmaxf(max_rgb, g); + call.set_green(g); + } + if (color.containsKey("b")) { + float b = float(color["b"]) / 255.0f; + max_rgb = fmaxf(max_rgb, b); + call.set_blue(b); + } + if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { + call.set_color_brightness(max_rgb); + } + + if (color.containsKey("c")) { + call.set_cold_white(float(color["c"]) / 255.0f); + } + if (color.containsKey("w")) { + // the HA scheme is ambigious here, the same key is used for white channel in RGBW and warm + // white channel in RGBWW. + if (color.containsKey("c")) { + call.set_warm_white(float(color["w"]) / 255.0f); + } else { + call.set_white(float(color["w"]) / 255.0f); + } + } + } + + if (root.containsKey("white_value")) { // legacy API + call.set_white(float(root["white_value"]) / 255.0f); + } + + if (root.containsKey("color_temp")) { + call.set_color_temperature(float(root["color_temp"])); + } +} + +void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject &root) { + LightJSONSchema::parse_color_json(state, call, root); + + if (root.containsKey("flash")) { + auto length = uint32_t(float(root["flash"]) * 1000); + call.set_flash_length(length); + } + + if (root.containsKey("transition")) { + auto length = uint32_t(float(root["transition"]) * 1000); + call.set_transition_length(length); + } + + if (root.containsKey("effect")) { + const char *effect = root["effect"]; + call.set_effect(effect); + } +} + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_json_schema.h b/esphome/components/light/light_json_schema.h new file mode 100644 index 0000000000..bb7ff812ab --- /dev/null +++ b/esphome/components/light/light_json_schema.h @@ -0,0 +1,25 @@ +#pragma once + +#include "light_call.h" +#include "light_state.h" + +#ifdef USE_JSON + +namespace esphome { +namespace light { + +class LightJSONSchema { + public: + /// Dump the state of a light as JSON. + static void dump_json(LightState &state, JsonObject &root); + /// Parse the JSON state of a light to a LightCall. + static void parse_json(LightState &state, LightCall &call, JsonObject &root); + + protected: + static void parse_color_json(LightState &state, LightCall &call, JsonObject &root); +}; + +} // namespace light +} // namespace esphome + +#endif diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index e32b15daf1..d5bf720f61 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -16,6 +16,7 @@ LightCall LightState::toggle() { return this->make_call().set_state(!this->remot LightCall LightState::make_call() { return LightCall(this); } struct LightStateRTCState { + ColorMode color_mode{ColorMode::UNKNOWN}; bool state{false}; float brightness{1.0f}; float color_brightness{1.0f}; @@ -24,6 +25,8 @@ struct LightStateRTCState { float blue{1.0f}; float white{1.0f}; float color_temp{1.0f}; + float cold_white{1.0f}; + float warm_white{1.0f}; uint32_t effect{0}; }; @@ -64,6 +67,7 @@ void LightState::setup() { break; } + call.set_color_mode_if_supported(recovered.color_mode); call.set_state(recovered.state); call.set_brightness_if_supported(recovered.brightness); call.set_color_brightness_if_supported(recovered.color_brightness); @@ -72,6 +76,8 @@ void LightState::setup() { call.set_blue_if_supported(recovered.blue); call.set_white_if_supported(recovered.white); call.set_color_temperature_if_supported(recovered.color_temp); + call.set_cold_white_if_supported(recovered.cold_white); + call.set_warm_white_if_supported(recovered.warm_white); if (recovered.effect != 0) { call.set_effect(recovered.effect); } else { @@ -81,11 +87,11 @@ void LightState::setup() { } void LightState::dump_config() { ESP_LOGCONFIG(TAG, "Light '%s'", this->get_name().c_str()); - if (this->get_traits().get_supports_brightness()) { + if (this->get_traits().supports_color_capability(ColorCapability::BRIGHTNESS)) { ESP_LOGCONFIG(TAG, " Default Transition Length: %.1fs", this->default_transition_length_ / 1e3f); ESP_LOGCONFIG(TAG, " Gamma Correct: %.2f", this->gamma_correct_); } - if (this->get_traits().get_supports_color_temperature()) { + if (this->get_traits().supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) { ESP_LOGCONFIG(TAG, " Min Mireds: %.1f", this->get_traits().get_min_mireds()); ESP_LOGCONFIG(TAG, " Max Mireds: %.1f", this->get_traits().get_max_mireds()); } @@ -141,14 +147,6 @@ void LightState::add_new_target_state_reached_callback(std::function &&s this->target_state_reached_callback_.add(std::move(send_callback)); } -#ifdef USE_JSON -void LightState::dump_json(JsonObject &root) { - if (this->supports_effects()) - root["effect"] = this->get_effect_name(); - this->remote_values.dump_json(root, this->output_->get_traits()); -} -#endif - void LightState::set_default_transition_length(uint32_t default_transition_length) { this->default_transition_length_ = default_transition_length; } @@ -169,18 +167,17 @@ void LightState::current_values_as_brightness(float *brightness) { } void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { auto traits = this->get_traits(); - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness, bool color_interlock) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, this->gamma_correct_, constant_brightness, - traits.get_supports_color_interlock()); + warm_white, this->gamma_correct_, constant_brightness, false); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); @@ -241,6 +238,7 @@ void LightState::set_transformer_(std::unique_ptr transformer) void LightState::save_remote_values_() { LightStateRTCState saved; + saved.color_mode = this->remote_values.get_color_mode(); saved.state = this->remote_values.is_on(); saved.brightness = this->remote_values.get_brightness(); saved.color_brightness = this->remote_values.get_color_brightness(); @@ -249,6 +247,8 @@ void LightState::save_remote_values_() { saved.blue = this->remote_values.get_blue(); saved.white = this->remote_values.get_white(); saved.color_temp = this->remote_values.get_color_temperature(); + saved.cold_white = this->remote_values.get_cold_white(); + saved.warm_white = this->remote_values.get_warm_white(); saved.effect = this->active_effect_index_; this->rtc_.save(&saved); } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index cd5e6ad1cb..fb34b76367 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -93,11 +93,6 @@ class LightState : public Nameable, public Component { */ void add_new_target_state_reached_callback(std::function &&send_callback); -#ifdef USE_JSON - /// Dump the state of this light as JSON. - void dump_json(JsonObject &root); -#endif - /// Set the default transition length, i.e. the transition length when no transition is provided. void set_default_transition_length(uint32_t default_transition_length); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index ed9c0d44ea..cbade64d77 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,5 +1,8 @@ #pragma once +#include "color_mode.h" +#include + namespace esphome { namespace light { @@ -8,35 +11,49 @@ class LightTraits { public: LightTraits() = default; - bool get_supports_brightness() const { return this->supports_brightness_; } - void set_supports_brightness(bool supports_brightness) { this->supports_brightness_ = supports_brightness; } - bool get_supports_rgb() const { return this->supports_rgb_; } - void set_supports_rgb(bool supports_rgb) { this->supports_rgb_ = supports_rgb; } - bool get_supports_rgb_white_value() const { return this->supports_rgb_white_value_; } - void set_supports_rgb_white_value(bool supports_rgb_white_value) { - this->supports_rgb_white_value_ = supports_rgb_white_value; + const std::set &get_supported_color_modes() const { return this->supported_color_modes_; } + void set_supported_color_modes(std::set supported_color_modes) { + this->supported_color_modes_ = std::move(supported_color_modes); } - bool get_supports_color_temperature() const { return this->supports_color_temperature_; } - void set_supports_color_temperature(bool supports_color_temperature) { - this->supports_color_temperature_ = supports_color_temperature; + + bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode); } + bool supports_color_capability(ColorCapability color_capability) const { + for (auto mode : this->supported_color_modes_) { + if (mode & color_capability) + return true; + } + return false; } - bool get_supports_color_interlock() const { return this->supports_color_interlock_; } - void set_supports_color_interlock(bool supports_color_interlock) { - this->supports_color_interlock_ = supports_color_interlock; + + ESPDEPRECATED("get_supports_brightness() is deprecated, use color modes instead.") + bool get_supports_brightness() const { return this->supports_color_capability(ColorCapability::BRIGHTNESS); } + ESPDEPRECATED("get_supports_rgb() is deprecated, use color modes instead.") + bool get_supports_rgb() const { return this->supports_color_capability(ColorCapability::RGB); } + ESPDEPRECATED("get_supports_rgb_white_value() is deprecated, use color modes instead.") + bool get_supports_rgb_white_value() const { + return this->supports_color_mode(ColorMode::RGB_WHITE) || + this->supports_color_mode(ColorMode::RGB_COLOR_TEMPERATURE); } + ESPDEPRECATED("get_supports_color_temperature() is deprecated, use color modes instead.") + bool get_supports_color_temperature() const { + return this->supports_color_capability(ColorCapability::COLOR_TEMPERATURE); + } + ESPDEPRECATED("get_supports_color_interlock() is deprecated, use color modes instead.") + bool get_supports_color_interlock() const { + return this->supports_color_mode(ColorMode::RGB) && + (this->supports_color_mode(ColorMode::WHITE) || this->supports_color_mode(ColorMode::COLD_WARM_WHITE) || + this->supports_color_mode(ColorMode::COLOR_TEMPERATURE)); + } + float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } void set_max_mireds(float max_mireds) { this->max_mireds_ = max_mireds; } protected: - bool supports_brightness_{false}; - bool supports_rgb_{false}; - bool supports_rgb_white_value_{false}; - bool supports_color_temperature_{false}; + std::set supported_color_modes_{}; float min_mireds_{0}; float max_mireds_{0}; - bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 222be7802c..ddd0f4dd22 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -51,24 +51,45 @@ class LightTransitionTransformer : public LightTransformer { : LightTransformer(start_time, length, start_values, target_values) { // When turning light on from off state, use colors from new. if (!this->start_values_.is_on() && this->target_values_.is_on()) { + this->start_values_ = LightColorValues(target_values); this->start_values_.set_brightness(0.0f); - this->start_values_.set_red(target_values.get_red()); - this->start_values_.set_green(target_values.get_green()); - this->start_values_.set_blue(target_values.get_blue()); - this->start_values_.set_white(target_values.get_white()); - this->start_values_.set_color_temperature(target_values.get_color_temperature()); + } + + // When changing color mode, go through off state, as color modes are orthogonal and there can't be two active. + if (this->start_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->changing_color_mode_ = true; + this->intermediate_values_ = LightColorValues(this->get_start_values_()); + this->intermediate_values_.set_state(false); } } LightColorValues get_values() override { - float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); - return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); + float p = this->get_progress(); + + // Halfway through, when intermediate state (off) is reached, flip it to the target, but remain off. + if (this->changing_color_mode_ && p > 0.5f && + this->intermediate_values_.get_color_mode() != this->target_values_.get_color_mode()) { + this->intermediate_values_ = LightColorValues(this->get_end_values()); + this->intermediate_values_.set_state(false); + } + + LightColorValues &start = this->changing_color_mode_ && p > 0.5f ? this->intermediate_values_ : this->start_values_; + LightColorValues &end = this->changing_color_mode_ && p < 0.5f ? this->intermediate_values_ : this->target_values_; + if (this->changing_color_mode_) + p = p < 0.5f ? p * 2 : (p - 0.5) * 2; + + float v = LightTransitionTransformer::smoothed_progress(p); + return LightColorValues::lerp(start, end, v); } bool publish_at_end() override { return false; } bool is_transition() override { return true; } static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } + + protected: + bool changing_color_mode_{false}; + LightColorValues intermediate_values_{}; }; class LightFlashTransformer : public LightTransformer { diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 7c96cda7b1..66329f7cf9 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -13,6 +13,20 @@ AddressableLightRef = AddressableLight.operator("ref") Color = cg.esphome_ns.class_("Color") LightColorValues = light_ns.class_("LightColorValues") +# Color modes +ColorMode = light_ns.enum("ColorMode", is_class=True) +COLOR_MODES = { + "ON_OFF": ColorMode.ON_OFF, + "BRIGHTNESS": ColorMode.BRIGHTNESS, + "WHITE": ColorMode.WHITE, + "COLOR_TEMPERATURE": ColorMode.COLOR_TEMPERATURE, + "COLD_WARM_WHITE": ColorMode.COLD_WARM_WHITE, + "RGB": ColorMode.RGB, + "RGB_WHITE": ColorMode.RGB_WHITE, + "RGB_COLOR_TEMPERATURE": ColorMode.RGB_COLOR_TEMPERATURE, + "RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE, +} + # Actions ToggleAction = light_ns.class_("ToggleAction", automation.Action) LightControlAction = light_ns.class_("LightControlAction", automation.Action) diff --git a/esphome/components/monochromatic/monochromatic_light_output.h b/esphome/components/monochromatic/monochromatic_light_output.h index c3a015ff3c..f1708ae70b 100644 --- a/esphome/components/monochromatic/monochromatic_light_output.h +++ b/esphome/components/monochromatic/monochromatic_light_output.h @@ -12,7 +12,7 @@ class MonochromaticLightOutput : public light::LightOutput { void set_output(output::FloatOutput *output) { output_ = output; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 4bd0882b8c..bf483bf947 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,4 +1,5 @@ #include "mqtt_light.h" +#include "esphome/components/light/light_json_schema.h" #include "esphome/core/log.h" #ifdef USE_LIGHT @@ -14,7 +15,9 @@ std::string MQTTJSONLightComponent::component_type() const { return "light"; } void MQTTJSONLightComponent::setup() { this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject &root) { - this->state_->make_call().parse_json(root).perform(); + LightCall call = this->state_->make_call(); + LightJSONSchema::parse_json(*this->state_, call, root); + call.perform(); }); auto f = std::bind(&MQTTJSONLightComponent::publish_state_, this); @@ -24,21 +27,40 @@ void MQTTJSONLightComponent::setup() { MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : MQTTComponent(), state_(state) {} bool MQTTJSONLightComponent::publish_state_() { - return this->publish_json(this->get_state_topic_(), [this](JsonObject &root) { this->state_->dump_json(root); }); + return this->publish_json(this->get_state_topic_(), + [this](JsonObject &root) { LightJSONSchema::dump_json(*this->state_, root); }); } LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } std::string MQTTJSONLightComponent::friendly_name() const { return this->state_->get_name(); } void MQTTJSONLightComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { root["schema"] = "json"; auto traits = this->state_->get_traits(); - if (traits.get_supports_brightness()) + + root["color_mode"] = true; + JsonArray &color_modes = root.createNestedArray("supported_color_modes"); + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE)) + color_modes.add("color_temp"); + if (traits.supports_color_mode(ColorMode::RGB)) + color_modes.add("rgb"); + if (traits.supports_color_mode(ColorMode::RGB_WHITE)) + color_modes.add("rgbw"); + if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) + color_modes.add("rgbww"); + if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) + color_modes.add("brightness"); + if (traits.supports_color_mode(ColorMode::ON_OFF)) + color_modes.add("onoff"); + + // legacy API + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) root["brightness"] = true; - if (traits.get_supports_rgb()) + if (traits.supports_color_capability(ColorCapability::RGB)) root["rgb"] = true; - if (traits.get_supports_color_temperature()) + if (traits.supports_color_capability(ColorCapability::COLOR_TEMPERATURE)) root["color_temp"] = true; - if (traits.get_supports_rgb_white_value()) + if (traits.supports_color_capability(ColorCapability::WHITE)) root["white_value"] = true; + if (this->state_->supports_effects()) { root["effect"] = true; JsonArray &effect_list = root.createNestedArray("effect_list"); diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 2f279e1c9b..c7f7badc5a 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -115,8 +115,7 @@ class NeoPixelRGBLightOutput : public NeoPixelBusLightOutputBasecolor_interlock_); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_WHITE}); return traits; } void write_state(light::LightState *state) override { diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 41cba1f7a1..37bc668215 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -27,12 +27,15 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), - cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, - cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, } ), + cv.has_none_or_all_keys( + [CONF_COLD_WHITE_COLOR_TEMPERATURE, CONF_WARM_WHITE_COLOR_TEMPERATURE] + ), light.validate_color_temperature_channels, ) @@ -50,10 +53,17 @@ async def to_code(config): cwhite = await cg.get_variable(config[CONF_COLD_WHITE]) cg.add(var.set_cold_white(cwhite)) - cg.add(var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + if CONF_COLD_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_cold_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE]) + ) wwhite = await cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + if CONF_WARM_WHITE_COLOR_TEMPERATURE in config: + cg.add( + var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE]) + ) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index e14b967530..2182cc201e 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -14,17 +14,16 @@ class RGBWWLightOutput : public light::LightOutput { void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_cold_white(output::FloatOutput *cold_white) { cold_white_ = cold_white; } void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } - void set_cold_white_temperature(int cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } - void set_warm_white_temperature(int warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } + void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); - traits.set_supports_brightness(true); - traits.set_supports_rgb(true); - traits.set_supports_rgb_white_value(true); - traits.set_supports_color_temperature(true); - traits.set_supports_color_interlock(this->color_interlock_); + if (this->color_interlock_) + traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::COLD_WARM_WHITE}); + else + traits.set_supported_color_modes({light::ColorMode::RGB_COLD_WARM_WHITE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; @@ -46,8 +45,8 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *blue_; output::FloatOutput *cold_white_; output::FloatOutput *warm_white_; - int cold_white_temperature_; - int warm_white_temperature_; + float cold_white_temperature_{0}; + float warm_white_temperature_{0}; bool constant_brightness_; bool color_interlock_{false}; }; diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index d7e3561328..e0ca026248 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -45,11 +45,14 @@ void TuyaLight::dump_config() { light::LightTraits TuyaLight::get_traits() { auto traits = light::LightTraits(); - traits.set_supports_brightness(this->dimmer_id_.has_value()); - traits.set_supports_color_temperature(this->color_temperature_id_.has_value()); - if (this->color_temperature_id_.has_value()) { + if (this->color_temperature_id_.has_value() && this->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::COLOR_TEMPERATURE}); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); + } else if (this->dimmer_id_.has_value()) { + traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS}); + } else { + traits.set_supported_color_modes({light::ColorMode::ON_OFF}); } return traits; } diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index b775d44211..5e45a87e26 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -8,6 +8,10 @@ #include +#ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" +#endif + #ifdef USE_LOGGER #include #endif @@ -528,7 +532,7 @@ std::string WebServer::light_json(light::LightState *obj) { return json::build_json([obj](JsonObject &root) { root["id"] = "light-" + obj->get_object_id(); root["state"] = obj->remote_values.is_on() ? "ON" : "OFF"; - obj->dump_json(root); + light::LightJSONSchema::dump_json(*obj, root); }); } #endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index aad147dbc9..9afed58f80 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -563,6 +563,23 @@ def has_at_most_one_key(*keys): return validate +def has_none_or_all_keys(*keys): + """Validate that none or all of the given keys exist in the config.""" + + def validate(obj): + if not isinstance(obj, dict): + raise Invalid("expected dictionary") + + number = sum(k in keys for k in obj) + if number != 0 and number != len(keys): + raise Invalid( + "Must specify either none or all of {}.".format(", ".join(keys)) + ) + return obj + + return validate + + TIME_PERIOD_ERROR = ( "Time period {} should be format number + unit, for example 5ms, 5s, 5min, 5h" ) diff --git a/esphome/const.py b/esphome/const.py index 6702e773c6..5c21836c7d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -120,6 +120,7 @@ CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" CONF_COLOR = "color" CONF_COLOR_BRIGHTNESS = "color_brightness" CONF_COLOR_CORRECT = "color_correct" +CONF_COLOR_MODE = "color_mode" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" CONF_COMMAND = "command" diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index a6cf8b779c..9e9c775899 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -79,6 +79,15 @@ float gamma_correct(float value, float gamma) { return powf(value, gamma); } +float gamma_uncorrect(float value, float gamma) { + if (value <= 0.0f) + return 0.0f; + if (gamma <= 0.0f) + return value; + + return powf(value, 1 / gamma); +} + std::string to_lowercase_underscore(std::string s) { std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::replace(s.begin(), s.end(), ' ', '_'); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 808f96d4b8..6f6281fd01 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -116,6 +116,8 @@ uint8_t fast_random_8(); /// Applies gamma correction with the provided gamma to value. float gamma_correct(float value, float gamma); +/// Reverts gamma correction with the provided gamma to value. +float gamma_uncorrect(float value, float gamma); /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); From 2966a624291ee4c55c5e139abe00f6ba8ae101e1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 30 Jul 2021 10:53:33 +1200 Subject: [PATCH 628/643] Set pulse meter total to use state class measurement and last reset type auto (#2097) --- esphome/components/pulse_meter/sensor.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index e732971c3a..4b8e8a1be0 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -11,8 +11,8 @@ from esphome.const import ( CONF_TOTAL, CONF_VALUE, ICON_PULSE, + LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - STATE_CLASS_NONE, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, DEVICE_CLASS_EMPTY, @@ -59,7 +59,12 @@ CONFIG_SCHEMA = sensor.sensor_schema( cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + UNIT_PULSES, + ICON_PULSE, + 0, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_AUTO, ), } ) From 34e8979d40d55702bb590d0bbea54f93cc30cb8e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 30 Jul 2021 00:54:10 +0200 Subject: [PATCH 629/643] Convert more code to async-def syntax (#2095) --- esphome/components/anova/climate.py | 8 ++++---- esphome/components/pvvx_mithermometer/sensor.py | 14 +++++++------- esphome/components/teleinfo/__init__.py | 6 +++--- esphome/components/teleinfo/sensor/__init__.py | 8 ++++---- .../components/teleinfo/text_sensor/__init__.py | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/esphome/components/anova/climate.py b/esphome/components/anova/climate.py index ab1c9045d8..763ae1f4be 100644 --- a/esphome/components/anova/climate.py +++ b/esphome/components/anova/climate.py @@ -18,8 +18,8 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - yield ble_client.register_ble_node(var, config) + await cg.register_component(var, config) + await climate.register_climate(var, config) + await ble_client.register_ble_node(var, config) diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py index cccf92b277..16b3a86dfd 100644 --- a/esphome/components/pvvx_mithermometer/sensor.py +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -51,22 +51,22 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) + await cg.register_component(var, config) + await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature(sens)) if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + sens = await sensor.new_sensor(config[CONF_HUMIDITY]) cg.add(var.set_humidity(sens)) if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) if CONF_BATTERY_VOLTAGE in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + sens = await sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) cg.add(var.set_battery_voltage(sens)) diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index d7bf8999ef..9a5712e10f 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -22,7 +22,7 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index ffdb1509be..f45052bae9 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -18,9 +18,9 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield sensor.register_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) diff --git a/esphome/components/teleinfo/text_sensor/__init__.py b/esphome/components/teleinfo/text_sensor/__init__.py index b1ade4df41..3bd73ff272 100644 --- a/esphome/components/teleinfo/text_sensor/__init__.py +++ b/esphome/components/teleinfo/text_sensor/__init__.py @@ -20,9 +20,9 @@ CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME]) - yield cg.register_component(var, config) - yield text_sensor.register_text_sensor(var, config) - teleinfo = yield cg.get_variable(config[CONF_TELEINFO_ID]) + await cg.register_component(var, config) + await text_sensor.register_text_sensor(var, config) + teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID]) cg.add(teleinfo.register_teleinfo_listener(var)) From 80076f935d1296956a34f12099e17297f4331c8f Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 30 Jul 2021 01:55:26 -0300 Subject: [PATCH 630/643] fix diplay trigger missing base class (#2099) --- esphome/components/display/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 2dff00da03..947b09a258 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -31,7 +31,9 @@ DisplayPageShowPrevAction = display_ns.class_( DisplayIsDisplayingPageCondition = display_ns.class_( "DisplayIsDisplayingPageCondition", automation.Condition ) -DisplayOnPageChangeTrigger = display_ns.class_("DisplayOnPageChangeTrigger") +DisplayOnPageChangeTrigger = display_ns.class_( + "DisplayOnPageChangeTrigger", automation.Trigger +) CONF_ON_PAGE_CHANGE = "on_page_change" From a8b90283d84ef4f3e87d2902d6e9752d0ea1b6a2 Mon Sep 17 00:00:00 2001 From: Paul Monigatti Date: Sat, 31 Jul 2021 23:20:10 +1200 Subject: [PATCH 631/643] Fix min/max keys in MQTT Number to match Home Assistant (#2102) --- esphome/components/mqtt/mqtt_number.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 0311526340..f209f4fe20 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -39,8 +39,8 @@ void MQTTNumberComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo // https://www.home-assistant.io/integrations/number.mqtt/ if (!traits.get_icon().empty()) root["icon"] = traits.get_icon(); - root["min_value"] = traits.get_min_value(); - root["max_value"] = traits.get_max_value(); + root["min"] = traits.get_min_value(); + root["max"] = traits.get_max_value(); root["step"] = traits.get_step(); config.command_topic = true; From 593a3d48fb15a6601169d1bfcc3c00976175e243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20=C5=9Aliwi=C5=84ski?= Date: Sat, 31 Jul 2021 14:37:48 +0200 Subject: [PATCH 632/643] Use proper schema for the analog pin shorthand (#2103) The wrong error message is displayed like: > GPIO17 (TOUT) is an analog-only pin on the ESP8266. in case of the analog pin validation because the method `shorthand_analog_pin` uses wrong `GPIO_FULL_INPUT_PIN_SCHEMA` instead of `GPIO_FULL_ANALOG_PIN_SCHEMA`. --- esphome/pins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/pins.py b/esphome/pins.py index 5eef60e15d..ff4ed9d9c1 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -271,7 +271,7 @@ def shorthand_input_pullup_pin(value): def shorthand_analog_pin(value): value = analog_pin(value) - return GPIO_FULL_INPUT_PIN_SCHEMA({CONF_NUMBER: value}) + return GPIO_FULL_ANALOG_PIN_SCHEMA({CONF_NUMBER: value}) def validate_has_interrupt(value): From 81ae6709e4671c34bc3e522116aedd9ba3a02fc3 Mon Sep 17 00:00:00 2001 From: Eric Severance Date: Sun, 1 Aug 2021 01:56:05 -0700 Subject: [PATCH 633/643] Fix parity bit calculation for ESP8266SoftwareSerial (#1873) --- esphome/components/uart/uart_esp8266.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index 6f7d4c1f8b..c45f48644c 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -242,9 +242,9 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { bool parity_bit = false; bool need_parity_bit = true; if (this->parity_ == UART_CONFIG_PARITY_EVEN) - parity_bit = true; - else if (this->parity_ == UART_CONFIG_PARITY_ODD) parity_bit = false; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + parity_bit = true; else need_parity_bit = false; From 5c65f9f9ad8af021f0d73cb0eb12babd2b784c1a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 1 Aug 2021 12:21:32 +0200 Subject: [PATCH 634/643] Convert sensor_schema to use kwargs (#2094) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/adc/sensor.py | 6 +- esphome/components/ade7953/sensor.py | 34 ++-- esphome/components/ads1115/sensor.py | 6 +- esphome/components/aht10/sensor.py | 19 +- esphome/components/am2320/sensor.py | 19 +- esphome/components/apds9960/sensor.py | 6 +- esphome/components/as3935/sensor.py | 15 +- .../components/atc_mithermometer/sensor.py | 33 ++-- esphome/components/atm90e32/sensor.py | 79 ++++----- esphome/components/b_parasite/sensor.py | 33 ++-- esphome/components/bh1750/sensor.py | 6 +- .../components/binary_sensor_map/sensor.py | 10 +- .../components/ble_client/sensor/__init__.py | 6 +- esphome/components/ble_rssi/sensor.py | 10 +- esphome/components/bme280/sensor.py | 28 ++- esphome/components/bme680/sensor.py | 38 ++-- esphome/components/bme680_bsec/sensor.py | 64 +++---- esphome/components/bmp085/sensor.py | 19 +- esphome/components/bmp280/sensor.py | 19 +- esphome/components/ccs811/sensor.py | 19 +- esphome/components/cs5460a/sensor.py | 13 +- esphome/components/cse7766/sensor.py | 20 ++- esphome/components/ct_clamp/sensor.py | 6 +- esphome/components/dallas/sensor.py | 6 +- esphome/components/dht/sensor.py | 15 +- esphome/components/dht12/sensor.py | 19 +- esphome/components/duty_cycle/sensor.py | 6 +- esphome/components/esp32_hall/sensor.py | 6 +- esphome/components/fingerprint_grow/sensor.py | 26 ++- esphome/components/gps/__init__.py | 30 ++-- esphome/components/havells_solar/sensor.py | 164 +++++++++--------- esphome/components/hdc1080/sensor.py | 19 +- esphome/components/hlw8012/sensor.py | 27 +-- esphome/components/hm3301/sensor.py | 37 ++-- esphome/components/hmc5883l/sensor.py | 11 +- .../homeassistant/sensor/__init__.py | 6 +- esphome/components/hrxl_maxsonar_wr/sensor.py | 10 +- esphome/components/htu21d/sensor.py | 19 +- esphome/components/hx711/sensor.py | 6 +- esphome/components/ina219/sensor.py | 25 ++- esphome/components/ina226/sensor.py | 25 ++- esphome/components/ina3221/sensor.py | 21 ++- .../components/inkbird_ibsth1_mini/sensor.py | 37 ++-- esphome/components/max31855/sensor.py | 16 +- esphome/components/max31856/sensor.py | 10 +- esphome/components/max31865/sensor.py | 6 +- esphome/components/max6675/sensor.py | 6 +- esphome/components/mcp9808/sensor.py | 6 +- esphome/components/mhz19/sensor.py | 20 +-- esphome/components/midea_ac/climate.py | 26 +-- esphome/components/mpu6050/sensor.py | 25 ++- .../mqtt_subscribe/sensor/__init__.py | 6 +- esphome/components/ms5611/sensor.py | 20 +-- esphome/components/nextion/sensor/__init__.py | 7 +- esphome/components/ntc/sensor.py | 6 +- esphome/components/pid/sensor/__init__.py | 6 +- esphome/components/pm1006/sensor.py | 10 +- esphome/components/pmsx003/sensor.py | 56 +++--- esphome/components/pulse_counter/sensor.py | 15 +- esphome/components/pulse_meter/sensor.py | 17 +- esphome/components/pulse_width/sensor.py | 6 +- .../components/pvvx_mithermometer/sensor.py | 17 +- esphome/components/pzem004t/sensor.py | 31 ++-- esphome/components/pzemac/sensor.py | 50 +++--- esphome/components/pzemdc/sensor.py | 20 ++- esphome/components/qmc5883l/sensor.py | 11 +- esphome/components/resistance/sensor.py | 6 +- esphome/components/rotary_encoder/sensor.py | 6 +- esphome/components/ruuvitag/sensor.py | 88 +++++----- esphome/components/scd30/sensor.py | 29 ++-- esphome/components/sdm_meter/sensor.py | 94 +++++----- esphome/components/sdp3x/sensor.py | 10 +- esphome/components/sds011/sensor.py | 19 +- esphome/components/selec_meter/sensor.py | 137 ++++++++------- esphome/components/senseair/sensor.py | 10 +- esphome/components/sensor/__init__.py | 74 ++++---- esphome/components/sgp30/sensor.py | 26 ++- esphome/components/sgp40/sensor.py | 6 +- esphome/components/sht3xd/sensor.py | 19 +- esphome/components/sht4x/sensor.py | 20 +-- esphome/components/shtcx/sensor.py | 19 +- esphome/components/sm300d2/sensor.py | 65 +++---- esphome/components/sps30/sensor.py | 91 +++++----- esphome/components/sts3x/sensor.py | 6 +- esphome/components/sun/sensor/__init__.py | 6 +- esphome/components/t6615/sensor.py | 10 +- esphome/components/tcs34725/sensor.py | 17 +- .../components/teleinfo/sensor/__init__.py | 4 +- .../components/template/sensor/__init__.py | 10 +- esphome/components/tmp102/sensor.py | 6 +- esphome/components/tmp117/sensor.py | 6 +- esphome/components/tof10120/sensor.py | 10 +- .../components/total_daily_energy/sensor.py | 12 +- esphome/components/tsl2561/sensor.py | 6 +- esphome/components/tx20/sensor.py | 15 +- esphome/components/ultrasonic/sensor.py | 10 +- esphome/components/uptime/sensor.py | 6 +- esphome/components/vl53l0x/sensor.py | 10 +- esphome/components/wifi_signal/sensor.py | 10 +- esphome/components/xiaomi_cgd1/sensor.py | 28 ++- esphome/components/xiaomi_cgdk2/sensor.py | 28 ++- esphome/components/xiaomi_cgg1/sensor.py | 28 ++- esphome/components/xiaomi_gcls002/sensor.py | 38 ++-- esphome/components/xiaomi_hhccjcy01/sensor.py | 47 +++-- .../components/xiaomi_hhccpot002/sensor.py | 19 +- esphome/components/xiaomi_jqjcy01ym/sensor.py | 38 ++-- esphome/components/xiaomi_lywsd02/sensor.py | 28 ++- .../components/xiaomi_lywsd03mmc/sensor.py | 28 ++- esphome/components/xiaomi_lywsdcgq/sensor.py | 28 ++- esphome/components/xiaomi_mhoc401/sensor.py | 28 ++- esphome/components/xiaomi_miscale/sensor.py | 10 +- esphome/components/xiaomi_miscale2/sensor.py | 15 +- .../xiaomi_mjyd02yla/binary_sensor.py | 25 ++- .../components/xiaomi_wx08zm/binary_sensor.py | 16 +- esphome/components/zyaura/sensor.py | 25 ++- 115 files changed, 1337 insertions(+), 1371 deletions(-) diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 7a944a7260..7c32e4a923 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_PIN, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, ) @@ -37,7 +36,10 @@ ADCSensor = adc_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 90873f1a5e..80dafe2417 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,27 +31,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(ADE7953), cv.Optional(CONF_IRQ_PIN): pins.input_pin, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT_A): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index c521769279..da33a39041 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_GAIN, CONF_MULTIPLEXER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, CONF_ID, @@ -53,7 +52,10 @@ ADS1115Sensor = ads1115_ns.class_( CONF_ADS1115_ID = "ads1115_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py index 35168be54a..654d645966 100644 --- a/esphome/components/aht10/sensor.py +++ b/esphome/components/aht10/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AHT10Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/am2320/sensor.py b/esphome/components/am2320/sensor.py index 5d6cb9eded..088978a8f1 100644 --- a/esphome/components/am2320/sensor.py +++ b/esphome/components/am2320/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(AM2320Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/apds9960/sensor.py b/esphome/components/apds9960/sensor.py index cb0c52735d..e1990ec26e 100644 --- a/esphome/components/apds9960/sensor.py +++ b/esphome/components/apds9960/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_TYPE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_LIGHTBULB, @@ -21,7 +20,10 @@ TYPES = { } CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index a571121742..271a29e0fc 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor from esphome.const import ( CONF_DISTANCE, CONF_LIGHTNING_ENERGY, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_KILOMETER, - UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH, ) @@ -19,14 +17,15 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935), cv.Optional(CONF_DISTANCE): sensor.sensor_schema( - UNIT_KILOMETER, - ICON_SIGNAL_DISTANCE_VARIANT, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER, + icon=ICON_SIGNAL_DISTANCE_VARIANT, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema( - UNIT_EMPTY, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py index efa3f2b51a..0f6cc1abcb 100644 --- a/esphome/components/atc_mithermometer/sensor.py +++ b/esphome/components/atc_mithermometer/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,28 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(ATCMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 2c34d76b52..28b49604ff 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -12,13 +12,11 @@ from esphome.const import ( CONF_FORWARD_ACTIVE_ENERGY, CONF_REVERSE_ACTIVE_ENERGY, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, ICON_LIGHTBULB, ICON_CURRENT_AC, LAST_RESET_TYPE_AUTO, @@ -27,7 +25,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, UNIT_WATT_HOURS, @@ -65,47 +62,47 @@ ATM90E32Component = atm90e32_ns.class_( ATM90E32_PHASE_SCHEMA = cv.Schema( { cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_EMPTY, - 2, - DEVICE_CLASS_VOLTAGE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_LIGHTBULB, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + icon=ICON_LIGHTBULB, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, @@ -120,18 +117,16 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py index d93e41816b..46ed64337f 100644 --- a/esphome/components/b_parasite/sensor.py +++ b/esphome/components/b_parasite/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -33,28 +32,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(BParasite), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index e688241dcc..156c7bb375 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_RESOLUTION, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, CONF_MEASUREMENT_DURATION, @@ -28,7 +27,10 @@ BH1750Sensor = bh1750_ns.class_( CONF_MEASUREMENT_TIME = "measurement_time" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 131a050052..946e2f9e62 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -7,8 +7,6 @@ from esphome.const import ( CONF_CHANNELS, CONF_VALUE, CONF_TYPE, - DEVICE_CLASS_EMPTY, - UNIT_EMPTY, ICON_CHECK_CIRCLE_OUTLINE, CONF_BINARY_SENSOR, CONF_GROUP, @@ -35,11 +33,9 @@ entry = { CONFIG_SCHEMA = cv.typed_schema( { CONF_GROUP: sensor.sensor_schema( - UNIT_EMPTY, - ICON_CHECK_CIRCLE_OUTLINE, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + icon=ICON_CHECK_CIRCLE_OUTLINE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(BinarySensorMap), diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py index c6f05932ef..efe4bf0e9a 100644 --- a/esphome/components/ble_client/sensor/__init__.py +++ b/esphome/components/ble_client/sensor/__init__.py @@ -2,12 +2,9 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, ble_client, esp32_ble_tracker from esphome.const import ( - DEVICE_CLASS_EMPTY, CONF_ID, CONF_LAMBDA, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, CONF_TRIGGER_ID, CONF_SERVICE_UUID, ) @@ -34,7 +31,8 @@ BLESensorNotifyTrigger = ble_client_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index 819b7c6fd7..bca73328f9 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( DEVICE_CLASS_SIGNAL_STRENGTH, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL, - ICON_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -20,11 +19,10 @@ BLERSSISensor = ble_rssi_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_DECIBEL, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py index 8c6cc7ae56..dcb842d879 100644 --- a/esphome/components/bme280/sensor.py +++ b/esphome/components/bme280/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_HECTOPASCAL, @@ -49,11 +48,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -62,11 +60,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -75,11 +72,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( diff --git a/esphome/components/bme680/sensor.py b/esphome/components/bme680/sensor.py index eaa158c9f8..76472c7562 100644 --- a/esphome/components/bme680/sensor.py +++ b/esphome/components/bme680/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_OVERSAMPLING, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, @@ -20,7 +19,6 @@ from esphome.const import ( UNIT_OHM, ICON_GAS_CYLINDER, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, UNIT_PERCENT, ) @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BME680Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -72,11 +69,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -85,11 +81,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -98,11 +93,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( - UNIT_OHM, - ICON_GAS_CYLINDER, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( IIR_FILTER_OPTIONS, upper=True diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py index 4520bf3480..8d00012150 100644 --- a/esphome/components/bme680_bsec/sensor.py +++ b/esphome/components/bme680_bsec/sensor.py @@ -6,13 +6,11 @@ from esphome.const import ( CONF_HUMIDITY, CONF_PRESSURE, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - UNIT_EMPTY, UNIT_HECTOPASCAL, UNIT_OHM, UNIT_PARTS_PER_MILLION, @@ -54,54 +52,60 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ).extend( {cv.Optional(CONF_SAMPLE_RATE): cv.enum(SAMPLE_RATE_OPTIONS, upper=True)} ), cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( - UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_GAS_CYLINDER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IAQ): sensor.sensor_schema( - UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_IAQ, + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_ACCURACY, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_TEST_TUBE, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_TEST_TUBE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp085/sensor.py b/esphome/components/bmp085/sensor.py index 1b48f2e440..52f554120a 100644 --- a/esphome/components/bmp085/sensor.py +++ b/esphome/components/bmp085/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, ) @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP085Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/bmp280/sensor.py b/esphome/components/bmp280/sensor.py index 48953d0259..95a9577f7e 100644 --- a/esphome/components/bmp280/sensor.py +++ b/esphome/components/bmp280/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_HECTOPASCAL, CONF_IIR_FILTER, CONF_OVERSAMPLING, @@ -46,11 +45,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(BMP280Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( @@ -59,11 +57,10 @@ CONFIG_SCHEMA = ( } ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index 4e81d6ac10..4c09a14c3e 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -28,18 +27,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(CCS811Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), diff --git a/esphome/components/cs5460a/sensor.py b/esphome/components/cs5460a/sensor.py index efb1d1d426..82df881bfc 100644 --- a/esphome/components/cs5460a/sensor.py +++ b/esphome/components/cs5460a/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - ICON_EMPTY, DEVICE_CLASS_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_VOLTAGE, @@ -80,13 +79,19 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VOLTAGE_HPF, default=True): cv.boolean, cv.Optional(CONF_PULSE_ENERGY, default=10.0): validate_energy, cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_VOLTAGE, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_CURRENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, ), } ) diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index 4ccb346efd..1c8efc4f72 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -28,17 +27,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(CSE7766Component), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index e44d46e7f4..049905d0a7 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_SENSOR, CONF_ID, DEVICE_CLASS_CURRENT, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, ) @@ -20,7 +19,10 @@ CTClampSensor = ct_clamp_ns.class_("CTClampSensor", sensor.Sensor, cg.PollingCom CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/dallas/sensor.py b/esphome/components/dallas/sensor.py index 1c8db8fa2f..5e09701bae 100644 --- a/esphome/components/dallas/sensor.py +++ b/esphome/components/dallas/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_INDEX, CONF_RESOLUTION, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, CONF_ID, @@ -18,7 +17,10 @@ DallasTemperatureSensor = dallas_ns.class_("DallasTemperatureSensor", sensor.Sen CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(DallasTemperatureSensor), diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index c33ddd2286..1334f0270c 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -36,14 +35,16 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(): cv.declare_id(DHT), cv.Required(CONF_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MODEL, default="auto detect"): cv.enum( DHT_MODELS, upper=True, space="_" diff --git a/esphome/components/dht12/sensor.py b/esphome/components/dht12/sensor.py index 14c01f5d34..ae2173ef22 100644 --- a/esphome/components/dht12/sensor.py +++ b/esphome/components/dht12/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(DHT12Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/duty_cycle/sensor.py b/esphome/components/duty_cycle/sensor.py index 39f6ebc88f..3537cb0973 100644 --- a/esphome/components/duty_cycle/sensor.py +++ b/esphome/components/duty_cycle/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_PERCENT, @@ -18,7 +17,10 @@ DutyCycleSensor = duty_cycle_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_PERCENT, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/esp32_hall/sensor.py b/esphome/components/esp32_hall/sensor.py index b800b3436a..4ba79de714 100644 --- a/esphome/components/esp32_hall/sensor.py +++ b/esphome/components/esp32_hall/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ESP_PLATFORM_ESP32, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA, @@ -19,7 +18,10 @@ ESP32HallSensor = esp32_hall_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py index f8a44eb0da..f359a10348 100644 --- a/esphome/components/fingerprint_grow/sensor.py +++ b/esphome/components/fingerprint_grow/sensor.py @@ -8,15 +8,12 @@ from esphome.const import ( CONF_LAST_FINGER_ID, CONF_SECURITY_LEVEL, CONF_STATUS, - DEVICE_CLASS_EMPTY, ICON_ACCOUNT, ICON_ACCOUNT_CHECK, ICON_DATABASE, - ICON_EMPTY, ICON_FINGERPRINT, ICON_SECURITY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent @@ -26,22 +23,33 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( - UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_FINGERPRINT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_STATUS): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_CAPACITY): sensor.sensor_schema( - UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_DATABASE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( - UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_SECURITY, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( - UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_ACCOUNT_CHECK, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 2867aa7325..0d5c3f3f3d 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -15,9 +15,6 @@ from esphome.const import ( UNIT_DEGREES, UNIT_KILOMETER_PER_HOUR, UNIT_METER, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["uart"] @@ -36,26 +33,33 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(GPS), cv.Optional(CONF_LATITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_EMPTY, - 6, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + accuracy_decimals=6, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_COURSE): sensor.sensor_schema( - UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=2, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( - UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_METER, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_SATELLITES): sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/havells_solar/sensor.py b/esphome/components/havells_solar/sensor.py index 1926d4d68a..1d685b9b2e 100644 --- a/esphome/components/havells_solar/sensor.py +++ b/esphome/components/havells_solar/sensor.py @@ -9,12 +9,10 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -63,24 +61,47 @@ HavellsSolar = havells_solar_ns.class_( ) PHASE_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), } PV_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE_SAMPLED_BY_SECONDARY_CPU: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_INSULATION_OF_P_TO_GROUND: sensor.sensor_schema( - UNIT_KOHM, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } @@ -101,107 +122,86 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PV1): PV_SCHEMA, cv.Optional(CONF_PV2): PV_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACTIVE_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY_PRODUCTION_DAY): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_ENERGY_PRODUCTION): sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_TOTAL_GENERATION_TIME): sensor.sensor_schema( - UNIT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_HOURS, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_TODAY_GENERATION_TIME): sensor.sensor_schema( - UNIT_MINUTE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + unit_of_measurement=UNIT_MINUTE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_INVERTER_MODULE_TEMP): sensor.sensor_schema( - UNIT_DEGREES, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INVERTER_INNER_TEMP): sensor.sensor_schema( - UNIT_DEGREES, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREES, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INVERTER_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_INSULATION_OF_PV_N_TO_GROUND): sensor.sensor_schema( - UNIT_KOHM, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KOHM, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_GFCI_VALUE): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_R): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_S): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_DCI_OF_T): sensor.sensor_schema( - UNIT_MILLIAMPERE, - ICON_EMPTY, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIAMPERE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hdc1080/sensor.py b/esphome/components/hdc1080/sensor.py index 26ec3ad0a9..39727f7159 100644 --- a/esphome/components/hdc1080/sensor.py +++ b/esphome/components/hdc1080/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HDC1080Component), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index face32872e..75590f8572 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -18,7 +18,6 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -58,21 +57,29 @@ CONFIG_SCHEMA = cv.Schema( pins.internal_gpio_input_pullup_pin_schema, pins.validate_has_interrupt ), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 1, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 48a29ed5f8..fe1c6008d4 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -43,32 +42,28 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(HM3301Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AQI): sensor.sensor_schema( - UNIT_INDEX, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_INDEX, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.Required(CONF_CALCULATION_TYPE): cv.enum( diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index 65469003ed..73e7472dcf 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -80,10 +79,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/homeassistant/sensor/__init__.py b/esphome/components/homeassistant/sensor/__init__.py index 0dadb78b73..cf29db8bb8 100644 --- a/esphome/components/homeassistant/sensor/__init__.py +++ b/esphome/components/homeassistant/sensor/__init__.py @@ -5,10 +5,7 @@ from esphome.const import ( CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_ID, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import homeassistant_ns @@ -19,7 +16,8 @@ HomeassistantSensor = homeassistant_ns.class_( ) CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ).extend( { cv.GenerateID(): cv.declare_id(HomeassistantSensor), diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py index 370ee04b98..dd43bd84a7 100644 --- a/esphome/components/hrxl_maxsonar_wr/sensor.py +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -19,11 +18,10 @@ HrxlMaxsonarWrComponent = hrxlmaxsonarwr_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/htu21d/sensor.py b/esphome/components/htu21d/sensor.py index 435c5bf1bb..37422f0329 100644 --- a/esphome/components/htu21d/sensor.py +++ b/esphome/components/htu21d/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(HTU21DComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/hx711/sensor.py b/esphome/components/hx711/sensor.py index 17a4e35d5f..cd06cc770f 100644 --- a/esphome/components/hx711/sensor.py +++ b/esphome/components/hx711/sensor.py @@ -6,10 +6,8 @@ from esphome.const import ( CONF_CLK_PIN, CONF_GAIN, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_SCALE, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) hx711_ns = cg.esphome_ns.namespace("hx711") @@ -26,7 +24,9 @@ GAINS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_SCALE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_SCALE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index ed88ace967..020be9bc6e 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -13,7 +13,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,20 +31,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA219Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py index e4ceda39c1..ee4036ce7e 100644 --- a/esphome/components/ina226/sensor.py +++ b/esphome/components/ina226/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -31,20 +30,28 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(INA226Component), cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0) diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 8b861d972d..9c42ecbb9d 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -32,16 +31,28 @@ INA3221Component = ina3221_ns.class_( INA3221_CHANNEL_SCHEMA = cv.Schema( { cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 2, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All( cv.resistance, cv.Range(min=0.0, max=32.0) diff --git a/esphome/components/inkbird_ibsth1_mini/sensor.py b/esphome/components/inkbird_ibsth1_mini/sensor.py index aaaaddb890..a71921f8ed 100644 --- a/esphome/components/inkbird_ibsth1_mini/sensor.py +++ b/esphome/components/inkbird_ibsth1_mini/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,32 +31,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(InkbirdUBSTH1_MINI), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index a8b5d25c61..c7732dfbe3 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_REFERENCE_TEMPERATURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,16 +15,19 @@ MAX31855Sensor = max31855_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema(UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE) + sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + ) .extend( { cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 9583c0bcf9..083d2ac30c 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_MAINS_FILTER, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -23,11 +22,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index 64495ebd7a..33d9c42be3 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -26,7 +25,10 @@ FILTER = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index ad0e89c028..dff8360226 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor, spi from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -16,7 +15,10 @@ MAX6675Sensor = max6675_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py index d417f45955..c7f6226e0b 100644 --- a/esphome/components/mcp9808/sensor.py +++ b/esphome/components/mcp9808/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ MCP9808Sensor = mcp9808_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index ebcecb84e2..1a111f7891 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -7,13 +7,11 @@ from esphome.const import ( CONF_CO2, CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, - ICON_EMPTY, ) DEPENDENCIES = ["uart"] @@ -33,18 +31,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MHZ19Component), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, } diff --git a/esphome/components/midea_ac/climate.py b/esphome/components/midea_ac/climate.py index 00aa979515..741741fd03 100644 --- a/esphome/components/midea_ac/climate.py +++ b/esphome/components/midea_ac/climate.py @@ -63,21 +63,25 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PRESET_SLEEP, default=False): cv.boolean, cv.Optional(CONF_PRESET_BOOST, default=False): cv.boolean, cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema( - UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index 05c26289b4..f9b61dcadc 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -4,10 +4,8 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, ICON_BRIEFCASE_DOWNLOAD, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER_PER_SECOND_SQUARED, ICON_SCREEN_ROTATION, @@ -30,21 +28,22 @@ MPU6050Component = mpu6050_ns.class_( ) accel_schema = sensor.sensor_schema( - UNIT_METER_PER_SECOND_SQUARED, - ICON_BRIEFCASE_DOWNLOAD, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED, + icon=ICON_BRIEFCASE_DOWNLOAD, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) gyro_schema = sensor.sensor_schema( - UNIT_DEGREE_PER_SECOND, - ICON_SCREEN_ROTATION, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DEGREE_PER_SECOND, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) temperature_schema = sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/mqtt_subscribe/sensor/__init__.py b/esphome/components/mqtt_subscribe/sensor/__init__.py index d640b254de..420d4f152c 100644 --- a/esphome/components/mqtt_subscribe/sensor/__init__.py +++ b/esphome/components/mqtt_subscribe/sensor/__init__.py @@ -6,9 +6,6 @@ from esphome.const import ( CONF_QOS, CONF_TOPIC, STATE_CLASS_NONE, - UNIT_EMPTY, - ICON_EMPTY, - DEVICE_CLASS_EMPTY, ) from .. import mqtt_subscribe_ns @@ -21,7 +18,8 @@ MQTTSubscribeSensor = mqtt_subscribe_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ms5611/sensor.py b/esphome/components/ms5611/sensor.py index 34198e04eb..5decb13436 100644 --- a/esphome/components/ms5611/sensor.py +++ b/esphome/components/ms5611/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ICON_GAUGE, @@ -26,18 +25,17 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(MS5611Component), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_GAUGE, - 1, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + icon=ICON_GAUGE, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/nextion/sensor/__init__.py b/esphome/components/nextion/sensor/__init__.py index f8a383e3ac..8a32adc1f6 100644 --- a/esphome/components/nextion/sensor/__init__.py +++ b/esphome/components/nextion/sensor/__init__.py @@ -4,10 +4,7 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, - UNIT_EMPTY, - ICON_EMPTY, CONF_COMPONENT_ID, - DEVICE_CLASS_EMPTY, ) from .. import nextion_ns, CONF_NEXTION_ID @@ -46,7 +43,9 @@ def _validate(config): CONFIG_SCHEMA = cv.All( - sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY) + sensor.sensor_schema( + accuracy_decimals=2, + ) .extend( { cv.GenerateID(): cv.declare_id(NextionSensor), diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index e7b8c03586..bf819ffd16 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_VALUE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -120,7 +119,10 @@ def process_calibration(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index 61669d4716..d1007fcbc4 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_GAUGE, @@ -30,7 +29,10 @@ PID_CLIMATE_SENSOR_TYPES = { CONF_CLIMATE_ID = "climate_id" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PERCENT, ICON_GAUGE, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_GAUGE, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 18e1b0d87c..8ea0e303f3 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_ID, CONF_PM_2_5, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_BLUR, @@ -21,11 +20,10 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(PM1006Component), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_BLUR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_BLUR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 80f2b80e5e..935208fe03 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -10,11 +10,9 @@ from esphome.const import ( CONF_PM_2_5, CONF_TEMPERATURE, CONF_TYPE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_CHEMICAL_WEAPON, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, @@ -63,46 +61,40 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(PMSX003Component), cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 71227ec491..767728fc80 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( CONF_RISING_EDGE, CONF_NUMBER, CONF_TOTAL, - DEVICE_CLASS_EMPTY, ICON_PULSE, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -67,11 +66,10 @@ def validate_count_mode(value): CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, - ICON_PULSE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { @@ -94,7 +92,10 @@ CONFIG_SCHEMA = ( ), cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, ICON_PULSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/pulse_meter/sensor.py b/esphome/components/pulse_meter/sensor.py index 4b8e8a1be0..18da842bad 100644 --- a/esphome/components/pulse_meter/sensor.py +++ b/esphome/components/pulse_meter/sensor.py @@ -15,7 +15,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_PULSES, UNIT_PULSES_PER_MINUTE, - DEVICE_CLASS_EMPTY, ) from esphome.core import CORE @@ -51,7 +50,10 @@ def validate_pulse_meter_pin(value): CONFIG_SCHEMA = sensor.sensor_schema( - UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PULSES_PER_MINUTE, + icon=ICON_PULSE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { cv.GenerateID(): cv.declare_id(PulseMeterSensor), @@ -59,12 +61,11 @@ CONFIG_SCHEMA = sensor.sensor_schema( cv.Optional(CONF_INTERNAL_FILTER, default="13us"): validate_internal_filter, cv.Optional(CONF_TIMEOUT, default="5min"): validate_timeout, cv.Optional(CONF_TOTAL): sensor.sensor_schema( - UNIT_PULSES, - ICON_PULSE, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_PULSES, + icon=ICON_PULSE, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/pulse_width/sensor.py b/esphome/components/pulse_width/sensor.py index 6a6147c6aa..6c91104036 100644 --- a/esphome/components/pulse_width/sensor.py +++ b/esphome/components/pulse_width/sensor.py @@ -5,7 +5,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, CONF_PIN, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_SECOND, ICON_TIMER, @@ -19,7 +18,10 @@ PulseWidthSensor = pulse_width_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/pvvx_mithermometer/sensor.py b/esphome/components/pvvx_mithermometer/sensor.py index 16b3a86dfd..d6c2d68b8c 100644 --- a/esphome/components/pvvx_mithermometer/sensor.py +++ b/esphome/components/pvvx_mithermometer/sensor.py @@ -12,7 +12,6 @@ from esphome.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, UNIT_CELSIUS, UNIT_PERCENT, UNIT_VOLT, @@ -33,16 +32,24 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(PVVXMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 2, DEVICE_CLASS_TEMPERATURE + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 2, DEVICE_CLASS_HUMIDITY + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 0, DEVICE_CLASS_BATTERY + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, ), } ) diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index b358b8c650..23502e849a 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_VOLT, @@ -30,25 +29,29 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEM004T), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 2, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 1dd77a0371..1616bf0ace 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -9,13 +9,11 @@ from esphome.const import ( CONF_VOLTAGE, CONF_FREQUENCY, CONF_POWER_FACTOR, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_ENERGY, - ICON_EMPTY, ICON_CURRENT_AC, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, @@ -23,7 +21,6 @@ from esphome.const import ( UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, - UNIT_EMPTY, UNIT_WATT_HOURS, ) @@ -37,39 +34,40 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMAC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER_FACTOR, - STATE_CLASS_MEASUREMENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py index 58afea8e30..08ec688afb 100644 --- a/esphome/components/pzemdc/sensor.py +++ b/esphome/components/pzemdc/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_CURRENT, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_VOLT, UNIT_AMPERE, @@ -26,17 +25,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PZEMDC), cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CURRENT): sensor.sensor_schema( - UNIT_AMPERE, - ICON_EMPTY, - 3, - DEVICE_CLASS_CURRENT, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER): sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py index d0fdf1b77a..27d1df5b29 100644 --- a/esphome/components/qmc5883l/sensor.py +++ b/esphome/components/qmc5883l/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, - DEVICE_CLASS_EMPTY, ICON_MAGNET, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, @@ -71,10 +70,16 @@ def validate_enum(enum_values, units=None, int=True): field_strength_schema = sensor.sensor_schema( - UNIT_MICROTESLA, ICON_MAGNET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_MICROTESLA, + icon=ICON_MAGNET, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) heading_schema = sensor.sensor_schema( - UNIT_DEGREES, ICON_SCREEN_ROTATION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SCREEN_ROTATION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/resistance/sensor.py b/esphome/components/resistance/sensor.py index ca1501195a..329192e902 100644 --- a/esphome/components/resistance/sensor.py +++ b/esphome/components/resistance/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_SENSOR, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_OHM, ICON_FLASH, @@ -25,7 +24,10 @@ CONFIGURATIONS = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_OHM, ICON_FLASH, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_FLASH, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 079f00d284..311e63a7ba 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_STEPS, ICON_ROTATE_RIGHT, @@ -58,7 +57,10 @@ def validate_min_max_value(config): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_STEPS, ICON_ROTATE_RIGHT, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_STEPS, + icon=ICON_ROTATE_RIGHT, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py index 12b8425d14..342a5eff24 100644 --- a/esphome/components/ruuvitag/sensor.py +++ b/esphome/components/ruuvitag/sensor.py @@ -14,13 +14,11 @@ from esphome.const import ( CONF_TX_POWER, CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_CELSIUS, @@ -29,7 +27,6 @@ from esphome.const import ( UNIT_HECTOPASCAL, UNIT_G, UNIT_DECIBEL_MILLIWATT, - UNIT_EMPTY, ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, @@ -52,69 +49,68 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(RuuviTag), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 2, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=2, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_X, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_X, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Y, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Y, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema( - UNIT_G, - ICON_ACCELERATION_Z, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_G, + icon=ICON_ACCELERATION_Z, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TX_POWER): sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema( - UNIT_EMPTY, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + icon=ICON_GAUGE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), } ) diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 7a08289474..c0317c96e0 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -3,13 +3,11 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, CONF_HUMIDITY, CONF_TEMPERATURE, CONF_CO2, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, @@ -33,25 +31,22 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SCD30Component), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All( diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index ce560b9d4b..13cc94786c 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -17,19 +17,16 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, ICON_FLASH, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, UNIT_DEGREES, - UNIT_EMPTY, UNIT_HERTZ, UNIT_VOLT, UNIT_VOLT_AMPS, @@ -46,27 +43,43 @@ sdm_meter_ns = cg.esphome_ns.namespace("sdm_meter") SDMMeter = sdm_meter_ns.class_("SDMMeter", cg.PollingComponent, modbus.ModbusDevice) PHASE_SENSORS = { - CONF_VOLTAGE: sensor.sensor_schema(UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE), + CONF_VOLTAGE: sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 2, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 2, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + CONF_PHASE_ANGLE: sensor.sensor_schema( + unit_of_measurement=UNIT_DEGREES, icon=ICON_FLASH, accuracy_decimals=3 ), - CONF_PHASE_ANGLE: sensor.sensor_schema(UNIT_DEGREES, ICON_FLASH, 3), } PHASE_SCHEMA = cv.Schema( @@ -81,43 +94,38 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_PHASE_B): PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( - UNIT_HERTZ, - ICON_CURRENT_AC, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_ACTIVE_ENERGY): sensor.sensor_schema( - UNIT_WATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_IMPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), cv.Optional(CONF_EXPORT_REACTIVE_ENERGY): sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), } ) diff --git a/esphome/components/sdp3x/sensor.py b/esphome/components/sdp3x/sensor.py index 68e3d2812c..08d7250f6e 100644 --- a/esphome/components/sdp3x/sensor.py +++ b/esphome/components/sdp3x/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_PRESSURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_HECTOPASCAL, ) @@ -17,11 +16,10 @@ SDP3XComponent = sdp3x_ns.class_("SDP3XComponent", cg.PollingComponent, i2c.I2CD CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_HECTOPASCAL, - ICON_EMPTY, - 3, - DEVICE_CLASS_PRESSURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=3, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sds011/sensor.py b/esphome/components/sds011/sensor.py index af482839a9..0997b47ef6 100644 --- a/esphome/components/sds011/sensor.py +++ b/esphome/components/sds011/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_PM_2_5, CONF_RX_ONLY, CONF_UPDATE_INTERVAL, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, @@ -39,18 +38,16 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SDS011Component), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_RX_ONLY, default=False): cv.boolean, cv.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_minutes, diff --git a/esphome/components/selec_meter/sensor.py b/esphome/components/selec_meter/sensor.py index 7eb526aec7..2d05d00380 100644 --- a/esphome/components/selec_meter/sensor.py +++ b/esphome/components/selec_meter/sensor.py @@ -15,17 +15,14 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, UNIT_AMPERE, - UNIT_EMPTY, UNIT_HERTZ, UNIT_VOLT, UNIT_VOLT_AMPS, @@ -54,98 +51,112 @@ SelecMeter = selec_meter_ns.class_( SENSORS = { CONF_TOTAL_ACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_IMPORT_ACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_EXPORT_ACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOWATT_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_TOTAL_REACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_IMPORT_REACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_EXPORT_REACTIVE_ENERGY: sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_APPARENT_ENERGY: sensor.sensor_schema( - UNIT_KILOVOLT_AMPS_HOURS, - ICON_EMPTY, - 2, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + unit_of_measurement=UNIT_KILOVOLT_AMPS_HOURS, + accuracy_decimals=2, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ), CONF_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_REACTIVE_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_VOLTAGE: sensor.sensor_schema( - UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_CURRENT: sensor.sensor_schema( - UNIT_AMPERE, ICON_EMPTY, 3, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_POWER_FACTOR: sensor.sensor_schema( - UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_FREQUENCY: sensor.sensor_schema( - UNIT_HERTZ, ICON_CURRENT_AC, 2, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_HERTZ, + icon=ICON_CURRENT_AC, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_ACTIVE_POWER: sensor.sensor_schema( - UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_WATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_REACTIVE_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS_REACTIVE, - ICON_EMPTY, - 3, - DEVICE_CLASS_POWER, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), CONF_MAXIMUM_DEMAND_APPARENT_POWER: sensor.sensor_schema( - UNIT_VOLT_AMPS, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_VOLT_AMPS, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, ), } diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 2d40e12a09..739a8ada50 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -6,7 +6,6 @@ from esphome.components import sensor, uart from esphome.const import ( CONF_CO2, CONF_ID, - DEVICE_CLASS_EMPTY, ICON_MOLECULE_CO2, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, @@ -39,11 +38,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SenseAirComponent), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 62ed07b408..6152f2cca5 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,5 +1,4 @@ import math -from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv @@ -34,8 +33,6 @@ from esphome.const import ( LAST_RESET_TYPE_AUTO, LAST_RESET_TYPE_NEVER, LAST_RESET_TYPE_NONE, - UNIT_EMPTY, - ICON_EMPTY, DEVICE_CLASS_EMPTY, DEVICE_CLASS_BATTERY, DEVICE_CLASS_CARBON_MONOXIDE, @@ -52,7 +49,6 @@ from esphome.const import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_VOLTAGE, - STATE_CLASS_NONE, ) from esphome.core import CORE, coroutine_with_priority from esphome.util import Registry @@ -169,19 +165,19 @@ CalibrateLinearFilter = sensor_ns.class_("CalibrateLinearFilter", Filter) CalibratePolynomialFilter = sensor_ns.class_("CalibratePolynomialFilter", Filter) SensorInRangeCondition = sensor_ns.class_("SensorInRangeCondition", Filter) -unit_of_measurement = cv.string_strict -accuracy_decimals = cv.int_ -icon = cv.icon -device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") +validate_unit_of_measurement = cv.string_strict +validate_accuracy_decimals = cv.int_ +validate_icon = cv.icon +validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSensorComponent), cv.GenerateID(): cv.declare_id(Sensor), - cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement, - cv.Optional(CONF_ICON): icon, - cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, - cv.Optional(CONF_DEVICE_CLASS): device_class, + cv.Optional(CONF_UNIT_OF_MEASUREMENT): validate_unit_of_measurement, + cv.Optional(CONF_ICON): validate_icon, + cv.Optional(CONF_ACCURACY_DECIMALS): validate_accuracy_decimals, + cv.Optional(CONF_DEVICE_CLASS): validate_device_class, cv.Optional(CONF_STATE_CLASS): validate_state_class, cv.Optional(CONF_LAST_RESET_TYPE): validate_last_reset_type, cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, @@ -211,47 +207,53 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( } ) +_UNDEF = object() + def sensor_schema( - unit_of_measurement_: str, - icon_: str, - accuracy_decimals_: int, - device_class_: Optional[str] = DEVICE_CLASS_EMPTY, - state_class_: Optional[str] = STATE_CLASS_NONE, - last_reset_type_: Optional[str] = LAST_RESET_TYPE_NONE, + unit_of_measurement: str = _UNDEF, + icon: str = _UNDEF, + accuracy_decimals: int = _UNDEF, + device_class: str = _UNDEF, + state_class: str = _UNDEF, + last_reset_type: str = _UNDEF, ) -> cv.Schema: schema = SENSOR_SCHEMA - if unit_of_measurement_ != UNIT_EMPTY: + if unit_of_measurement is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement_ - ): unit_of_measurement + CONF_UNIT_OF_MEASUREMENT, default=unit_of_measurement + ): validate_unit_of_measurement } ) - if icon_ != ICON_EMPTY: - schema = schema.extend({cv.Optional(CONF_ICON, default=icon_): icon}) - if accuracy_decimals_ != 0: + if icon is not _UNDEF: + schema = schema.extend({cv.Optional(CONF_ICON, default=icon): validate_icon}) + if accuracy_decimals is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_ACCURACY_DECIMALS, default=accuracy_decimals_ - ): accuracy_decimals, + CONF_ACCURACY_DECIMALS, default=accuracy_decimals + ): validate_accuracy_decimals, } ) - if device_class_ != DEVICE_CLASS_EMPTY: - schema = schema.extend( - {cv.Optional(CONF_DEVICE_CLASS, default=device_class_): device_class} - ) - if state_class_ != STATE_CLASS_NONE: - schema = schema.extend( - {cv.Optional(CONF_STATE_CLASS, default=state_class_): validate_state_class} - ) - if last_reset_type_ != LAST_RESET_TYPE_NONE: + if device_class is not _UNDEF: schema = schema.extend( { cv.Optional( - CONF_LAST_RESET_TYPE, default=last_reset_type_ + CONF_DEVICE_CLASS, default=device_class + ): validate_device_class + } + ) + if state_class is not _UNDEF: + schema = schema.extend( + {cv.Optional(CONF_STATE_CLASS, default=state_class): validate_state_class} + ) + if last_reset_type is not _UNDEF: + schema = schema.extend( + { + cv.Optional( + CONF_LAST_RESET_TYPE, default=last_reset_type ): validate_last_reset_type } ) diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 7a3e870f6d..3e33af3b4a 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -4,14 +4,12 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, CONF_BASELINE, - DEVICE_CLASS_EMPTY, CONF_ECO2, CONF_TVOC, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_PARTS_PER_BILLION, - UNIT_EMPTY, ICON_MOLECULE_CO2, ) @@ -33,24 +31,24 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SGP30Component), cv.Required(CONF_ECO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_TVOC): sensor.sensor_schema( - UNIT_PARTS_PER_BILLION, - ICON_RADIATOR, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_BILLION, + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ECO2_BASELINE): sensor.sensor_schema( - UNIT_EMPTY, ICON_MOLECULE_CO2, 0, DEVICE_CLASS_EMPTY + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, ), cv.Optional(CONF_TVOC_BASELINE): sensor.sensor_schema( - UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY + icon=ICON_RADIATOR, + accuracy_decimals=0, ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_BASELINE): cv.Schema( diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py index 36e039d2b5..0f562048ac 100644 --- a/esphome/components/sgp40/sensor.py +++ b/esphome/components/sgp40/sensor.py @@ -3,10 +3,8 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, ICON_RADIATOR, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) DEPENDENCIES = ["i2c"] @@ -26,7 +24,9 @@ CONF_VOC_BASELINE = "voc_baseline" CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sht3xd/sensor.py b/esphome/components/sht3xd/sensor.py index 2a4bb6594e..b9e7bce733 100644 --- a/esphome/components/sht3xd/sensor.py +++ b/esphome/components/sht3xd/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT3XDComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py index a746ecde07..a66ca1a526 100644 --- a/esphome/components/sht4x/sensor.py +++ b/esphome/components/sht4x/sensor.py @@ -51,18 +51,18 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHT4XComponent), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_THERMOMETER, - 2, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=2, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 2, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=2, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py index af9379218c..ba2283a9b4 100644 --- a/esphome/components/shtcx/sensor.py +++ b/esphome/components/shtcx/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SHTCXComponent), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Required(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sm300d2/sensor.py b/esphome/components/sm300d2/sensor.py index 3d522b3bd5..73cada0eb3 100644 --- a/esphome/components/sm300d2/sensor.py +++ b/esphome/components/sm300d2/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( CONF_PM_10_0, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT, @@ -18,7 +17,6 @@ from esphome.const import ( UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, UNIT_PERCENT, - ICON_EMPTY, ICON_MOLECULE_CO2, ICON_FLASK, ICON_CHEMICAL_WEAPON, @@ -35,53 +33,46 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SM300D2Sensor), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_FLASK, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TVOC): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_GRAIN, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_GRAIN, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 0, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py index 219f68c5c8..959b427861 100644 --- a/esphome/components/sps30/sensor.py +++ b/esphome/components/sps30/sensor.py @@ -13,7 +13,6 @@ from esphome.const import ( CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, @@ -33,74 +32,64 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(SPS30Component), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_2_5): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_4_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_10_0): sensor.sensor_schema( - UNIT_MICROGRAMS_PER_CUBIC_METER, - ICON_CHEMICAL_WEAPON, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_CHEMICAL_WEAPON, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_0_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_1_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_2_5): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_4_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PMC_10_0): sensor.sensor_schema( - UNIT_COUNTS_PER_CUBIC_METER, - ICON_COUNTER, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_COUNTS_PER_CUBIC_METER, + icon=ICON_COUNTER, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_PM_SIZE): sensor.sensor_schema( - UNIT_MICROMETER, - ICON_RULER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROMETER, + icon=ICON_RULER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py index 9de077c20a..b02c835ef8 100644 --- a/esphome/components/sts3x/sensor.py +++ b/esphome/components/sts3x/sensor.py @@ -4,7 +4,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -19,7 +18,10 @@ STS3XComponent = sts3x_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py index 644490ffc6..236acfadef 100644 --- a/esphome/components/sun/sensor/__init__.py +++ b/esphome/components/sun/sensor/__init__.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_DEGREES, ICON_WEATHER_SUNSET, @@ -22,7 +21,10 @@ TYPES = { CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DEGREES, ICON_WEATHER_SUNSET, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_WEATHER_SUNSET, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py index efe4994a97..71a099d635 100644 --- a/esphome/components/t6615/sensor.py +++ b/esphome/components/t6615/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_CO2, CONF_ID, DEVICE_CLASS_CARBON_DIOXIDE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, ) @@ -21,11 +20,10 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(T6615Component), cv.Required(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_EMPTY, - 0, - DEVICE_CLASS_CARBON_DIOXIDE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + accuracy_decimals=0, + device_class=DEVICE_CLASS_CARBON_DIOXIDE, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index d0fa0c1732..6c74c86faf 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -7,9 +7,7 @@ from esphome.const import ( CONF_ID, CONF_ILLUMINANCE, CONF_INTEGRATION_TIME, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, ICON_LIGHTBULB, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, @@ -49,13 +47,22 @@ TCS34725_GAINS = { } color_channel_schema = sensor.sensor_schema( - UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_LIGHTBULB, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) color_temperature_schema = sensor.sensor_schema( - UNIT_KELVIN, ICON_THERMOMETER, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_KELVIN, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ) illuminance_schema = sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) CONFIG_SCHEMA = ( diff --git a/esphome/components/teleinfo/sensor/__init__.py b/esphome/components/teleinfo/sensor/__init__.py index f45052bae9..e7cc2fcb1b 100644 --- a/esphome/components/teleinfo/sensor/__init__.py +++ b/esphome/components/teleinfo/sensor/__init__.py @@ -9,7 +9,9 @@ CONF_TAG_NAME = "tag_name" TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0).extend( +CONFIG_SCHEMA = sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, icon=ICON_FLASH, accuracy_decimals=0 +).extend( { cv.GenerateID(): cv.declare_id(TeleInfoSensor), cv.GenerateID(CONF_TELEINFO_ID): cv.use_id(TeleInfo), diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 47027583bf..75fb505d91 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -6,10 +6,7 @@ from esphome.const import ( CONF_ID, CONF_LAMBDA, CONF_STATE, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_NONE, - UNIT_EMPTY, ) from .. import template_ns @@ -19,11 +16,8 @@ TemplateSensor = template_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_NONE, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py index b54d5646ba..c5ffbb8df5 100644 --- a/esphome/components/tmp102/sensor.py +++ b/esphome/components/tmp102/sensor.py @@ -13,7 +13,6 @@ from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -28,7 +27,10 @@ TMP102Component = tmp102_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py index c3fed06953..054864dd83 100644 --- a/esphome/components/tmp117/sensor.py +++ b/esphome/components/tmp117/sensor.py @@ -5,7 +5,6 @@ from esphome.const import ( CONF_ID, CONF_UPDATE_INTERVAL, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, ) @@ -20,7 +19,10 @@ TMP117Component = tmp117_ns.class_( CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py index 2110cbfcf8..2d3add2399 100644 --- a/esphome/components/tof10120/sensor.py +++ b/esphome/components/tof10120/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -19,11 +18,10 @@ TOF10120Sensor = tof10120_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 3, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=3, + state_class=STATE_CLASS_MEASUREMENT, ) .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) .extend(cv.polling_component_schema("60s")) diff --git a/esphome/components/total_daily_energy/sensor.py b/esphome/components/total_daily_energy/sensor.py index 7fdd176d42..df823d2997 100644 --- a/esphome/components/total_daily_energy/sensor.py +++ b/esphome/components/total_daily_energy/sensor.py @@ -5,10 +5,8 @@ from esphome.const import ( CONF_ID, CONF_TIME_ID, DEVICE_CLASS_ENERGY, - ICON_EMPTY, LAST_RESET_TYPE_AUTO, STATE_CLASS_MEASUREMENT, - UNIT_EMPTY, ) DEPENDENCIES = ["time"] @@ -21,12 +19,10 @@ TotalDailyEnergy = total_daily_energy_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_EMPTY, - ICON_EMPTY, - 0, - DEVICE_CLASS_ENERGY, - STATE_CLASS_MEASUREMENT, - LAST_RESET_TYPE_AUTO, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset_type=LAST_RESET_TYPE_AUTO, ) .extend( { diff --git a/esphome/components/tsl2561/sensor.py b/esphome/components/tsl2561/sensor.py index c05079f668..cf3837cb4d 100644 --- a/esphome/components/tsl2561/sensor.py +++ b/esphome/components/tsl2561/sensor.py @@ -6,7 +6,6 @@ from esphome.const import ( CONF_ID, CONF_INTEGRATION_TIME, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_LUX, ) @@ -41,7 +40,10 @@ TSL2561Sensor = tsl2561_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_LUX, ICON_EMPTY, 1, DEVICE_CLASS_ILLUMINANCE, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_LUX, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py index 57c3165d16..ceb9b88d8d 100644 --- a/esphome/components/tx20/sensor.py +++ b/esphome/components/tx20/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_WIND_SPEED, CONF_PIN, CONF_WIND_DIRECTION_DEGREES, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_KILOMETER_PER_HOUR, @@ -23,14 +22,16 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Tx20Component), cv.Optional(CONF_WIND_SPEED): sensor.sensor_schema( - UNIT_KILOMETER_PER_HOUR, - ICON_WEATHER_WINDY, - 1, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOMETER_PER_HOUR, + icon=ICON_WEATHER_WINDY, + accuracy_decimals=1, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_WIND_DIRECTION_DEGREES): sensor.sensor_schema( - UNIT_DEGREES, ICON_SIGN_DIRECTION, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_DEGREES, + icon=ICON_SIGN_DIRECTION, + accuracy_decimals=1, + state_class=STATE_CLASS_NONE, ), cv.Required(CONF_PIN): cv.All( pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index d5f2cef05f..f7026e884c 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -7,7 +7,6 @@ from esphome.const import ( CONF_ID, CONF_TRIGGER_PIN, CONF_TIMEOUT, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -22,11 +21,10 @@ UltrasonicSensorComponent = ultrasonic_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/uptime/sensor.py b/esphome/components/uptime/sensor.py index eaaee5a2d5..6ea3cca189 100644 --- a/esphome/components/uptime/sensor.py +++ b/esphome/components/uptime/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_NONE, UNIT_SECOND, ICON_TIMER, @@ -14,7 +13,10 @@ UptimeSensor = uptime_ns.class_("UptimeSensor", sensor.Sensor, cg.PollingCompone CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_SECOND, ICON_TIMER, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_SECOND, + icon=ICON_TIMER, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ) .extend( { diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 8a9667a1bd..0ce3197366 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import ( CONF_ID, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, @@ -42,11 +41,10 @@ def check_timeout(value): CONFIG_SCHEMA = cv.All( sensor.sensor_schema( - UNIT_METER, - ICON_ARROW_EXPAND_VERTICAL, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_METER, + icon=ICON_ARROW_EXPAND_VERTICAL, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index f1807966a2..37bee75928 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -4,7 +4,6 @@ from esphome.components import sensor from esphome.const import ( CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_DECIBEL_MILLIWATT, ) @@ -17,11 +16,10 @@ WiFiSignalSensor = wifi_signal_ns.class_( CONFIG_SCHEMA = ( sensor.sensor_schema( - UNIT_DECIBEL_MILLIWATT, - ICON_EMPTY, - 0, - DEVICE_CLASS_SIGNAL_STRENGTH, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_DECIBEL_MILLIWATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, ) .extend( { diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py index e7f18a6be9..774c87fee9 100644 --- a/esphome/components/xiaomi_cgd1/sensor.py +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -10,7 +10,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgdk2/sensor.py b/esphome/components/xiaomi_cgdk2/sensor.py index 6b2c144911..d4e7230fd0 100644 --- a/esphome/components/xiaomi_cgdk2/sensor.py +++ b/esphome/components/xiaomi_cgdk2/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py index f26a7ae54e..4e606d95f8 100644 --- a/esphome/components/xiaomi_cgg1/sensor.py +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -32,25 +31,22 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py index a5c702aa9d..4154b64233 100644 --- a/esphome/components/xiaomi_gcls002/sensor.py +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -35,32 +33,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiGCLS002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 03289a6219..1818731a0f 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -4,10 +4,8 @@ from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, CONF_TEMPERATURE, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, ICON_WATER_PERCENT, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, @@ -37,39 +35,34 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py index 8393de5e5a..82ee12d8d1 100644 --- a/esphome/components/xiaomi_hhccpot002/sensor.py +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_MAC_ADDRESS, - DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_WATER_PERCENT, @@ -28,18 +27,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_MOISTURE): sensor.sensor_schema( - UNIT_PERCENT, - ICON_WATER_PERCENT, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + icon=ICON_WATER_PERCENT, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema( - UNIT_MICROSIEMENS_PER_CENTIMETER, - ICON_FLOWER, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MICROSIEMENS_PER_CENTIMETER, + icon=ICON_FLOWER, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py index 70036eb5d9..40991c3d0f 100644 --- a/esphome/components/xiaomi_jqjcy01ym/sensor.py +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -7,10 +7,8 @@ from esphome.const import ( CONF_TEMPERATURE, CONF_ID, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, UNIT_PERCENT, @@ -34,32 +32,28 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_FORMALDEHYDE): sensor.sensor_schema( - UNIT_MILLIGRAMS_PER_CUBIC_METER, - ICON_FLASK_OUTLINE, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_MILLIGRAMS_PER_CUBIC_METER, + icon=ICON_FLASK_OUTLINE, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py index ca55f28176..339c5e673a 100644 --- a/esphome/components/xiaomi_lywsd02/sensor.py +++ b/esphome/components/xiaomi_lywsd02/sensor.py @@ -9,7 +9,6 @@ from esphome.const import ( DEVICE_CLASS_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_BATTERY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSD02), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 05b3798955..f27cee3800 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -34,25 +33,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py index 82bb4c83fb..39a207327e 100644 --- a/esphome/components/xiaomi_lywsdcgq/sensor.py +++ b/esphome/components/xiaomi_lywsdcgq/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -30,25 +29,22 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 1, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py index 5180bdbb89..57b2190150 100644 --- a/esphome/components/xiaomi_mhoc401/sensor.py +++ b/esphome/components/xiaomi_mhoc401/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( CONF_TEMPERATURE, STATE_CLASS_MEASUREMENT, UNIT_CELSIUS, - ICON_EMPTY, UNIT_PERCENT, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, @@ -33,25 +32,22 @@ CONFIG_SCHEMA = ( cv.Required(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_HUMIDITY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_miscale/sensor.py b/esphome/components/xiaomi_miscale/sensor.py index 9fe76c0645..3a112dfa34 100644 --- a/esphome/components/xiaomi_miscale/sensor.py +++ b/esphome/components/xiaomi_miscale/sensor.py @@ -8,7 +8,6 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_KILOGRAM, ICON_SCALE_BATHROOM, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -24,11 +23,10 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiMiscale), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOGRAM, + icon=ICON_SCALE_BATHROOM, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_miscale2/sensor.py b/esphome/components/xiaomi_miscale2/sensor.py index 9944098407..7cc5984c62 100644 --- a/esphome/components/xiaomi_miscale2/sensor.py +++ b/esphome/components/xiaomi_miscale2/sensor.py @@ -11,7 +11,6 @@ from esphome.const import ( UNIT_OHM, CONF_IMPEDANCE, ICON_OMEGA, - DEVICE_CLASS_EMPTY, ) DEPENDENCIES = ["esp32_ble_tracker"] @@ -27,14 +26,16 @@ CONFIG_SCHEMA = ( cv.GenerateID(): cv.declare_id(XiaomiMiscale2), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_WEIGHT): sensor.sensor_schema( - UNIT_KILOGRAM, - ICON_SCALE_BATHROOM, - 2, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_KILOGRAM, + icon=ICON_SCALE_BATHROOM, + accuracy_decimals=2, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_IMPEDANCE): sensor.sensor_schema( - UNIT_OHM, ICON_OMEGA, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_OHM, + icon=ICON_OMEGA, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 90b971c08a..fd4bae60c1 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -9,9 +9,7 @@ from esphome.const import ( CONF_LIGHT, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_ILLUMINANCE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, STATE_CLASS_NONE, UNIT_PERCENT, @@ -43,21 +41,22 @@ CONFIG_SCHEMA = cv.All( CONF_DEVICE_CLASS, default="motion" ): binary_sensor.device_class, cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema( - UNIT_MINUTE, ICON_TIMELAPSE, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + unit_of_measurement=UNIT_MINUTE, + icon=ICON_TIMELAPSE, + accuracy_decimals=0, + state_class=STATE_CLASS_NONE, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( - UNIT_LUX, - ICON_EMPTY, - 0, - DEVICE_CLASS_ILLUMINANCE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_LUX, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend( { diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py index 90d4702da4..d2b353beff 100644 --- a/esphome/components/xiaomi_wx08zm/binary_sensor.py +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -6,8 +6,6 @@ from esphome.const import ( CONF_MAC_ADDRESS, CONF_TABLET, DEVICE_CLASS_BATTERY, - DEVICE_CLASS_EMPTY, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PERCENT, ICON_BUG, @@ -32,14 +30,16 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TABLET): sensor.sensor_schema( - UNIT_PERCENT, ICON_BUG, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + icon=ICON_BUG, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( - UNIT_PERCENT, - ICON_EMPTY, - 0, - DEVICE_CLASS_BATTERY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, ), } ) diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index 5f9a5e3add..f2273afa9e 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -9,10 +9,8 @@ from esphome.const import ( CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, - DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - ICON_EMPTY, STATE_CLASS_MEASUREMENT, UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, @@ -34,21 +32,22 @@ CONFIG_SCHEMA = cv.Schema( pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt ), cv.Optional(CONF_CO2): sensor.sensor_schema( - UNIT_PARTS_PER_MILLION, - ICON_MOLECULE_CO2, - 0, - DEVICE_CLASS_EMPTY, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_PARTS_PER_MILLION, + icon=ICON_MOLECULE_CO2, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - UNIT_CELSIUS, - ICON_EMPTY, - 1, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY, STATE_CLASS_MEASUREMENT + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), } ).extend(cv.polling_component_schema("60s")) From f751c3828ed614cdc140d93c1f6b3995ba95275f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 1 Aug 2021 12:33:10 +0200 Subject: [PATCH 635/643] Fix MQTT light include (#2104) Fixes https://github.com/esphome/issues/issues/2285 --- esphome/components/mqtt/mqtt_light.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index bf483bf947..be662867cf 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -1,9 +1,9 @@ #include "mqtt_light.h" -#include "esphome/components/light/light_json_schema.h" #include "esphome/core/log.h" #ifdef USE_LIGHT +#include "esphome/components/light/light_json_schema.h" namespace esphome { namespace mqtt { From 69c7cf783e775a6e63fd3be21ed6f6349e6a3925 Mon Sep 17 00:00:00 2001 From: "John K. Luebs" Date: Sun, 1 Aug 2021 14:13:46 -0500 Subject: [PATCH 636/643] Fix missing include in light_traits.h (#2105) --- esphome/components/light/light_traits.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index cbade64d77..50da299134 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/helpers.h" #include "color_mode.h" #include From 76991cdcc4d682b4d2b0c75d0f522d8ef18fed0b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:00:51 +1200 Subject: [PATCH 637/643] Add select entities and implement template select (#2067) Co-authored-by: Otto Winter --- CODEOWNERS | 1 + esphome/components/api/api.proto | 37 ++++ esphome/components/api/api_connection.cpp | 35 ++++ esphome/components/api/api_connection.h | 5 + esphome/components/api/api_pb2.cpp | 166 ++++++++++++++++++ esphome/components/api/api_pb2.h | 39 ++++ esphome/components/api/api_pb2_service.cpp | 36 ++++ esphome/components/api/api_pb2_service.h | 15 ++ esphome/components/api/api_server.cpp | 9 + esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 4 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.cpp | 5 + esphome/components/api/subscribe_state.h | 3 + esphome/components/api/util.cpp | 15 ++ esphome/components/api/util.h | 6 + esphome/components/mqtt/__init__.py | 1 + esphome/components/mqtt/mqtt_select.cpp | 58 ++++++ esphome/components/mqtt/mqtt_select.h | 46 +++++ esphome/components/select/__init__.py | 102 +++++++++++ esphome/components/select/automation.h | 33 ++++ esphome/components/select/select.cpp | 43 +++++ esphome/components/select/select.h | 87 +++++++++ .../components/template/select/__init__.py | 74 ++++++++ .../template/select/template_select.cpp | 74 ++++++++ .../template/select/template_select.h | 37 ++++ esphome/components/web_server/web_server.cpp | 48 +++++ esphome/components/web_server/web_server.h | 9 + esphome/const.py | 3 + esphome/core/application.h | 19 ++ esphome/core/controller.cpp | 6 + esphome/core/controller.h | 6 + esphome/core/defines.h | 1 + script/ci-custom.py | 1 + tests/test5.yaml | 23 +++ 35 files changed, 1053 insertions(+) create mode 100644 esphome/components/mqtt/mqtt_select.cpp create mode 100644 esphome/components/mqtt/mqtt_select.h create mode 100644 esphome/components/select/__init__.py create mode 100644 esphome/components/select/automation.h create mode 100644 esphome/components/select/select.cpp create mode 100644 esphome/components/select/select.h create mode 100644 esphome/components/template/select/__init__.py create mode 100644 esphome/components/template/select/template_select.cpp create mode 100644 esphome/components/template/select/template_select.h diff --git a/CODEOWNERS b/CODEOWNERS index 8806489884..b235643539 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -100,6 +100,7 @@ esphome/components/script/* @esphome/core esphome/components/sdm_meter/* @jesserockz @polyfaces esphome/components/sdp3x/* @Azimath esphome/components/selec_meter/* @sourabhjaiswal +esphome/components/select/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw esphome/components/sht4x/* @sjtrny diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c04e0a32f1..c0bbbaaeab 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -39,6 +39,7 @@ service APIConnection { rpc camera_image (CameraImageRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {} rpc number_command (NumberCommandRequest) returns (void) {} + rpc select_command (SelectCommandRequest) returns (void) {} } @@ -867,3 +868,39 @@ message NumberCommandRequest { fixed32 key = 1; float state = 2; } + +// ==================== SELECT ==================== +message ListEntitiesSelectResponse { + option (id) = 52; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + repeated string options = 6; +} +message SelectStateResponse { + option (id) = 53; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the select does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; +} +message SelectCommandRequest { + option (id) = 54; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index b92c67b042..94522a13be 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -609,6 +609,41 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { } #endif +#ifdef USE_SELECT +bool APIConnection::send_select_state(select::Select *select, std::string state) { + if (!this->state_subscription_) + return false; + + SelectStateResponse resp{}; + resp.key = select->get_object_id_hash(); + resp.state = std::move(state); + resp.missing_state = !select->has_state(); + return this->send_select_state_response(resp); +} +bool APIConnection::send_select_info(select::Select *select) { + ListEntitiesSelectResponse msg; + msg.key = select->get_object_id_hash(); + msg.object_id = select->get_object_id(); + msg.name = select->get_name(); + msg.unique_id = get_default_unique_id("select", select); + msg.icon = select->traits.get_icon(); + + for (const auto &option : select->traits.get_options()) + msg.options.push_back(option); + + return this->send_list_entities_select_response(msg); +} +void APIConnection::select_command(const SelectCommandRequest &msg) { + select::Select *select = App.get_select_by_key(msg.key); + if (select == nullptr) + return; + + auto call = select->make_call(); + call.set_option(msg.state); + call.perform(); +} +#endif + #ifdef USE_ESP32_CAMERA void APIConnection::send_camera_state(std::shared_ptr image) { if (!this->state_subscription_) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 1d7fc48563..bc9839a423 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -67,6 +67,11 @@ class APIConnection : public APIServerConnection { bool send_number_state(number::Number *number, float state); bool send_number_info(number::Number *number); void number_command(const NumberCommandRequest &msg) override; +#endif +#ifdef USE_SELECT + bool send_select_state(select::Select *select, std::string state); + bool send_select_info(select::Select *select); + void select_command(const SelectCommandRequest &msg) override; #endif bool send_log_message(int level, const char *tag, const char *line); void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 79092cb511..210ba49dfc 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3574,6 +3574,172 @@ void NumberCommandRequest::dump_to(std::string &out) const { out.append("\n"); out.append("}"); } +bool ListEntitiesSelectResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 1: { + this->object_id = value.as_string(); + return true; + } + case 3: { + this->name = value.as_string(); + return true; + } + case 4: { + this->unique_id = value.as_string(); + return true; + } + case 5: { + this->icon = value.as_string(); + return true; + } + case 6: { + this->options.push_back(value.as_string()); + return true; + } + default: + return false; + } +} +bool ListEntitiesSelectResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); + buffer.encode_string(4, this->unique_id); + buffer.encode_string(5, this->icon); + for (auto &it : this->options) { + buffer.encode_string(6, it, true); + } +} +void ListEntitiesSelectResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("ListEntitiesSelectResponse {\n"); + out.append(" object_id: "); + out.append("'").append(this->object_id).append("'"); + out.append("\n"); + + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" name: "); + out.append("'").append(this->name).append("'"); + out.append("\n"); + + out.append(" unique_id: "); + out.append("'").append(this->unique_id).append("'"); + out.append("\n"); + + out.append(" icon: "); + out.append("'").append(this->icon).append("'"); + out.append("\n"); + + for (const auto &it : this->options) { + out.append(" options: "); + out.append("'").append(it).append("'"); + out.append("\n"); + } + out.append("}"); +} +bool SelectStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 3: { + this->missing_state = value.as_bool(); + return true; + } + default: + return false; + } +} +bool SelectStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectStateResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); + buffer.encode_bool(3, this->missing_state); +} +void SelectStateResponse::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectStateResponse {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + + out.append(" missing_state: "); + out.append(YESNO(this->missing_state)); + out.append("\n"); + out.append("}"); +} +bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->state = value.as_string(); + return true; + } + default: + return false; + } +} +bool SelectCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 1: { + this->key = value.as_fixed32(); + return true; + } + default: + return false; + } +} +void SelectCommandRequest::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->key); + buffer.encode_string(2, this->state); +} +void SelectCommandRequest::dump_to(std::string &out) const { + char buffer[64]; + out.append("SelectCommandRequest {\n"); + out.append(" key: "); + sprintf(buffer, "%u", this->key); + out.append(buffer); + out.append("\n"); + + out.append(" state: "); + out.append("'").append(this->state).append("'"); + out.append("\n"); + out.append("}"); +} } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 4c92b7b00d..47b0229dd5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -849,6 +849,45 @@ class NumberCommandRequest : public ProtoMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; +class ListEntitiesSelectResponse : public ProtoMessage { + public: + std::string object_id{}; + uint32_t key{0}; + std::string name{}; + std::string unique_id{}; + std::string icon{}; + std::vector options{}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; +class SelectStateResponse : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + bool missing_state{false}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SelectCommandRequest : public ProtoMessage { + public: + uint32_t key{0}; + std::string state{}; + void encode(ProtoWriteBuffer buffer) const override; + void dump_to(std::string &out) const override; + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; +}; } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 440a5d0ab3..682317801a 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -198,6 +198,20 @@ bool APIServerConnectionBase::send_number_state_response(const NumberStateRespon #endif #ifdef USE_NUMBER #endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_list_entities_select_response(const ListEntitiesSelectResponse &msg) { + ESP_LOGVV(TAG, "send_list_entities_select_response: %s", msg.dump().c_str()); + return this->send_message_(msg, 52); +} +#endif +#ifdef USE_SELECT +bool APIServerConnectionBase::send_select_state_response(const SelectStateResponse &msg) { + ESP_LOGVV(TAG, "send_select_state_response: %s", msg.dump().c_str()); + return this->send_message_(msg, 53); +} +#endif +#ifdef USE_SELECT +#endif bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { switch (msg_type) { case 1: { @@ -372,6 +386,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, msg.decode(msg_data, msg_size); ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str()); this->on_number_command_request(msg); +#endif + break; + } + case 54: { +#ifdef USE_SELECT + SelectCommandRequest msg; + msg.decode(msg_data, msg_size); + ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); + this->on_select_command_request(msg); #endif break; } @@ -583,6 +606,19 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest & this->number_command(msg); } #endif +#ifdef USE_SELECT +void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { + if (!this->is_connection_setup()) { + this->on_no_setup_connection(); + return; + } + if (!this->is_authenticated()) { + this->on_unauthenticated_access(); + return; + } + this->select_command(msg); +} +#endif } // namespace api } // namespace esphome diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 398c10a811..1b8d990b05 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -120,6 +120,15 @@ class APIServerConnectionBase : public ProtoService { #endif #ifdef USE_NUMBER virtual void on_number_command_request(const NumberCommandRequest &value){}; +#endif +#ifdef USE_SELECT + bool send_list_entities_select_response(const ListEntitiesSelectResponse &msg); +#endif +#ifdef USE_SELECT + bool send_select_state_response(const SelectStateResponse &msg); +#endif +#ifdef USE_SELECT + virtual void on_select_command_request(const SelectCommandRequest &value){}; #endif protected: bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; @@ -159,6 +168,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_NUMBER virtual void number_command(const NumberCommandRequest &msg) = 0; +#endif +#ifdef USE_SELECT + virtual void select_command(const SelectCommandRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -194,6 +206,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_NUMBER void on_number_command_request(const NumberCommandRequest &msg) override; #endif +#ifdef USE_SELECT + void on_select_command_request(const SelectCommandRequest &msg) override; +#endif }; } // namespace api diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 7434030565..d48c0a4fd8 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -206,6 +206,15 @@ void APIServer::on_number_update(number::Number *obj, float state) { } #endif +#ifdef USE_SELECT +void APIServer::on_select_update(select::Select *obj, const std::string &state) { + if (obj->is_internal()) + return; + for (auto *c : this->clients_) + c->send_select_state(obj, state); +} +#endif + float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } void APIServer::set_port(uint16_t port) { this->port_ = port; } APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 68d1df2c1f..96b3192e9e 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -62,6 +62,9 @@ class APIServer : public Component, public Controller { #endif #ifdef USE_NUMBER void on_number_update(number::Number *obj, float state) override; +#endif +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; #endif void send_homeassistant_service_call(const HomeassistantServiceResponse &call); void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 8897758073..745dd92c89 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -55,5 +55,9 @@ bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this-> bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); } #endif +#ifdef USE_SELECT +bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } +#endif + } // namespace api } // namespace esphome diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index c55ba5089e..c728fb0a97 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -42,6 +42,9 @@ class ListEntitiesIterator : public ComponentIterator { #endif #ifdef USE_NUMBER bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif bool on_end() override; diff --git a/esphome/components/api/subscribe_state.cpp b/esphome/components/api/subscribe_state.cpp index 25aa7c8b31..1b8453f233 100644 --- a/esphome/components/api/subscribe_state.cpp +++ b/esphome/components/api/subscribe_state.cpp @@ -42,6 +42,11 @@ bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number, number->state); } #endif +#ifdef USE_SELECT +bool InitialStateIterator::on_select(select::Select *select) { + return this->client_->send_select_state(select, select->state); +} +#endif InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client) : ComponentIterator(server), client_(client) {} diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index f03322ac4a..beb9b947d4 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -39,6 +39,9 @@ class InitialStateIterator : public ComponentIterator { #endif #ifdef USE_NUMBER bool on_number(number::Number *number) override; +#endif +#ifdef USE_SELECT + bool on_select(select::Select *select) override; #endif protected: APIConnection *client_; diff --git a/esphome/components/api/util.cpp b/esphome/components/api/util.cpp index 6e05d49b74..5085994607 100644 --- a/esphome/components/api/util.cpp +++ b/esphome/components/api/util.cpp @@ -182,6 +182,21 @@ void ComponentIterator::advance() { } } break; +#endif +#ifdef USE_SELECT + case IteratorState::SELECT: + if (this->at_ >= App.get_selects().size()) { + advance_platform = true; + } else { + auto *select = App.get_selects()[this->at_]; + if (select->is_internal()) { + success = true; + break; + } else { + success = this->on_select(select); + } + } + break; #endif case IteratorState::MAX: if (this->on_end()) { diff --git a/esphome/components/api/util.h b/esphome/components/api/util.h index f8b248056b..e404a95619 100644 --- a/esphome/components/api/util.h +++ b/esphome/components/api/util.h @@ -50,6 +50,9 @@ class ComponentIterator { #endif #ifdef USE_NUMBER virtual bool on_number(number::Number *number) = 0; +#endif +#ifdef USE_SELECT + virtual bool on_select(select::Select *select) = 0; #endif virtual bool on_end(); @@ -87,6 +90,9 @@ class ComponentIterator { #endif #ifdef USE_NUMBER NUMBER, +#endif +#ifdef USE_SELECT + SELECT, #endif MAX, } state_{IteratorState::NONE}; diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 3559fce046..56ea9027af 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -92,6 +92,7 @@ MQTTSensorComponent = mqtt_ns.class_("MQTTSensorComponent", MQTTComponent) MQTTSwitchComponent = mqtt_ns.class_("MQTTSwitchComponent", MQTTComponent) MQTTTextSensor = mqtt_ns.class_("MQTTTextSensor", MQTTComponent) MQTTNumberComponent = mqtt_ns.class_("MQTTNumberComponent", MQTTComponent) +MQTTSelectComponent = mqtt_ns.class_("MQTTSelectComponent", MQTTComponent) def validate_config(value): diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp new file mode 100644 index 0000000000..c0ac472d46 --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -0,0 +1,58 @@ +#include "mqtt_select.h" +#include "esphome/core/log.h" + +#ifdef USE_SELECT + +namespace esphome { +namespace mqtt { + +static const char *const TAG = "mqtt.select"; + +using namespace esphome::select; + +MQTTSelectComponent::MQTTSelectComponent(Select *select) : MQTTComponent(), select_(select) {} + +void MQTTSelectComponent::setup() { + this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &state) { + auto call = this->select_->make_call(); + call.set_option(state); + call.perform(); + }); + this->select_->add_on_state_callback([this](const std::string &state) { this->publish_state(state); }); +} + +void MQTTSelectComponent::dump_config() { + ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str()); + LOG_MQTT_COMPONENT(true, false) +} + +std::string MQTTSelectComponent::component_type() const { return "select"; } + +std::string MQTTSelectComponent::friendly_name() const { return this->select_->get_name(); } +void MQTTSelectComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { + const auto &traits = select_->traits; + // https://www.home-assistant.io/integrations/select.mqtt/ + if (!traits.get_icon().empty()) + root["icon"] = traits.get_icon(); + JsonArray &options = root.createNestedArray("options"); + for (const auto &option : traits.get_options()) + options.add(option); + + config.command_topic = true; +} +bool MQTTSelectComponent::send_initial_state() { + if (this->select_->has_state()) { + return this->publish_state(this->select_->state); + } else { + return true; + } +} +bool MQTTSelectComponent::is_internal() { return this->select_->is_internal(); } +bool MQTTSelectComponent::publish_state(const std::string &value) { + return this->publish(this->get_state_topic_(), value); +} + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h new file mode 100644 index 0000000000..013e905ead --- /dev/null +++ b/esphome/components/mqtt/mqtt_select.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_SELECT + +#include "esphome/components/select/select.h" +#include "mqtt_component.h" + +namespace esphome { +namespace mqtt { + +class MQTTSelectComponent : public mqtt::MQTTComponent { + public: + /** Construct this MQTTSelectComponent instance with the provided friendly_name and select + * + * @param select The select. + */ + explicit MQTTSelectComponent(select::Select *select); + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + /// Override setup. + void setup() override; + void dump_config() override; + + void send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) override; + + bool send_initial_state() override; + bool is_internal() override; + + bool publish_state(const std::string &value); + + protected: + /// Override for MQTTComponent, returns "select". + std::string component_type() const override; + + std::string friendly_name() const override; + + select::Select *select_; +}; + +} // namespace mqtt +} // namespace esphome + +#endif diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py new file mode 100644 index 0000000000..5be09b0832 --- /dev/null +++ b/esphome/components/select/__init__.py @@ -0,0 +1,102 @@ +from typing import List +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import mqtt +from esphome.const import ( + CONF_ICON, + CONF_ID, + CONF_INTERNAL, + CONF_ON_VALUE, + CONF_OPTION, + CONF_TRIGGER_ID, + CONF_NAME, + CONF_MQTT_ID, + ICON_EMPTY, +) +from esphome.core import CORE, coroutine_with_priority + +CODEOWNERS = ["@esphome/core"] +IS_PLATFORM_COMPONENT = True + +select_ns = cg.esphome_ns.namespace("select") +Select = select_ns.class_("Select", cg.Nameable) +SelectPtr = Select.operator("ptr") + +# Triggers +SelectStateTrigger = select_ns.class_( + "SelectStateTrigger", automation.Trigger.template(cg.float_) +) + +# Actions +SelectSetAction = select_ns.class_("SelectSetAction", automation.Action) + +icon = cv.icon + + +SELECT_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( + { + cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTSelectComponent), + cv.GenerateID(): cv.declare_id(Select), + cv.Optional(CONF_ICON, default=ICON_EMPTY): icon, + cv.Optional(CONF_ON_VALUE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SelectStateTrigger), + } + ), + } +) + + +async def setup_select_core_(var, config, *, options: List[str]): + cg.add(var.set_name(config[CONF_NAME])) + if CONF_INTERNAL in config: + cg.add(var.set_internal(config[CONF_INTERNAL])) + + cg.add(var.traits.set_icon(config[CONF_ICON])) + cg.add(var.traits.set_options(options)) + + for conf in config.get(CONF_ON_VALUE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + if CONF_MQTT_ID in config: + mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var) + await mqtt.register_mqtt_component(mqtt_, config) + + +async def register_select(var, config, *, options: List[str]): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + cg.add(cg.App.register_select(var)) + await setup_select_core_(var, config, options=options) + + +async def new_select(config, *, options: List[str]): + var = cg.new_Pvariable(config[CONF_ID]) + await register_select(var, config, options=options) + return var + + +@coroutine_with_priority(40.0) +async def to_code(config): + cg.add_define("USE_SELECT") + cg.add_global(select_ns.using) + + +@automation.register_action( + "select.set", + SelectSetAction, + cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(Select), + cv.Required(CONF_OPTION): cv.templatable(cv.string_strict), + } + ), +) +async def select_set_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_OPTION], args, str) + cg.add(var.set_option(template_)) + return var diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h new file mode 100644 index 0000000000..59525f879e --- /dev/null +++ b/esphome/components/select/automation.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "select.h" + +namespace esphome { +namespace select { + +class SelectStateTrigger : public Trigger { + public: + explicit SelectStateTrigger(Select *parent) { + parent->add_on_state_callback([this](std::string value) { this->trigger(std::move(value)); }); + } +}; + +template class SelectSetAction : public Action { + public: + SelectSetAction(Select *select) : select_(select) {} + TEMPLATABLE_VALUE(std::string, option) + + void play(Ts... x) override { + auto call = this->select_->make_call(); + call.set_option(this->option_.value(x...)); + call.perform(); + } + + protected: + Select *select_; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp new file mode 100644 index 0000000000..14f4d9277d --- /dev/null +++ b/esphome/components/select/select.cpp @@ -0,0 +1,43 @@ +#include "select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace select { + +static const char *const TAG = "select"; + +void SelectCall::perform() { + ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str()); + if (!this->option_.has_value()) { + ESP_LOGW(TAG, "No value set for SelectCall"); + return; + } + + const auto &traits = this->parent_->traits; + auto value = *this->option_; + auto options = traits.get_options(); + + if (std::find(options.begin(), options.end(), value) == options.end()) { + ESP_LOGW(TAG, " Option %s is not a valid option.", value.c_str()); + return; + } + + ESP_LOGD(TAG, " Option: %s", (*this->option_).c_str()); + this->parent_->control(*this->option_); +} + +void Select::publish_state(const std::string &state) { + this->has_state_ = true; + this->state = state; + ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str()); + this->state_callback_.call(state); +} + +void Select::add_on_state_callback(std::function &&callback) { + this->state_callback_.add(std::move(callback)); +} + +uint32_t Select::hash_base() { return 2812997003UL; } + +} // namespace select +} // namespace esphome diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h new file mode 100644 index 0000000000..414a8daabb --- /dev/null +++ b/esphome/components/select/select.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace select { + +#define LOG_SELECT(prefix, type, obj) \ + if ((obj) != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, (obj)->get_name().c_str()); \ + if (!(obj)->traits.get_icon().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->traits.get_icon().c_str()); \ + } \ + } + +class Select; + +class SelectCall { + public: + explicit SelectCall(Select *parent) : parent_(parent) {} + void perform(); + + SelectCall &set_option(const std::string &option) { + option_ = option; + return *this; + } + const optional &get_option() const { return option_; } + + protected: + Select *const parent_; + optional option_; +}; + +class SelectTraits { + public: + void set_options(std::vector options) { this->options_ = std::move(options); } + const std::vector get_options() const { return this->options_; } + void set_icon(std::string icon) { icon_ = std::move(icon); } + const std::string &get_icon() const { return icon_; } + + protected: + std::vector options_; + std::string icon_; +}; + +/** Base-class for all selects. + * + * A select can use publish_state to send out a new value. + */ +class Select : public Nameable { + public: + std::string state; + + void publish_state(const std::string &state); + + SelectCall make_call() { return SelectCall(this); } + void set(const std::string &value) { make_call().set_option(value).perform(); } + + void add_on_state_callback(std::function &&callback); + + SelectTraits traits; + + /// Return whether this select has gotten a full state yet. + bool has_state() const { return has_state_; } + + protected: + friend class SelectCall; + + /** Set the value of the select, this is a virtual method that each select integration must implement. + * + * This method is called by the SelectCall. + * + * @param value The value as validated by the SelectCall. + */ + virtual void control(const std::string &value) = 0; + + uint32_t hash_base() override; + + CallbackManager state_callback_; + bool has_state_{false}; +}; + +} // namespace select +} // namespace esphome diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py new file mode 100644 index 0000000000..4044a407f3 --- /dev/null +++ b/esphome/components/template/select/__init__.py @@ -0,0 +1,74 @@ +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import select +from esphome.const import ( + CONF_ID, + CONF_INITIAL_OPTION, + CONF_LAMBDA, + CONF_OPTIONS, + CONF_OPTIMISTIC, + CONF_RESTORE_VALUE, +) +from .. import template_ns + +TemplateSelect = template_ns.class_( + "TemplateSelect", select.Select, cg.PollingComponent +) + +CONF_SET_ACTION = "set_action" + + +def validate_initial_value_in_options(config): + if CONF_INITIAL_OPTION in config: + if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: + raise cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + ) + else: + config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] + return config + + +CONFIG_SCHEMA = cv.All( + select.SELECT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(TemplateSelect), + cv.Required(CONF_OPTIONS): cv.All( + cv.ensure_list(cv.string_strict), cv.Length(min=1) + ), + cv.Optional(CONF_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_OPTIMISTIC): cv.boolean, + cv.Optional(CONF_SET_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_INITIAL_OPTION): cv.string_strict, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ).extend(cv.polling_component_schema("60s")), + validate_initial_value_in_options, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await select.register_select(var, config, options=config[CONF_OPTIONS]) + + if CONF_LAMBDA in config: + template_ = await cg.process_lambda( + config[CONF_LAMBDA], [], return_type=cg.optional.template(str) + ) + cg.add(var.set_template(template_)) + + else: + if CONF_OPTIMISTIC in config: + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + + cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) + + if CONF_RESTORE_VALUE in config: + cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) + + if CONF_SET_ACTION in config: + await automation.build_automation( + var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] + ) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp new file mode 100644 index 0000000000..782c0ee6f9 --- /dev/null +++ b/esphome/components/template/select/template_select.cpp @@ -0,0 +1,74 @@ +#include "template_select.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace template_ { + +static const char *const TAG = "template.select"; + +void TemplateSelect::setup() { + if (this->f_.has_value()) + return; + + std::string value; + ESP_LOGD(TAG, "Setting up Template Number"); + if (!this->restore_value_) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial: %s", value.c_str()); + } else { + size_t index; + this->pref_ = global_preferences.make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&index)) { + value = this->initial_option_; + ESP_LOGD(TAG, "State from initial (could not load): %s", value.c_str()); + } else { + value = this->traits.get_options().at(index); + ESP_LOGD(TAG, "State from restore: %s", value.c_str()); + } + } + + this->publish_state(value); +} + +void TemplateSelect::update() { + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (!val.has_value()) + return; + + auto options = this->traits.get_options(); + if (std::find(options.begin(), options.end(), *val) == options.end()) { + ESP_LOGE(TAG, "lambda returned an invalid option %s", (*val).c_str()); + return; + } + + this->publish_state(*val); +} + +void TemplateSelect::control(const std::string &value) { + this->set_trigger_->trigger(value); + + if (this->optimistic_) + this->publish_state(value); + + if (this->restore_value_) { + auto options = this->traits.get_options(); + size_t index = std::find(options.begin(), options.end(), value) - options.begin(); + + this->pref_.save(&index); + } +} +void TemplateSelect::dump_config() { + LOG_SELECT("", "Template Select", this); + LOG_UPDATE_INTERVAL(this); + if (this->f_.has_value()) + return; + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); + ESP_LOGCONFIG(TAG, " Initial Option: %s", this->initial_option_.c_str()); + ESP_LOGCONFIG(TAG, " Restore Value: %s", YESNO(this->restore_value_)); +} + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h new file mode 100644 index 0000000000..e24eb6e880 --- /dev/null +++ b/esphome/components/template/select/template_select.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace template_ { + +class TemplateSelect : public select::Select, public PollingComponent { + public: + void set_template(std::function()> &&f) { this->f_ = f; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + Trigger *get_set_trigger() const { return this->set_trigger_; } + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_initial_option(std::string initial_option) { this->initial_option_ = std::move(initial_option); } + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(const std::string &value) override; + bool optimistic_ = false; + std::string initial_option_; + bool restore_value_ = false; + Trigger *set_trigger_ = new Trigger(); + optional()>> f_; + + ESPPreferenceObject pref_; +}; + +} // namespace template_ +} // namespace esphome diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 5e45a87e26..9dad61bb5b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -129,6 +129,12 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->number_json(obj, obj->state).c_str(), "state"); #endif + +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + if (!obj->is_internal()) + client->send(this->select_json(obj, obj->state).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -211,6 +217,11 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "number", ""); #endif +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) + write_row(stream, obj, "select", ""); +#endif + stream->print(F("

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

events_.send(this->select_json(obj, state).c_str(), "state"); +} +void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) { + for (auto *obj : App.get_selects()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + std::string data = this->select_json(obj, obj->state); + request->send(200, "text/json", data.c_str()); + return; + } + request->send(404); +} +std::string WebServer::select_json(select::Select *obj, const std::string &value) { + return json::build_json([obj, value](JsonObject &root) { + root["id"] = "select-" + obj->get_object_id(); + root["state"] = value; + root["value"] = value; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; @@ -683,6 +719,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_SELECT + if (request->method() == HTTP_GET && match.domain == "select") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -765,6 +806,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_SELECT + if (match.domain == "select") { + this->handle_select_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 4789c6e1c0..54d7356ac9 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -163,6 +163,15 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string number_json(number::Number *obj, float value); #endif +#ifdef USE_SELECT + void on_select_update(select::Select *obj, const std::string &state) override; + /// Handle a select request under '/select/'. + void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match); + + /// Dump the number state with its value as a JSON string. + std::string select_json(select::Select *obj, const std::string &value); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. diff --git a/esphome/const.py b/esphome/const.py index 5c21836c7d..ae6c5716d9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -278,6 +278,7 @@ CONF_INCLUDES = "includes" CONF_INDEX = "index" CONF_INDOOR = "indoor" CONF_INITIAL_MODE = "initial_mode" +CONF_INITIAL_OPTION = "initial_option" CONF_INITIAL_VALUE = "initial_value" CONF_INTEGRATION_TIME = "integration_time" CONF_INTENSITY = "intensity" @@ -407,6 +408,8 @@ CONF_OPEN_DRAIN_INTERRUPT = "open_drain_interrupt" CONF_OPEN_DURATION = "open_duration" CONF_OPEN_ENDSTOP = "open_endstop" CONF_OPTIMISTIC = "optimistic" +CONF_OPTION = "option" +CONF_OPTIONS = "options" CONF_OR = "or" CONF_OSCILLATING = "oscillating" CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" diff --git a/esphome/core/application.h b/esphome/core/application.h index e065552a74..edbfbd130b 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -35,6 +35,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { @@ -89,6 +92,10 @@ class Application { void register_number(number::Number *number) { this->numbers_.push_back(number); } #endif +#ifdef USE_SELECT + void register_select(select::Select *select) { this->selects_.push_back(select); } +#endif + /// Register the component in this Application instance. template C *register_component(C *c) { static_assert(std::is_base_of::value, "Only Component subclasses can be registered"); @@ -224,6 +231,15 @@ class Application { return nullptr; } #endif +#ifdef USE_SELECT + const std::vector &get_selects() { return this->selects_; } + select::Select *get_select_by_key(uint32_t key, bool include_internal = false) { + for (auto *obj : this->selects_) + if (obj->get_object_id_hash() == key && (include_internal || !obj->is_internal())) + return obj; + return nullptr; + } +#endif Scheduler scheduler; @@ -264,6 +280,9 @@ class Application { #ifdef USE_NUMBER std::vector numbers_{}; #endif +#ifdef USE_SELECT + std::vector selects_{}; +#endif std::string name_; std::string compilation_time_; diff --git a/esphome/core/controller.cpp b/esphome/core/controller.cpp index 305fe93532..1d25be41f2 100644 --- a/esphome/core/controller.cpp +++ b/esphome/core/controller.cpp @@ -59,6 +59,12 @@ void Controller::setup_controller() { obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); }); } #endif +#ifdef USE_SELECT + for (auto *obj : App.get_selects()) { + if (!obj->is_internal()) + obj->add_on_state_callback([this, obj](const std::string &state) { this->on_select_update(obj, state); }); + } +#endif } } // namespace esphome diff --git a/esphome/core/controller.h b/esphome/core/controller.h index 746658075f..0de8f7ea19 100644 --- a/esphome/core/controller.h +++ b/esphome/core/controller.h @@ -28,6 +28,9 @@ #ifdef USE_NUMBER #include "esphome/components/number/number.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif namespace esphome { @@ -61,6 +64,9 @@ class Controller { #ifdef USE_NUMBER virtual void on_number_update(number::Number *obj, float state){}; #endif +#ifdef USE_SELECT + virtual void on_select_update(select::Select *obj, const std::string &state){}; +#endif }; } // namespace esphome diff --git a/esphome/core/defines.h b/esphome/core/defines.h index cac03fc703..5c176d1b33 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -14,6 +14,7 @@ #define USE_LIGHT #define USE_CLIMATE #define USE_NUMBER +#define USE_SELECT #define USE_MQTT #define USE_POWER_SUPPLY #define USE_HOMEASSISTANT_TIME diff --git a/script/ci-custom.py b/script/ci-custom.py index d79e5b5e2f..5dad3e2445 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -563,6 +563,7 @@ def lint_inclusive_language(fname, match): "esphome/components/output/binary_output.h", "esphome/components/output/float_output.h", "esphome/components/nextion/nextion_base.h", + "esphome/components/select/select.h", "esphome/components/sensor/sensor.h", "esphome/components/stepper/stepper.h", "esphome/components/switch/switch.h", diff --git a/tests/test5.yaml b/tests/test5.yaml index 1a2dde1010..6ccb83a11a 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -82,6 +82,29 @@ number: min_value: 0 step: 5 +select: + - platform: template + name: My template select + id: template_select_id + optimistic: true + initial_option: two + restore_value: true + on_value: + - logger.log: + format: "Select changed to %s" + args: ["x.c_str()"] + set_action: + - logger.log: + format: "Template Select set to %s" + args: ["x.c_str()"] + - select.set: + id: template_select_id + option: two + options: + - one + - two + - three + sensor: - platform: selec_meter total_active_energy: From b58ca46a465b022ab03aa824bfeb7df79f119530 Mon Sep 17 00:00:00 2001 From: Carlos Garcia Saura Date: Mon, 2 Aug 2021 10:28:25 +0200 Subject: [PATCH 638/643] [duty_cycle] initialize two missing variables (#2088) --- esphome/components/duty_cycle/duty_cycle_sensor.cpp | 1 + esphome/components/duty_cycle/duty_cycle_sensor.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 8b7446b681..c989421948 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -13,6 +13,7 @@ void DutyCycleSensor::setup() { this->store_.pin = this->pin_->to_isr(); this->store_.last_level = this->pin_->digital_read(); this->last_update_ = micros(); + this->store_.last_interrupt = micros(); this->pin_->attach_interrupt(DutyCycleSensorStore::gpio_intr, &this->store_, CHANGE); } diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index 2205bec729..e168f20eff 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -29,7 +29,7 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { protected: GPIOPin *pin_; - DutyCycleSensorStore store_; + DutyCycleSensorStore store_{}; uint32_t last_update_; }; From fb24e55c8de8468b1e0b681b43b2b6d1dbcdbc9e Mon Sep 17 00:00:00 2001 From: "John \"Warthog9\" Hawley" Date: Mon, 2 Aug 2021 01:32:08 -0700 Subject: [PATCH 639/643] pmsx003: add standard particle, particle counts (#1694) --- esphome/components/pmsx003/pmsx003.cpp | 114 ++++++++++++++++++++----- esphome/components/pmsx003/pmsx003.h | 29 +++++++ esphome/components/pmsx003/sensor.py | 100 ++++++++++++++++++++++ esphome/const.py | 10 +++ tests/test3.yaml | 60 +++++++++++++ 5 files changed, 290 insertions(+), 23 deletions(-) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index abea287c0b..0474d6ffd0 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -6,9 +6,39 @@ namespace pmsx003 { static const char *const TAG = "pmsx003"; +void PMSX003Component::set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { + pm_1_0_std_sensor_ = pm_1_0_std_sensor; +} +void PMSX003Component::set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { + pm_2_5_std_sensor_ = pm_2_5_std_sensor; +} +void PMSX003Component::set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor) { + pm_10_0_std_sensor_ = pm_10_0_std_sensor; +} + void PMSX003Component::set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void PMSX003Component::set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void PMSX003Component::set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + +void PMSX003Component::set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor) { + pm_particles_03um_sensor_ = pm_particles_03um_sensor; +} +void PMSX003Component::set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor) { + pm_particles_05um_sensor_ = pm_particles_05um_sensor; +} +void PMSX003Component::set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor) { + pm_particles_10um_sensor_ = pm_particles_10um_sensor; +} +void PMSX003Component::set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor) { + pm_particles_25um_sensor_ = pm_particles_25um_sensor; +} +void PMSX003Component::set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor) { + pm_particles_50um_sensor_ = pm_particles_50um_sensor; +} +void PMSX003Component::set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor) { + pm_particles_100um_sensor_ = pm_particles_100um_sensor; +} + void PMSX003Component::set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } @@ -102,19 +132,68 @@ optional PMSX003Component::check_byte_() { void PMSX003Component::parse_data_() { switch (this->type_) { + case PMSX003_TYPE_5003ST: { + uint16_t formaldehyde = this->get_16_bit_uint_(28); + float temperature = this->get_16_bit_uint_(30) / 10.0f; + float humidity = this->get_16_bit_uint_(32) / 10.0f; + + ESP_LOGD(TAG, "Got Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", temperature, humidity, + formaldehyde); + + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + if (this->formaldehyde_sensor_ != nullptr) + this->formaldehyde_sensor_->publish_state(formaldehyde); + // The rest of the PMS5003ST matches the PMS5003, continue on + } case PMSX003_TYPE_X003: { + uint16_t pm_1_0_std_concentration = this->get_16_bit_uint_(4); + uint16_t pm_2_5_std_concentration = this->get_16_bit_uint_(6); + uint16_t pm_10_0_std_concentration = this->get_16_bit_uint_(8); + uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); + + uint16_t pm_particles_03um = this->get_16_bit_uint_(16); + uint16_t pm_particles_05um = this->get_16_bit_uint_(18); + uint16_t pm_particles_10um = this->get_16_bit_uint_(20); + uint16_t pm_particles_25um = this->get_16_bit_uint_(22); + uint16_t pm_particles_50um = this->get_16_bit_uint_(24); + uint16_t pm_particles_100um = this->get_16_bit_uint_(26); + ESP_LOGD(TAG, "Got PM1.0 Concentration: %u µg/m^3, PM2.5 Concentration %u µg/m^3, PM10.0 Concentration: %u µg/m^3", pm_1_0_concentration, pm_2_5_concentration, pm_10_0_concentration); + + if (this->pm_1_0_std_sensor_ != nullptr) + this->pm_1_0_std_sensor_->publish_state(pm_1_0_std_concentration); + if (this->pm_2_5_std_sensor_ != nullptr) + this->pm_2_5_std_sensor_->publish_state(pm_2_5_std_concentration); + if (this->pm_10_0_std_sensor_ != nullptr) + this->pm_10_0_std_sensor_->publish_state(pm_10_0_std_concentration); + if (this->pm_1_0_sensor_ != nullptr) this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); if (this->pm_2_5_sensor_ != nullptr) this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); if (this->pm_10_0_sensor_ != nullptr) this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); + + if (this->pm_particles_03um_sensor_ != nullptr) + this->pm_particles_03um_sensor_->publish_state(pm_particles_03um); + if (this->pm_particles_05um_sensor_ != nullptr) + this->pm_particles_05um_sensor_->publish_state(pm_particles_05um); + if (this->pm_particles_10um_sensor_ != nullptr) + this->pm_particles_10um_sensor_->publish_state(pm_particles_10um); + if (this->pm_particles_25um_sensor_ != nullptr) + this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); + if (this->pm_particles_50um_sensor_ != nullptr) + this->pm_particles_50um_sensor_->publish_state(pm_particles_50um); + if (this->pm_particles_100um_sensor_ != nullptr) + this->pm_particles_100um_sensor_->publish_state(pm_particles_100um); break; } case PMSX003_TYPE_5003T: { @@ -131,29 +210,6 @@ void PMSX003Component::parse_data_() { this->humidity_sensor_->publish_state(humidity); break; } - case PMSX003_TYPE_5003ST: { - uint16_t pm_1_0_concentration = this->get_16_bit_uint_(10); - uint16_t pm_2_5_concentration = this->get_16_bit_uint_(12); - uint16_t pm_10_0_concentration = this->get_16_bit_uint_(14); - uint16_t formaldehyde = this->get_16_bit_uint_(28); - float temperature = this->get_16_bit_uint_(30) / 10.0f; - float humidity = this->get_16_bit_uint_(32) / 10.0f; - ESP_LOGD(TAG, "Got PM2.5 Concentration: %u µg/m^3, Temperature: %.1f°C, Humidity: %.1f%% Formaldehyde: %u µg/m^3", - pm_2_5_concentration, temperature, humidity, formaldehyde); - if (this->pm_1_0_sensor_ != nullptr) - this->pm_1_0_sensor_->publish_state(pm_1_0_concentration); - if (this->pm_2_5_sensor_ != nullptr) - this->pm_2_5_sensor_->publish_state(pm_2_5_concentration); - if (this->pm_10_0_sensor_ != nullptr) - this->pm_10_0_sensor_->publish_state(pm_10_0_concentration); - if (this->temperature_sensor_ != nullptr) - this->temperature_sensor_->publish_state(temperature); - if (this->humidity_sensor_ != nullptr) - this->humidity_sensor_->publish_state(humidity); - if (this->formaldehyde_sensor_ != nullptr) - this->formaldehyde_sensor_->publish_state(formaldehyde); - break; - } } this->status_clear_warning(); @@ -163,9 +219,21 @@ uint16_t PMSX003Component::get_16_bit_uint_(uint8_t start_index) { } void PMSX003Component::dump_config() { ESP_LOGCONFIG(TAG, "PMSX003:"); + LOG_SENSOR(" ", "PM1.0STD", this->pm_1_0_std_sensor_); + LOG_SENSOR(" ", "PM2.5STD", this->pm_2_5_std_sensor_); + LOG_SENSOR(" ", "PM10.0STD", this->pm_10_0_std_sensor_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + + LOG_SENSOR(" ", "PM0.3um", this->pm_particles_03um_sensor_); + LOG_SENSOR(" ", "PM0.5um", this->pm_particles_05um_sensor_); + LOG_SENSOR(" ", "PM1.0um", this->pm_particles_10um_sensor_); + LOG_SENSOR(" ", "PM2.5um", this->pm_particles_25um_sensor_); + LOG_SENSOR(" ", "PM5.0um", this->pm_particles_50um_sensor_); + LOG_SENSOR(" ", "PM10.0um", this->pm_particles_100um_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index 163d25c694..a5adecb534 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -21,9 +21,22 @@ class PMSX003Component : public uart::UARTDevice, public Component { void dump_config() override; void set_type(PMSX003Type type) { type_ = type; } + + void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor); + void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor); + void set_pm_10_0_std_sensor(sensor::Sensor *pm_10_0_std_sensor); + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor); void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor); void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor); + + void set_pm_particles_03um_sensor(sensor::Sensor *pm_particles_03um_sensor); + void set_pm_particles_05um_sensor(sensor::Sensor *pm_particles_05um_sensor); + void set_pm_particles_10um_sensor(sensor::Sensor *pm_particles_10um_sensor); + void set_pm_particles_25um_sensor(sensor::Sensor *pm_particles_25um_sensor); + void set_pm_particles_50um_sensor(sensor::Sensor *pm_particles_50um_sensor); + void set_pm_particles_100um_sensor(sensor::Sensor *pm_particles_100um_sensor); + void set_temperature_sensor(sensor::Sensor *temperature_sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_formaldehyde_sensor(sensor::Sensor *formaldehyde_sensor); @@ -37,9 +50,25 @@ class PMSX003Component : public uart::UARTDevice, public Component { uint8_t data_index_{0}; uint32_t last_transmission_{0}; PMSX003Type type_; + + // "Standard Particle" + sensor::Sensor *pm_1_0_std_sensor_{nullptr}; + sensor::Sensor *pm_2_5_std_sensor_{nullptr}; + sensor::Sensor *pm_10_0_std_sensor_{nullptr}; + + // "Under Atmospheric Pressure" sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + + // Particle counts by size + sensor::Sensor *pm_particles_03um_sensor_{nullptr}; + sensor::Sensor *pm_particles_05um_sensor_{nullptr}; + sensor::Sensor *pm_particles_10um_sensor_{nullptr}; + sensor::Sensor *pm_particles_25um_sensor_{nullptr}; + sensor::Sensor *pm_particles_50um_sensor_{nullptr}; + sensor::Sensor *pm_particles_100um_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *formaldehyde_sensor_{nullptr}; diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 935208fe03..fcded50c25 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -8,6 +8,15 @@ from esphome.const import ( CONF_PM_10_0, CONF_PM_1_0, CONF_PM_2_5, + CONF_PM_10_0_STD, + CONF_PM_1_0_STD, + CONF_PM_2_5_STD, + CONF_PM_0_3UM, + CONF_PM_0_5UM, + CONF_PM_1_0UM, + CONF_PM_2_5UM, + CONF_PM_5_0UM, + CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, DEVICE_CLASS_HUMIDITY, @@ -16,6 +25,7 @@ from esphome.const import ( STATE_CLASS_MEASUREMENT, UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_CELSIUS, + UNIT_COUNT_DECILITRE, UNIT_PERCENT, ) @@ -60,6 +70,24 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PMSX003Component), cv.Required(CONF_TYPE): cv.enum(PMSX003_TYPES, upper=True), + cv.Optional(CONF_PM_1_0_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0_STD): sensor.sensor_schema( + UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), cv.Optional(CONF_PM_1_0): sensor.sensor_schema( unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, icon=ICON_CHEMICAL_WEAPON, @@ -78,6 +106,42 @@ CONFIG_SCHEMA = ( accuracy_decimals=0, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_PM_0_3UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_0_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_1_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_2_5UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_5_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), + cv.Optional(CONF_PM_10_0UM): sensor.sensor_schema( + UNIT_COUNT_DECILITRE, + ICON_CHEMICAL_WEAPON, + 0, + DEVICE_CLASS_EMPTY, + ), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, accuracy_decimals=1, @@ -110,6 +174,18 @@ async def to_code(config): cg.add(var.set_type(config[CONF_TYPE])) + if CONF_PM_1_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0_STD]) + cg.add(var.set_pm_1_0_std_sensor(sens)) + + if CONF_PM_2_5_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5_STD]) + cg.add(var.set_pm_2_5_std_sensor(sens)) + + if CONF_PM_10_0_STD in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0_STD]) + cg.add(var.set_pm_10_0_std_sensor(sens)) + if CONF_PM_1_0 in config: sens = await sensor.new_sensor(config[CONF_PM_1_0]) cg.add(var.set_pm_1_0_sensor(sens)) @@ -122,6 +198,30 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_PM_0_3UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_3UM]) + cg.add(var.set_pm_particles_03um_sensor(sens)) + + if CONF_PM_0_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_0_5UM]) + cg.add(var.set_pm_particles_05um_sensor(sens)) + + if CONF_PM_1_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_1_0UM]) + cg.add(var.set_pm_particles_10um_sensor(sens)) + + if CONF_PM_2_5UM in config: + sens = await sensor.new_sensor(config[CONF_PM_2_5UM]) + cg.add(var.set_pm_particles_25um_sensor(sens)) + + if CONF_PM_5_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_5_0UM]) + cg.add(var.set_pm_particles_50um_sensor(sens)) + + if CONF_PM_10_0UM in config: + sens = await sensor.new_sensor(config[CONF_PM_10_0UM]) + cg.add(var.set_pm_particles_100um_sensor(sens)) + if CONF_TEMPERATURE in config: sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/const.py b/esphome/const.py index ae6c5716d9..8327921211 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -441,10 +441,19 @@ CONF_PINS = "pins" CONF_PIXEL_MAPPER = "pixel_mapper" CONF_PLATFORM = "platform" CONF_PLATFORMIO_OPTIONS = "platformio_options" +CONF_PM_0_3UM = "pm_0_3um" +CONF_PM_0_5UM = "pm_0_5um" CONF_PM_1_0 = "pm_1_0" +CONF_PM_1_0_STD = "pm_1_0_std" +CONF_PM_1_0UM = "pm_1_0um" CONF_PM_10_0 = "pm_10_0" +CONF_PM_10_0_STD = "pm_10_0_std" +CONF_PM_10_0UM = "pm_10_0um" CONF_PM_2_5 = "pm_2_5" +CONF_PM_2_5_STD = "pm_2_5_std" +CONF_PM_2_5UM = "pm_2_5um" CONF_PM_4_0 = "pm_4_0" +CONF_PM_5_0UM = "pm_5_0um" CONF_PM_SIZE = "pm_size" CONF_PMC_0_5 = "pmc_0_5" CONF_PMC_1_0 = "pmc_1_0" @@ -709,6 +718,7 @@ ICON_WIFI = "mdi:wifi" UNIT_AMPERE = "A" UNIT_CELSIUS = "°C" +UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" UNIT_DECIBEL = "dB" diff --git a/tests/test3.yaml b/tests/test3.yaml index acd975d794..d4c8e526fe 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -450,6 +450,66 @@ sensor: name: 'PM 2.5 Concentration' pm_10_0: name: 'PM 10.0 Concentration' + pm_1_0_std: + name: 'PM 1.0 Standard Atmospher Concentration' + pm_2_5_std: + name: 'PM 2.5 Standard Atmospher Concentration' + pm_10_0_std: + name: 'PM 10.0 Standard Atmospher Concentration' + pm_0_3um: + name: 'Particulate Count >0.3um' + pm_0_5um: + name: 'Particulate Count >0.5um' + pm_1_0um: + name: 'Particulate Count >1.0um' + pm_2_5um: + name: 'Particulate Count >2.5um' + pm_5_0um: + name: 'Particulate Count >5.0um' + pm_10_0um: + name: 'Particulate Count >10.0um' + - platform: pmsx003 + uart_id: uart2 + type: PMS5003T + pm_2_5: + name: 'PM 2.5 Concentration' + temperature: + name: 'PMS Temperature' + humidity: + name: 'PMS Humidity' + - platform: pmsx003 + uart_id: uart2 + type: PMS5003ST + pm_1_0: + name: 'PM 1.0 Concentration' + pm_2_5: + name: 'PM 2.5 Concentration' + pm_10_0: + name: 'PM 10.0 Concentration' + pm_1_0_std: + name: 'PM 1.0 Standard Atmospher Concentration' + pm_2_5_std: + name: 'PM 2.5 Standard Atmospher Concentration' + pm_10_0_std: + name: 'PM 10.0 Standard Atmospher Concentration' + pm_0_3um: + name: 'Particulate Count >0.3um' + pm_0_5um: + name: 'Particulate Count >0.5um' + pm_1_0um: + name: 'Particulate Count >1.0um' + pm_2_5um: + name: 'Particulate Count >2.5um' + pm_5_0um: + name: 'Particulate Count >5.0um' + pm_10_0um: + name: 'Particulate Count >10.0um' + temperature: + name: 'PMS Temperature' + humidity: + name: 'PMS Humidity' + formaldehyde: + name: 'PMS Formaldehyde Concentration' - platform: cse7766 uart_id: uart3 voltage: From f7311aa02576cb3d64c2b37173f81e58d2522c97 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:33:00 +1200 Subject: [PATCH 640/643] Dont force 0 state instead of min_power unless explicit config set (#2107) --- esphome/components/output/__init__.py | 5 +++++ esphome/components/output/float_output.cpp | 4 +++- esphome/components/output/float_output.h | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4471794033..4f1fb33fe7 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -17,6 +17,8 @@ from esphome.core import CORE CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +CONF_ZERO_MEANS_ZERO = "zero_means_zero" + BINARY_OUTPUT_SCHEMA = cv.Schema( { cv.Optional(CONF_POWER_SUPPLY): cv.use_id(power_supply.PowerSupply), @@ -28,6 +30,7 @@ FLOAT_OUTPUT_SCHEMA = BINARY_OUTPUT_SCHEMA.extend( { cv.Optional(CONF_MAX_POWER): cv.percentage, cv.Optional(CONF_MIN_POWER): cv.percentage, + cv.Optional(CONF_ZERO_MEANS_ZERO, default=False): cv.boolean, } ) @@ -53,6 +56,8 @@ async def setup_output_platform_(obj, config): cg.add(obj.set_max_power(config[CONF_MAX_POWER])) if CONF_MIN_POWER in config: cg.add(obj.set_min_power(config[CONF_MIN_POWER])) + if CONF_ZERO_MEANS_ZERO in config: + cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO])) async def register_output(var, config): diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 99ba798cb9..5820a2d7cd 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -17,6 +17,8 @@ void FloatOutput::set_min_power(float min_power) { this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX } +void FloatOutput::set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; } + float FloatOutput::get_min_power() const { return this->min_power_; } void FloatOutput::set_level(float state) { @@ -31,7 +33,7 @@ void FloatOutput::set_level(float state) { #endif if (this->is_inverted()) state = 1.0f - state; - if (state == 0.0f) { // regardless of min_power_, 0.0 means off + if (state == 0.0f && this->zero_means_zero_) { // regardless of min_power_, 0.0 means off this->write_state(state); return; } diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 1b969c9225..3e2b3ada8d 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,6 +46,12 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); + /** Sets this output to ignore min_power for a 0 state + * + * @param zero True if a 0 state should mean 0 and not min_power. + */ + void set_zero_means_zero(bool zero_means_zero); + /** Set the level of this float output, this is called from the front-end. * * @param state The new state. @@ -76,6 +82,7 @@ class FloatOutput : public BinaryOutput { float max_power_{1.0f}; float min_power_{0.0f}; + bool zero_means_zero_; }; } // namespace output From 9b04e657db0afb4c2cb4026f7c7ff8989ec6fecd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:53:34 +1200 Subject: [PATCH 641/643] Fix import (#2108) --- esphome/components/pmsx003/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index fcded50c25..c3dd7d5a97 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -19,6 +19,7 @@ from esphome.const import ( CONF_PM_10_0UM, CONF_TEMPERATURE, CONF_TYPE, + DEVICE_CLASS_EMPTY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, ICON_CHEMICAL_WEAPON, From 335210d788498b92177a882353980676ce01a9c1 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 2 Aug 2021 04:08:24 -0500 Subject: [PATCH 642/643] Thermostat enhancements and code clean-up (#2073) --- esphome/components/thermostat/climate.py | 177 +++++---- .../thermostat/thermostat_climate.cpp | 347 ++++++++++++------ .../thermostat/thermostat_climate.h | 42 ++- esphome/const.py | 6 + 4 files changed, 355 insertions(+), 217 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index bf3195a5dd..55fb534682 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -6,7 +6,9 @@ from esphome.const import ( CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, + CONF_COOL_DEADBAND, CONF_COOL_MODE, + CONF_COOL_OVERRUN, CONF_DEFAULT_MODE, CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, @@ -22,14 +24,18 @@ from esphome.const import ( CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_COOLING, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, + CONF_HEAT_DEADBAND, CONF_HEAT_MODE, + CONF_HEAT_OVERRUN, CONF_HYSTERESIS, CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, + CONF_SET_POINT_MINIMUM_DIFFERENTIAL, CONF_SWING_BOTH_ACTION, CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, @@ -62,99 +68,59 @@ validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) def validate_thermostat(config): # verify corresponding climate action action exists for any defined climate mode action - if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE) - ) - if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE) - ) - if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format( - CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE - ) - ) - if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE) - ) - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): - raise cv.Invalid( - "{} must be defined when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, + requirements = { + CONF_AUTO_MODE: [CONF_COOL_ACTION, CONF_HEAT_ACTION], + CONF_COOL_MODE: [CONF_COOL_ACTION], + CONF_DRY_MODE: [CONF_DRY_ACTION], + CONF_FAN_ONLY_MODE: [CONF_FAN_ONLY_ACTION], + CONF_HEAT_MODE: [CONF_HEAT_ACTION], + } + for config_mode, req_actions in requirements.items(): + for req_action in req_actions: + if config_mode in config and req_action not in config: + raise cv.Invalid(f"{req_action} must be defined to use {config_mode}") + + # determine validation requirements based on fan_only_cooling setting + if config[CONF_FAN_ONLY_COOLING] is True: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [ CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION, - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: - raise cv.Invalid( - "{} must be defined when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION - ) - ) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid( - "{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) + ], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + else: + requirements = { + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH: [CONF_COOL_ACTION], + CONF_DEFAULT_TARGET_TEMPERATURE_LOW: [CONF_HEAT_ACTION], + } + + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in config and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in config and req_action not in config: + raise cv.Invalid(f"{config_temp} is defined with no {req_action}") if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and ( - CONF_COOL_ACTION in config or CONF_FAN_ONLY_ACTION in config - ): - raise cv.Invalid( - "{} must be defined in away configuration when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away - and CONF_HEAT_ACTION in config - ): - raise cv.Invalid( - "{} must be defined in away configuration when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and ( - CONF_COOL_ACTION not in config and CONF_FAN_ONLY_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, - CONF_COOL_ACTION, - CONF_FAN_ONLY_ACTION, - ) - ) - if ( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away - and CONF_HEAT_ACTION not in config - ): - raise cv.Invalid( - "{} is defined in away configuration with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION - ) - ) + for config_temp, req_actions in requirements.items(): + for req_action in req_actions: + # verify corresponding default target temperature exists when a given climate action exists + if config_temp not in away and req_action in config: + raise cv.Invalid( + f"{config_temp} must be defined in away configuration when using {req_action}" + ) + # if a given climate action is NOT defined, it should not have a default target temperature + if config_temp in away and req_action not in config: + raise cv.Invalid( + f"{config_temp} is defined in away configuration with no {req_action}" + ) + # verify default climate mode is valid given above configuration default_mode = config[CONF_DEFAULT_MODE] requirements = { @@ -241,7 +207,15 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional( + CONF_SET_POINT_MINIMUM_DIFFERENTIAL, default=0.5 + ): cv.temperature, + cv.Optional(CONF_COOL_DEADBAND): cv.temperature, + cv.Optional(CONF_COOL_OVERRUN): cv.temperature, + cv.Optional(CONF_HEAT_DEADBAND): cv.temperature, + cv.Optional(CONF_HEAT_OVERRUN): cv.temperature, cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_FAN_ONLY_COOLING, default=False): cv.boolean, cv.Optional(CONF_AWAY_CONFIG): cv.Schema( { cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, @@ -269,8 +243,32 @@ async def to_code(config): sens = await cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE])) + cg.add( + var.set_set_point_minimum_differential( + config[CONF_SET_POINT_MINIMUM_DIFFERENTIAL] + ) + ) cg.add(var.set_sensor(sens)) - cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + if CONF_COOL_DEADBAND in config: + cg.add(var.set_cool_deadband(config[CONF_COOL_DEADBAND])) + else: + cg.add(var.set_cool_deadband(config[CONF_HYSTERESIS])) + + if CONF_COOL_OVERRUN in config: + cg.add(var.set_cool_overrun(config[CONF_COOL_OVERRUN])) + else: + cg.add(var.set_cool_overrun(config[CONF_HYSTERESIS])) + + if CONF_HEAT_DEADBAND in config: + cg.add(var.set_heat_deadband(config[CONF_HEAT_DEADBAND])) + else: + cg.add(var.set_heat_deadband(config[CONF_HYSTERESIS])) + + if CONF_HEAT_OVERRUN in config: + cg.add(var.set_heat_overrun(config[CONF_HEAT_OVERRUN])) + else: + cg.add(var.set_heat_overrun(config[CONF_HYSTERESIS])) if two_points_available is True: cg.add(var.set_supports_two_points(True)) @@ -288,6 +286,7 @@ async def to_code(config): normal_config = ThermostatClimateTargetTempConfig( config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] ) + cg.add(var.set_supports_fan_only_cooling(config[CONF_FAN_ONLY_COOLING])) cg.add(var.set_normal_config(normal_config)) await automation.build_automation( diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 4610d5caad..1176a75514 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -7,6 +7,7 @@ namespace thermostat { static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { + // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; // required action may have changed, recompute, refresh @@ -29,7 +30,12 @@ void ThermostatClimate::setup() { this->setup_complete_ = true; this->publish_state(); } -float ThermostatClimate::hysteresis() { return this->hysteresis_; } + +float ThermostatClimate::cool_deadband() { return this->cool_deadband_; } +float ThermostatClimate::cool_overrun() { return this->cool_overrun_; } +float ThermostatClimate::heat_deadband() { return this->heat_deadband_; } +float ThermostatClimate::heat_overrun() { return this->heat_overrun_; } + void ThermostatClimate::refresh() { this->switch_to_mode_(this->mode); this->switch_to_action_(compute_action_()); @@ -38,6 +44,77 @@ void ThermostatClimate::refresh() { this->check_temperature_change_trigger_(); this->publish_state(); } + +bool ThermostatClimate::hysteresis_valid() { + if ((this->supports_cool_ || (this->supports_fan_only_ && this->supports_fan_only_cooling_)) && + (isnan(this->cool_deadband_) || isnan(this->cool_overrun_))) + return false; + + if (this->supports_heat_ && (isnan(this->heat_deadband_) || isnan(this->heat_overrun_))) + return false; + + return true; +} + +void ThermostatClimate::validate_target_temperature() { + if (isnan(this->target_temperature)) { + this->target_temperature = + ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + + this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature must be between the visual minimum and the visual maximum + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } +} + +void ThermostatClimate::validate_target_temperatures() { + if (this->supports_two_points_) { + validate_target_temperature_low(); + validate_target_temperature_high(); + } else { + validate_target_temperature(); + } +} + +void ThermostatClimate::validate_target_temperature_low() { + if (isnan(this->target_temperature_low)) { + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + } else { + // target_temperature_low must not be lower than the visual minimum + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ + if (this->target_temperature_low > + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) + this->target_temperature_low = + this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; + // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high + if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) + this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; + } +} + +void ThermostatClimate::validate_target_temperature_high() { + if (isnan(this->target_temperature_high)) { + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + } else { + // target_temperature_high must not be lower than the visual maximum + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ + if (this->target_temperature_high < + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) + this->target_temperature_high = + this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; + // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low + if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) + this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; + } +} + void ThermostatClimate::control(const climate::ClimateCall &call) { if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot @@ -53,29 +130,25 @@ void ThermostatClimate::control(const climate::ClimateCall &call) { this->fan_mode = *call.get_fan_mode(); if (call.get_swing_mode().has_value()) this->swing_mode = *call.get_swing_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - if (call.get_target_temperature_low().has_value()) - this->target_temperature_low = *call.get_target_temperature_low(); - if (call.get_target_temperature_high().has_value()) - this->target_temperature_high = *call.get_target_temperature_high(); - // set point validation if (this->supports_two_points_) { - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - if (this->target_temperature_high < this->target_temperature_low) - this->target_temperature_high = this->target_temperature_low; + if (call.get_target_temperature_low().has_value()) { + this->target_temperature_low = *call.get_target_temperature_low(); + validate_target_temperature_low(); + } + if (call.get_target_temperature_high().has_value()) { + this->target_temperature_high = *call.get_target_temperature_high(); + validate_target_temperature_high(); + } } else { - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + validate_target_temperature(); + } } // make any changes happen refresh(); } + climate::ClimateTraits ThermostatClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); @@ -127,114 +200,54 @@ climate::ClimateTraits ThermostatClimate::traits() { traits.set_supports_action(true); return traits; } + climate::ClimateAction ThermostatClimate::compute_action_() { - // we need to know the current climate action before anything else happens here - climate::ClimateAction target_action = this->action; - // if the climate mode is OFF then the climate action must be OFF - if (this->mode == climate::CLIMATE_MODE_OFF) { + auto target_action = climate::CLIMATE_ACTION_IDLE; + // if any hysteresis values or current_temperature is not valid, we go to OFF; + if (isnan(this->current_temperature) || !this->hysteresis_valid()) { return climate::CLIMATE_ACTION_OFF; - } else if (this->action == climate::CLIMATE_ACTION_OFF) { - // ...but if the climate mode is NOT OFF then the climate action must not be OFF - target_action = climate::CLIMATE_ACTION_IDLE; } - - if (this->supports_two_points_) { - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || - isnan(this->target_temperature_high) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_HEAT_COOL: - case climate::CLIMATE_MODE_COOL: - case climate::CLIMATE_MODE_HEAT: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature_low - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } - } else { - if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { - target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_COOL: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - case climate::CLIMATE_MODE_HEAT: - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } + // ensure set point(s) is/are valid before computing the action + this->validate_target_temperatures(); + // everything has been validated so we can now safely compute the action + switch (this->mode) { + // if the climate mode is OFF then the climate action must be OFF + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->fanning_required_()) + target_action = climate::CLIMATE_ACTION_FAN; + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (this->cooling_required_() && this->heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + case climate::CLIMATE_MODE_COOL: + if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } + break; + case climate::CLIMATE_MODE_HEAT: + if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; } - // do not switch to an action that isn't enabled per the active climate mode - if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) - target_action = climate::CLIMATE_ACTION_IDLE; - if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) - target_action = climate::CLIMATE_ACTION_IDLE; - return target_action; } + void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { // setup_complete_ helps us ensure an action is called immediately after boot if ((action == this->action) && this->setup_complete_) @@ -284,6 +297,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { this->action = action; this->prev_action_trigger_ = trig; } + void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) @@ -335,6 +349,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { this->prev_fan_mode_ = fan_mode; this->prev_fan_mode_trigger_ = trig; } + void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((mode == this->prev_mode_) && this->setup_complete_) @@ -377,6 +392,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; } + void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { // setup_complete_ helps us ensure an action is called immediately after boot if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) @@ -413,6 +429,7 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; } + void ThermostatClimate::check_temperature_change_trigger_() { if (this->supports_two_points_) { // setup_complete_ helps us ensure an action is called immediately after boot @@ -437,6 +454,70 @@ void ThermostatClimate::check_temperature_change_trigger_() { assert(trig != nullptr); trig->trigger(); } + +bool ThermostatClimate::cooling_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_cool_) { + if (this->current_temperature > (temperature + this->cool_deadband_)) { + // if the current temperature exceeds the target + deadband, cooling is required + return true; + } else if (this->current_temperature < (temperature - this->cool_overrun_)) { + // if the current temperature is less than the target - overrun, cooling should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_COOLING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_COOL)); + } + } + return false; +} + +bool ThermostatClimate::fanning_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_high : this->target_temperature; + + if (this->supports_fan_only_) { + if (this->supports_fan_only_cooling_) { + if (this->current_temperature > (temperature + this->cool_deadband_)) { + // if the current temperature exceeds the target + deadband, fanning is required + return true; + } else if (this->current_temperature < (temperature - this->cool_overrun_)) { + // if the current temperature is less than the target - overrun, fanning should stop + return false; + } else { + // if we get here, the current temperature is between target + deadband and target - overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_FAN) && (this->mode == climate::CLIMATE_MODE_FAN_ONLY); + } + } else { + return true; + } + } + return false; +} + +bool ThermostatClimate::heating_required_() { + auto temperature = this->supports_two_points_ ? this->target_temperature_low : this->target_temperature; + + if (this->supports_heat_) { + if (this->current_temperature < temperature - this->heat_deadband_) { + // if the current temperature is below the target - deadband, heating is required + return true; + } else if (this->current_temperature > temperature + this->heat_overrun_) { + // if the current temperature is above the target + overrun, heating should stop + return false; + } else { + // if we get here, the current temperature is between target - deadband and target + overrun, + // so the action should not change unless it conflicts with the current mode + return (this->action == climate::CLIMATE_ACTION_HEATING) && + ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_HEAT)); + } + } + return false; +} + void ThermostatClimate::change_away_(bool away) { if (!away) { if (this->supports_two_points_) { @@ -453,13 +534,16 @@ void ThermostatClimate::change_away_(bool away) { } this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; } + void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { this->normal_config_ = normal_config; } + void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { this->supports_away_ = true; this->away_config_ = away_config; } + ThermostatClimate::ThermostatClimate() : cool_action_trigger_(new Trigger<>()), cool_mode_trigger_(new Trigger<>()), @@ -486,8 +570,15 @@ ThermostatClimate::ThermostatClimate() swing_mode_horizontal_trigger_(new Trigger<>()), swing_mode_vertical_trigger_(new Trigger<>()), temperature_change_trigger_(new Trigger<>()) {} + void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; } -void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } +void ThermostatClimate::set_set_point_minimum_differential(float differential) { + this->set_point_minimum_differential_ = differential; +} +void ThermostatClimate::set_cool_deadband(float deadband) { this->cool_deadband_ = deadband; } +void ThermostatClimate::set_cool_overrun(float overrun) { this->cool_overrun_ = overrun; } +void ThermostatClimate::set_heat_deadband(float deadband) { this->heat_deadband_ = deadband; } +void ThermostatClimate::set_heat_overrun(float overrun) { this->heat_overrun_ = overrun; } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) { this->supports_heat_cool_ = supports_heat_cool; @@ -496,6 +587,9 @@ void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_a void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_fan_only_cooling(bool supports_fan_only_cooling) { + this->supports_fan_only_cooling_ = supports_fan_only_cooling; +} void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { this->supports_fan_mode_on_ = supports_fan_mode_on; @@ -539,6 +633,7 @@ void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mod void ThermostatClimate::set_supports_two_points(bool supports_two_points) { this->supports_two_points_ = supports_two_points; } + Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } @@ -564,6 +659,7 @@ Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this-> Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } + void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); if (this->supports_heat_) { @@ -578,12 +674,17 @@ void ThermostatClimate::dump_config() { else ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); } - ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); + ESP_LOGCONFIG(TAG, " Cool Deadband: %.1f°C", this->cool_deadband_); + ESP_LOGCONFIG(TAG, " Cool Overrun: %.1f°C", this->cool_overrun_); + ESP_LOGCONFIG(TAG, " Heat Deadband: %.1f°C", this->heat_deadband_); + ESP_LOGCONFIG(TAG, " Heat Overrun: %.1f°C", this->heat_overrun_); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports HEAT/COOL: %s", YESNO(this->supports_heat_cool_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY_COOLING: %s", YESNO(this->supports_fan_only_cooling_)); ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); @@ -619,8 +720,10 @@ void ThermostatClimate::dump_config() { } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) : default_temperature(default_temperature) {} + ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index bff9e9bdc1..67f002a22a 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -17,7 +17,10 @@ struct ThermostatClimateTargetTempConfig { float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; - float hysteresis{NAN}; + float cool_deadband_{NAN}; + float cool_overrun_{NAN}; + float heat_deadband_{NAN}; + float heat_overrun_{NAN}; }; class ThermostatClimate : public climate::Climate, public Component { @@ -27,13 +30,18 @@ class ThermostatClimate : public climate::Climate, public Component { void dump_config() override; void set_default_mode(climate::ClimateMode default_mode); - void set_hysteresis(float hysteresis); + void set_set_point_minimum_differential(float differential); + void set_cool_deadband(float deadband); + void set_cool_overrun(float overrun); + void set_heat_deadband(float deadband); + void set_heat_overrun(float overrun); void set_sensor(sensor::Sensor *sensor); void set_supports_auto(bool supports_auto); void set_supports_heat_cool(bool supports_heat_cool); void set_supports_cool(bool supports_cool); void set_supports_dry(bool supports_dry); void set_supports_fan_only(bool supports_fan_only); + void set_supports_fan_only_cooling(bool supports_fan_only_cooling); void set_supports_heat(bool supports_heat); void set_supports_fan_mode_on(bool supports_fan_mode_on); void set_supports_fan_mode_off(bool supports_fan_mode_off); @@ -78,10 +86,19 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_swing_mode_off_trigger() const; Trigger<> *get_swing_mode_vertical_trigger() const; Trigger<> *get_temperature_change_trigger() const; - /// Get current hysteresis value - float hysteresis(); + /// Get current hysteresis values + float cool_deadband(); + float cool_overrun(); + float heat_deadband(); + float heat_overrun(); /// Call triggers based on updated climate states (modes/actions) void refresh(); + /// Set point and hysteresis validation + bool hysteresis_valid(); // returns true if valid + void validate_target_temperature(); + void validate_target_temperatures(); + void validate_target_temperature_low(); + void validate_target_temperature_high(); protected: /// Override control to change settings of the climate device. @@ -111,6 +128,11 @@ class ThermostatClimate : public climate::Climate, public Component { /// Check if the temperature change trigger should be called. void check_temperature_change_trigger_(); + /// Check if cooling/fanning/heating actions are required; returns true if so + bool cooling_required_(); + bool fanning_required_(); + bool heating_required_(); + /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -124,6 +146,8 @@ class ThermostatClimate : public climate::Climate, public Component { bool supports_dry_{false}; bool supports_fan_only_{false}; bool supports_heat_{false}; + /// Special flag -- enables fan to be switched based on target_temperature_high + bool supports_fan_only_cooling_{false}; /// Whether the controller supports turning on or off just the fan. /// @@ -278,8 +302,14 @@ class ThermostatClimate : public climate::Climate, public Component { ThermostatClimateTargetTempConfig normal_config_{}; ThermostatClimateTargetTempConfig away_config_{}; - /// Hysteresis value used for computing climate actions - float hysteresis_{0}; + /// Minimum differential required between set points + float set_point_minimum_differential_{0}; + + /// Hysteresis values used for computing climate actions + float cool_deadband_{0}; + float cool_overrun_{0}; + float heat_deadband_{0}; + float heat_overrun_{0}; /// setup_complete_ blocks modifying/resetting the temps immediately after boot bool setup_complete_{false}; diff --git a/esphome/const.py b/esphome/const.py index 8327921211..1ed8aeb402 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -134,7 +134,9 @@ CONF_CONDITION_ID = "condition_id" CONF_CONDUCTIVITY = "conductivity" CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" +CONF_COOL_DEADBAND = "cool_deadband" CONF_COOL_MODE = "cool_mode" +CONF_COOL_OVERRUN = "cool_overrun" CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" CONF_COURSE = "course" @@ -219,6 +221,7 @@ CONF_FAN_MODE_MIDDLE_ACTION = "fan_mode_middle_action" CONF_FAN_MODE_OFF_ACTION = "fan_mode_off_action" CONF_FAN_MODE_ON_ACTION = "fan_mode_on_action" CONF_FAN_ONLY_ACTION = "fan_only_action" +CONF_FAN_ONLY_COOLING = "fan_only_cooling" CONF_FAN_ONLY_MODE = "fan_only_mode" CONF_FAST_CONNECT = "fast_connect" CONF_FILE = "file" @@ -248,7 +251,9 @@ CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" +CONF_HEAT_DEADBAND = "heat_deadband" CONF_HEAT_MODE = "heat_mode" +CONF_HEAT_OVERRUN = "heat_overrun" CONF_HEATER = "heater" CONF_HEIGHT = "height" CONF_HIDDEN = "hidden" @@ -541,6 +546,7 @@ CONF_SERVERS = "servers" CONF_SERVICE = "service" CONF_SERVICE_UUID = "service_uuid" CONF_SERVICES = "services" +CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential" CONF_SETUP_MODE = "setup_mode" CONF_SETUP_PRIORITY = "setup_priority" CONF_SHUNT_RESISTANCE = "shunt_resistance" From 4c8a70308445956615070af7a51d2da5566e4c91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Aug 2021 08:49:45 +0200 Subject: [PATCH 643/643] Bump esptool from 2.8 to 3.1 (#1839) Bumps [esptool](https://github.com/espressif/esptool) from 2.8 to 3.1. - [Release notes](https://github.com/espressif/esptool/releases) - [Commits](https://github.com/espressif/esptool/compare/v2.8...v3.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 561bf0f4d5..b4d557f06e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,6 @@ pytz==2021.1 pyserial==3.5 ifaddr==0.1.7 platformio==5.1.1 -esptool==2.8 +esptool==3.1 click==7.1.2 esphome-dashboard==20210728.0