diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index bcd20a4c7..caae29e56 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,27 +1,27 @@
// https://github.com/microsoft/vscode-dev-containers/tree/v0.183.0/containers/python-3
{
- "name": "ESPHome - docs",
- "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.6",
- "postCreateCommand": "pip3 install -r requirements.txt",
- "forwardPorts": [8000],
- "settings": {
- "python.pythonPath": "/usr/local/bin/python",
- "python.languageServer": "Pylance",
- "python.linting.enabled": true,
- "python.linting.pylintEnabled": true,
- "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
- "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
- "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
- "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
- "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
- "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
- "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
- "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
- "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
- },
+ "name": "ESPHome - docs",
+ "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.6",
+ "postCreateCommand": "pip3 install -r requirements.txt -r requirements_test.txt",
+ "forwardPorts": [8000],
+ "settings": {
+ "python.pythonPath": "/usr/local/bin/python",
+ "python.languageServer": "Pylance",
+ "python.linting.enabled": true,
+ "python.linting.pylintEnabled": true,
+ "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
+ "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
+ "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
+ "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
+ "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
+ "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
+ "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
+ "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
+ "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
+ },
- "extensions": [
- "ms-python.python",
- "ms-python.vscode-pylance"
- ]
+ "extensions": [
+ "ms-python.python",
+ "ms-python.vscode-pylance"
+ ]
}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 813f10af8..e087ce429 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -25,8 +25,12 @@ jobs:
with:
python-version: 3.8
- name: Install dependencies
- run: pip install -r requirements.txt
- - name: Lint
+ run: pip install -r requirements.txt -r requirements_test.txt
+ - name: Register problem matchers
run: |
- python travis.py
- make html-strict
+ echo "::add-matcher::.github/workflows/matchers/ci-custom.json"
+
+ - name: Lint
+ run: python lint.py
+ - name: html-strict
+ run: make html-strict
diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json
new file mode 100644
index 000000000..1d5f2551c
--- /dev/null
+++ b/.github/workflows/matchers/ci-custom.json
@@ -0,0 +1,16 @@
+{
+ "problemMatcher": [
+ {
+ "owner": "ci-custom",
+ "pattern": [
+ {
+ "regexp": "^(.*):(\\d+):(\\d+):\\s+lint:\\s+(.*)$",
+ "file": 1,
+ "line": 2,
+ "column": 3,
+ "message": 4
+ }
+ ]
+ }
+ ]
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 74a11f924..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-language: python
-python: "3.6"
-install: pip install -r requirements.txt
-script:
-- python3 travis.py
-- make html-strict
diff --git a/Makefile b/Makefile
index 2e4e69dd4..b8e3a0592 100644
--- a/Makefile
+++ b/Makefile
@@ -51,7 +51,7 @@ copy-svg2png:
netlify: netlify-dependencies netlify-api html copy-svg2png
lint: html-strict
- python3 travis.py
+ python3 lint.py
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
diff --git a/changelog/2022.2.0.rst b/changelog/2022.2.0.rst
index c4f496bc2..d7cda5f0b 100644
--- a/changelog/2022.2.0.rst
+++ b/changelog/2022.2.0.rst
@@ -17,7 +17,7 @@ ESPHome 2022.2.0 - 16th February 2022
Touchscreen Core, components/touchscreen/index, folder-open.svg
EKTF2232, components/touchscreen/ektf2232, ektf2232.svg
- Lilygo T5 4.7", components/touchscreen/lilygo_t5_47, lilygo_t5_47_touch.png
+ Lilygo T5 4.7", components/touchscreen/lilygo_t5_47, lilygo_t5_47_touch.jpg
MLX90393, components/sensor/mlx90393, mlx90393.jpg
Wake-on-LAN Button, components/button/wake_on_lan, power_settings.svg
diff --git a/components/climate/ir_climate.rst b/components/climate/ir_climate.rst
old mode 100755
new mode 100644
diff --git a/components/sensor/images/dsmr-request-pin-circuit-example.png b/components/sensor/images/dsmr-request-pin-circuit-example.png
old mode 100755
new mode 100644
diff --git a/components/sensor/pmsa003i.rst b/components/sensor/pmsa003i.rst
index 4a33dc47a..ca44c6675 100644
--- a/components/sensor/pmsa003i.rst
+++ b/components/sensor/pmsa003i.rst
@@ -3,7 +3,7 @@ PMSA003I Particulate Matter Sensor
.. seo::
:description: Instructions for setting up PMSX003 Particulate matter sensors
- :image: pmsa003i-full.jpg
+ :image: pmsa003i.jpg
The ``pmsa003i`` sensor platform allows you to use your Plantower PMSA003I
particulate matter sensor
diff --git a/components/switch/images/safemode-ui.png b/components/switch/images/safemode-ui.png
old mode 100755
new mode 100644
diff --git a/components/touchscreen/lilygo_t5_47.rst b/components/touchscreen/lilygo_t5_47.rst
index 4e01bf01a..ba6a8336e 100644
--- a/components/touchscreen/lilygo_t5_47.rst
+++ b/components/touchscreen/lilygo_t5_47.rst
@@ -3,7 +3,7 @@ Lilygo T5 4.7" Touchscreen
.. seo::
:description: Instructions for setting up the Lilygo T5 4.7" Touchscreen with ESPHome
- :image: lilygo_t5_47_touch.png
+ :image: lilygo_t5_47_touch.jpg
:keywords: Lilygo T5 4.7" Touchscreen
The ``liygo_t5_47`` touchscreen platform allows using the touchscreen controller
diff --git a/cookbook/images/leak-detector-m5stickC_LeakDetected.gif b/cookbook/images/leak-detector-m5stickC_LeakDetected.gif
index be7122549..5517088bf 100644
Binary files a/cookbook/images/leak-detector-m5stickC_LeakDetected.gif and b/cookbook/images/leak-detector-m5stickC_LeakDetected.gif differ
diff --git a/devices/images/sonoff_t1_uk_3g_backplate_v1.1.jpg b/devices/images/sonoff_t1_uk_3g_backplate_v1.1.jpg
old mode 100755
new mode 100644
diff --git a/devices/images/sonoff_t1_uk_3g_touchpads_v1.1.jpg b/devices/images/sonoff_t1_uk_3g_touchpads_v1.1.jpg
old mode 100755
new mode 100644
diff --git a/images/donate-patreon.png b/images/donate-patreon.png
deleted file mode 100644
index 79754db09..000000000
Binary files a/images/donate-patreon.png and /dev/null differ
diff --git a/images/donate-paypal.png b/images/donate-paypal.png
deleted file mode 100644
index af4509e8c..000000000
Binary files a/images/donate-paypal.png and /dev/null differ
diff --git a/images/esp32-modbus.jpg b/images/esp32-modbus.jpg
index 93ed7d4db..cffc5a99a 100644
Binary files a/images/esp32-modbus.jpg and b/images/esp32-modbus.jpg differ
diff --git a/images/improv-social.png b/images/improv-social.png
index 37e7a1c26..5c643d2d7 100644
Binary files a/images/improv-social.png and b/images/improv-social.png differ
diff --git a/images/index.rst b/images/index.rst
index d48ef2697..ca71f1271 100644
--- a/images/index.rst
+++ b/images/index.rst
@@ -4,22 +4,10 @@ Images
This is a dummy file to include images in the sphinx output
that will only be used in raw HTML and thus not auto-included.
-.. image:: shield-donate.svg
-
.. image:: hero.png
.. image:: logo.svg
-.. image:: logo-core.svg
-
.. image:: logo-docs.svg
-.. image:: logo-flasher.svg
-
-.. image:: logo-release.svg
-
.. image:: logo-text.svg
-
-.. image:: donate-patreon.png
-
-.. image:: donate-paypal.png
diff --git a/images/lilygo_t5_47_touch.jpg b/images/lilygo_t5_47_touch.jpg
new file mode 100644
index 000000000..16cd817a2
Binary files /dev/null and b/images/lilygo_t5_47_touch.jpg differ
diff --git a/images/lilygo_t5_47_touch.png b/images/lilygo_t5_47_touch.png
deleted file mode 100644
index a9ce05038..000000000
Binary files a/images/lilygo_t5_47_touch.png and /dev/null differ
diff --git a/images/mcp3008.jpg b/images/mcp3008.jpg
index 0aa0670b8..5f542d51a 100644
Binary files a/images/mcp3008.jpg and b/images/mcp3008.jpg differ
diff --git a/images/mcp3204.jpg b/images/mcp3204.jpg
index 54107be81..39968e00c 100644
Binary files a/images/mcp3204.jpg and b/images/mcp3204.jpg differ
diff --git a/images/pipsolar.jpg b/images/pipsolar.jpg
index 74f00cd30..2aaac1da6 100644
Binary files a/images/pipsolar.jpg and b/images/pipsolar.jpg differ
diff --git a/images/pmsa003i-full.jpg b/images/pmsa003i-full.jpg
deleted file mode 100644
index 9adbd3bbb..000000000
Binary files a/images/pmsa003i-full.jpg and /dev/null differ
diff --git a/images/pwm.png b/images/pwm.png
index 689811f7e..c4a44b8aa 100644
Binary files a/images/pwm.png and b/images/pwm.png differ
diff --git a/images/rf_bridge.jpg b/images/rf_bridge.jpg
index 6fcfafedf..b7c6a1084 100644
Binary files a/images/rf_bridge.jpg and b/images/rf_bridge.jpg differ
diff --git a/images/rs485.jpg b/images/rs485.jpg
index e19ef9e90..57016a3e8 100644
Binary files a/images/rs485.jpg and b/images/rs485.jpg differ
diff --git a/images/scd30.jpg b/images/scd30.jpg
index 60c5a889e..ebb35f39b 100644
Binary files a/images/scd30.jpg and b/images/scd30.jpg differ
diff --git a/images/sgp40.jpg b/images/sgp40.jpg
index 947b7d251..05be6682e 100644
Binary files a/images/sgp40.jpg and b/images/sgp40.jpg differ
diff --git a/images/shield-donate.svg b/images/shield-donate.svg
deleted file mode 100644
index 86e56b817..000000000
--- a/images/shield-donate.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/images/sht4x.jpg b/images/sht4x.jpg
index 2a47d8536..2b816ac33 100644
Binary files a/images/sht4x.jpg and b/images/sht4x.jpg differ
diff --git a/images/sps30.jpg b/images/sps30.jpg
index 5041acf7b..3e32484df 100644
Binary files a/images/sps30.jpg and b/images/sps30.jpg differ
diff --git a/images/teleinfo-full.jpg b/images/teleinfo-full.jpg
deleted file mode 100644
index c1ade04a8..000000000
Binary files a/images/teleinfo-full.jpg and /dev/null differ
diff --git a/images/tuya.png b/images/tuya.png
index aa0aeee3b..a0549e20f 100644
Binary files a/images/tuya.png and b/images/tuya.png differ
diff --git a/images/vl53l0x.jpg b/images/vl53l0x.jpg
index cb5c5385b..1c5725a75 100644
Binary files a/images/vl53l0x.jpg and b/images/vl53l0x.jpg differ
diff --git a/images/xiaomi_miscale1&2.jpg b/images/xiaomi_miscale1&2.jpg
index 52414148d..19f6a757b 100644
Binary files a/images/xiaomi_miscale1&2.jpg and b/images/xiaomi_miscale1&2.jpg differ
diff --git a/images/xiaomi_miscale2.jpg b/images/xiaomi_miscale2.jpg
index aaabc11c9..568701622 100644
Binary files a/images/xiaomi_miscale2.jpg and b/images/xiaomi_miscale2.jpg differ
diff --git a/index.rst b/index.rst
index 23d8b5d2e..74af7f4af 100644
--- a/index.rst
+++ b/index.rst
@@ -539,7 +539,7 @@ Touchscreen Components
Touchscreen Core, components/touchscreen/index, folder-open.svg
EKTF2232, components/touchscreen/ektf2232, ektf2232.svg, Inkplate 6 Plus
- Lilygo T5 4.7", components/touchscreen/lilygo_t5_47, lilygo_t5_47_touch.png
+ Lilygo T5 4.7", components/touchscreen/lilygo_t5_47, lilygo_t5_47_touch.jpg
Cover Components
----------------
@@ -720,7 +720,6 @@ documentation for others to copy. See :doc:`Contributing `
:hidden:
web-api/index
- misc/index
components/index
cookbook/index
devices/index
diff --git a/lint.py b/lint.py
new file mode 100644
index 000000000..ed2e4110c
--- /dev/null
+++ b/lint.py
@@ -0,0 +1,453 @@
+#!/usr/bin/env python3
+
+import argparse
+import collections
+from pathlib import Path
+from typing import Optional
+import colorama
+import fnmatch
+import functools
+import os.path
+import re
+import sys
+import os
+import subprocess
+
+try:
+ from PIL import Image
+
+ PILLOW_INSTALLED = True
+except ImportError:
+ print("Pillow could not be imported - will not run image checks")
+ print("Install pillow with `pip3 install pillow`")
+ PILLOW_INSTALLED = False
+
+
+class AnsiFore:
+ KEEP = ""
+ BLACK = "\033[30m"
+ RED = "\033[31m"
+ GREEN = "\033[32m"
+ YELLOW = "\033[33m"
+ BLUE = "\033[34m"
+ MAGENTA = "\033[35m"
+ CYAN = "\033[36m"
+ WHITE = "\033[37m"
+ RESET = "\033[39m"
+
+ BOLD_BLACK = "\033[1;30m"
+ BOLD_RED = "\033[1;31m"
+ BOLD_GREEN = "\033[1;32m"
+ BOLD_YELLOW = "\033[1;33m"
+ BOLD_BLUE = "\033[1;34m"
+ BOLD_MAGENTA = "\033[1;35m"
+ BOLD_CYAN = "\033[1;36m"
+ BOLD_WHITE = "\033[1;37m"
+ BOLD_RESET = "\033[1;39m"
+
+
+class AnsiStyle:
+ BRIGHT = "\033[1m"
+ BOLD = "\033[1m"
+ DIM = "\033[2m"
+ THIN = "\033[2m"
+ NORMAL = "\033[22m"
+ RESET_ALL = "\033[0m"
+
+
+Fore = AnsiFore()
+Style = AnsiStyle()
+
+
+def print_error_for_file(file: str, body: Optional[str]):
+ print(f"{AnsiFore.GREEN}### File {AnsiStyle.BRIGHT}{file}{AnsiStyle.RESET_ALL}")
+ print()
+ if body is not None:
+ print(body)
+ print()
+
+
+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, _ = proc.communicate()
+ lines = [x.split() for x in output.decode("utf-8").splitlines()]
+ return {s[3].strip(): int(s[0]) for s in lines}
+
+
+def find_all(a_str, sub):
+ if not a_str.find(sub):
+ # Optimization: If str is not in whole text, then do not try
+ # on each line
+ return
+ for i, line in enumerate(a_str.split("\n")):
+ column = 0
+ while True:
+ column = line.find(sub, column)
+ if column == -1:
+ break
+ yield i, column
+ column += len(sub)
+
+
+colorama.init()
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+ "files", nargs="*", default=[], help="files to be processed (regex on path)"
+)
+args = parser.parse_args()
+
+EXECUTABLE_BIT = git_ls_files()
+files = list(EXECUTABLE_BIT.keys())
+# Match against re
+file_name_re = re.compile("|".join(args.files))
+files = [p for p in files if file_name_re.search(p)]
+
+files.sort()
+
+file_types = (
+ ".cfg",
+ ".css",
+ ".gif",
+ ".h",
+ ".html",
+ ".ico",
+ ".jpg",
+ ".js",
+ ".json",
+ ".md",
+ ".png",
+ ".py",
+ ".rst",
+ ".svg",
+ ".toml",
+ ".txt",
+ ".webmanifest",
+ ".xml",
+ ".yaml",
+ ".yml",
+ "",
+)
+docs_types = [".rst"]
+image_types = [".jpg", ".ico", ".png", ".svg", ".gif"]
+
+LINT_FILE_CHECKS = []
+LINT_CONTENT_CHECKS = []
+LINT_POST_CHECKS = []
+
+
+def run_check(lint_obj, fname, *args):
+ include = lint_obj["include"]
+ exclude = lint_obj["exclude"]
+ func = lint_obj["func"]
+ if include is not None:
+ for incl in include:
+ if fnmatch.fnmatch(fname, incl):
+ break
+ else:
+ return None
+ for excl in exclude:
+ if fnmatch.fnmatch(fname, excl):
+ return None
+ return func(*args)
+
+
+def run_checks(lints, fname, *args):
+ for lint in lints:
+ try:
+ add_errors(fname, run_check(lint, fname, *args))
+ except Exception:
+ print(f"Check {lint['func'].__name__} on file {fname} failed:")
+ raise
+
+
+def _add_check(checks, func, include=None, exclude=None):
+ checks.append(
+ {
+ "include": include,
+ "exclude": exclude or [],
+ "func": func,
+ }
+ )
+
+
+def lint_file_check(**kwargs):
+ def decorator(func):
+ _add_check(LINT_FILE_CHECKS, func, **kwargs)
+ return func
+
+ return decorator
+
+
+def lint_content_check(**kwargs):
+ def decorator(func):
+ _add_check(LINT_CONTENT_CHECKS, func, **kwargs)
+ return func
+
+ return decorator
+
+
+def lint_post_check(func):
+ _add_check(LINT_POST_CHECKS, func)
+ return func
+
+
+def lint_re_check(regex, **kwargs):
+ flags = kwargs.pop("flags", re.MULTILINE)
+ prog = re.compile(regex, flags)
+ decor = lint_content_check(**kwargs)
+
+ def decorator(func):
+ @functools.wraps(func)
+ def new_func(fname, content):
+ errors = []
+ for match in prog.finditer(content):
+ 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((lineno, col + 1, err))
+ return errors
+
+ return decor(new_func)
+
+ return decorator
+
+
+def lint_content_find_check(find, only_first=False, **kwargs):
+ decor = lint_content_check(**kwargs)
+
+ def decorator(func):
+ @functools.wraps(func)
+ def new_func(fname, content):
+ find_ = find
+ if callable(find):
+ find_ = find(fname, content)
+ errors = []
+ for line, col in find_all(content, find_):
+ err = func(fname)
+ errors.append((line + 1, col + 1, err))
+ if only_first:
+ break
+ return errors
+
+ return decor(new_func)
+
+ return decorator
+
+
+@lint_file_check(exclude=[f"*{f}" for f in file_types])
+def lint_ext_check(fname: str, stat: os.stat_result):
+ return (
+ "This file extension is not a registered file type. If this is an error, please "
+ "update the script/ci-custom.py script."
+ )
+
+
+@lint_file_check(exclude=["script/*", "lint.py"])
+def lint_executable_bit(fname: str, stat: os.stat_result):
+ ex = EXECUTABLE_BIT[fname]
+ if ex != 100644:
+ return (
+ "File has invalid executable bit {}. If running from a windows machine please "
+ "see disabling executable bit in git.".format(ex)
+ )
+ return None
+
+
+@lint_file_check(
+ include=[f"images/*{ext}" for ext in image_types], exclude=["images/hero.png"]
+)
+def lint_index_images_size(fname: str, stat: os.stat_result):
+ if stat.st_size > 40 * 1024:
+ return (
+ "Image is too large. The files in the images/ folder are displayed on esphome's "
+ "front page and thus should be small (no more than 300x300px, and <40kb). "
+ "Use a tool like https://compress-or-die.com/ to reduce the image size. "
+ f"Size of file: {stat.st_size / 1024:.0f}kb"
+ )
+ return None
+
+
+@lint_file_check(include=[f"*{ext}" for ext in image_types])
+def lint_all_images_size(fname: str, stat: os.stat_result):
+ if stat.st_size > 1024 * 1024:
+ return (
+ "Image is too large. Images in ESPHome's codebase should be 1MB in size max. "
+ "Use a tool like https://compress-or-die.com/ to reduce the image size. "
+ f"Size of file: {stat.st_size / 1024:.0f}kb"
+ )
+ return None
+
+
+if PILLOW_INSTALLED:
+
+ @lint_file_check(
+ include=["images/*.jpg", "images/*.png"], exclude=["images/hero.png"]
+ )
+ def lint_index_images_dimensions(fname: str, stat: os.stat_result):
+ img = Image.open(fname)
+ if img.width > 300 or img.height > 300:
+ return (
+ "Image has too large dimensions. The images in the images/ folder are displayed on "
+ "ESPHome's main page, so need to be lightweight. We allow a max of 300x300 for images on this page. "
+ "Use a tool like https://compress-or-die.com/ to reduce the image size. "
+ f"Dimensions of this image: {img.width}x{img.height}"
+ )
+ return None
+
+
+@lint_content_find_check(
+ "\t",
+ only_first=True,
+ exclude=[
+ "Makefile",
+ ],
+)
+def lint_tabs(fname):
+ return "File contains tab character. Please convert tabs to spaces."
+
+
+@lint_content_find_check("\r", only_first=True)
+def lint_newline(fname):
+ return "File contains Windows newline. Please set your editor to Unix newline mode."
+
+
+@lint_content_check(exclude=["*.svg", "runtime.txt", "_static/*"])
+def lint_end_newline(fname, content):
+ if content and not content.endswith("\n"):
+ return "File does not end with a newline, please add an empty line at the end of the file."
+ return None
+
+
+section_regex = re.compile(r"^(=+|-+|\*+|~+)$")
+directive_regex = re.compile(r"^(\s*)\.\. (.*)::.*$")
+directive_arg_regex = re.compile(r"^(\s+):.*:\s*.*$")
+
+
+@lint_content_check(include=["*.rst"])
+def lint_directive_formatting(fname, content):
+ errors = []
+ lines = content.splitlines(keepends=False)
+
+ for i, line in enumerate(lines):
+ m = directive_regex.match(line)
+ if m is None:
+ continue
+ base_indentation = len(m.group(1))
+ directive_name = m.group(2)
+ if directive_name.startswith("|") or directive_name == "seo":
+ continue
+ # Match directive args
+ for j in range(i + 1, len(lines)):
+ if not directive_arg_regex.match(lines[j]):
+ break
+ else:
+ # Reached end of file
+ continue
+
+ # Empty line must follow
+ if lines[j]:
+ errors.append(
+ "Directive '{}' is not followed by an empty line. Please insert an "
+ "empty line after {}:{}".format(directive_name, f, j)
+ )
+ continue
+
+ k = j + 1
+ for j in range(k, len(lines)):
+ if not lines[j]:
+ # Ignore Empty lines
+ continue
+
+ num_spaces = len(lines[j]) - len(lines[j].lstrip())
+ if num_spaces <= base_indentation:
+ # Finished with this directive
+ break
+ num_indent = num_spaces - base_indentation
+ if j == k and num_indent != 4:
+ errors.append(
+ "Directive '{}' must be indented with 4 spaces, not {}. See "
+ "{}:{}".format(directive_name, num_indent, f, j + 1)
+ )
+ break
+
+
+@lint_re_check(
+ r"https://esphome.io/",
+ include=["*.rst"],
+ exclude=[
+ "components/web_server.rst",
+ ],
+)
+def lint_esphome_io_link(fname, match):
+ return (
+ "All links to esphome.io should be relative, please remove esphome.io from URL"
+ )
+
+
+def highlight(s):
+ return f"\033[36m{s}\033[0m"
+
+
+errors = collections.defaultdict(list)
+
+
+def add_errors(fname, errs):
+ if not isinstance(errs, list):
+ errs = [errs]
+ for err in errs:
+ 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 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:
+ p = Path(fname)
+ if not p.is_file():
+ # file deleted but in git index
+ continue
+ run_checks(LINT_FILE_CHECKS, fname, fname, p.stat())
+ if p.suffix in image_types:
+ continue
+ try:
+ with open(fname, "r") as f_handle:
+ content = f_handle.read()
+ except UnicodeDecodeError:
+ add_errors(
+ fname,
+ "File is not readable as UTF-8. Please set your editor to UTF-8 mode.",
+ )
+ continue
+ run_checks(LINT_CONTENT_CHECKS, fname, fname, content)
+
+run_checks(LINT_POST_CHECKS, "POST")
+
+for f, errs in sorted(errors.items()):
+ err_str = (
+ f"{AnsiStyle.BOLD}{f}:{lineno}:{col}:{AnsiStyle.RESET_ALL} "
+ f"{AnsiStyle.BOLD}{AnsiFore.RED}{'lint:'}{AnsiStyle.RESET_ALL} {msg}\n"
+ for lineno, col, msg in errs
+ )
+ print_error_for_file(f, "\n".join(err_str))
+
+sys.exit(len(errors))
diff --git a/misc/index.rst b/misc/index.rst
deleted file mode 100644
index f67cb0a1f..000000000
--- a/misc/index.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Misc
-====
-
-.. toctree::
- :glob:
-
- *
diff --git a/misc/privacy.rst b/misc/privacy.rst
deleted file mode 100644
index 73b4180f6..000000000
--- a/misc/privacy.rst
+++ /dev/null
@@ -1,129 +0,0 @@
-Privacy Policy
-==============
-
-*Effective date: December 31, 2018*
-
-The esphomelib team ("us", "we", or "our") operates the https://esphomelib.com/ and https://esphome.io/ website
-(the "Service").
-
-This page informs you of our policies regarding the collection, use, and disclosure of personal data when you use
-our Service and the choices you have associated with that data.
-
-We will not use or share your information with anyone except as described in this Privacy Policy.
-
-Information Collection And Use
-------------------------------
-
-We collect several different types of information for various purposes to provide and improve our Service to you.
-
-We may also collect information how the Service is accessed and used ("Usage Data"). This Usage Data may include
-information such as your computer's Internet Protocol address (e.g. IP address), browser type,
-browser version, the pages of our Service that you visit, the time and date of your visit,
-the time spent on those pages, unique device identifiers and other diagnostic data.
-
-From time to time, we may release non-personally-identifying information in the aggregate,
-e.g., by publishing a report on trends in the usage of our website.
-
-In addition, we may use third party services such as Google Analytics that collect, monitor and analyze
-this type of information in order to increase our Service's functionality. These third party
-service providers have their own privacy policies addressing how they use such information.
-
-Gathering of Personally-Identifying Information
------------------------------------------------
-
-Certain visitors to our websites choose to interact with us in ways that require us to gather personally-identifying
-information. The amount and type of information that we gather depends on the nature of the interaction.
-For example, we ask visitors who create a forum account to provide a username and an email address.
-Those who engage in transactions with us are asked to provide additional information, including as necessary the
-personal and financial information required to process those transactions. In each case, we collect such information
-only insofar as is necessary or appropriate to fulfill the purpose of the visitor’s interaction. Visitors can always
-refuse to supply personally-identifying information, with the caveat that it may prevent them from engaging in certain
-website-related activities.
-
-Service Providers
------------------
-
-We may employ third party companies and individuals to facilitate our Service, to provide the Service on our behalf,
-to perform Service-related services or to assist us in analyzing how our Service is used.
-
-Google Analytics
-****************
-
-Google Analytics is a web analytics service offered by Google that tracks and reports website traffic.
-Google uses the data collected to track and monitor the use of our Service. This data is shared with other Google
-services. Google may use the collected data to contextualize and personalize the ads of its own advertising network.
-
-You can opt-out of having made your activity on the Service available to Google Analytics by installing the
-Google Analytics opt-out browser add-on (see http://tools.google.com/dlpage/gaoptout).
-The add-on prevents the Google Analytics JavaScript (ga.js, analytics.js, and dc.js) from sharing information with Google Analytics about visits activity.
-
-This website uses the anonymize IP function to crop your IP address in accordance with the IP masking requirements
-of EU privacy laws. This means that your IP address is shortened so that it can not be used to identify you
-and is no longer considered Personal Information.
-
-For more information on the privacy practices of Google, please visit the Google Privacy & Terms web page: https://policies.google.com/privacy?hl=en
-
-Cloudflare
-**********
-
-The Internet presence of this company is provided by the technology partner CloudFlare Inc., based in the USA.
-All data passed to or from this website pass through the worldwide network of CloudFlare, Inc.
-
-For more information, see CloudFlare's Privacy Statement at:
-https://www.cloudflare.com/security-policy/
-
-GitHub Pages
-************
-
-Our website is served through the GitHub pages service, operated by GitHub Inc., based in the USA.
-
-For more information, see GitHub's Privacy Statement at:
-https://help.github.com/articles/github-privacy-statement/
-
-Disqus
-******
-
-Our website uses the comment system provided by `Disqus `__ to enable commenting
-on pages directly under the page content. On all pages with a comment section, disqus may track you across website
-visits. When you choose to add a comment, you will additionally be asked to log in to your disqus account
-with your personal information. We're committed to your privacy, therefore all settings related to tracking
-by Disqus have been disabled for our websites.
-
-For more information, see Disqus's Privacy Statement at:
-https://help.disqus.com/terms-and-policies/disqus-privacy-policy
-
-Cookies
--------
-
-A cookie is a string of information that a website stores on a visitor’s computer, and that the visitor’s browser
-provides to the website each time the visitor returns. We uses cookies to help us identify and track visitors,
-their usage of our website, and their website access preferences. Visitors who do not wish to have cookies placed
-on their computers should set their browsers to refuse cookies before using our websites, with the drawback that
-certain features of our websites may not function properly without the aid of cookies.
-
-Links To Other Sites
---------------------
-
-Our Service may contain links to other sites that are not operated by us. If you click on a third party link,
-you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of
-every site you visit.
-
-We have no control over and assume no responsibility for the content, privacy policies or practices of any
-third party sites or services.
-
-Changes To This Privacy Policy
-------------------------------
-
-We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new
-Privacy Policy on this page.
-
-We will let you know via a prominent notice on our Service, prior to the change becoming effective and update
-the "effective date" at the top of this Privacy Policy.
-
-You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective
-when they are posted on this page.
-
-Contact Us
-----------
-
-If you have any questions about this Privacy Policy, please contact us by email: esphome@nabucasa.com
diff --git a/requirements_test.txt b/requirements_test.txt
new file mode 100644
index 000000000..aa3d908cf
--- /dev/null
+++ b/requirements_test.txt
@@ -0,0 +1,2 @@
+# for lint.py checking of images
+pillow
diff --git a/travis.py b/travis.py
deleted file mode 100644
index fd5b96c15..000000000
--- a/travis.py
+++ /dev/null
@@ -1,125 +0,0 @@
-from pathlib import Path
-import re
-import sys
-
-errors = []
-
-
-def find_all(a_str, sub):
- for i, line in enumerate(a_str.splitlines()):
- column = 0
- while True:
- column = line.find(sub, column)
- if column == -1:
- break
- yield i, column
- column += len(sub)
-
-
-section_regex = re.compile(r"^(=+|-+|\*+|~+)$")
-directive_regex = re.compile(r"^(\s*)\.\. (.*)::.*$")
-directive_arg_regex = re.compile(r"^(\s+):.*:\s*.*$")
-esphome_io_regex = re.compile(r"https://esphome.io/")
-
-
-for f in sorted(Path(".").glob("**/*.rst")):
- try:
- content = f.read_text("utf-8")
- except UnicodeDecodeError:
- errors.append(
- "File {} is not readable as UTF-8. Please set your editor to UTF-8 mode."
- "".format(f)
- )
- continue
-
- if not content.endswith("\n"):
- errors.append(
- "Newline at end of file missing. Please insert an empty line at end "
- "of file {}".format(f)
- )
-
- # Check tab character
- for line, col in find_all(content, "\t"):
- errors.append(
- "File {} contains tab character on line {}:{}. "
- "Please convert tabs to spaces.".format(f, line + 1, col)
- )
- # Check windows newline
- for line, col in find_all(content, "\r"):
- errors.append(
- "File {} contains windows newline on line {}:{}. "
- "Please set your editor to unix newline mode.".format(f, line + 1, col)
- )
-
- lines = content.splitlines(keepends=False)
-
- # for i, line in enumerate(lines):
- # if i == 0:
- # continue
- #
- # if not section_regex.match(line):
- # continue
- # line_above = lines[i - 1]
- # if len(line_above) != len(line):
- # errors.append("The title length must match the bar length below it. See {}:{}"
- # "".format(f, i+1))
- # if i + 1 < len(lines) and lines[i + 1]:
- # errors.append("Empty line after heading is missing. Please insert an "
- # "empty line. See {}:{}".format(f, i+1))
-
- for i, line in enumerate(lines):
- m = directive_regex.match(line)
- if m is None:
- continue
- base_indentation = len(m.group(1))
- directive_name = m.group(2)
- if directive_name.startswith("|") or directive_name == "seo":
- continue
- # Match directive args
- for j in range(i + 1, len(lines)):
- if not directive_arg_regex.match(lines[j]):
- break
- else:
- # Reached end of file
- continue
-
- # Empty line must follow
- if lines[j]:
- errors.append(
- "Directive '{}' is not followed by an empty line. Please insert an "
- "empty line after {}:{}".format(directive_name, f, j)
- )
- continue
-
- k = j + 1
- for j in range(k, len(lines)):
- if not lines[j]:
- # Ignore Empty lines
- continue
-
- num_spaces = len(lines[j]) - len(lines[j].lstrip())
- if num_spaces <= base_indentation:
- # Finished with this directive
- break
- num_indent = num_spaces - base_indentation
- if j == k and num_indent != 4:
- errors.append(
- "Directive '{}' must be indented with 4 spaces, not {}. See "
- "{}:{}".format(directive_name, num_indent, f, j + 1)
- )
- break
-
- for i, line in enumerate(lines):
- if esphome_io_regex.search(line):
- if "privacy.rst" in str(f) or "web_server.rst" in str(f):
- continue
- errors.append(
- "All links to esphome.io should be relative, please remove esphome.io "
- "from URL. See {}:{}".format(f, i + 1)
- )
-
-
-for error in errors:
- print(error)
-
-sys.exit(len(errors))