1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-06 13:22:19 +01:00

Merge branch 'dev' into bump-1.15.0b1

This commit is contained in:
Otto Winter
2020-07-26 22:12:53 +02:00
650 changed files with 29231 additions and 6310 deletions

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: api_options.proto

View File

@@ -1,6 +1,19 @@
"""Python 3 script to automatically generate C++ classes for ESPHome's native API.
It's pretty crappy spaghetti code, but it works.
you need to install protobuf-compiler:
running protc --version should return
libprotoc 3.6.1
then run this script with python3 and the files
esphome/components/api/api_pb2_service.h
esphome/components/api/api_pb2_service.cpp
esphome/components/api/api_pb2.h
esphome/components/api/api_pb2.cpp
will be generated, they still need to be formatted
"""
import re
@@ -10,24 +23,28 @@ from subprocess import call
# Generate with
# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto
import api_options_pb2 as pb
import google.protobuf.descriptor_pb2 as descriptor
cwd = Path(__file__).parent
file_header = '// This file was automatically generated with a tool.\n'
file_header += '// See scripts/api_protobuf/api_protobuf.py\n'
cwd = Path(__file__).resolve().parent
root = cwd.parent.parent / 'esphome' / 'components' / 'api'
prot = cwd / 'api.protoc'
call(['protoc', '-o', prot, '-I', root, 'api.proto'])
prot = root / 'api.protoc'
call(['protoc', '-o', str(prot), '-I', str(root), 'api.proto'])
content = prot.read_bytes()
d = descriptor.FileDescriptorSet.FromString(content)
def indent_list(text, padding=u' '):
def indent_list(text, padding=' '):
return [padding + line for line in text.splitlines()]
def indent(text, padding=u' '):
return u'\n'.join(indent_list(text, padding))
def indent(text, padding=' '):
return '\n'.join(indent_list(text, padding))
def camel_to_snake(name):
@@ -344,7 +361,7 @@ class UInt32Type(TypeInfo):
class EnumType(TypeInfo):
@property
def cpp_type(self):
return "Enum" + self._field.type_name[1:]
return f'enums::{self._field.type_name[1:]}'
@property
def decode_varint(self):
@@ -415,7 +432,7 @@ class SInt64Type(TypeInfo):
class RepeatedTypeInfo(TypeInfo):
def __init__(self, field):
super(RepeatedTypeInfo, self).__init__(field)
super().__init__(field)
self._ti = TYPE_INFO[field.type](field)
@property
@@ -497,17 +514,17 @@ class RepeatedTypeInfo(TypeInfo):
def build_enum_type(desc):
name = "Enum" + desc.name
name = desc.name
out = f"enum {name} : uint32_t {{\n"
for v in desc.value:
out += f' {v.name} = {v.number},\n'
out += '};\n'
cpp = f"template<>\n"
cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n"
cpp += f"const char *proto_enum_to_string<enums::{name}>(enums::{name} value) {{\n"
cpp += f" switch (value) {{\n"
for v in desc.value:
cpp += f' case {v.name}: return "{v.name}";\n'
cpp += f' case enums::{v.name}: return "{v.name}";\n'
cpp += f' default: return "UNKNOWN";\n'
cpp += f' }}\n'
cpp += f'}}\n'
@@ -617,7 +634,8 @@ def build_message_type(desc):
file = d.file[0]
content = '''\
content = file_header
content += '''\
#pragma once
#include "proto.h"
@@ -627,7 +645,8 @@ namespace api {
'''
cpp = '''\
cpp = file_header
cpp += '''\
#include "api_pb2.h"
#include "esphome/core/log.h"
@@ -636,11 +655,15 @@ namespace api {
'''
content += 'namespace enums {\n\n'
for enum in file.enum_type:
s, c = build_enum_type(enum)
content += s
cpp += c
content += '\n} // namespace enums\n\n'
mt = file.message_type
for m in mt:
@@ -708,7 +731,7 @@ def build_service_message_type(mt):
cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n'
if log:
cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n'
cout += f' this->set_nodelay({str(nodelay).lower()});\n'
# cout += f' this->set_nodelay({str(nodelay).lower()});\n'
cout += f' return this->send_message_<{mt.name}>(msg, {id_});\n'
cout += f'}}\n'
if source in (SOURCE_BOTH, SOURCE_CLIENT):
@@ -735,7 +758,8 @@ def build_service_message_type(mt):
return hout, cout
hpp = '''\
hpp = file_header
hpp += '''\
#pragma once
#include "api_pb2.h"
@@ -746,7 +770,8 @@ namespace api {
'''
cpp = '''\
cpp = file_header
cpp += '''\
#include "api_pb2_service.h"
#include "esphome/core/log.h"

96
script/build_codeowners.py Executable file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
import argparse
from collections import defaultdict
from esphome.helpers import write_file_if_changed
from esphome.config import get_component, get_platform
from esphome.core import CORE
parser = argparse.ArgumentParser()
parser.add_argument('--check', help="Check if the CODEOWNERS file is up to date.",
action='store_true')
args = parser.parse_args()
# The root directory of the repo
root = Path(__file__).parent.parent
components_dir = root / 'esphome' / 'components'
BASE = """
# This file is generated by script/build_codeowners.py
# People marked here will be automatically requested for a review
# when the code that they own is touched.
#
# Every time an issue is created with a label corresponding to an integration,
# the integration's code owner is automatically notified.
# Core Code
setup.py @esphome/core
esphome/*.py @esphome/core
esphome/core/* @esphome/core
# Integrations
""".strip()
parts = [BASE]
# Fake some diretory so that get_component works
CORE.config_path = str(root)
codeowners = defaultdict(list)
for path in components_dir.iterdir():
if not path.is_dir():
continue
if not (path / '__init__.py').is_file():
continue
name = path.name
comp = get_component(name)
codeowners[f'esphome/components/{name}/*'].extend(comp.codeowners)
for platform_path in path.iterdir():
platform_name = platform_path.stem
platform = get_platform(platform_name, name)
if platform is None:
continue
if platform_path.is_dir():
# Sub foldered platforms get their own line
if not (platform_path / '__init__.py').is_file():
continue
codeowners[f'esphome/components/{name}/{platform_name}/*'].extend(platform.codeowners)
continue
# Non-subfoldered platforms add to codeowners at component level
if not platform_path.is_file() or platform_path.name == '__init__.py':
continue
codeowners[f'esphome/components/{name}/*'].extend(platform.codeowners)
for path, owners in sorted(codeowners.items()):
owners = sorted(set(owners))
if not owners:
continue
for owner in owners:
if not owner.startswith('@'):
print(f"Codeowner {owner} for integration {path} must start with an '@' symbol!")
sys.exit(1)
parts.append(f"{path} {' '.join(owners)}")
# End newline
parts.append('')
content = '\n'.join(parts)
codeowners_file = root / 'CODEOWNERS'
if args.check:
if codeowners_file.read_text() != content:
print("CODEOWNERS file is not up to date.")
print("Please run `script/build_codeowners.py`")
sys.exit(1)
print("CODEOWNERS file is up to date")
else:
write_file_if_changed(codeowners_file, content)
print("Wrote CODEOWNERS")

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
import sys
import os.path

85
script/bump-version.py Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env python3
import argparse
import re
import subprocess
from dataclasses import dataclass
import sys
@dataclass
class Version:
major: int
minor: int
patch: int
beta: int = 0
dev: bool = False
def __str__(self):
return f'{self.major}.{self.minor}.{self.full_patch}'
@property
def full_patch(self):
res = f'{self.patch}'
if self.beta > 0:
res += f'b{self.beta}'
if self.dev:
res += '-dev'
return res
@classmethod
def parse(cls, value):
match = re.match(r'(\d+).(\d+).(\d+)(b\d+)?(-dev)?', value)
assert match is not None
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
beta = int(match[4][1:]) if match[4] else 0
dev = bool(match[5])
return Version(
major=major, minor=minor, patch=patch,
beta=beta, dev=dev
)
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: 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}'"
)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('new_version', type=str)
args = parser.parse_args()
version = Version.parse(args.new_version)
print(f"Bumping to {version}")
write_version(version)
return 0
if __name__ == "__main__":
sys.exit(main() or 0)

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python
from __future__ import print_function
#!/usr/bin/env python3
import codecs
import collections
import fnmatch
import os.path
import re
import subprocess
import sys
import re
@@ -92,7 +92,8 @@ def lint_post_check(func):
def lint_re_check(regex, **kwargs):
prog = re.compile(regex, re.MULTILINE)
flags = kwargs.pop('flags', re.MULTILINE)
prog = re.compile(regex, flags)
decor = lint_content_check(**kwargs)
def decorator(func):
@@ -102,10 +103,12 @@ def lint_re_check(regex, **kwargs):
if 'NOLINT' in match.group(0):
continue
lineno = content.count("\n", 0, match.start()) + 1
substr = content[:match.start()]
col = len(substr) - substr.rfind('\n')
err = func(fname, match)
if err is None:
continue
errors.append("{} See line {}.".format(err, lineno))
errors.append((lineno, col+1, err))
return errors
return decor(new_func)
return decorator
@@ -122,8 +125,7 @@ def lint_content_find_check(find, **kwargs):
errors = []
for line, col in find_all(content, find_):
err = func(fname)
errors.append("{err} See line {line}:{col}."
"".format(err=err, line=line+1, col=col+1))
errors.append((line+1, col+1, err))
return errors
return decor(new_func)
return decorator
@@ -134,7 +136,7 @@ def lint_ino(fname):
return "This file extension (.ino) is not allowed. Please use either .cpp or .h"
@lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [
@lint_file_check(exclude=[f'*{f}' for f in file_types] + [
'.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc',
'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*',
])
@@ -177,7 +179,7 @@ CPP_RE_EOL = r'\s*?(?://.*?)?$'
def highlight(s):
return '\033[36m{}\033[0m'.format(s)
return f'\033[36m{s}\033[0m'
@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL,
@@ -216,9 +218,10 @@ def lint_const_ordered(fname, content):
continue
target = next(i for i, l in ordered if l == ml)
target_text = next(l for i, l in matching if target == i)
errors.append("Constant {} is not ordered, please make sure all constants are ordered. "
"See line {} (should go to line {}, {})"
"".format(highlight(ml), mi, target, target_text))
errors.append((ml, None,
"Constant {} is not ordered, please make sure all constants are ordered. "
"See line {} (should go to line {}, {})"
"".format(highlight(ml), mi, target, target_text)))
return errors
@@ -253,6 +256,63 @@ def lint_conf_from_const_py(fname, match):
"const.py directly.".format(highlight(name)))
RAW_PIN_ACCESS_RE = r'^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)'
@lint_re_check(RAW_PIN_ACCESS_RE, include=cpp_include)
def lint_no_raw_pin_access(fname, match):
func = match.group(1)
pin = match.group(2)
mode = match.group(3)
new_func = {
'pinMode': 'pin_mode',
'digitalWrite': 'digital_write',
'digitalRead': 'digital_read',
}[func]
new_code = highlight(f'{pin}->{new_func}({mode})')
return (f"Don't use raw {func} calls. Instead, use the `->{new_func}` function: {new_code}")
# Functions from Arduino framework that are forbidden to use directly
ARDUINO_FORBIDDEN = [
'digitalWrite', 'digitalRead', 'pinMode',
'shiftOut', 'shiftIn',
'radians', 'degrees',
'interrupts', 'noInterrupts',
'lowByte', 'highByte',
'bitRead', 'bitSet', 'bitClear', 'bitWrite',
'bit', 'analogRead', 'analogWrite',
'pulseIn', 'pulseInLong',
'tone',
]
ARDUINO_FORBIDDEN_RE = r'[^\w\d](' + r'|'.join(ARDUINO_FORBIDDEN) + r')\(.*'
@lint_re_check(ARDUINO_FORBIDDEN_RE, include=cpp_include, exclude=[
'esphome/components/mqtt/custom_mqtt_device.h',
'esphome/core/esphal.*',
])
def lint_no_arduino_framework_functions(fname, match):
nolint = highlight("// NOLINT")
return (
f"The function {highlight(match.group(1))} from the Arduino framework is forbidden to be "
f"used directly in the ESPHome codebase. Please use ESPHome's abstractions and equivalent "
f"C++ instead.\n"
f"\n"
f"(If the function is strictly necessary, please add `{nolint}` to the end of the line)"
)
@lint_re_check(r'[^\w\d]byte\s+[\w\d]+\s*=.*', include=cpp_include, exclude={
'esphome/components/tuya/tuya.h',
})
def lint_no_byte_datatype(fname, match):
return (
f"The datatype {highlight('byte')} is not allowed to be used in ESPHome. "
f"Please use {highlight('uint8_t')} instead."
)
@lint_post_check
def lint_constants_usage():
errors = []
@@ -268,7 +328,7 @@ def lint_constants_usage():
def relative_cpp_search_text(fname, content):
parts = fname.split('/')
integration = parts[2]
return '#include "esphome/components/{}'.format(integration)
return f'#include "esphome/components/{integration}'
@lint_content_find_check(relative_cpp_search_text, include=['esphome/components/*.cpp'])
@@ -284,7 +344,7 @@ def lint_relative_cpp_import(fname):
def relative_py_search_text(fname, content):
parts = fname.split('/')
integration = parts[2]
return 'esphome.components.{}'.format(integration)
return f'esphome.components.{integration}'
@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'],
@@ -303,7 +363,7 @@ def lint_relative_py_import(fname):
def lint_namespace(fname, content):
expected_name = re.match(r'^esphome/components/([^/]+)/.*',
fname.replace(os.path.sep, '/')).group(1)
search = 'namespace {}'.format(expected_name)
search = f'namespace {expected_name}'
if search in content:
return None
return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \
@@ -326,6 +386,24 @@ def lint_pragma_once(fname, content):
return None
@lint_re_check(r'(whitelist|blacklist|slave)', exclude=['script/ci-custom.py'],
flags=re.IGNORECASE | re.MULTILINE)
def lint_inclusive_language(fname, match):
# From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb
return ("Avoid the use of whitelist/blacklist/slave.\n"
"Recommended replacements for 'master / slave' are:\n"
" '{primary,main} / {secondary,replica,subordinate}\n"
" '{initiator,requester} / {target,responder}'\n"
" '{controller,host} / {device,worker,proxy}'\n"
" 'leader / follower'\n"
" 'director / performer'\n"
"\n"
"Recommended replacements for 'blacklist/whitelist' are:\n"
" 'denylist / allowlist'\n"
" 'blocklist / passlist'")
@lint_content_find_check('ESP_LOG', include=['*.h', '*.tcc'], exclude=[
'esphome/components/binary_sensor/binary_sensor.h',
'esphome/components/cover/cover.h',
@@ -355,13 +433,22 @@ errors = collections.defaultdict(list)
def add_errors(fname, errs):
if not isinstance(errs, list):
errs = [errs]
errs = [x for x in errs if x is not None]
for err in errs:
if not isinstance(err, str):
if err is None:
continue
try:
lineno, col, msg = err
except ValueError:
lineno = 1
col = 1
msg = err
if not isinstance(msg, str):
raise ValueError("Error is not instance of string!")
if not errs:
return
errors[fname].extend(errs)
if not isinstance(lineno, int):
raise ValueError("Line number is not an int!")
if not isinstance(col, int):
raise ValueError("Column number is not an int!")
errors[fname].append((lineno, col, msg))
for fname in files:
@@ -380,9 +467,9 @@ for fname in files:
run_checks(LINT_POST_CHECKS, 'POST')
for f, errs in sorted(errors.items()):
print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f))
for err in errs:
print(err)
print(f"\033[0;32m************* File \033[1;32m{f}\033[0m")
for lineno, col, msg in errs:
print(f"ERROR {f}:{lineno}:{col} - {msg}")
print()
sys.exit(len(errors))

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from __future__ import print_function

View File

@@ -1,21 +1,20 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from __future__ import print_function
import argparse
import multiprocessing
import os
import re
import pexpect
import shutil
import subprocess
import sys
import tempfile
import argparse
import click
import threading
import click
import pexpect
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
@@ -49,7 +48,7 @@ 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)
timeout=15 * 60)
with lock:
if rc != 0:
print()
@@ -65,6 +64,11 @@ def progress_bar_show(value):
return ''
def split_list(a, n):
k, m = divmod(len(a), n)
return [a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-j', '--jobs', type=int,
@@ -77,6 +81,10 @@ def main():
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.',
default=None)
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')
args = parser.parse_args()
@@ -114,7 +122,10 @@ def main():
files.sort()
if args.all_headers:
if args.split_num:
files = split_list(files, args.split_num)[args.split_at - 1]
if args.all_headers and args.split_at in (None, 1):
files.insert(0, temp_header_file)
tmpdir = None
@@ -157,8 +168,8 @@ def main():
print('Error applying fixes.\n', file=sys.stderr)
raise
sys.exit(return_code)
return return_code
if __name__ == '__main__':
main()
sys.exit(main())

View File

@@ -9,4 +9,5 @@ set -x
script/ci-custom.py
script/lint-python
script/lint-cpp
script/unit_test
script/test

View File

@@ -12,11 +12,11 @@ temp_header_file = os.path.join(root_path, '.temp-clang-tidy.cpp')
def shlex_quote(s):
if not s:
return u"''"
return "''"
if re.search(r'[^\w@%+=:,./-]', s) is None:
return s
return u"'" + s.replace(u"'", u"'\"'\"'") + u"'"
return "'" + s.replace("'", "'\"'\"'") + "'"
def build_all_include():
@@ -29,7 +29,7 @@ def build_all_include():
if ext in filetypes:
path = os.path.relpath(path, root_path)
include_p = path.replace(os.path.sep, '/')
headers.append('#include "{}"'.format(include_p))
headers.append(f'#include "{include_p}"')
headers.sort()
headers.append('')
content = '\n'.join(headers)
@@ -47,7 +47,7 @@ def build_compile_commands():
gcc_flags = json.load(f)
exec_path = gcc_flags['execPath']
include_paths = gcc_flags['gccIncludePaths'].split(',')
includes = ['-I{}'.format(p) for p in include_paths]
includes = [f'-I{p}' for p in include_paths]
cpp_flags = gcc_flags['gccDefaultCppFlags'].split(' ')
defines = [flag for flag in cpp_flags if flag.startswith('-D')]
command = [exec_path]
@@ -101,8 +101,10 @@ def splitlines_no_ends(string):
def changed_files():
for remote in ('upstream', 'origin'):
command = ['git', 'merge-base', '{}/dev'.format(remote), 'HEAD']
check_remotes = ['upstream', 'origin']
check_remotes.extend(splitlines_no_ends(get_output('git', 'remote')))
for remote in check_remotes:
command = ['git', 'merge-base', f'refs/remotes/{remote}/dev', 'HEAD']
try:
merge_base = splitlines_no_ends(get_output(*command))[0]
break
@@ -124,7 +126,7 @@ def filter_changed(files):
if not files:
print(" No changed files!")
for c in files:
print(" {}".format(c))
print(f" {c}")
return files

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
from __future__ import print_function
@@ -61,7 +61,7 @@ def main():
continue
file_ = line[0]
linno = line[1]
msg = (u':'.join(line[3:])).strip()
msg = (':'.join(line[3:])).strip()
print_error(file_, linno, msg)
errors += 1
@@ -74,7 +74,7 @@ def main():
continue
file_ = line[0]
linno = line[1]
msg = (u':'.join(line[2:])).strip()
msg = (':'.join(line[2:])).strip()
print_error(file_, linno, msg)
errors += 1

View File

@@ -4,5 +4,5 @@
set -e
cd "$(dirname "$0")/.."
pip install -r requirements_test.txt
pip install -e .
pip3 install -r requirements.txt -r requirements_test.txt
pip3 install -e .

View File

@@ -9,3 +9,4 @@ set -x
esphome tests/test1.yaml compile
esphome tests/test2.yaml compile
esphome tests/test3.yaml compile
esphome tests/test4.yaml compile

9
script/unit_test Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")/.."
set -x
pytest tests/unit_tests