diff --git a/.editorconfig b/.editorconfig index f24d70487a..29cbb1e32f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,3 +25,4 @@ indent_size = 2 [*.{yaml,yml}] indent_style = space indent_size = 2 +quote_type = single \ No newline at end of file diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 8036470f52..e7642c6434 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -18,6 +18,7 @@ jobs: name: Build docker containers runs-on: ubuntu-latest strategy: + fail-fast: false matrix: arch: [amd64, armv7, aarch64] build_type: ["hassio", "docker"] @@ -37,9 +38,9 @@ jobs: dockerfile="docker/Dockerfile" fi - echo "::set-env name=BUILD_FROM::${build_from}" - echo "::set-env name=BUILD_TO::${build_to}" - echo "::set-env name=DOCKERFILE::${dockerfile}" + 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b26f7e2f43..8136b0678e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,45 +11,6 @@ on: pull_request: jobs: - # A fast overview job that checks only changed files - overview: - runs-on: ubuntu-latest - container: esphome/esphome-lint:latest - steps: - # Also fetch history and dev branch so that we can check which files changed - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Fetch dev branch - run: git fetch origin dev - - # Cache the .pio directory with (primarily) library dependencies - - name: Cache .pio lib_deps - uses: actions/cache@v1 - with: - path: .pio - key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} - restore-keys: | - lint-cpp-pio- - - name: Set up python environment - run: script/setup - # 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/ci-custom.json" - echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" - echo "::add-matcher::.github/workflows/matchers/gcc.json" - echo "::add-matcher::.github/workflows/matchers/lint-python.json" - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Run a quick lint over all changed files - run: script/quicklint - - name: Suggest changes - run: script/ci-suggest-changes - lint-clang-format: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* @@ -83,6 +44,7 @@ jobs: container: esphome/esphome-lint:latest # 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: @@ -146,6 +108,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: test: - test1 diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 81c3a80b05..22ac278af7 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -41,6 +41,7 @@ jobs: container: esphome/esphome-lint:latest # 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: @@ -104,6 +105,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: test: - test1 @@ -187,7 +189,7 @@ jobs: - name: Set TAG run: | TAG="${GITHUB_SHA:0:7}" - echo "::set-env name=TAG::${TAG}" + echo "TAG=${TAG}" >> $GITHUB_ENV - name: Set up env variables run: | base_version="2.6.0" @@ -202,9 +204,9 @@ jobs: dockerfile="docker/Dockerfile" fi - echo "::set-env name=BUILD_FROM::${build_from}" - echo "::set-env name=BUILD_TO::${build_to}" - echo "::set-env name=DOCKERFILE::${dockerfile}" + 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 @@ -241,7 +243,7 @@ jobs: - name: Set TAG run: | TAG="${GITHUB_SHA:0:7}" - echo "::set-env name=TAG::${TAG}" + echo "TAG=${TAG}" >> $GITHUB_ENV - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7ac80355f2..bbaefe4ea6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -40,6 +40,7 @@ jobs: container: esphome/esphome-lint:latest # 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: @@ -103,6 +104,7 @@ jobs: test: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: test: - test1 @@ -207,7 +209,7 @@ jobs: - name: Set TAG run: | TAG="${GITHUB_REF#refs/tags/v}" - echo "::set-env name=TAG::${TAG}" + echo "TAG=${TAG}" >> $GITHUB_ENV - name: Set up env variables run: | base_version="2.6.0" @@ -229,10 +231,10 @@ jobs: fi # Set env variables so these values don't need to be calculated again - echo "::set-env name=BUILD_FROM::${build_from}" - echo "::set-env name=BUILD_TO::${build_to}" - echo "::set-env name=DOCKERFILE::${dockerfile}" - echo "::set-env name=CACHE_TAG::${cache_tag}" + 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 @@ -277,7 +279,7 @@ jobs: - name: Set TAG run: | TAG="${GITHUB_REF#refs/tags/v}" - echo "::set-env name=TAG::${TAG}" + echo "TAG=${TAG}" >> $GITHUB_ENV - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} diff --git a/.gitignore b/.gitignore index 3afc3dbb66..86a27eb4b4 100644 --- a/.gitignore +++ b/.gitignore @@ -81,7 +81,8 @@ venv.bak/ .pioenvs .piolibdeps .pio -.vscode +.vscode/ +!.vscode/tasks.json CMakeListsPrivate.txt CMakeLists.txt @@ -119,4 +120,4 @@ config/ tests/build/ tests/.esphome/ /.temp-clang-tidy.cpp -/.idea/ +.pio/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000..e11600b093 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,11 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "run", + "type": "shell", + "command": "python3 -m esphome config dashboard", + "problemMatcher": [] + } + ] +} diff --git a/CODEOWNERS b/CODEOWNERS index 1722f482ac..0106b87fa8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,10 +13,13 @@ esphome/core/* @esphome/core # Integrations esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core +esphome/components/animation/* @syndlex esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter +esphome/components/atc_mithermometer/* @ahpohl esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet @@ -26,24 +29,37 @@ esphome/components/ct_clamp/* @jesserockz esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter +esphome/components/ds1307/* @badbadc0ffee esphome/components/exposure_notifications/* @OttoWinter +esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/homeassistant/* @OttoWinter esphome/components/i2c/* @esphome/core +esphome/components/inkplate6/* @jesserockz esphome/components/integration/* @OttoWinter esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter esphome/components/ledc/* @OttoWinter esphome/components/light/* @esphome/core esphome/components/logger/* @esphome/core +esphome/components/mcp23s08/* @SenexCrenshaw +esphome/components/mcp23s17/* @SenexCrenshaw +esphome/components/mcp2515/* @danielschramm @mvturnho +esphome/components/mcp9808/* @k7hpn esphome/components/network/* @esphome/core +esphome/components/nfc/* @jesserockz esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/pid/* @OttoWinter -esphome/components/pn532/* @OttoWinter +esphome/components/pn532/* @OttoWinter @jesserockz +esphome/components/pn532_i2c/* @OttoWinter @jesserockz +esphome/components/pn532_spi/* @OttoWinter @jesserockz esphome/components/power_supply/* @esphome/core +esphome/components/rc522/* @glmnet +esphome/components/rc522_i2c/* @glmnet +esphome/components/rc522_spi/* @glmnet esphome/components/restart/* @esphome/core esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet @@ -52,12 +68,28 @@ esphome/components/sensor/* @esphome/core esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/spi/* @esphome/core +esphome/components/ssd1322_base/* @kbx81 +esphome/components/ssd1322_spi/* @kbx81 +esphome/components/ssd1325_base/* @kbx81 +esphome/components/ssd1325_spi/* @kbx81 +esphome/components/ssd1327_base/* @kbx81 +esphome/components/ssd1327_i2c/* @kbx81 +esphome/components/ssd1327_spi/* @kbx81 +esphome/components/ssd1331_base/* @kbx81 +esphome/components/ssd1331_spi/* @kbx81 +esphome/components/ssd1351_base/* @kbx81 +esphome/components/ssd1351_spi/* @kbx81 +esphome/components/st7735/* @SenexCrenshaw +esphome/components/st7789v/* @kbx81 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core esphome/components/tcl112/* @glmnet +esphome/components/teleinfo/* @0hax +esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tm1637/* @glmnet +esphome/components/tmp102/* @timsavage esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz @@ -67,3 +99,5 @@ esphome/components/ultrasonic/* @OttoWinter esphome/components/version/* @esphome/core esphome/components/web_server_base/* @OttoWinter esphome/components/whirlpool/* @glmnet +esphome/components/xiaomi_lywsd03mmc/* @ahpohl +esphome/components/xiaomi_mhoc401/* @vevsvevs diff --git a/docker/Dockerfile b/docker/Dockerfile index 865741a39f..12b06ec284 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -12,6 +12,13 @@ RUN pip3 install --no-cache-dir -e . # Settings for dashboard ENV USERNAME="" PASSWORD="" +# Expose the dashboard to Docker +EXPOSE 6052 + +# Run healthcheck (heartbeat) +HEALTHCHECK --interval=5m --timeout=3s \ + CMD curl --fail http://localhost:6052 || exit 1 + # The directory the user should mount their configuration files to WORKDIR /config # Set entrypoint to esphome so that the user doesn't have to type 'esphome' diff --git a/esphome/__main__.py b/esphome/__main__.py index 79488e9b55..200ab2d7d7 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -235,6 +235,9 @@ def setup_log(debug=False, quiet=False): logging.getLogger('urllib3').setLevel(logging.WARNING) try: + import colorama + colorama.init(strip=True) + from colorlog import ColoredFormatter logging.getLogger().handlers[0].setFormatter(ColoredFormatter( colorfmt, diff --git a/esphome/api/client.py b/esphome/api/client.py index fcea90e3b4..a32c239819 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -181,7 +181,7 @@ class APIClient(threading.Thread): self._address) _LOGGER.warning("(If this error persists, please set a static IP address: " "https://esphome.io/components/wifi.html#manual-ips)") - raise APIConnectionError(err) + raise APIConnectionError(err) from err _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip) self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -346,12 +346,12 @@ class APIClient(threading.Thread): raise APIConnectionError("No socket!") try: val = self._socket.recv(amount - len(ret)) - except AttributeError: - raise APIConnectionError("Socket was closed") + except AttributeError as err: + raise APIConnectionError("Socket was closed") from err except socket.timeout: continue except OSError as err: - raise APIConnectionError(f"Error while receiving data: {err}") + raise APIConnectionError(f"Error while receiving data: {err}") from err ret += val return ret diff --git a/esphome/automation.py b/esphome/automation.py index 5df884e7c2..4b5e39b0f5 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -84,6 +84,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): return cv.Schema([schema])(value) except cv.Invalid as err2: if 'extra keys not allowed' in str(err2) and len(err2.path) == 2: + # pylint: disable=raise-missing-from raise err if 'Unable to find action' in str(err): raise err2 diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index c4752abf39..d55f585b26 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -8,6 +8,9 @@ static const char *TAG = "ade7953"; void ADE7953::dump_config() { ESP_LOGCONFIG(TAG, "ADE7953:"); + if (this->has_irq_) { + ESP_LOGCONFIG(TAG, " IRQ Pin: GPIO%u", this->irq_pin_number_); + } LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h index 7591bc1684..e0fadf37c3 100644 --- a/esphome/components/ade7953/ade7953.h +++ b/esphome/components/ade7953/ade7953.h @@ -9,6 +9,10 @@ namespace ade7953 { class ADE7953 : public i2c::I2CDevice, public PollingComponent { public: + void set_irq_pin(uint8_t irq_pin) { + has_irq_ = true; + irq_pin_number_ = irq_pin; + } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } @@ -20,6 +24,11 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { } void setup() override { + if (this->has_irq_) { + auto pin = GPIOPin(this->irq_pin_number_, INPUT); + this->irq_pin_ = &pin; + this->irq_pin_->setup(); + } this->set_timeout(100, [this]() { this->ade_write_(0x0010, 0x04); this->ade_write_(0x00FE, 0xAD); @@ -55,6 +64,9 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { return result; } + bool has_irq_ = false; + uint8_t irq_pin_number_; + GPIOPin *irq_pin_{nullptr}; bool is_setup_{false}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_a_sensor_{nullptr}; diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index b048b1ed71..d29e2dd13e 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -1,14 +1,16 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, i2c +from esphome import pins from esphome.const import CONF_ID, CONF_VOLTAGE, \ UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT DEPENDENCIES = ['i2c'] -ace7953_ns = cg.esphome_ns.namespace('ade7953') -ADE7953 = ace7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice) +ade7953_ns = cg.esphome_ns.namespace('ade7953') +ADE7953 = ade7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice) +CONF_IRQ_PIN = 'irq_pin' CONF_CURRENT_A = 'current_a' CONF_CURRENT_B = 'current_b' CONF_ACTIVE_POWER_A = 'active_power_a' @@ -16,7 +18,7 @@ CONF_ACTIVE_POWER_B = 'active_power_b' CONFIG_SCHEMA = cv.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_FLASH, 1), cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), @@ -30,6 +32,9 @@ def to_code(config): yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + if CONF_IRQ_PIN in config: + cg.add(var.set_irq_pin(config[CONF_IRQ_PIN])) + for key in [CONF_VOLTAGE, CONF_CURRENT_A, CONF_CURRENT_B, CONF_ACTIVE_POWER_A, CONF_ACTIVE_POWER_B]: if key not in config: diff --git a/esphome/components/animation/__init__.py b/esphome/components/animation/__init__.py new file mode 100644 index 0000000000..f19dbd6946 --- /dev/null +++ b/esphome/components/animation/__init__.py @@ -0,0 +1,94 @@ +import logging + +from esphome import core +from esphome.components import display, font +import esphome.components.image as espImage +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE +from esphome.core import CORE, HexInt + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['display'] +MULTI_CONF = True + +Animation_ = display.display_ns.class_('Animation') + +CONF_RAW_DATA_ID = 'raw_data_id' + +ANIMATION_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(Animation_), + cv.Required(CONF_FILE): cv.file_, + cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default='BINARY'): cv.enum(espImage.IMAGE_TYPE, upper=True), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), +}) + +CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) + +CODEOWNERS = ['@syndlex'] + + +def to_code(config): + from PIL import Image + + path = CORE.relative_config_path(config[CONF_FILE]) + try: + image = Image.open(path) + except Exception as e: + raise core.EsphomeError(f"Could not load image file {path}: {e}") + + width, height = image.size + frames = image.n_frames + if CONF_RESIZE in config: + image.thumbnail(config[CONF_RESIZE]) + width, height = image.size + else: + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using" + " the resize parameter.") + + if config[CONF_TYPE] == 'GRAYSCALE': + data = [0 for _ in range(height * width * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert('L', dither=Image.NONE) + pixels = list(frame.getdata()) + for pix in pixels: + data[pos] = pix + pos += 1 + + elif config[CONF_TYPE] == 'RGB24': + data = [0 for _ in range(height * width * 3 * frames)] + pos = 0 + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert('RGB') + pixels = list(frame.getdata()) + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == 'BINARY': + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range((height * width8 // 8) * frames)] + for frameIndex in range(frames): + image.seek(frameIndex) + frame = image.convert('1', dither=Image.NONE) + for y in range(height): + for x in range(width): + if frame.getpixel((x, y)): + continue + pos = x + y * width8 + (height * width8 * frameIndex) + data[pos // 8] |= 0x80 >> (pos % 8) + + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, frames, + espImage.IMAGE_TYPE[config[CONF_TYPE]]) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 403242d236..271b1d6c5f 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -679,7 +679,7 @@ enum ClimateSwingMode { CLIMATE_SWING_OFF = 0; CLIMATE_SWING_BOTH = 1; CLIMATE_SWING_VERTICAL = 2; - CLIMATE_SWINT_HORIZONTAL = 3; + CLIMATE_SWING_HORIZONTAL = 3; } enum ClimateAction { CLIMATE_ACTION_OFF = 0; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1956f3119d..431be5b4dc 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -676,8 +676,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) } } - this->client_->add(reinterpret_cast(header.data()), header.size()); - this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size()); + this->client_->add(reinterpret_cast(header.data()), header.size(), + ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); + this->client_->add(reinterpret_cast(buffer.get_buffer()->data()), buffer.get_buffer()->size(), + ASYNC_WRITE_FLAG_COPY); bool ret = this->client_->send(); return ret; } diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index c659561aa8..a7e521c699 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -154,8 +154,8 @@ template<> const char *proto_enum_to_string(enums::Clim return "CLIMATE_SWING_BOTH"; case enums::CLIMATE_SWING_VERTICAL: return "CLIMATE_SWING_VERTICAL"; - case enums::CLIMATE_SWINT_HORIZONTAL: - return "CLIMATE_SWINT_HORIZONTAL"; + case enums::CLIMATE_SWING_HORIZONTAL: + return "CLIMATE_SWING_HORIZONTAL"; default: return "UNKNOWN"; } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 306bdbf5a9..b97320b48a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -74,7 +74,7 @@ enum ClimateSwingMode : uint32_t { CLIMATE_SWING_OFF = 0, CLIMATE_SWING_BOTH = 1, CLIMATE_SWING_VERTICAL = 2, - CLIMATE_SWINT_HORIZONTAL = 3, + CLIMATE_SWING_HORIZONTAL = 3, }; enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index 3d2f669f54..4bd22af769 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -62,8 +62,7 @@ void ProtoMessage::decode(const uint8_t *buffer, size_t length) { error = true; break; } - uint32_t val = (uint32_t(buffer[i]) << 0) | (uint32_t(buffer[i + 1]) << 8) | (uint32_t(buffer[i + 2]) << 16) | - (uint32_t(buffer[i + 3]) << 24); + uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %u with value %u!", field_id, val); } diff --git a/esphome/components/atc_mithermometer/__init__.py b/esphome/components/atc_mithermometer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.cpp b/esphome/components/atc_mithermometer/atc_mithermometer.cpp new file mode 100644 index 0000000000..1a555cfa83 --- /dev/null +++ b/esphome/components/atc_mithermometer/atc_mithermometer.cpp @@ -0,0 +1,137 @@ +#include "atc_mithermometer.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace atc_mithermometer { + +static const char *TAG = "atc_mithermometer"; + +void ATCMiThermometer::dump_config() { + ESP_LOGCONFIG(TAG, "ATC 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 ATCMiThermometer::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 ATCMiThermometer::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[12]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[12]; + result.is_duplicate = false; + + return result; +} + +bool ATCMiThermometer::parse_message(const std::vector &message, ParseResult &result) { + // Byte 0-5 mac in correct order + // Byte 6-7 Temperature in uint16 + // Byte 8 Humidity in percent + // Byte 9 Battery in percent + // Byte 10-11 Battery in mV uint16_t + // Byte 12 frame packet counter + + const uint8_t *data = message.data(); + const int data_length = 13; + + if (message.size() != data_length) { + ESP_LOGVV(TAG, "parse_message(): payload has wrong size (%d)!", message.size()); + return false; + } + + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + const int16_t temperature = uint16_t(data[7]) | (uint16_t(data[6]) << 8); + result.temperature = temperature / 10.0f; + + // humidity, 1 byte, 8-bit unsigned integer, 1.0 % + result.humidity = data[8]; + + // battery, 1 byte, 8-bit unsigned integer, 1.0 % + result.battery_level = data[9]; + + // battery, 2 bytes, 16-bit unsigned integer, 0.001 V + const int16_t battery_voltage = uint16_t(data[11]) | (uint16_t(data[10]) << 8); + result.battery_voltage = battery_voltage / 1.0e3f; + + return true; +} + +bool ATCMiThermometer::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 ATC MiThermometer (%s):", address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f °C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.0f %%", *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 atc_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/atc_mithermometer/atc_mithermometer.h b/esphome/components/atc_mithermometer/atc_mithermometer.h new file mode 100644 index 0000000000..203dca3200 --- /dev/null +++ b/esphome/components/atc_mithermometer/atc_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 atc_mithermometer { + +struct ParseResult { + optional temperature; + optional humidity; + optional battery_level; + optional battery_voltage; + bool is_duplicate; + int raw_offset; +}; + +class ATCMiThermometer : 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 atc_mithermometer +} // namespace esphome + +#endif diff --git a/esphome/components/atc_mithermometer/sensor.py b/esphome/components/atc_mithermometer/sensor.py new file mode 100644 index 0000000000..550cd8d0f8 --- /dev/null +++ b/esphome/components/atc_mithermometer/sensor.py @@ -0,0 +1,45 @@ +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, UNIT_CELSIUS, UNIT_PERCENT, UNIT_VOLT, \ + ICON_BATTERY, ICON_THERMOMETER, ICON_WATER_PERCENT + +CODEOWNERS = ['@ahpohl'] + +DEPENDENCIES = ['esp32_ble_tracker'] + +atc_mithermometer_ns = cg.esphome_ns.namespace('atc_mithermometer') +ATCMiThermometer = atc_mithermometer_ns.class_('ATCMiThermometer', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.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_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_BATTERY, 3), +}).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/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 8361ee8004..753010310c 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -104,6 +104,7 @@ def parse_multi_click_timing_str(value): try: state = cv.boolean(parts[0]) except cv.Invalid: + # pylint: disable=raise-missing-from raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) if parts[1] != 'for': diff --git a/esphome/components/canbus/__init__.py b/esphome/components/canbus/__init__.py new file mode 100644 index 0000000000..24decc14b0 --- /dev/null +++ b/esphome/components/canbus/__init__.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.core import CORE, coroutine +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_DATA + +CODEOWNERS = ['@mvturnho', '@danielschramm'] +IS_PLATFORM_COMPONENT = True + +CONF_CAN_ID = 'can_id' +CONF_USE_EXTENDED_ID = 'use_extended_id' +CONF_CANBUS_ID = 'canbus_id' +CONF_BIT_RATE = 'bit_rate' +CONF_ON_FRAME = 'on_frame' +CONF_CANBUS_SEND = 'canbus.send' + + +def validate_id(id_value, id_ext): + if not id_ext: + if id_value > 0x7ff: + raise cv.Invalid("Standard IDs must be 11 Bit (0x000-0x7ff / 0-2047)") + + +def validate_raw_data(value): + if isinstance(value, str): + return value.encode('utf-8') + if isinstance(value, list): + return cv.Schema([cv.hex_uint8_t])(value) + raise cv.Invalid("data must either be a string wrapped in quotes or a list of bytes") + + +canbus_ns = cg.esphome_ns.namespace('canbus') +CanbusComponent = canbus_ns.class_('CanbusComponent', cg.Component) +CanbusTrigger = canbus_ns.class_('CanbusTrigger', + automation.Trigger.template(cg.std_vector.template(cg.uint8)), + cg.Component) +CanSpeed = canbus_ns.enum('CAN_SPEED') + +CAN_SPEEDS = { + '5KBPS': CanSpeed.CAN_5KBPS, + '10KBPS': CanSpeed.CAN_10KBPS, + '20KBPS': CanSpeed.CAN_20KBPS, + '31K25BPS': CanSpeed.CAN_31K25BPS, + '33KBPS': CanSpeed.CAN_33KBPS, + '40KBPS': CanSpeed.CAN_40KBPS, + '50KBPS': CanSpeed.CAN_50KBPS, + '80KBPS': CanSpeed.CAN_80KBPS, + '83K3BPS': CanSpeed.CAN_83K3BPS, + '95KBPS': CanSpeed.CAN_95KBPS, + '100KBPS': CanSpeed.CAN_100KBPS, + '125KBPS': CanSpeed.CAN_125KBPS, + '200KBPS': CanSpeed.CAN_200KBPS, + '250KBPS': CanSpeed.CAN_250KBPS, + '500KBPS': CanSpeed.CAN_500KBPS, + '1000KBPS': CanSpeed.CAN_1000KBPS, +} + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(CanbusComponent), + cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1fffffff), + cv.Optional(CONF_BIT_RATE, default='125KBPS'): cv.enum(CAN_SPEEDS, upper=True), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Optional(CONF_ON_FRAME): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger), + cv.GenerateID(CONF_CAN_ID): cv.int_range(min=0, max=0x1fffffff), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + }), +}).extend(cv.COMPONENT_SCHEMA) + + +@coroutine +def setup_canbus_core_(var, config): + validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) + yield cg.register_component(var, config) + cg.add(var.set_can_id([config[CONF_CAN_ID]])) + cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]])) + cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]])) + + for conf in config.get(CONF_ON_FRAME, []): + can_id = conf[CONF_CAN_ID] + ext_id = conf[CONF_USE_EXTENDED_ID] + validate_id(can_id, ext_id) + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id) + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [(cg.std_vector.template(cg.uint8), 'x')], conf) + + +@coroutine +def register_canbus(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.new_Pvariable(config[CONF_ID], var) + yield setup_canbus_core_(var, config) + + +# Actions +@automation.register_action(CONF_CANBUS_SEND, + canbus_ns.class_('CanbusSendAction', automation.Action), + cv.maybe_simple_value({ + cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent), + cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1fffffff), + cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean, + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + }, key=CONF_DATA)) +def canbus_action_to_code(config, action_id, template_arg, args): + validate_id(config[CONF_CAN_ID], config[CONF_USE_EXTENDED_ID]) + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_CANBUS_ID]) + + if CONF_CAN_ID in config: + can_id = yield cg.templatable(config[CONF_CAN_ID], args, cg.uint32) + cg.add(var.set_can_id(can_id)) + + use_extended_id = yield cg.templatable(config[CONF_USE_EXTENDED_ID], args, cg.uint32) + cg.add(var.set_use_extended_id(use_extended_id)) + + data = config[CONF_DATA] + if isinstance(data, bytes): + data = [int(x) for x in data] + if cg.is_template(data): + templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + yield var diff --git a/esphome/components/canbus/canbus.cpp b/esphome/components/canbus/canbus.cpp new file mode 100644 index 0000000000..20afd4b296 --- /dev/null +++ b/esphome/components/canbus/canbus.cpp @@ -0,0 +1,87 @@ +#include "canbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace canbus { + +static const char *TAG = "canbus"; + +void Canbus::setup() { + ESP_LOGCONFIG(TAG, "Setting up Canbus..."); + if (!this->setup_internal()) { + ESP_LOGE(TAG, "setup error!"); + this->mark_failed(); + } +} + +void Canbus::dump_config() { + if (this->use_extended_id_) { + ESP_LOGCONFIG(TAG, "config extended id=0x%08x", this->can_id_); + } else { + ESP_LOGCONFIG(TAG, "config standard id=0x%03x", this->can_id_); + } +} + +void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector &data) { + struct CanFrame can_message; + + uint8_t size = static_cast(data.size()); + if (use_extended_id) { + ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size); + } else { + ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size); + } + if (size > CAN_MAX_DATA_LENGTH) + size = CAN_MAX_DATA_LENGTH; + can_message.can_data_length_code = size; + can_message.can_id = can_id; + can_message.use_extended_id = use_extended_id; + + for (int i = 0; i < size; i++) { + can_message.data[i] = data[i]; + ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]); + } + + this->send_message(&can_message); +} + +void Canbus::add_trigger(CanbusTrigger *trigger) { + if (trigger->use_extended_id_) { + ESP_LOGVV(TAG, "add trigger for extended canid=0x%08x", trigger->can_id_); + } else { + ESP_LOGVV(TAG, "add trigger for std canid=0x%03x", trigger->can_id_); + } + this->triggers_.push_back(trigger); +}; + +void Canbus::loop() { + struct CanFrame can_message; + // readmessage + if (this->read_message(&can_message) == canbus::ERROR_OK) { + if (can_message.use_extended_id) { + ESP_LOGD(TAG, "received can message extended can_id=0x%x size=%d", can_message.can_id, + can_message.can_data_length_code); + } else { + ESP_LOGD(TAG, "received can message std can_id=0x%x size=%d", can_message.can_id, + can_message.can_data_length_code); + } + + std::vector data; + + // show data received + for (int i = 0; i < can_message.can_data_length_code; i++) { + ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]); + data.push_back(can_message.data[i]); + } + + // fire all triggers + for (auto trigger : this->triggers_) { + if ((trigger->can_id_ == can_message.can_id) && (trigger->use_extended_id_ == can_message.use_extended_id)) { + trigger->trigger(data); + } + } + } +} + +} // namespace canbus +} // namespace esphome diff --git a/esphome/components/canbus/canbus.h b/esphome/components/canbus/canbus.h new file mode 100644 index 0000000000..37adf0bc9c --- /dev/null +++ b/esphome/components/canbus/canbus.h @@ -0,0 +1,134 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/optional.h" + +namespace esphome { +namespace canbus { + +enum Error : uint8_t { + ERROR_OK = 0, + ERROR_FAIL = 1, + ERROR_ALLTXBUSY = 2, + ERROR_FAILINIT = 3, + ERROR_FAILTX = 4, + ERROR_NOMSG = 5 +}; + +enum CanSpeed : uint8_t { + CAN_5KBPS, + CAN_10KBPS, + CAN_20KBPS, + CAN_31K25BPS, + CAN_33KBPS, + CAN_40KBPS, + CAN_50KBPS, + CAN_80KBPS, + CAN_83K3BPS, + CAN_95KBPS, + CAN_100KBPS, + CAN_125KBPS, + CAN_200KBPS, + CAN_250KBPS, + CAN_500KBPS, + CAN_1000KBPS +}; + +class CanbusTrigger; +template class CanbusSendAction; + +/* CAN payload length definitions according to ISO 11898-1 */ +static const uint8_t CAN_MAX_DATA_LENGTH = 8; + +/* +Can Frame describes a normative CAN Frame +The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used +So currently the flag is passed to and from the hardware but currently ignored to the user application. +*/ +struct CanFrame { + bool use_extended_id = false; + bool remote_transmission_request = false; + uint32_t can_id; /* 29 or 11 bit CAN_ID */ + uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */ + uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8))); +}; + +class Canbus : public Component { + public: + Canbus(){}; + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + void send_data(uint32_t can_id, bool use_extended_id, const std::vector &data); + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; } + + void add_trigger(CanbusTrigger *trigger); + + protected: + template friend class CanbusSendAction; + std::vector triggers_{}; + uint32_t can_id_; + bool use_extended_id_; + CanSpeed bit_rate_; + + virtual bool setup_internal(); + virtual Error send_message(struct CanFrame *frame); + virtual Error read_message(struct CanFrame *frame); +}; + +template class CanbusSendAction : public Action, public Parented { + public: + void set_data_template(const std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void set_can_id(uint32_t can_id) { this->can_id_ = can_id; } + + void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; } + + void play(Ts... x) override { + auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; + auto use_extended_id = + this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; + if (this->static_) { + this->parent_->send_data(can_id, use_extended_id, this->data_static_); + } else { + auto val = this->data_func_(x...); + this->parent_->send_data(can_id, use_extended_id, val); + } + } + + protected: + optional can_id_{}; + optional use_extended_id_{}; + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +class CanbusTrigger : public Trigger>, public Component { + friend class Canbus; + + public: + explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const bool use_extended_id) + : parent_(parent), can_id_(can_id), use_extended_id_(use_extended_id){}; + void setup() override { this->parent_->add_trigger(this); } + + protected: + Canbus *parent_; + uint32_t can_id_; + bool use_extended_id_; +}; + +} // namespace canbus +} // namespace esphome diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 538d7fe1f5..e7928d4d8b 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -102,10 +102,14 @@ void CCS811Component::send_env_data_() { // temperature has a 25° offset to allow negative temperatures temperature += 25; - // only 0.5 fractions are supported (application note) - auto hum_value = static_cast(roundf(humidity * 2)); - auto temp_value = static_cast(roundf(temperature * 2)); - this->write_bytes(0x05, {hum_value, 0x00, temp_value, 0x00}); + // At page 18 of: + // https://cdn.sparkfun.com/datasheets/BreakoutBoards/CCS811_Programming_Guide.pdf + // Reference code: + // https://github.com/adafruit/Adafruit_CCS811/blob/0990f5c620354d8bc087c4706bec091d8e6e5dfd/Adafruit_CCS811.cpp#L135-L142 + uint16_t hum_conv = static_cast(lroundf(humidity * 512.0f + 0.5f)); + uint16_t temp_conv = static_cast(lroundf(temperature * 512.0f + 0.5f)); + this->write_bytes(0x05, {(uint8_t)((hum_conv >> 8) & 0xff), (uint8_t)((hum_conv & 0xff)), + (uint8_t)((temp_conv >> 8) & 0xff), (uint8_t)((temp_conv & 0xff))}); } void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp index b6e80d62a7..0701344a8b 100644 --- a/esphome/components/daikin/daikin.cpp +++ b/esphome/components/daikin/daikin.cpp @@ -12,8 +12,10 @@ void DaikinClimate::transmit_state() { 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00}; remote_state[21] = this->operation_mode_(); - remote_state[24] = this->fan_speed_(); remote_state[22] = this->temperature_(); + uint16_t fan_speed = this->fan_speed_(); + remote_state[24] = fan_speed >> 8; + remote_state[25] = fan_speed & 0xff; // Calculate checksum for (int i = 16; i < 34; i++) { @@ -90,25 +92,38 @@ uint8_t DaikinClimate::operation_mode_() { return operating_mode; } -uint8_t DaikinClimate::fan_speed_() { - uint8_t fan_speed; +uint16_t DaikinClimate::fan_speed_() { + uint16_t fan_speed; switch (this->fan_mode) { case climate::CLIMATE_FAN_LOW: - fan_speed = DAIKIN_FAN_1; + fan_speed = DAIKIN_FAN_1 << 8; break; case climate::CLIMATE_FAN_MEDIUM: - fan_speed = DAIKIN_FAN_3; + fan_speed = DAIKIN_FAN_3 << 8; break; case climate::CLIMATE_FAN_HIGH: - fan_speed = DAIKIN_FAN_5; + fan_speed = DAIKIN_FAN_5 << 8; break; case climate::CLIMATE_FAN_AUTO: default: - fan_speed = DAIKIN_FAN_AUTO; + fan_speed = DAIKIN_FAN_AUTO << 8; } // If swing is enabled switch first 4 bits to 1111 - return this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? fan_speed | 0xF : fan_speed; + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + fan_speed |= 0x0F00; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + fan_speed |= 0x000F; + break; + case climate::CLIMATE_SWING_BOTH: + fan_speed |= 0x0F0F; + break; + default: + break; + } + return fan_speed; } uint8_t DaikinClimate::temperature_() { @@ -159,13 +174,19 @@ bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { this->target_temperature = temperature >> 1; } uint8_t fan_mode = frame[8]; - if (fan_mode & 0xF) + uint8_t swing_mode = frame[9]; + if (fan_mode & 0xF && swing_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_BOTH; + else if (fan_mode & 0xF) this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + else if (swing_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; else this->swing_mode = climate::CLIMATE_SWING_OFF; switch (fan_mode & 0xF0) { case DAIKIN_FAN_1: case DAIKIN_FAN_2: + case DAIKIN_FAN_SILENT: this->fan_mode = climate::CLIMATE_FAN_LOW; break; case DAIKIN_FAN_3: diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h index 4671d57570..c0a472bce7 100644 --- a/esphome/components/daikin/daikin.h +++ b/esphome/components/daikin/daikin.h @@ -21,6 +21,7 @@ const uint8_t DAIKIN_MODE_ON = 0x01; // Fan Speed const uint8_t DAIKIN_FAN_AUTO = 0xA0; +const uint8_t DAIKIN_FAN_SILENT = 0xB0; const uint8_t DAIKIN_FAN_1 = 0x30; const uint8_t DAIKIN_FAN_2 = 0x40; const uint8_t DAIKIN_FAN_3 = 0x50; @@ -46,13 +47,14 @@ class DaikinClimate : public 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}) {} + std::vector{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; uint8_t operation_mode_(); - uint8_t fan_speed_(); + uint16_t fan_speed_(); uint8_t temperature_(); // Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index bd468dcbc3..8b65ce72e1 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -299,7 +299,7 @@ void DisplayBuffer::printf(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::printf(int x, int y, Font *font, const char *format, ...) { va_list arg; va_start(arg, format); - this->vprintf_(x, y, font, COLOR_ON, TextAlign::CENTER_LEFT, format, arg); + this->vprintf_(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, arg); va_end(arg); } void DisplayBuffer::set_writer(display_writer_t &&writer) { this->writer_ = writer; } @@ -474,6 +474,51 @@ ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height, ImageType type) : width_(width), height_(height), type_(type), data_start_(data_start) {} +bool Animation::get_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return false; + const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; + const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return false; + const uint32_t pos = x + y * width_8 + frame_index; + return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); +} +Color Animation::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return 0; + const uint32_t pos = (x + y * this->width_ + frame_index) * 3; + const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | + (pgm_read_byte(this->data_start_ + pos + 1) << 8) | + (pgm_read_byte(this->data_start_ + pos + 0) << 16); + return Color(color32); +} +Color Animation::get_grayscale_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; + if (frame_index >= this->width_ * this->height_ * this->animation_frame_count_) + return 0; + const uint32_t pos = (x + y * this->width_ + frame_index); + const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + return Color(gray | gray << 8 | gray << 16 | gray << 24); +} +Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) + : Image(data_start, width, height, type), animation_frame_count_(animation_frame_count) { + current_frame_ = 0; +} +int Animation::get_animation_frame_count() const { return this->animation_frame_count_; } +int Animation::get_current_frame() const { return this->current_frame_; } +void Animation::next_frame() { + this->current_frame_++; + if (this->current_frame_ >= animation_frame_count_) { + this->current_frame_ = 0; + } +} + DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} void DisplayPage::show() { this->parent_->show_page(this); } void DisplayPage::show_next() { this->next_->show(); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index e402b4b021..235224d42e 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -388,9 +388,9 @@ class Font { class Image { public: Image(const uint8_t *data_start, int width, int height, ImageType type); - bool get_pixel(int x, int y) const; - Color get_color_pixel(int x, int y) const; - Color get_grayscale_pixel(int x, int y) const; + virtual bool get_pixel(int x, int y) const; + virtual Color get_color_pixel(int x, int y) const; + virtual Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; ImageType get_type() const; @@ -402,6 +402,22 @@ class Image { const uint8_t *data_start_; }; +class Animation : public Image { + public: + Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); + bool get_pixel(int x, int y) const override; + Color get_color_pixel(int x, int y) const override; + Color get_grayscale_pixel(int x, int y) const override; + + int get_animation_frame_count() const; + int get_current_frame() const; + void next_frame(); + + protected: + int current_frame_; + int animation_frame_count_; +}; + template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) diff --git a/esphome/components/ds1307/__init__.py b/esphome/components/ds1307/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp new file mode 100644 index 0000000000..2f33768132 --- /dev/null +++ b/esphome/components/ds1307/ds1307.cpp @@ -0,0 +1,105 @@ +#include "ds1307.h" +#include "esphome/core/log.h" + +// Datasheet: +// - https://datasheets.maximintegrated.com/en/ds/DS1307.pdf + +namespace esphome { +namespace ds1307 { + +static const char *TAG = "ds1307"; + +void DS1307Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up DS1307..."); + if (!this->read_rtc_()) { + this->mark_failed(); + } +} + +void DS1307Component::update() { this->read_time(); } + +void DS1307Component::dump_config() { + ESP_LOGCONFIG(TAG, "DS1307:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with DS1307 failed!"); + } + ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); +} + +float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } + +void DS1307Component::read_time() { + if (!this->read_rtc_()) { + return; + } + if (ds1307_.reg.ch) { + ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); + return; + } + time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), + .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), + .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), + .day_of_week = uint8_t(ds1307_.reg.weekday), + .day_of_month = uint8_t(ds1307_.reg.day + 10u * ds1307_.reg.day_10), + .day_of_year = 1, // ignored by recalc_timestamp_utc(false) + .month = uint8_t(ds1307_.reg.month + 10u * ds1307_.reg.month_10), + .year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000)}; + rtc_time.recalc_timestamp_utc(false); + if (!rtc_time.is_valid()) { + ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock."); + return; + } + time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp); +} + +void DS1307Component::write_time() { + auto now = time::RealTimeClock::utcnow(); + if (!now.is_valid()) { + ESP_LOGE(TAG, "Invalid system time, not syncing to RTC."); + return; + } + ds1307_.reg.year = (now.year - 2000) % 10; + ds1307_.reg.year_10 = (now.year - 2000) / 10 % 10; + ds1307_.reg.month = now.month % 10; + ds1307_.reg.month_10 = now.month / 10; + ds1307_.reg.day = now.day_of_month % 10; + ds1307_.reg.day_10 = now.day_of_month / 10; + ds1307_.reg.weekday = now.day_of_week; + ds1307_.reg.hour = now.hour % 10; + ds1307_.reg.hour_10 = now.hour / 10; + ds1307_.reg.minute = now.minute % 10; + ds1307_.reg.minute_10 = now.minute / 10; + ds1307_.reg.second = now.second % 10; + ds1307_.reg.second_10 = now.second / 10; + ds1307_.reg.ch = false; + + this->write_rtc_(); +} + +bool DS1307Component::read_rtc_() { + if (!this->read_bytes(0, this->ds1307_.raw, sizeof(this->ds1307_.raw))) { + ESP_LOGE(TAG, "Can't read I2C data."); + return false; + } + ESP_LOGD(TAG, "Read %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds1307_.reg.hour_10, + ds1307_.reg.hour, ds1307_.reg.minute_10, ds1307_.reg.minute, ds1307_.reg.second_10, ds1307_.reg.second, + ds1307_.reg.year_10, ds1307_.reg.year, ds1307_.reg.month_10, ds1307_.reg.month, ds1307_.reg.day_10, + ds1307_.reg.day, ONOFF(ds1307_.reg.ch), ds1307_.reg.rs, ONOFF(ds1307_.reg.sqwe), ONOFF(ds1307_.reg.out)); + + return true; +} + +bool DS1307Component::write_rtc_() { + if (!this->write_bytes(0, this->ds1307_.raw, sizeof(this->ds1307_.raw))) { + ESP_LOGE(TAG, "Can't write I2C data."); + return false; + } + ESP_LOGD(TAG, "Write %0u%0u:%0u%0u:%0u%0u 20%0u%0u-%0u%0u-%0u%0u CH:%s RS:%0u SQWE:%s OUT:%s", ds1307_.reg.hour_10, + ds1307_.reg.hour, ds1307_.reg.minute_10, ds1307_.reg.minute, ds1307_.reg.second_10, ds1307_.reg.second, + ds1307_.reg.year_10, ds1307_.reg.year, ds1307_.reg.month_10, ds1307_.reg.month, ds1307_.reg.day_10, + ds1307_.reg.day, ONOFF(ds1307_.reg.ch), ds1307_.reg.rs, ONOFF(ds1307_.reg.sqwe), ONOFF(ds1307_.reg.out)); + return true; +} +} // namespace ds1307 +} // namespace esphome diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h new file mode 100644 index 0000000000..2e9ac2275c --- /dev/null +++ b/esphome/components/ds1307/ds1307.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace ds1307 { + +class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + void read_time(); + void write_time(); + + protected: + bool read_rtc_(); + bool write_rtc_(); + union DS1307Reg { + struct { + uint8_t second : 4; + uint8_t second_10 : 3; + bool ch : 1; + + uint8_t minute : 4; + uint8_t minute_10 : 3; + uint8_t unused_1 : 1; + + uint8_t hour : 4; + uint8_t hour_10 : 2; + uint8_t unused_2 : 2; + + uint8_t weekday : 3; + uint8_t unused_3 : 5; + + uint8_t day : 4; + uint8_t day_10 : 2; + uint8_t unused_4 : 2; + + uint8_t month : 4; + uint8_t month_10 : 1; + uint8_t unused_5 : 3; + + uint8_t year : 4; + uint8_t year_10 : 4; + + uint8_t rs : 2; + uint8_t unused_6 : 2; + bool sqwe : 1; + uint8_t unused_7 : 2; + bool out : 1; + } reg; + mutable uint8_t raw[sizeof(reg)]; + } ds1307_; +}; + +template class WriteAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->write_time(); } +}; + +template class ReadAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->read_time(); } +}; +} // namespace ds1307 +} // namespace esphome diff --git a/esphome/components/ds1307/time.py b/esphome/components/ds1307/time.py new file mode 100644 index 0000000000..371dc85be8 --- /dev/null +++ b/esphome/components/ds1307/time.py @@ -0,0 +1,44 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome import automation +from esphome.components import i2c, time +from esphome.const import CONF_ID + + +CODEOWNERS = ['@badbadc0ffee'] +DEPENDENCIES = ['i2c'] +ds1307_ns = cg.esphome_ns.namespace('ds1307') +DS1307Component = ds1307_ns.class_('DS1307Component', time.RealTimeClock, i2c.I2CDevice) +WriteAction = ds1307_ns.class_('WriteAction', automation.Action) +ReadAction = ds1307_ns.class_('ReadAction', automation.Action) + + +CONFIG_SCHEMA = time.TIME_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(DS1307Component), +}).extend(i2c.i2c_device_schema(0x68)) + + +@automation.register_action('ds1307.write_time', WriteAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DS1307Component), +})) +def ds1307_write_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('ds1307.read_time', ReadAction, automation.maybe_simple_id({ + cv.GenerateID(): cv.use_id(DS1307Component), +})) +def ds1307_read_time_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield time.register_time(var, config) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 81980d9d38..57a6394c8a 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \ - ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS + ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS, \ + CONF_CONTRAST ESP_PLATFORMS = [ESP_PLATFORM_ESP32] DEPENDENCIES = ['api'] @@ -47,7 +48,6 @@ CONF_IDLE_FRAMERATE = 'idle_framerate' CONF_JPEG_QUALITY = 'jpeg_quality' CONF_VERTICAL_FLIP = 'vertical_flip' CONF_HORIZONTAL_MIRROR = 'horizontal_mirror' -CONF_CONTRAST = 'contrast' CONF_SATURATION = 'saturation' CONF_TEST_PATTERN = 'test_pattern' diff --git a/esphome/components/ezo/__init__.py b/esphome/components/ezo/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp new file mode 100644 index 0000000000..97caa1495b --- /dev/null +++ b/esphome/components/ezo/ezo.cpp @@ -0,0 +1,86 @@ +#include "ezo.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ezo { + +static const char *TAG = "ezo.sensor"; + +static const uint16_t EZO_STATE_WAIT = 1; +static const uint16_t EZO_STATE_SEND_TEMP = 2; +static const uint16_t EZO_STATE_WAIT_TEMP = 4; + +void EZOSensor::dump_config() { + LOG_SENSOR("", "EZO", this); + LOG_I2C_DEVICE(this); + if (this->is_failed()) + ESP_LOGE(TAG, "Communication with EZO circuit failed!"); + LOG_UPDATE_INTERVAL(this); +} + +void EZOSensor::update() { + if (this->state_ & EZO_STATE_WAIT) { + ESP_LOGE(TAG, "update overrun, still waiting for previous response"); + return; + } + uint8_t c = 'R'; + this->write_bytes_raw(&c, 1); + this->state_ |= EZO_STATE_WAIT; + this->start_time_ = millis(); + this->wait_time_ = 900; +} + +void EZOSensor::loop() { + uint8_t buf[20]; + if (!(this->state_ & EZO_STATE_WAIT)) { + if (this->state_ & EZO_STATE_SEND_TEMP) { + int len = sprintf((char *) buf, "T,%0.3f", this->tempcomp_); + this->write_bytes_raw(buf, len); + this->state_ = EZO_STATE_WAIT | EZO_STATE_WAIT_TEMP; + this->start_time_ = millis(); + this->wait_time_ = 300; + } + return; + } + if (millis() - this->start_time_ < this->wait_time_) + return; + buf[0] = 0; + if (!this->read_bytes_raw(buf, 20)) { + ESP_LOGE(TAG, "read error"); + this->state_ = 0; + return; + } + switch (buf[0]) { + case 1: + break; + case 2: + ESP_LOGE(TAG, "device returned a syntax error"); + break; + case 254: + return; // keep waiting + case 255: + ESP_LOGE(TAG, "device returned no data"); + break; + default: + ESP_LOGE(TAG, "device returned an unknown response: %d", buf[0]); + break; + } + if (this->state_ & EZO_STATE_WAIT_TEMP) { + this->state_ = 0; + return; + } + this->state_ &= ~EZO_STATE_WAIT; + if (buf[0] != 1) + return; + + float val = strtof((char *) &buf[1], nullptr); + this->publish_state(val); +} + +void EZOSensor::set_tempcomp_value(float temp) { + this->tempcomp_ = temp; + this->state_ |= EZO_STATE_SEND_TEMP; +} + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h new file mode 100644 index 0000000000..b2b59e38d4 --- /dev/null +++ b/esphome/components/ezo/ezo.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ezo { + +/// This class implements support for the EZO circuits in i2c mode +class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void loop() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void set_tempcomp_value(float temp); + + protected: + unsigned long start_time_ = 0; + unsigned long wait_time_ = 0; + uint16_t state_ = 0; + float tempcomp_; +}; + +} // namespace ezo +} // namespace esphome diff --git a/esphome/components/ezo/sensor.py b/esphome/components/ezo/sensor.py new file mode 100644 index 0000000000..ee896eea15 --- /dev/null +++ b/esphome/components/ezo/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID + +CODEOWNERS = ['@ssieb'] + +DEPENDENCIES = ['i2c'] + +ezo_ns = cg.esphome_ns.namespace('ezo') + +EZOSensor = ezo_ns.class_('EZOSensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(EZOSensor), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(None)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index 0729941e31..59d143dbef 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -38,7 +38,7 @@ class FastLEDLightOutput : public light::AddressableLight { return *this->controller_; } - template + template CLEDController &add_leds(int num_leds) { switch (CHIPSET) { case LPD8806: { diff --git a/esphome/components/fastled_spi/light.py b/esphome/components/fastled_spi/light.py index 959c8a1b19..ef14c05738 100644 --- a/esphome/components/fastled_spi/light.py +++ b/esphome/components/fastled_spi/light.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import fastled_base -from esphome.const import CONF_CHIPSET, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_NUM_LEDS, CONF_RGB_ORDER +from esphome.const import CONF_CHIPSET, CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_DATA_RATE, \ + CONF_NUM_LEDS, CONF_RGB_ORDER AUTO_LOAD = ['fastled_base'] @@ -21,15 +22,24 @@ CONFIG_SCHEMA = fastled_base.BASE_SCHEMA.extend({ cv.Required(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), cv.Required(CONF_DATA_PIN): pins.output_pin, cv.Required(CONF_CLOCK_PIN): pins.output_pin, + cv.Optional(CONF_DATA_RATE): cv.frequency, }) def to_code(config): var = yield fastled_base.new_fastled_light(config) - rgb_order = None - if CONF_RGB_ORDER in config: - rgb_order = cg.RawExpression(config[CONF_RGB_ORDER]) + rgb_order = cg.RawExpression(config[CONF_RGB_ORDER] if CONF_RGB_ORDER in config else "RGB") + data_rate = None + + if CONF_DATA_RATE in config: + data_rate_khz = int(config[CONF_DATA_RATE] / 1000) + if data_rate_khz < 1000: + data_rate = cg.RawExpression(f"DATA_RATE_KHZ({data_rate_khz})") + else: + data_rate_mhz = int(data_rate_khz / 1000) + data_rate = cg.RawExpression(f"DATA_RATE_MHZ({data_rate_mhz})") template_args = cg.TemplateArguments(cg.RawExpression(config[CONF_CHIPSET]), - config[CONF_DATA_PIN], config[CONF_CLOCK_PIN], rgb_order) + config[CONF_DATA_PIN], config[CONF_CLOCK_PIN], rgb_order, + data_rate) cg.add(var.add_leds(template_args, config[CONF_NUM_LEDS])) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 5b19dc74e0..ee50b10830 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -42,9 +42,9 @@ def validate_glyphs(value): def validate_pillow_installed(value): try: import PIL - except ImportError: + except ImportError as err: raise cv.Invalid("Please install the pillow python package to use this feature. " - "(pip install pillow)") + "(pip install pillow)") from err if PIL.__version__[0] < '4': raise cv.Invalid("Please update your pillow installation to at least 4.0.x. " diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 261d8be258..f611464248 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -36,7 +36,10 @@ const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01; const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02; const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03; const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04; -const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000; +const uint8_t FUJITSU_GENERAL_SWING_NONE_BYTE10 = 0x00; +const uint8_t FUJITSU_GENERAL_SWING_VERTICAL_BYTE10 = 0x01; +const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL_BYTE10 = 0x02; +const uint8_t FUJITSU_GENERAL_SWING_BOTH_BYTE10 = 0x03; const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00; const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00; @@ -74,7 +77,12 @@ const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000; const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000; -FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {} +FujitsuGeneralClimate::FujitsuGeneralClimate() + : ClimateIR( + FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_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}) {} void FujitsuGeneralClimate::transmit_state() { if (this->mode == climate::CLIMATE_MODE_OFF) { @@ -101,8 +109,8 @@ void FujitsuGeneralClimate::transmit_state() { remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15; // Set temperature - uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN); - safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX); + auto safecelsius = + (uint8_t) roundf(clamp(this->target_temperature, FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX)); remote_state[8] = (byte) safecelsius - 16; remote_state[8] = remote_state[8] << 4; @@ -119,18 +127,52 @@ void FujitsuGeneralClimate::transmit_state() { case climate::CLIMATE_MODE_HEAT: remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9; break; + case climate::CLIMATE_MODE_DRY: + remote_state[9] = FUJITSU_GENERAL_MODE_DRY_BYTE9; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[9] = FUJITSU_GENERAL_MODE_FAN_BYTE9; + break; case climate::CLIMATE_MODE_AUTO: default: remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9; break; - // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome + // TODO: CLIMATE_MODE_10C are missing in esphome } - // TODO: missing support for fan speed - remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + // Set fan + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state[10] = FUJITSU_GENERAL_FAN_HIGH_BYTE10; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[10] = FUJITSU_GENERAL_FAN_MEDIUM_BYTE10; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[10] = FUJITSU_GENERAL_FAN_LOW_BYTE10; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + break; + } - // TODO: missing support for swing - // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10; + // Set swing + switch (this->swing_mode) { + case climate::CLIMATE_SWING_VERTICAL: + remote_state[10] = (byte) remote_state[10] | (FUJITSU_GENERAL_SWING_VERTICAL_BYTE10 << 4); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + remote_state[10] = (byte) remote_state[10] | (FUJITSU_GENERAL_SWING_HORIZONTAL_BYTE10 << 4); + break; + case climate::CLIMATE_SWING_BOTH: + remote_state[10] = (byte) remote_state[10] | (FUJITSU_GENERAL_SWING_BOTH_BYTE10 << 4); + break; + case climate::CLIMATE_SWING_OFF: + default: + remote_state[10] = (byte) remote_state[10] | (FUJITSU_GENERAL_SWING_NONE_BYTE10 << 4); + break; + } // TODO: missing support for outdoor unit low noise // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14; diff --git a/esphome/components/gps/time/__init__.py b/esphome/components/gps/time/__init__.py index bf746d19b2..421d2e6717 100644 --- a/esphome/components/gps/time/__init__.py +++ b/esphome/components/gps/time/__init__.py @@ -6,12 +6,12 @@ from .. import gps_ns, GPSListener, CONF_GPS_ID, GPS DEPENDENCIES = ['gps'] -GPSTime = gps_ns.class_('GPSTime', time_.RealTimeClock, GPSListener) +GPSTime = gps_ns.class_('GPSTime', cg.PollingComponent, time_.RealTimeClock, GPSListener) CONFIG_SCHEMA = time_.TIME_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(GPSTime), cv.GenerateID(CONF_GPS_ID): cv.use_id(GPS), -}).extend(cv.COMPONENT_SCHEMA) +}).extend(cv.polling_component_schema('5min')) def to_code(config): diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index f6462be3e0..a1f69a7130 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -9,13 +9,11 @@ namespace gps { class GPSTime : public time::RealTimeClock, public GPSListener { public: + void update() override { this->from_tiny_gps_(this->get_tiny_gps()); }; void on_update(TinyGPSPlus &tiny_gps) override { if (!this->has_time_) this->from_tiny_gps_(tiny_gps); } - void setup() override { - this->set_interval(5 * 60 * 1000, [this]() { this->from_tiny_gps_(this->get_tiny_gps()); }); - } protected: void from_tiny_gps_(TinyGPSPlus &tiny_gps); diff --git a/esphome/components/hbridge/__init__.py b/esphome/components/hbridge/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hbridge/hbridge_light_output.h b/esphome/components/hbridge/hbridge_light_output.h new file mode 100644 index 0000000000..03a5b3a88c --- /dev/null +++ b/esphome/components/hbridge/hbridge_light_output.h @@ -0,0 +1,76 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/light/light_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace hbridge { + +// Using PollingComponent as the updates are more consistent and reduces flickering +class HBridgeLightOutput : public PollingComponent, public light::LightOutput { + public: + HBridgeLightOutput() : PollingComponent(1) {} + + void set_pina_pin(output::FloatOutput *pina_pin) { pina_pin_ = pina_pin; } + void set_pinb_pin(output::FloatOutput *pinb_pin) { pinb_pin_ = pinb_pin; } + + 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); + return traits; + } + + void setup() override { this->forward_direction_ = false; } + + void update() override { + // 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->forward_direction_ = true; + } else { // Second LED Direction + this->pina_pin_->set_level(this->duty_off_); + this->pinb_pin_->set_level(this->pinb_duty_); + this->forward_direction_ = false; + } + } + + 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; + } + } + + protected: + output::FloatOutput *pina_pin_; + output::FloatOutput *pinb_pin_; + float pina_duty_ = 0; + float pinb_duty_ = 0; + float duty_off_ = 0; + bool forward_direction_ = false; +}; + +} // namespace hbridge +} // namespace esphome diff --git a/esphome/components/hbridge/light.py b/esphome/components/hbridge/light.py new file mode 100644 index 0000000000..abaf7152d7 --- /dev/null +++ b/esphome/components/hbridge/light.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import light, output +from esphome.const import CONF_OUTPUT_ID, CONF_PIN_A, CONF_PIN_B + +hbridge_ns = cg.esphome_ns.namespace('hbridge') +HBridgeLightOutput = hbridge_ns.class_('HBridgeLightOutput', cg.PollingComponent, light.LightOutput) + +CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(HBridgeLightOutput), + cv.Required(CONF_PIN_A): cv.use_id(output.FloatOutput), + cv.Required(CONF_PIN_B): cv.use_id(output.FloatOutput), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + yield light.register_light(var, config) + + hside = yield cg.get_variable(config[CONF_PIN_A]) + cg.add(var.set_pina_pin(hside)) + lside = yield cg.get_variable(config[CONF_PIN_B]) + cg.add(var.set_pinb_pin(lside)) diff --git a/esphome/components/hitachi_ac344/__init__.py b/esphome/components/hitachi_ac344/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hitachi_ac344/climate.py b/esphome/components/hitachi_ac344/climate.py new file mode 100644 index 0000000000..fb1c21b200 --- /dev/null +++ b/esphome/components/hitachi_ac344/climate.py @@ -0,0 +1,18 @@ +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'] + +hitachi_ac344_ns = cg.esphome_ns.namespace('hitachi_ac344') +HitachiClimate = hitachi_ac344_ns.class_('HitachiClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(HitachiClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.cpp b/esphome/components/hitachi_ac344/hitachi_ac344.cpp new file mode 100644 index 0000000000..8d56c7f51c --- /dev/null +++ b/esphome/components/hitachi_ac344/hitachi_ac344.cpp @@ -0,0 +1,365 @@ +#include "hitachi_ac344.h" + +namespace esphome { +namespace hitachi_ac344 { + +static const char *TAG = "climate.hitachi_ac344"; + +void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, const uint8_t data) { + if (offset >= 8 || !nbits) + return; // Short circuit as it won't change. + // Calculate the mask for the supplied value. + uint8_t mask = UINT8_MAX >> (8 - ((nbits > 8) ? 8 : nbits)); + // Calculate the mask & clear the space for the data. + // Clear the destination bits. + *dst &= ~(uint8_t)(mask << offset); + // Merge in the data. + *dst |= ((data & mask) << offset); +} + +void set_bit(uint8_t *const data, const uint8_t position, const bool on) { + uint8_t mask = 1 << position; + if (on) + *data |= mask; + else + *data &= ~mask; +} + +uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) { + for (uint16_t i = 1; i < length; i += 2) { + // Code done this way to avoid a compiler warning bug. + uint8_t inv = ~*(ptr + i - 1); + *(ptr + i) = inv; + } + return ptr; +} + +bool HitachiClimate::get_power_() { return remote_state_[HITACHI_AC344_POWER_BYTE] == HITACHI_AC344_POWER_ON; } + +void HitachiClimate::set_power_(bool on) { + set_button_(HITACHI_AC344_BUTTON_POWER); + remote_state_[HITACHI_AC344_POWER_BYTE] = on ? HITACHI_AC344_POWER_ON : HITACHI_AC344_POWER_OFF; +} + +uint8_t HitachiClimate::get_mode_() { return remote_state_[HITACHI_AC344_MODE_BYTE] & 0xF; } + +void HitachiClimate::set_mode_(uint8_t mode) { + uint8_t new_mode = mode; + switch (mode) { + // Fan mode sets a special temp. + case HITACHI_AC344_MODE_FAN: + set_temp_(HITACHI_AC344_TEMP_FAN, false); + break; + case HITACHI_AC344_MODE_HEAT: + case HITACHI_AC344_MODE_COOL: + case HITACHI_AC344_MODE_DRY: + break; + default: + new_mode = HITACHI_AC344_MODE_COOL; + } + set_bits(&remote_state_[HITACHI_AC344_MODE_BYTE], 0, 4, new_mode); + if (new_mode != HITACHI_AC344_MODE_FAN) + set_temp_(previous_temp_); + set_fan_(get_fan_()); // Reset the fan speed after the mode change. + set_power_(true); +} + +void HitachiClimate::set_temp_(uint8_t celsius, bool set_previous) { + uint8_t temp; + temp = std::min(celsius, HITACHI_AC344_TEMP_MAX); + temp = std::max(temp, HITACHI_AC344_TEMP_MIN); + set_bits(&remote_state_[HITACHI_AC344_TEMP_BYTE], HITACHI_AC344_TEMP_OFFSET, HITACHI_AC344_TEMP_SIZE, temp); + if (previous_temp_ > temp) + set_button_(HITACHI_AC344_BUTTON_TEMP_DOWN); + else if (previous_temp_ < temp) + set_button_(HITACHI_AC344_BUTTON_TEMP_UP); + if (set_previous) + previous_temp_ = temp; +} + +uint8_t HitachiClimate::get_fan_() { return remote_state_[HITACHI_AC344_FAN_BYTE] >> 4 & 0xF; } + +void HitachiClimate::set_fan_(uint8_t speed) { + uint8_t new_speed = std::max(speed, HITACHI_AC344_FAN_MIN); + uint8_t fan_max = HITACHI_AC344_FAN_MAX; + + // Only 2 x low speeds in Dry mode or Auto + if (get_mode_() == HITACHI_AC344_MODE_DRY && speed == HITACHI_AC344_FAN_AUTO) { + fan_max = HITACHI_AC344_FAN_AUTO; + } else if (get_mode_() == HITACHI_AC344_MODE_DRY) { + fan_max = HITACHI_AC344_FAN_MAX_DRY; + } else if (get_mode_() == HITACHI_AC344_MODE_FAN && speed == HITACHI_AC344_FAN_AUTO) { + // Fan Mode does not have auto. Set to safe low + new_speed = HITACHI_AC344_FAN_MIN; + } + + new_speed = std::min(new_speed, fan_max); + // Handle the setting the button value if we are going to change the value. + if (new_speed != get_fan_()) + set_button_(HITACHI_AC344_BUTTON_FAN); + // Set the values + + set_bits(&remote_state_[HITACHI_AC344_FAN_BYTE], 4, 4, new_speed); + remote_state_[9] = 0x92; + + // When fan is at min/max, additional bytes seem to be set + if (new_speed == HITACHI_AC344_FAN_MIN) + remote_state_[9] = 0x98; + remote_state_[29] = 0x01; +} + +void HitachiClimate::set_swing_v_toggle_(bool on) { + uint8_t button = get_button_(); // Get the current button value. + if (on) + button = HITACHI_AC344_BUTTON_SWINGV; // Set the button to SwingV. + else if (button == HITACHI_AC344_BUTTON_SWINGV) // Asked to unset it + // It was set previous, so use Power as a default + button = HITACHI_AC344_BUTTON_POWER; + set_button_(button); +} + +bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC344_BUTTON_SWINGV; } + +void HitachiClimate::set_swing_v_(bool on) { + set_swing_v_toggle_(on); // Set the button value. + set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on); +} + +bool HitachiClimate::get_swing_v_() { + return GETBIT8(remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET); +} + +void HitachiClimate::set_swing_h_(uint8_t position) { + if (position > HITACHI_AC344_SWINGH_LEFT_MAX) + return set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + set_bits(&remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE, position); + set_button_(HITACHI_AC344_BUTTON_SWINGH); +} + +uint8_t HitachiClimate::get_swing_h_() { + return GETBITS8(remote_state_[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE); +} + +uint8_t HitachiClimate::get_button_() { return remote_state_[HITACHI_AC344_BUTTON_BYTE]; } + +void HitachiClimate::set_button_(uint8_t button) { remote_state_[HITACHI_AC344_BUTTON_BYTE] = button; } + +void HitachiClimate::transmit_state() { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + set_mode_(HITACHI_AC344_MODE_COOL); + break; + case climate::CLIMATE_MODE_DRY: + set_mode_(HITACHI_AC344_MODE_DRY); + break; + case climate::CLIMATE_MODE_HEAT: + set_mode_(HITACHI_AC344_MODE_HEAT); + break; + case climate::CLIMATE_MODE_AUTO: + set_mode_(HITACHI_AC344_MODE_AUTO); + break; + case climate::CLIMATE_MODE_FAN_ONLY: + set_mode_(HITACHI_AC344_MODE_FAN); + break; + case climate::CLIMATE_MODE_OFF: + set_power_(false); + break; + } + + set_temp_(static_cast(this->target_temperature)); + + switch (this->fan_mode) { + case climate::CLIMATE_FAN_LOW: + set_fan_(HITACHI_AC344_FAN_LOW); + break; + case climate::CLIMATE_FAN_MEDIUM: + set_fan_(HITACHI_AC344_FAN_MEDIUM); + break; + case climate::CLIMATE_FAN_HIGH: + set_fan_(HITACHI_AC344_FAN_HIGH); + break; + case climate::CLIMATE_FAN_ON: + case climate::CLIMATE_FAN_AUTO: + default: + set_fan_(HITACHI_AC344_FAN_AUTO); + } + + switch (this->swing_mode) { + case climate::CLIMATE_SWING_BOTH: + set_swing_v_(true); + set_swing_h_(HITACHI_AC344_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_VERTICAL: + set_swing_v_(true); + set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + set_swing_v_(false); + set_swing_h_(HITACHI_AC344_SWINGH_AUTO); + break; + case climate::CLIMATE_SWING_OFF: + set_swing_v_(false); + set_swing_h_(HITACHI_AC344_SWINGH_MIDDLE); + break; + } + + // TODO: find change value to set button, now always set to power button + set_button_(HITACHI_AC344_BUTTON_POWER); + + invert_byte_pairs(remote_state_ + 3, HITACHI_AC344_STATE_LENGTH - 3); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(HITACHI_AC344_FREQ); + + uint8_t repeat = 0; + for (uint8_t r = 0; r <= repeat; r++) { + // Header + data->item(HITACHI_AC344_HDR_MARK, HITACHI_AC344_HDR_SPACE); + // Data + for (uint8_t i : remote_state_) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(HITACHI_AC344_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? HITACHI_AC344_ONE_SPACE : HITACHI_AC344_ZERO_SPACE); + } + } + // Footer + data->item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_MIN_GAP); + } + transmit.perform(); + + dump_state_("Sent", remote_state_); +} + +bool HitachiClimate::parse_mode_(const uint8_t remote_state[]) { + uint8_t power = remote_state[HITACHI_AC344_POWER_BYTE]; + ESP_LOGV(TAG, "Power: %02X %02X", remote_state[HITACHI_AC344_POWER_BYTE], power); + uint8_t mode = remote_state[HITACHI_AC344_MODE_BYTE] & 0xF; + ESP_LOGV(TAG, "Mode: %02X %02X", remote_state[HITACHI_AC344_MODE_BYTE], mode); + if (power == HITACHI_AC344_POWER_ON) { + switch (mode) { + case HITACHI_AC344_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case HITACHI_AC344_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case HITACHI_AC344_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case HITACHI_AC344_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + case HITACHI_AC344_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + return true; +} + +bool HitachiClimate::parse_temperature_(const uint8_t remote_state[]) { + uint8_t temperature = + GETBITS8(remote_state[HITACHI_AC344_TEMP_BYTE], HITACHI_AC344_TEMP_OFFSET, HITACHI_AC344_TEMP_SIZE); + this->target_temperature = temperature; + ESP_LOGV(TAG, "Temperature: %02X %02u %04f", remote_state[HITACHI_AC344_TEMP_BYTE], temperature, + this->target_temperature); + return true; +} + +bool HitachiClimate::parse_fan_(const uint8_t remote_state[]) { + uint8_t fan_mode = remote_state[HITACHI_AC344_FAN_BYTE] >> 4 & 0xF; + ESP_LOGV(TAG, "Fan: %02X %02X", remote_state[HITACHI_AC344_FAN_BYTE], fan_mode); + switch (fan_mode) { + case HITACHI_AC344_FAN_MIN: + case HITACHI_AC344_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case HITACHI_AC344_FAN_MEDIUM: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case HITACHI_AC344_FAN_HIGH: + case HITACHI_AC344_FAN_MAX: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case HITACHI_AC344_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + return true; +} + +bool HitachiClimate::parse_swing_(const uint8_t remote_state[]) { + uint8_t swing_modeh = + GETBITS8(remote_state[HITACHI_AC344_SWINGH_BYTE], HITACHI_AC344_SWINGH_OFFSET, HITACHI_AC344_SWINGH_SIZE); + ESP_LOGV(TAG, "SwingH: %02X %02X", remote_state[HITACHI_AC344_SWINGH_BYTE], swing_modeh); + + if ((swing_modeh & 0x7) == 0x0) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else if ((swing_modeh & 0x3) == 0x3) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } else { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } + + return true; +} + +bool HitachiClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(HITACHI_AC344_HDR_MARK, HITACHI_AC344_HDR_SPACE)) { + ESP_LOGVV(TAG, "Header fail"); + return false; + } + + uint8_t recv_state[HITACHI_AC344_STATE_LENGTH] = {0}; + // Read all bytes. + for (uint8_t pos = 0; pos < HITACHI_AC344_STATE_LENGTH; pos++) { + // Read bit + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_ONE_SPACE)) + recv_state[pos] |= 1 << bit; + else if (!data.expect_item(HITACHI_AC344_BIT_MARK, HITACHI_AC344_ZERO_SPACE)) { + ESP_LOGVV(TAG, "Byte %d bit %d fail", pos, bit); + return false; + } + } + } + + // Validate footer + if (!data.expect_mark(HITACHI_AC344_BIT_MARK)) { + ESP_LOGVV(TAG, "Footer fail"); + return false; + } + + dump_state_("Recv", recv_state); + + // parse mode + this->parse_mode_(recv_state); + // parse temperature + this->parse_temperature_(recv_state); + // parse fan + this->parse_fan_(recv_state); + // parse swingv + this->parse_swing_(recv_state); + this->publish_state(); + for (uint8_t i = 0; i < HITACHI_AC344_STATE_LENGTH; i++) + remote_state_[i] = recv_state[i]; + + return true; +} + +void HitachiClimate::dump_state_(const char action[], uint8_t state[]) { + for (uint16_t i = 0; i < HITACHI_AC344_STATE_LENGTH - 10; i += 10) { + ESP_LOGV(TAG, "%s: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", action, state[i + 0], state[i + 1], + state[i + 2], state[i + 3], state[i + 4], state[i + 5], state[i + 6], state[i + 7], state[i + 8], + state[i + 9]); + } + ESP_LOGV(TAG, "%s: %02X %02X %02X", action, state[40], state[41], state[42]); +} + +} // namespace hitachi_ac344 +} // namespace esphome diff --git a/esphome/components/hitachi_ac344/hitachi_ac344.h b/esphome/components/hitachi_ac344/hitachi_ac344.h new file mode 100644 index 0000000000..9e850d9b53 --- /dev/null +++ b/esphome/components/hitachi_ac344/hitachi_ac344.h @@ -0,0 +1,122 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace hitachi_ac344 { + +const uint16_t HITACHI_AC344_HDR_MARK = 3300; // ac +const uint16_t HITACHI_AC344_HDR_SPACE = 1700; // ac +const uint16_t HITACHI_AC344_BIT_MARK = 400; +const uint16_t HITACHI_AC344_ONE_SPACE = 1250; +const uint16_t HITACHI_AC344_ZERO_SPACE = 500; +const uint32_t HITACHI_AC344_MIN_GAP = 100000; // just a guess. +const uint16_t HITACHI_AC344_FREQ = 38000; // Hz. + +const uint8_t HITACHI_AC344_BUTTON_BYTE = 11; +const uint8_t HITACHI_AC344_BUTTON_POWER = 0x13; +const uint8_t HITACHI_AC344_BUTTON_SLEEP = 0x31; +const uint8_t HITACHI_AC344_BUTTON_MODE = 0x41; +const uint8_t HITACHI_AC344_BUTTON_FAN = 0x42; +const uint8_t HITACHI_AC344_BUTTON_TEMP_DOWN = 0x43; +const uint8_t HITACHI_AC344_BUTTON_TEMP_UP = 0x44; +const uint8_t HITACHI_AC344_BUTTON_SWINGV = 0x81; +const uint8_t HITACHI_AC344_BUTTON_SWINGH = 0x8C; +const uint8_t HITACHI_AC344_BUTTON_MILDEWPROOF = 0xE2; + +const uint8_t HITACHI_AC344_TEMP_BYTE = 13; +const uint8_t HITACHI_AC344_TEMP_OFFSET = 2; +const uint8_t HITACHI_AC344_TEMP_SIZE = 6; +const uint8_t HITACHI_AC344_TEMP_MIN = 16; // 16C +const uint8_t HITACHI_AC344_TEMP_MAX = 32; // 32C +const uint8_t HITACHI_AC344_TEMP_FAN = 27; // 27C + +const uint8_t HITACHI_AC344_TIMER_BYTE = 15; + +const uint8_t HITACHI_AC344_MODE_BYTE = 25; +const uint8_t HITACHI_AC344_MODE_FAN = 1; +const uint8_t HITACHI_AC344_MODE_COOL = 3; +const uint8_t HITACHI_AC344_MODE_DRY = 5; +const uint8_t HITACHI_AC344_MODE_HEAT = 6; +const uint8_t HITACHI_AC344_MODE_AUTO = 7; + +const uint8_t HITACHI_AC344_FAN_BYTE = HITACHI_AC344_MODE_BYTE; +const uint8_t HITACHI_AC344_FAN_MIN = 1; +const uint8_t HITACHI_AC344_FAN_LOW = 2; +const uint8_t HITACHI_AC344_FAN_MEDIUM = 3; +const uint8_t HITACHI_AC344_FAN_HIGH = 4; +const uint8_t HITACHI_AC344_FAN_AUTO = 5; +const uint8_t HITACHI_AC344_FAN_MAX = 6; +const uint8_t HITACHI_AC344_FAN_MAX_DRY = 2; + +const uint8_t HITACHI_AC344_POWER_BYTE = 27; +const uint8_t HITACHI_AC344_POWER_ON = 0xF1; +const uint8_t HITACHI_AC344_POWER_OFF = 0xE1; + +const uint8_t HITACHI_AC344_SWINGH_BYTE = 35; +const uint8_t HITACHI_AC344_SWINGH_OFFSET = 0; // Mask 0b00000xxx +const uint8_t HITACHI_AC344_SWINGH_SIZE = 3; // Mask 0b00000xxx +const uint8_t HITACHI_AC344_SWINGH_AUTO = 0; // 0b000 +const uint8_t HITACHI_AC344_SWINGH_RIGHT_MAX = 1; // 0b001 +const uint8_t HITACHI_AC344_SWINGH_RIGHT = 2; // 0b010 +const uint8_t HITACHI_AC344_SWINGH_MIDDLE = 3; // 0b011 +const uint8_t HITACHI_AC344_SWINGH_LEFT = 4; // 0b100 +const uint8_t HITACHI_AC344_SWINGH_LEFT_MAX = 5; // 0b101 + +const uint8_t HITACHI_AC344_SWINGV_BYTE = 37; +const uint8_t HITACHI_AC344_SWINGV_OFFSET = 5; // Mask 0b00x00000 + +const uint8_t HITACHI_AC344_MILDEWPROOF_BYTE = HITACHI_AC344_SWINGV_BYTE; +const uint8_t HITACHI_AC344_MILDEWPROOF_OFFSET = 2; // Mask 0b00000x00 + +const uint16_t HITACHI_AC344_STATE_LENGTH = 43; +const uint16_t HITACHI_AC344_BITS = HITACHI_AC344_STATE_LENGTH * 8; + +#define GETBIT8(a, b) (a & ((uint8_t) 1 << b)) +#define GETBITS8(data, offset, size) (((data) & (((uint8_t) UINT8_MAX >> (8 - (size))) << (offset))) >> (offset)) + +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}) {} + + protected: + uint8_t remote_state_[HITACHI_AC344_STATE_LENGTH]{0x01, 0x10, 0x00, 0x40, 0x00, 0xFF, 0x00, 0xCC, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x80, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + uint8_t previous_temp_{27}; + // Transmit via IR the state of this climate controller. + void transmit_state() override; + bool get_power_(); + void set_power_(bool on); + uint8_t get_mode_(); + void set_mode_(uint8_t mode); + void set_temp_(uint8_t celsius, bool set_previous = false); + uint8_t get_fan_(); + void set_fan_(uint8_t speed); + void set_swing_v_toggle_(bool on); + bool get_swing_v_toggle_(); + void set_swing_v_(bool on); + bool get_swing_v_(); + void set_swing_h_(uint8_t position); + uint8_t get_swing_h_(); + uint8_t get_button_(); + void set_button_(uint8_t button); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_mode_(const uint8_t remote_state[]); + bool parse_temperature_(const uint8_t remote_state[]); + bool parse_fan_(const uint8_t remote_state[]); + bool parse_swing_(const uint8_t remote_state[]); + bool parse_state_frame_(const uint8_t frame[]); + void dump_state_(const char action[], uint8_t remote_state[]); +}; + +} // namespace hitachi_ac344 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp index 3b9a9a11cc..41e538a399 100644 --- a/esphome/components/hm3301/aqi_calculator.cpp +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -17,7 +17,7 @@ class AQICalculator : public AbstractAQICalculator { int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 45}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 35}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, {255, 354}, {355, 424}, {425, 604}}; diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e9d97690fb..9ace8cf67f 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -10,17 +10,13 @@ void HomeassistantTime::dump_config() { ESP_LOGCONFIG(TAG, "Home Assistant Time:"); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } -float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } -void HomeassistantTime::setup() { - global_homeassistant_time = this; - this->set_interval(15 * 60 * 1000, []() { - // re-request time every 15 minutes - api::global_api_server->request_time(); - }); -} +float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } + +void HomeassistantTime::setup() { global_homeassistant_time = this; } + +void HomeassistantTime::update() { api::global_api_server->request_time(); } HomeassistantTime *global_homeassistant_time = nullptr; - } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 8ab09d1185..94f4704c2f 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -10,6 +10,7 @@ namespace homeassistant { class HomeassistantTime : public time::RealTimeClock { public: void setup() override; + void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } float get_setup_priority() const override; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index b003736d89..f7ec3cdbbf 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -43,8 +43,8 @@ def validate_url(value): value = cv.string(value) try: parsed = list(urlparse.urlparse(value)) - except Exception: - raise cv.Invalid('Invalid URL') + except Exception as err: + raise cv.Invalid('Invalid URL') from err if not parsed[0] or not parsed[1]: raise cv.Invalid('URL must have a URL scheme and host') diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 46b0910b5e..0e73a7956a 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -12,9 +12,20 @@ void HttpRequestComponent::dump_config() { ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); } +void HttpRequestComponent::set_url(std::string url) { + this->url_ = url; + this->secure_ = url.compare(0, 6, "https:") == 0; + + if (!this->last_url_.empty() && this->url_ != this->last_url_) { + // Close connection if url has been changed + this->client_.setReuse(false); + this->client_.end(); + } + this->client_.setReuse(true); +} + void HttpRequestComponent::send() { bool begin_status = false; - this->client_.setReuse(true); const String url = this->url_.c_str(); #ifdef ARDUINO_ARCH_ESP32 begin_status = this->client_.begin(url); @@ -78,7 +89,10 @@ WiFiClient *HttpRequestComponent::get_wifi_client_() { } #endif -void HttpRequestComponent::close() { this->client_.end(); } +void HttpRequestComponent::close() { + this->last_url_ = this->url_; + this->client_.end(); +} const char *HttpRequestComponent::get_string() { static const String STR = this->client_.getString(); diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index e6c0510b32..c69683db0e 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -27,10 +27,7 @@ class HttpRequestComponent : public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } - void set_url(std::string url) { - this->url_ = url; - this->secure_ = url.compare(0, 6, "https:") == 0; - } + void set_url(std::string url); void set_method(const char *method) { this->method_ = method; } void set_useragent(const char *useragent) { this->useragent_ = useragent; } void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } @@ -43,6 +40,7 @@ class HttpRequestComponent : public Component { protected: HTTPClient client_{}; std::string url_; + std::string last_url_; const char *method_; const char *useragent_{nullptr}; bool secure_; diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 562bd26771..8e6d9f32fa 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -56,8 +56,8 @@ void I2CComponent::raw_begin_transmission(uint8_t address) { ESP_LOGVV(TAG, "Beginning Transmission to 0x%02X:", address); this->wire_->beginTransmission(address); } -bool I2CComponent::raw_end_transmission(uint8_t address) { - uint8_t status = this->wire_->endTransmission(); +bool I2CComponent::raw_end_transmission(uint8_t address, bool send_stop) { + uint8_t status = this->wire_->endTransmission(send_stop); ESP_LOGVV(TAG, " Transmission ended. Status code: 0x%02X", status); switch (status) { diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index c4ed40e268..72777f8eb0 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -94,7 +94,7 @@ class I2CComponent : public Component { void raw_begin_transmission(uint8_t address); /// End a write transmission to an address, return true if successful. - bool raw_end_transmission(uint8_t address); + bool raw_end_transmission(uint8_t address, bool send_stop = true); /** Request data from an address with a number of (8-bit) bytes. * @@ -173,6 +173,17 @@ class I2CDevice { I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + /// Begin a write transmission. + void raw_begin_transmission() { this->parent_->raw_begin_transmission(this->address_); }; + + /// End a write transmission, return true if successful. + bool raw_end_transmission(bool send_stop = true) { + return this->parent_->raw_end_transmission(this->address_, send_stop); + }; + + /// Write len amount of bytes from data. begin_transmission_ must be called before this. + void raw_write(const uint8_t *data, uint8_t len) { this->parent_->raw_write(this->address_, data, len); }; + /** Read len amount of bytes from a register into data. Optionally with a conversion time after * writing the register value to the bus. * diff --git a/esphome/components/ili9341/__init__.py b/esphome/components/ili9341/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ili9341/display.py b/esphome/components/ili9341/display.py new file mode 100644 index 0000000000..0a3e9e16cc --- /dev/null +++ b/esphome/components/ili9341/display.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import CONF_DC_PIN, \ + CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_PAGES, CONF_RESET_PIN + +DEPENDENCIES = ['spi'] + +CONF_LED_PIN = 'led_pin' + +ili9341_ns = cg.esphome_ns.namespace('ili9341') +ili9341 = ili9341_ns.class_('ILI9341Display', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +ILI9341M5Stack = ili9341_ns.class_('ILI9341M5Stack', ili9341) +ILI9341TFT24 = ili9341_ns.class_('ILI9341TFT24', ili9341) + +ILI9341Model = ili9341_ns.enum('ILI9341Model') + +MODELS = { + 'M5STACK': ILI9341Model.M5STACK, + 'TFT_2.4': ILI9341Model.TFT_24, +} + +ILI9341_MODEL = cv.enum(MODELS, upper=True, space="_") + +CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ili9341), + cv.Required(CONF_MODEL): ILI9341_MODEL, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_LED_PIN): pins.gpio_output_pin_schema, +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + if config[CONF_MODEL] == 'M5STACK': + lcd_type = ILI9341M5Stack + if config[CONF_MODEL] == 'TFT_2.4': + lcd_type = ILI9341TFT24 + rhs = lcd_type.new() + var = cg.Pvariable(config[CONF_ID], rhs) + + yield cg.register_component(var, config) + yield display.register_display(var, config) + yield spi.register_spi_device(var, config) + cg.add(var.set_model(config[CONF_MODEL])) + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_LED_PIN in config: + led_pin = yield cg.gpio_pin_expression(config[CONF_LED_PIN]) + cg.add(var.set_led_pin(led_pin)) diff --git a/esphome/components/ili9341/ili9341_defines.h b/esphome/components/ili9341/ili9341_defines.h new file mode 100644 index 0000000000..6b3d4c0dcf --- /dev/null +++ b/esphome/components/ili9341/ili9341_defines.h @@ -0,0 +1,83 @@ +#pragma once + +namespace esphome { +namespace ili9341 { + +// Color definitions +// clang-format off +static const uint8_t MADCTL_MY = 0x80; ///< Bit 7 Bottom to top +static const uint8_t MADCTL_MX = 0x40; ///< Bit 6 Right to left +static const uint8_t MADCTL_MV = 0x20; ///< Bit 5 Reverse Mode +static const uint8_t MADCTL_ML = 0x10; ///< Bit 4 LCD refresh Bottom to top +static const uint8_t MADCTL_RGB = 0x00; ///< Bit 3 Red-Green-Blue pixel order +static const uint8_t MADCTL_BGR = 0x08; ///< Bit 3 Blue-Green-Red pixel order +static const uint8_t MADCTL_MH = 0x04; ///< Bit 2 LCD refresh right to left +// clang-format on + +static const uint16_t ILI9341_TFTWIDTH = 320; ///< ILI9341 max TFT width +static const uint16_t ILI9341_TFTHEIGHT = 240; ///< ILI9341 max TFT height + +// All ILI9341 specific commands some are used by init() +static const uint8_t ILI9341_NOP = 0x00; +static const uint8_t ILI9341_SWRESET = 0x01; +static const uint8_t ILI9341_RDDID = 0x04; +static const uint8_t ILI9341_RDDST = 0x09; + +static const uint8_t ILI9341_SLPIN = 0x10; +static const uint8_t ILI9341_SLPOUT = 0x11; +static const uint8_t ILI9341_PTLON = 0x12; +static const uint8_t ILI9341_NORON = 0x13; + +static const uint8_t ILI9341_RDMODE = 0x0A; +static const uint8_t ILI9341_RDMADCTL = 0x0B; +static const uint8_t ILI9341_RDPIXFMT = 0x0C; +static const uint8_t ILI9341_RDIMGFMT = 0x0A; +static const uint8_t ILI9341_RDSELFDIAG = 0x0F; + +static const uint8_t ILI9341_INVOFF = 0x20; +static const uint8_t ILI9341_INVON = 0x21; +static const uint8_t ILI9341_GAMMASET = 0x26; +static const uint8_t ILI9341_DISPOFF = 0x28; +static const uint8_t ILI9341_DISPON = 0x29; + +static const uint8_t ILI9341_CASET = 0x2A; +static const uint8_t ILI9341_PASET = 0x2B; +static const uint8_t ILI9341_RAMWR = 0x2C; +static const uint8_t ILI9341_RAMRD = 0x2E; + +static const uint8_t ILI9341_PTLAR = 0x30; +static const uint8_t ILI9341_VSCRDEF = 0x33; +static const uint8_t ILI9341_MADCTL = 0x36; +static const uint8_t ILI9341_VSCRSADD = 0x37; +static const uint8_t ILI9341_PIXFMT = 0x3A; + +static const uint8_t ILI9341_WRDISBV = 0x51; +static const uint8_t ILI9341_RDDISBV = 0x52; +static const uint8_t ILI9341_WRCTRLD = 0x53; + +static const uint8_t ILI9341_FRMCTR1 = 0xB1; +static const uint8_t ILI9341_FRMCTR2 = 0xB2; +static const uint8_t ILI9341_FRMCTR3 = 0xB3; +static const uint8_t ILI9341_INVCTR = 0xB4; +static const uint8_t ILI9341_DFUNCTR = 0xB6; + +static const uint8_t ILI9341_PWCTR1 = 0xC0; +static const uint8_t ILI9341_PWCTR2 = 0xC1; +static const uint8_t ILI9341_PWCTR3 = 0xC2; +static const uint8_t ILI9341_PWCTR4 = 0xC3; +static const uint8_t ILI9341_PWCTR5 = 0xC4; +static const uint8_t ILI9341_VMCTR1 = 0xC5; +static const uint8_t ILI9341_VMCTR2 = 0xC7; + +static const uint8_t ILI9341_RDID4 = 0xD3; +static const uint8_t ILI9341_RDINDEX = 0xD9; +static const uint8_t ILI9341_RDID1 = 0xDA; +static const uint8_t ILI9341_RDID2 = 0xDB; +static const uint8_t ILI9341_RDID3 = 0xDC; +static const uint8_t ILI9341_RDIDX = 0xDD; // TBC + +static const uint8_t ILI9341_GMCTRP1 = 0xE0; +static const uint8_t ILI9341_GMCTRN1 = 0xE1; + +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.cpp b/esphome/components/ili9341/ili9341_display.cpp new file mode 100644 index 0000000000..c0e7873284 --- /dev/null +++ b/esphome/components/ili9341/ili9341_display.cpp @@ -0,0 +1,240 @@ +#include "ili9341_display.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ili9341 { + +static const char *TAG = "ili9341"; + +void ILI9341Display::setup_pins_() { + this->init_internal_(this->get_buffer_length_()); + this->dc_pin_->setup(); // OUTPUT + this->dc_pin_->digital_write(false); + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); // OUTPUT + this->reset_pin_->digital_write(true); + } + if (this->led_pin_ != nullptr) { + this->led_pin_->setup(); + this->led_pin_->digital_write(true); + } + this->spi_setup(); + + this->reset_(); +} + +void ILI9341Display::dump_config() { + LOG_DISPLAY("", "ili9341", this); + ESP_LOGCONFIG(TAG, " Width: %d, Height: %d, Rotation: %d", this->width_, this->height_, this->rotation_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_PIN(" Backlight Pin: ", this->led_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ILI9341Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void ILI9341Display::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void ILI9341Display::reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + delay(10); + } +} + +void ILI9341Display::data(uint8_t value) { + this->start_data_(); + this->write_byte(value); + this->end_data_(); +} + +void ILI9341Display::send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->command(command_byte); // Send the command byte + this->start_data_(); + this->write_array(data_bytes, num_data_bytes); + this->end_data_(); +} + +uint8_t ILI9341Display::read_command(uint8_t command_byte, uint8_t index) { + uint8_t data = 0x10 + index; + this->send_command(0xD9, &data, 1); // Set Index Register + uint8_t result; + this->start_command_(); + this->write_byte(command_byte); + this->start_data_(); + do { + result = this->read_byte(); + } while (index--); + this->end_data_(); + return result; +} + +void ILI9341Display::update() { + this->do_update_(); + this->display_(); +} + +void ILI9341Display::display_() { + // we will only update the changed window to the display + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + + set_addr_window_(this->x_low_, this->y_low_, w, h); + this->start_data_(); + uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); + for (uint16_t row = 0; row < h; row++) { + for (uint16_t col = 0; col < w; col++) { + uint32_t pos = start_pos + (row * width_) + col; + + uint16_t color = convert_to_16bit_color_(buffer_[pos]); + this->write_byte(color >> 8); + this->write_byte(color); + } + } + this->end_data_(); + + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +uint16_t ILI9341Display::convert_to_16bit_color_(uint8_t color_8bit) { + int r = color_8bit >> 5; + int g = (color_8bit >> 2) & 0x07; + int b = color_8bit & 0x03; + uint16_t color = (r * 0x04) << 11; + color |= (g * 0x09) << 5; + color |= (b * 0x0A); + + return color; +} + +uint8_t ILI9341Display::convert_to_8bit_color_(uint16_t color_16bit) { + // convert 16bit color to 8 bit buffer + uint8_t r = color_16bit >> 11; + uint8_t g = (color_16bit >> 5) & 0x3F; + uint8_t b = color_16bit & 0x1F; + + return ((b / 0x0A) | ((g / 0x09) << 2) | ((r / 0x04) << 5)); +} + +void ILI9341Display::fill(Color color) { + auto color565 = color.to_rgb_565(); + memset(this->buffer_, convert_to_8bit_color_(color565), this->get_buffer_length_()); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->get_width_internal() - 1; + this->y_high_ = this->get_height_internal() - 1; +} + +void ILI9341Display::fill_internal_(Color color) { + this->set_addr_window_(0, 0, this->get_width_internal(), this->get_height_internal()); + this->start_data_(); + + auto color565 = color.to_rgb_565(); + for (uint32_t i = 0; i < (this->get_width_internal()) * (this->get_height_internal()); i++) { + this->write_byte(color565 >> 8); + this->write_byte(color565); + buffer_[i] = 0; + } + this->end_data_(); +} + +void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + // low and high watermark may speed up drawing from buffer + this->x_low_ = (x < this->x_low_) ? x : this->x_low_; + this->y_low_ = (y < this->y_low_) ? y : this->y_low_; + this->x_high_ = (x > this->x_high_) ? x : this->x_high_; + this->y_high_ = (y > this->y_high_) ? y : this->y_high_; + + uint32_t pos = (y * width_) + x; + auto color565 = color.to_rgb_565(); + buffer_[pos] = convert_to_8bit_color_(color565); +} + +// should return the total size: return this->get_width_internal() * this->get_height_internal() * 2 // 16bit color +// values per bit is huge +uint32_t ILI9341Display::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal(); } + +void ILI9341Display::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} + +void ILI9341Display::end_command_() { this->disable(); } +void ILI9341Display::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +void ILI9341Display::end_data_() { this->disable(); } + +void ILI9341Display::init_lcd_(const uint8_t *init_cmd) { + uint8_t cmd, x, num_args; + const uint8_t *addr = init_cmd; + while ((cmd = pgm_read_byte(addr++)) > 0) { + x = pgm_read_byte(addr++); + num_args = x & 0x7F; + send_command(cmd, addr, num_args); + addr += num_args; + if (x & 0x80) + delay(150); // NOLINT + } +} + +void ILI9341Display::set_addr_window_(uint16_t x1, uint16_t y1, uint16_t w, uint16_t h) { + uint16_t x2 = (x1 + w - 1), y2 = (y1 + h - 1); + this->command(ILI9341_CASET); // Column address set + this->start_data_(); + this->write_byte(x1 >> 8); + this->write_byte(x1); + this->write_byte(x2 >> 8); + this->write_byte(x2); + this->end_data_(); + this->command(ILI9341_PASET); // Row address set + this->start_data_(); + this->write_byte(y1 >> 8); + this->write_byte(y1); + this->write_byte(y2 >> 8); + this->write_byte(y2); + this->end_data_(); + this->command(ILI9341_RAMWR); // Write to RAM +} + +void ILI9341Display::invert_display_(bool invert) { this->command(invert ? ILI9341_INVON : ILI9341_INVOFF); } + +int ILI9341Display::get_width_internal() { return this->width_; } +int ILI9341Display::get_height_internal() { return this->height_; } + +// M5Stack display +void ILI9341M5Stack::initialize() { + this->init_lcd_(INITCMD_M5STACK); + this->width_ = 320; + this->height_ = 240; + this->invert_display_(true); + this->fill_internal_(COLOR_BLACK); +} + +// 24_TFT display +void ILI9341TFT24::initialize() { + this->init_lcd_(INITCMD_TFT); + this->width_ = 240; + this->height_ = 320; + this->fill_internal_(COLOR_BLACK); +} + +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_display.h b/esphome/components/ili9341/ili9341_display.h new file mode 100644 index 0000000000..2b6ecc6871 --- /dev/null +++ b/esphome/components/ili9341/ili9341_display.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" +#include "ili9341_defines.h" +#include "ili9341_init.h" + +namespace esphome { +namespace ili9341 { + +enum ILI9341Model { + M5STACK = 0, + TFT_24, +}; + +class ILI9341Display : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + float get_setup_priority() const override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_led_pin(GPIOPin *led) { this->led_pin_ = led; } + void set_model(ILI9341Model model) { this->model_ = model; } + + void command(uint8_t value); + void data(uint8_t value); + void send_command(uint8_t command_byte, const uint8_t *data_bytes, uint8_t num_data_bytes); + uint8_t read_command(uint8_t command_byte, uint8_t index); + virtual void initialize() = 0; + + void update() override; + + void fill(Color color) override; + + void dump_config() override; + void setup() override { + this->setup_pins_(); + this->initialize(); + } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void setup_pins_(); + + void init_lcd_(const uint8_t *init_cmd); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void invert_display_(bool invert); + void reset_(); + void fill_internal_(Color color); + void display_(); + uint16_t convert_to_16bit_color_(uint8_t color_8bit); + uint8_t convert_to_8bit_color_(uint16_t color_16bit); + + ILI9341Model model_; + int16_t width_{320}; ///< Display width as modified by current rotation + int16_t height_{240}; ///< Display height as modified by current rotation + uint16_t x_low_{0}; + uint16_t y_low_{0}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + uint32_t get_buffer_length_(); + int get_width_internal() override; + int get_height_internal() override; + + void start_command_(); + void end_command_(); + void start_data_(); + void end_data_(); + + GPIOPin *reset_pin_{nullptr}; + GPIOPin *led_pin_{nullptr}; + GPIOPin *dc_pin_; + GPIOPin *busy_pin_{nullptr}; +}; + +//----------- M5Stack display -------------- +class ILI9341M5Stack : public ILI9341Display { + public: + void initialize() override; +}; + +//----------- ILI9341_24_TFT display -------------- +class ILI9341TFT24 : public ILI9341Display { + public: + void initialize() override; +}; +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/ili9341/ili9341_init.h b/esphome/components/ili9341/ili9341_init.h new file mode 100644 index 0000000000..9282895e2e --- /dev/null +++ b/esphome/components/ili9341/ili9341_init.h @@ -0,0 +1,70 @@ +#pragma once +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ili9341 { + +// clang-format off +static const uint8_t PROGMEM INITCMD_M5STACK[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9341_VMCTR2 , 1, 0x86, // VCM control2 + ILI9341_MADCTL , 1, MADCTL_BGR, // Memory Access Control + ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9341_PIXFMT , 1, 0x55, + ILI9341_FRMCTR1 , 2, 0x00, 0x13, + ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9341_SLPOUT , 0x80, // Exit Sleep + ILI9341_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +static const uint8_t PROGMEM INITCMD_TFT[] = { + 0xEF, 3, 0x03, 0x80, 0x02, + 0xCF, 3, 0x00, 0xC1, 0x30, + 0xED, 4, 0x64, 0x03, 0x12, 0x81, + 0xE8, 3, 0x85, 0x00, 0x78, + 0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, + 0xF7, 1, 0x20, + 0xEA, 2, 0x00, 0x00, + ILI9341_PWCTR1 , 1, 0x23, // Power control VRH[5:0] + ILI9341_PWCTR2 , 1, 0x10, // Power control SAP[2:0];BT[3:0] + ILI9341_VMCTR1 , 2, 0x3e, 0x28, // VCM control + ILI9341_VMCTR2 , 1, 0x86, // VCM control2 + ILI9341_MADCTL , 1, 0x48, // Memory Access Control + ILI9341_VSCRSADD, 1, 0x00, // Vertical scroll zero + ILI9341_PIXFMT , 1, 0x55, + ILI9341_FRMCTR1 , 2, 0x00, 0x18, + ILI9341_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control + 0xF2, 1, 0x00, // 3Gamma Function Disable + ILI9341_GAMMASET , 1, 0x01, // Gamma curve selected + ILI9341_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma + 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, + 0x0E, 0x09, 0x00, + ILI9341_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma + 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, + 0x31, 0x36, 0x0F, + ILI9341_SLPOUT , 0x80, // Exit Sleep + ILI9341_DISPON , 0x80, // Display on + 0x00 // End of list +}; + +// clang-format on +} // namespace ili9341 +} // namespace esphome diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index dfe387afd1..3558d9660e 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -4,14 +4,14 @@ from esphome import core from esphome.components import display, font import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE +from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE, CONF_DITHER from esphome.core import CORE, HexInt + _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True - ImageType = display.display_ns.enum('ImageType') IMAGE_TYPE = { 'BINARY': ImageType.IMAGE_TYPE_BINARY, @@ -28,6 +28,7 @@ IMAGE_SCHEMA = cv.Schema({ cv.Required(CONF_FILE): cv.file_, cv.Optional(CONF_RESIZE): cv.dimensions, cv.Optional(CONF_TYPE, default='BINARY'): cv.enum(IMAGE_TYPE, upper=True), + cv.Optional(CONF_DITHER, default='NONE'): cv.one_of("NONE", "FLOYDSTEINBERG", upper=True), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), }) @@ -53,8 +54,9 @@ def to_code(config): _LOGGER.warning("The image you requested is very big. Please consider using" " the resize parameter.") + dither = Image.NONE if config[CONF_DITHER] == 'NONE' else Image.FLOYDSTEINBERG if config[CONF_TYPE] == 'GRAYSCALE': - image = image.convert('L', dither=Image.NONE) + image = image.convert('L', dither=dither) pixels = list(image.getdata()) data = [0 for _ in range(height * width)] pos = 0 @@ -76,7 +78,7 @@ def to_code(config): pos += 1 elif config[CONF_TYPE] == 'BINARY': - image = image.convert('1', dither=Image.NONE) + image = image.convert('1', dither=dither) width8 = ((width + 7) // 8) * 8 data = [0 for _ in range(height * width8 // 8)] for y in range(height): diff --git a/esphome/components/inkplate6/__init__.py b/esphome/components/inkplate6/__init__.py new file mode 100644 index 0000000000..ba7653988b --- /dev/null +++ b/esphome/components/inkplate6/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@jesserockz'] diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py new file mode 100644 index 0000000000..4f35901bd4 --- /dev/null +++ b/esphome/components/inkplate6/display.py @@ -0,0 +1,141 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, i2c +from esphome.const import CONF_FULL_UPDATE_EVERY, CONF_ID, CONF_LAMBDA, CONF_PAGES, \ + CONF_WAKEUP_PIN, ESP_PLATFORM_ESP32 + +DEPENDENCIES = ['i2c'] +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + +CONF_DISPLAY_DATA_0_PIN = 'display_data_0_pin' +CONF_DISPLAY_DATA_1_PIN = 'display_data_1_pin' +CONF_DISPLAY_DATA_2_PIN = 'display_data_2_pin' +CONF_DISPLAY_DATA_3_PIN = 'display_data_3_pin' +CONF_DISPLAY_DATA_4_PIN = 'display_data_4_pin' +CONF_DISPLAY_DATA_5_PIN = 'display_data_5_pin' +CONF_DISPLAY_DATA_6_PIN = 'display_data_6_pin' +CONF_DISPLAY_DATA_7_PIN = 'display_data_7_pin' + +CONF_CL_PIN = 'cl_pin' +CONF_CKV_PIN = 'ckv_pin' +CONF_GREYSCALE = 'greyscale' +CONF_GMOD_PIN = 'gmod_pin' +CONF_GPIO0_ENABLE_PIN = 'gpio0_enable_pin' +CONF_LE_PIN = 'le_pin' +CONF_OE_PIN = 'oe_pin' +CONF_PARTIAL_UPDATING = 'partial_updating' +CONF_POWERUP_PIN = 'powerup_pin' +CONF_SPH_PIN = 'sph_pin' +CONF_SPV_PIN = 'spv_pin' +CONF_VCOM_PIN = 'vcom_pin' + + +inkplate6_ns = cg.esphome_ns.namespace('inkplate6') +Inkplate6 = inkplate6_ns.class_('Inkplate6', cg.PollingComponent, i2c.I2CDevice, + display.DisplayBuffer) + +CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(Inkplate6), + cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, + cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, + cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + # Control pins + cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, + # Data pins + cv.Optional(CONF_DISPLAY_DATA_0_PIN, default=4): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_1_PIN, default=5): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_2_PIN, default=18): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_3_PIN, default=19): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_4_PIN, default=23): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_5_PIN, default=25): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_6_PIN, default=26): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_DISPLAY_DATA_7_PIN, default=27): pins.internal_gpio_output_pin_schema, +}).extend(cv.polling_component_schema('5s').extend(i2c.i2c_device_schema(0x48))), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + yield cg.register_component(var, config) + yield display.register_display(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) + + cg.add(var.set_greyscale(config[CONF_GREYSCALE])) + cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + + ckv = yield cg.gpio_pin_expression(config[CONF_CKV_PIN]) + cg.add(var.set_ckv_pin(ckv)) + + gmod = yield cg.gpio_pin_expression(config[CONF_GMOD_PIN]) + cg.add(var.set_gmod_pin(gmod)) + + gpio0_enable = yield cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) + cg.add(var.set_gpio0_enable_pin(gpio0_enable)) + + oe = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe)) + + powerup = yield cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) + cg.add(var.set_powerup_pin(powerup)) + + sph = yield cg.gpio_pin_expression(config[CONF_SPH_PIN]) + cg.add(var.set_sph_pin(sph)) + + spv = yield cg.gpio_pin_expression(config[CONF_SPV_PIN]) + cg.add(var.set_spv_pin(spv)) + + vcom = yield cg.gpio_pin_expression(config[CONF_VCOM_PIN]) + cg.add(var.set_vcom_pin(vcom)) + + wakeup = yield cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) + cg.add(var.set_wakeup_pin(wakeup)) + + cl = yield cg.gpio_pin_expression(config[CONF_CL_PIN]) + cg.add(var.set_cl_pin(cl)) + + le = yield cg.gpio_pin_expression(config[CONF_LE_PIN]) + cg.add(var.set_le_pin(le)) + + display_data_0 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) + cg.add(var.set_display_data_0_pin(display_data_0)) + + display_data_1 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) + cg.add(var.set_display_data_1_pin(display_data_1)) + + display_data_2 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) + cg.add(var.set_display_data_2_pin(display_data_2)) + + display_data_3 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) + cg.add(var.set_display_data_3_pin(display_data_3)) + + display_data_4 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) + cg.add(var.set_display_data_4_pin(display_data_4)) + + display_data_5 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) + cg.add(var.set_display_data_5_pin(display_data_5)) + + display_data_6 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) + cg.add(var.set_display_data_6_pin(display_data_6)) + + display_data_7 = yield cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) + cg.add(var.set_display_data_7_pin(display_data_7)) + + cg.add_build_flag('-DBOARD_HAS_PSRAM') diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate6/inkplate.cpp new file mode 100644 index 0000000000..3b17ba8f52 --- /dev/null +++ b/esphome/components/inkplate6/inkplate.cpp @@ -0,0 +1,630 @@ +#include "inkplate.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace inkplate6 { + +static const char *TAG = "inkplate"; + +void Inkplate6::setup() { + this->initialize_(); + + this->vcom_pin_->setup(); + this->powerup_pin_->setup(); + this->wakeup_pin_->setup(); + this->gpio0_enable_pin_->setup(); + this->gpio0_enable_pin_->digital_write(true); + + this->cl_pin_->setup(); + this->le_pin_->setup(); + this->ckv_pin_->setup(); + this->gmod_pin_->setup(); + this->oe_pin_->setup(); + this->sph_pin_->setup(); + this->spv_pin_->setup(); + + this->display_data_0_pin_->setup(); + this->display_data_1_pin_->setup(); + this->display_data_2_pin_->setup(); + this->display_data_3_pin_->setup(); + this->display_data_4_pin_->setup(); + this->display_data_5_pin_->setup(); + this->display_data_6_pin_->setup(); + this->display_data_7_pin_->setup(); + + this->clean(); + this->display(); +} +void Inkplate6::initialize_() { + uint32_t buffer_size = this->get_buffer_length_(); + + if (this->partial_buffer_ != nullptr) { + free(this->partial_buffer_); // NOLINT + } + if (this->partial_buffer_2_ != nullptr) { + free(this->partial_buffer_2_); // NOLINT + } + if (this->buffer_ != nullptr) { + free(this->buffer_); // NOLINT + } + + this->buffer_ = (uint8_t *) ps_malloc(buffer_size); + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate buffer for display!"); + this->mark_failed(); + return; + } + if (!this->greyscale_) { + this->partial_buffer_ = (uint8_t *) ps_malloc(buffer_size); + if (this->partial_buffer_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate partial buffer for display!"); + this->mark_failed(); + return; + } + this->partial_buffer_2_ = (uint8_t *) ps_malloc(buffer_size * 2); + if (this->partial_buffer_2_ == nullptr) { + ESP_LOGE(TAG, "Could not allocate partial buffer 2 for display!"); + this->mark_failed(); + return; + } + memset(this->partial_buffer_, 0, buffer_size); + memset(this->partial_buffer_2_, 0, buffer_size * 2); + } + + memset(this->buffer_, 0, buffer_size); +} +float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } +size_t Inkplate6::get_buffer_length_() { + if (this->greyscale_) { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; + } else { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + } +} +void Inkplate6::update() { + this->do_update_(); + + if (this->full_update_every_ > 0 && this->partial_updates_ >= this->full_update_every_) { + this->block_partial_ = true; + } + + this->display(); +} +void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) + return; + + if (this->greyscale_) { + int x1 = x / 2; + int x_sub = x % 2; + uint32_t pos = (x1 + y * (this->get_width_internal() / 2)); + uint8_t current = this->buffer_[pos]; + + // float px = (0.2126 * (color.red / 255.0)) + (0.7152 * (color.green / 255.0)) + (0.0722 * (color.blue / 255.0)); + // px = pow(px, 1.5); + // uint8_t gs = (uint8_t)(px*7); + + uint8_t gs = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; + this->buffer_[pos] = (pixelMaskGLUT[x_sub] & current) | (x_sub ? gs : gs << 4); + + } else { + int x1 = x / 8; + int x_sub = x % 8; + uint32_t pos = (x1 + y * (this->get_width_internal() / 8)); + uint8_t current = this->partial_buffer_[pos]; + this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); + } +} +void Inkplate6::dump_config() { + LOG_DISPLAY("", "Inkplate", this); + ESP_LOGCONFIG(TAG, " Greyscale: %s", YESNO(this->greyscale_)); + ESP_LOGCONFIG(TAG, " Partial Updating: %s", YESNO(this->partial_updating_)); + ESP_LOGCONFIG(TAG, " Full Update Every: %d", this->full_update_every_); + // Log pins + LOG_PIN(" CKV Pin: ", this->ckv_pin_); + LOG_PIN(" CL Pin: ", this->cl_pin_); + LOG_PIN(" GPIO0 Enable Pin: ", this->gpio0_enable_pin_); + LOG_PIN(" GMOD Pin: ", this->gmod_pin_); + LOG_PIN(" LE Pin: ", this->le_pin_); + LOG_PIN(" OE Pin: ", this->oe_pin_); + LOG_PIN(" POWERUP Pin: ", this->powerup_pin_); + LOG_PIN(" SPH Pin: ", this->sph_pin_); + LOG_PIN(" SPV Pin: ", this->spv_pin_); + LOG_PIN(" VCOM Pin: ", this->vcom_pin_); + LOG_PIN(" WAKEUP Pin: ", this->wakeup_pin_); + + LOG_PIN(" Data 0 Pin: ", this->display_data_0_pin_); + LOG_PIN(" Data 1 Pin: ", this->display_data_1_pin_); + LOG_PIN(" Data 2 Pin: ", this->display_data_2_pin_); + LOG_PIN(" Data 3 Pin: ", this->display_data_3_pin_); + LOG_PIN(" Data 4 Pin: ", this->display_data_4_pin_); + LOG_PIN(" Data 5 Pin: ", this->display_data_5_pin_); + LOG_PIN(" Data 6 Pin: ", this->display_data_6_pin_); + LOG_PIN(" Data 7 Pin: ", this->display_data_7_pin_); + + LOG_UPDATE_INTERVAL(this); +} +void Inkplate6::eink_off_() { + ESP_LOGV(TAG, "Eink off called"); + unsigned long start_time = millis(); + if (panel_on_ == 0) + return; + panel_on_ = 0; + this->gmod_pin_->digital_write(false); + this->oe_pin_->digital_write(false); + + GPIO.out &= ~(get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin())); + + this->sph_pin_->digital_write(false); + this->spv_pin_->digital_write(false); + + this->powerup_pin_->digital_write(false); + this->wakeup_pin_->digital_write(false); + this->vcom_pin_->digital_write(false); + + pins_z_state_(); +} +void Inkplate6::eink_on_() { + ESP_LOGV(TAG, "Eink on called"); + unsigned long start_time = millis(); + if (panel_on_ == 1) + return; + panel_on_ = 1; + pins_as_outputs_(); + this->wakeup_pin_->digital_write(true); + this->powerup_pin_->digital_write(true); + this->vcom_pin_->digital_write(true); + + this->write_byte(0x01, 0x3F); + + delay(40); + + this->write_byte(0x0D, 0x80); + + delay(2); + + this->read_byte(0x00, &temperature_, 0); + + this->le_pin_->digital_write(false); + this->oe_pin_->digital_write(false); + this->cl_pin_->digital_write(false); + this->sph_pin_->digital_write(true); + this->gmod_pin_->digital_write(true); + this->spv_pin_->digital_write(true); + this->ckv_pin_->digital_write(false); + this->oe_pin_->digital_write(true); +} +void Inkplate6::fill(Color color) { + ESP_LOGV(TAG, "Fill called"); + unsigned long start_time = millis(); + + if (this->greyscale_) { + uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = (fill << 4) | fill; + } else { + uint8_t fill = color.is_on() ? 0x00 : 0xFF; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->partial_buffer_[i] = fill; + } + + ESP_LOGV(TAG, "Fill finished (%lums)", millis() - start_time); +} +void Inkplate6::display() { + ESP_LOGV(TAG, "Display called"); + unsigned long start_time = millis(); + + if (this->greyscale_) { + this->display3b_(); + } else { + if (this->partial_updating_ && this->partial_update_()) { + ESP_LOGV(TAG, "Display finished (partial) (%lums)", millis() - start_time); + return; + } + this->display1b_(); + } + ESP_LOGV(TAG, "Display finished (full) (%lums)", millis() - start_time); +} +void Inkplate6::display1b_() { + ESP_LOGV(TAG, "Display1b called"); + unsigned long start_time = millis(); + + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->buffer_[i] &= this->partial_buffer_[i]; + this->buffer_[i] |= this->partial_buffer_[i]; + } + + uint16_t pos; + uint32_t send; + uint8_t data; + uint8_t buffer_value; + eink_on_(); + clean_fast_(0, 1); + clean_fast_(1, 5); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + + ESP_LOGV(TAG, "Display1b start loops (%lums)", millis() - start_time); + for (int k = 0; k < 3; k++) { + pos = this->get_buffer_length_() - 1; + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + buffer_value = this->buffer_[pos]; + data = LUTB[(buffer_value >> 4) & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + hscan_start_(send); + data = LUTB[buffer_value & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + pos--; + + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + buffer_value = this->buffer_[pos]; + data = LUTB[(buffer_value >> 4) & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + data = LUTB[buffer_value & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + pos--; + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + } + ESP_LOGV(TAG, "Display1b first loop x %d (%lums)", 3, millis() - start_time); + + pos = this->get_buffer_length_() - 1; + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + buffer_value = this->buffer_[pos]; + data = LUT2[(buffer_value >> 4) & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + hscan_start_(send); + data = LUT2[buffer_value & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + pos--; + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + buffer_value = this->buffer_[pos]; + data = LUT2[(buffer_value >> 4) & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + data = LUT2[buffer_value & 0x0F]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + pos--; + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Display1b second loop (%lums)", millis() - start_time); + + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + buffer_value = this->buffer_[pos]; + data = 0b00000000; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + hscan_start_(send); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Display1b third loop (%lums)", millis() - start_time); + + vscan_start_(); + eink_off_(); + this->block_partial_ = false; + this->partial_updates_ = 0; + ESP_LOGV(TAG, "Display1b finished (%lums)", millis() - start_time); +} +void Inkplate6::display3b_() { + ESP_LOGV(TAG, "Display3b called"); + unsigned long start_time = millis(); + + eink_on_(); + clean_fast_(0, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 11); + + for (int k = 0; k < 8; k++) { + uint32_t pos = this->get_buffer_length_() - 1; + uint32_t send; + uint8_t pix1; + uint8_t pix2; + uint8_t pix3; + uint8_t pix4; + uint8_t pixel; + uint8_t pixel2; + + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + pix1 = this->buffer_[pos--]; + pix2 = this->buffer_[pos--]; + pix3 = this->buffer_[pos--]; + pix4 = this->buffer_[pos--]; + pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); + pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); + + send = ((pixel & B00000011) << 4) | (((pixel & B00001100) >> 2) << 18) | (((pixel & B00010000) >> 4) << 23) | + (((pixel & B11100000) >> 5) << 25); + hscan_start_(send); + send = ((pixel2 & B00000011) << 4) | (((pixel2 & B00001100) >> 2) << 18) | (((pixel2 & B00010000) >> 4) << 23) | + (((pixel2 & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + + for (int j = 0; j < (this->get_width_internal() / 8) - 1; j++) { + pix1 = this->buffer_[pos--]; + pix2 = this->buffer_[pos--]; + pix3 = this->buffer_[pos--]; + pix4 = this->buffer_[pos--]; + pixel = (waveform3Bit[pix1 & 0x07][k] << 6) | (waveform3Bit[(pix1 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix2 & 0x07][k] << 2) | (waveform3Bit[(pix2 >> 4) & 0x07][k] << 0); + pixel2 = (waveform3Bit[pix3 & 0x07][k] << 6) | (waveform3Bit[(pix3 >> 4) & 0x07][k] << 4) | + (waveform3Bit[pix4 & 0x07][k] << 2) | (waveform3Bit[(pix4 >> 4) & 0x07][k] << 0); + + send = ((pixel & B00000011) << 4) | (((pixel & B00001100) >> 2) << 18) | (((pixel & B00010000) >> 4) << 23) | + (((pixel & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + + send = ((pixel2 & B00000011) << 4) | (((pixel2 & B00001100) >> 2) << 18) | (((pixel2 & B00010000) >> 4) << 23) | + (((pixel2 & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + } + clean_fast_(2, 1); + clean_fast_(3, 1); + vscan_start_(); + eink_off_(); + ESP_LOGV(TAG, "Display3b finished (%lums)", millis() - start_time); +} +bool Inkplate6::partial_update_() { + ESP_LOGV(TAG, "Partial update called"); + unsigned long start_time = millis(); + if (this->greyscale_) + return false; + if (this->block_partial_) + return false; + + this->partial_updates_++; + + uint16_t pos = this->get_buffer_length_() - 1; + uint32_t send; + uint8_t data; + uint8_t diffw, diffb; + uint32_t n = (this->get_buffer_length_() * 2) - 1; + + for (int i = 0; i < this->get_height_internal(); i++) { + for (int j = 0; j < (this->get_width_internal() / 8); j++) { + diffw = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & ~(this->partial_buffer_[pos]); + diffb = (this->buffer_[pos] ^ this->partial_buffer_[pos]) & this->partial_buffer_[pos]; + pos--; + this->partial_buffer_2_[n--] = LUTW[diffw >> 4] & LUTB[diffb >> 4]; + this->partial_buffer_2_[n--] = LUTW[diffw & 0x0F] & LUTB[diffb & 0x0F]; + } + } + ESP_LOGV(TAG, "Partial update buffer built after (%lums)", millis() - start_time); + + eink_on_(); + for (int k = 0; k < 5; k++) { + vscan_start_(); + n = (this->get_buffer_length_() * 2) - 1; + for (int i = 0; i < this->get_height_internal(); i++) { + data = this->partial_buffer_2_[n--]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + hscan_start_(send); + for (int j = 0; j < (this->get_width_internal() / 4) - 1; j++) { + data = this->partial_buffer_2_[n--]; + send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Partial update loop k=%d (%lums)", k, millis() - start_time); + } + clean_fast_(2, 2); + clean_fast_(3, 1); + vscan_start_(); + eink_off_(); + + for (int i = 0; i < this->get_buffer_length_(); i++) { + this->buffer_[i] = this->partial_buffer_[i]; + } + ESP_LOGV(TAG, "Partial update finished (%lums)", millis() - start_time); + return true; +} +void Inkplate6::vscan_start_() { + this->ckv_pin_->digital_write(true); + delayMicroseconds(7); + this->spv_pin_->digital_write(false); + delayMicroseconds(10); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(8); + this->spv_pin_->digital_write(true); + delayMicroseconds(10); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(18); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); + delayMicroseconds(18); + this->ckv_pin_->digital_write(false); + delayMicroseconds(0); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::vscan_write_() { + this->ckv_pin_->digital_write(false); + this->le_pin_->digital_write(true); + this->le_pin_->digital_write(false); + delayMicroseconds(0); + this->sph_pin_->digital_write(false); + this->cl_pin_->digital_write(true); + this->cl_pin_->digital_write(false); + this->sph_pin_->digital_write(true); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::hscan_start_(uint32_t d) { + this->sph_pin_->digital_write(false); + GPIO.out_w1ts = (d) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + this->sph_pin_->digital_write(true); +} +void Inkplate6::vscan_end_() { + this->ckv_pin_->digital_write(false); + this->le_pin_->digital_write(true); + this->le_pin_->digital_write(false); + delayMicroseconds(1); + this->ckv_pin_->digital_write(true); +} +void Inkplate6::clean() { + ESP_LOGV(TAG, "Clean called"); + unsigned long start_time = millis(); + + eink_on_(); + clean_fast_(0, 1); // White + clean_fast_(0, 8); // White to White + clean_fast_(0, 1); // White to Black + clean_fast_(0, 8); // Black to Black + clean_fast_(2, 1); // Black to White + clean_fast_(1, 10); // White to White + ESP_LOGV(TAG, "Clean finished (%lums)", millis() - start_time); +} +void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { + ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); + unsigned long start_time = millis(); + + eink_on_(); + uint8_t data = 0; + if (c == 0) // White + data = B10101010; + else if (c == 1) // Black + data = B01010101; + else if (c == 2) // Discharge + data = B00000000; + else if (c == 3) // Skip + data = B11111111; + + uint32_t send = ((data & B00000011) << 4) | (((data & B00001100) >> 2) << 18) | (((data & B00010000) >> 4) << 23) | + (((data & B11100000) >> 5) << 25); + + for (int k = 0; k < rep; k++) { + vscan_start_(); + for (int i = 0; i < this->get_height_internal(); i++) { + hscan_start_(send); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + for (int j = 0; j < this->get_width_internal() / 8; j++) { + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + } + GPIO.out_w1ts = (send) | (1 << this->cl_pin_->get_pin()); + GPIO.out_w1tc = get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()); + vscan_end_(); + } + delayMicroseconds(230); + ESP_LOGV(TAG, "Clean fast rep loop %d finished (%lums)", k, millis() - start_time); + } + ESP_LOGV(TAG, "Clean fast finished (%lums)", millis() - start_time); +} +void Inkplate6::pins_z_state_() { + this->ckv_pin_->pin_mode(INPUT); + this->sph_pin_->pin_mode(INPUT); + + this->oe_pin_->pin_mode(INPUT); + this->gmod_pin_->pin_mode(INPUT); + this->spv_pin_->pin_mode(INPUT); + + this->display_data_0_pin_->pin_mode(INPUT); + this->display_data_1_pin_->pin_mode(INPUT); + this->display_data_2_pin_->pin_mode(INPUT); + this->display_data_3_pin_->pin_mode(INPUT); + this->display_data_4_pin_->pin_mode(INPUT); + this->display_data_5_pin_->pin_mode(INPUT); + this->display_data_6_pin_->pin_mode(INPUT); + this->display_data_7_pin_->pin_mode(INPUT); +} +void Inkplate6::pins_as_outputs_() { + this->ckv_pin_->pin_mode(OUTPUT); + this->sph_pin_->pin_mode(OUTPUT); + + this->oe_pin_->pin_mode(OUTPUT); + this->gmod_pin_->pin_mode(OUTPUT); + this->spv_pin_->pin_mode(OUTPUT); + + this->display_data_0_pin_->pin_mode(OUTPUT); + this->display_data_1_pin_->pin_mode(OUTPUT); + this->display_data_2_pin_->pin_mode(OUTPUT); + this->display_data_3_pin_->pin_mode(OUTPUT); + this->display_data_4_pin_->pin_mode(OUTPUT); + this->display_data_5_pin_->pin_mode(OUTPUT); + this->display_data_6_pin_->pin_mode(OUTPUT); + this->display_data_7_pin_->pin_mode(OUTPUT); +} + +} // namespace inkplate6 +} // namespace esphome + +#endif diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate6/inkplate.h new file mode 100644 index 0000000000..94d14b4f6e --- /dev/null +++ b/esphome/components/inkplate6/inkplate.h @@ -0,0 +1,157 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/display/display_buffer.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace inkplate6 { + +class Inkplate6 : public PollingComponent, public display::DisplayBuffer, public i2c::I2CDevice { + public: + const uint8_t LUT2[16] = {B10101010, B10101001, B10100110, B10100101, B10011010, B10011001, B10010110, B10010101, + B01101010, B01101001, B01100110, B01100101, B01011010, B01011001, B01010110, B01010101}; + const uint8_t LUTW[16] = {B11111111, B11111110, B11111011, B11111010, B11101111, B11101110, B11101011, B11101010, + B10111111, B10111110, B10111011, B10111010, B10101111, B10101110, B10101011, B10101010}; + const uint8_t LUTB[16] = {B11111111, B11111101, B11110111, B11110101, B11011111, B11011101, B11010111, B11010101, + B01111111, B01111101, B01110111, B01110101, B01011111, B01011101, B01010111, B01010101}; + const uint8_t pixelMaskLUT[8] = {B00000001, B00000010, B00000100, B00001000, + B00010000, B00100000, B01000000, B10000000}; + const uint8_t pixelMaskGLUT[2] = {B00001111, B11110000}; + const uint8_t waveform3Bit[8][8] = {{0, 0, 0, 0, 1, 1, 1, 0}, {1, 2, 2, 2, 1, 1, 1, 0}, {0, 1, 2, 1, 1, 2, 1, 0}, + {0, 2, 1, 2, 1, 2, 1, 0}, {0, 0, 0, 1, 1, 1, 2, 0}, {2, 1, 1, 1, 2, 1, 2, 0}, + {1, 1, 1, 2, 1, 2, 2, 0}, {0, 0, 0, 0, 0, 0, 2, 0}}; + const uint32_t waveform[50] = { + 0x00000008, 0x00000008, 0x00200408, 0x80281888, 0x60a81898, 0x60a8a8a8, 0x60a8a8a8, 0x6068a868, 0x6868a868, + 0x6868a868, 0x68686868, 0x6a686868, 0x5a686868, 0x5a686868, 0x5a586a68, 0x5a5a6a68, 0x5a5a6a68, 0x55566a68, + 0x55565a64, 0x55555654, 0x55555556, 0x55555556, 0x55555556, 0x55555516, 0x55555596, 0x15555595, 0x95955595, + 0x95959595, 0x95949495, 0x94949495, 0x94949495, 0xa4949494, 0x9494a4a4, 0x84a49494, 0x84948484, 0x84848484, + 0x84848484, 0x84848484, 0xa5a48484, 0xa9a4a4a8, 0xa9a8a8a8, 0xa5a9a9a4, 0xa5a5a5a4, 0xa1a5a5a1, 0xa9a9a9a9, + 0xa9a9a9a9, 0xa9a9a9a9, 0xa9a9a9a9, 0x15151515, 0x11111111}; + + void set_greyscale(bool greyscale) { + this->greyscale_ = greyscale; + this->initialize_(); + this->block_partial_ = true; + } + void set_partial_updating(bool partial_updating) { this->partial_updating_ = partial_updating; } + void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } + + void set_display_data_0_pin(GPIOPin *data) { this->display_data_0_pin_ = data; } + void set_display_data_1_pin(GPIOPin *data) { this->display_data_1_pin_ = data; } + void set_display_data_2_pin(GPIOPin *data) { this->display_data_2_pin_ = data; } + void set_display_data_3_pin(GPIOPin *data) { this->display_data_3_pin_ = data; } + void set_display_data_4_pin(GPIOPin *data) { this->display_data_4_pin_ = data; } + void set_display_data_5_pin(GPIOPin *data) { this->display_data_5_pin_ = data; } + void set_display_data_6_pin(GPIOPin *data) { this->display_data_6_pin_ = data; } + void set_display_data_7_pin(GPIOPin *data) { this->display_data_7_pin_ = data; } + + void set_ckv_pin(GPIOPin *ckv) { this->ckv_pin_ = ckv; } + void set_cl_pin(GPIOPin *cl) { this->cl_pin_ = cl; } + void set_gpio0_enable_pin(GPIOPin *gpio0_enable) { this->gpio0_enable_pin_ = gpio0_enable; } + void set_gmod_pin(GPIOPin *gmod) { this->gmod_pin_ = gmod; } + void set_le_pin(GPIOPin *le) { this->le_pin_ = le; } + void set_oe_pin(GPIOPin *oe) { this->oe_pin_ = oe; } + void set_powerup_pin(GPIOPin *powerup) { this->powerup_pin_ = powerup; } + void set_sph_pin(GPIOPin *sph) { this->sph_pin_ = sph; } + void set_spv_pin(GPIOPin *spv) { this->spv_pin_ = spv; } + void set_vcom_pin(GPIOPin *vcom) { this->vcom_pin_ = vcom; } + void set_wakeup_pin(GPIOPin *wakeup) { this->wakeup_pin_ = wakeup; } + + float get_setup_priority() const override; + + void dump_config() override; + + void display(); + void clean(); + void fill(Color color) override; + + void update() override; + + void setup() override; + + uint8_t get_panel_state() { return this->panel_on_; } + bool get_greyscale() { return this->greyscale_; } + bool get_partial_updating() { return this->partial_updating_; } + uint8_t get_temperature() { return this->temperature_; } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void display1b_(); + void display3b_(); + void initialize_(); + bool partial_update_(); + void clean_fast_(uint8_t c, uint8_t rep); + + void hscan_start_(uint32_t d); + void vscan_end_(); + void vscan_start_(); + void vscan_write_(); + + void eink_off_(); + void eink_on_(); + + void setup_pins_(); + void pins_z_state_(); + void pins_as_outputs_(); + + int get_width_internal() override { return 800; } + + int get_height_internal() override { return 600; } + + size_t get_buffer_length_(); + + int get_data_pin_mask_() { + int data = 0; + data |= (1 << this->display_data_0_pin_->get_pin()); + data |= (1 << this->display_data_1_pin_->get_pin()); + data |= (1 << this->display_data_2_pin_->get_pin()); + data |= (1 << this->display_data_3_pin_->get_pin()); + data |= (1 << this->display_data_4_pin_->get_pin()); + data |= (1 << this->display_data_5_pin_->get_pin()); + data |= (1 << this->display_data_6_pin_->get_pin()); + data |= (1 << this->display_data_7_pin_->get_pin()); + return data; + } + + uint8_t panel_on_ = 0; + uint8_t temperature_; + + uint8_t *partial_buffer_{nullptr}; + uint8_t *partial_buffer_2_{nullptr}; + + uint32_t full_update_every_; + uint32_t partial_updates_{0}; + + bool block_partial_; + bool greyscale_; + bool partial_updating_; + + GPIOPin *display_data_0_pin_; + GPIOPin *display_data_1_pin_; + GPIOPin *display_data_2_pin_; + GPIOPin *display_data_3_pin_; + GPIOPin *display_data_4_pin_; + GPIOPin *display_data_5_pin_; + GPIOPin *display_data_6_pin_; + GPIOPin *display_data_7_pin_; + + GPIOPin *ckv_pin_; + GPIOPin *cl_pin_; + GPIOPin *gpio0_enable_pin_; + GPIOPin *gmod_pin_; + GPIOPin *le_pin_; + GPIOPin *oe_pin_; + GPIOPin *powerup_pin_; + GPIOPin *sph_pin_; + GPIOPin *spv_pin_; + GPIOPin *vcom_pin_; + GPIOPin *wakeup_pin_; +}; + +} // namespace inkplate6 +} // namespace esphome + +#endif diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index a95d70f274..4e7ec4b931 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -149,10 +149,10 @@ struct ESPColor { return ESPColor(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)), uint8_t((uint16_t(b) * 255U / max_rgb)), w); } - ESPColor fade_to_white(uint8_t amnt) { return ESPColor(255, 255, 255, 255) - (*this * amnt); } - ESPColor fade_to_black(uint8_t amnt) { return *this * amnt; } - ESPColor lighten(uint8_t delta) { return *this + delta; } - ESPColor darken(uint8_t delta) { return *this - delta; } + ESPColor fade_to_white(uint8_t amnt) const { return ESPColor(255, 255, 255, 255) - (*this * amnt); } + ESPColor fade_to_black(uint8_t amnt) const { return *this * amnt; } + ESPColor lighten(uint8_t delta) const { return *this + delta; } + ESPColor darken(uint8_t delta) const { return *this - delta; } static const ESPColor BLACK; static const ESPColor WHITE; diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 88f9e836f9..868cd4f16a 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -41,10 +41,10 @@ void MAX31855Sensor::read_data_() { this->read_array(data, 4); this->disable(); - const uint32_t mem = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3] << 0; + const uint32_t mem = encode_uint32(data[0], data[1], data[2], data[3]); // Verify we got data - if (mem != 0 && mem != 0xFFFFFFFF) { + if (mem != 0xFFFFFFFF) { this->status_clear_error(); } else { ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index eb6d112a64..e9aba13287 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -11,6 +11,7 @@ CONF_SCROLL_DWELL = 'scroll_dwell' CONF_SCROLL_DELAY = 'scroll_delay' CONF_SCROLL_ENABLE = 'scroll_enable' CONF_SCROLL_MODE = 'scroll_mode' +CONF_REVERSE_ENABLE = 'reverse_enable' SCROLL_MODES = { 'CONTINUOUS': 0, @@ -39,6 +40,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_SCROLL_SPEED, default='250ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCROLL_DELAY, default='1000ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCROLL_DWELL, default='1000ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REVERSE_ENABLE, default=False): cv.boolean, }).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(cs_pin_required=True)) @@ -56,6 +58,7 @@ def to_code(config): cg.add(var.set_scroll_delay(config[CONF_SCROLL_DELAY])) cg.add(var.set_scroll(config[CONF_SCROLL_ENABLE])) cg.add(var.set_scroll_mode(config[CONF_SCROLL_MODE])) + cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) if CONF_LAMBDA in config: lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')], diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index 111c17223d..01bc5e0531 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -108,7 +108,11 @@ void MAX7219Component::display() { // Send the data to the chip for (uint8_t i = 0; i < this->num_chips_; i++) { for (uint8_t j = 0; j < 8; j++) { - pixels[j] = this->max_displaybuffer_[i * 8 + j]; + if (this->reverse_) { + pixels[j] = this->max_displaybuffer_[(this->num_chips_ - i - 1) * 8 + j]; + } else { + pixels[j] = this->max_displaybuffer_[i * 8 + j]; + } } this->send64pixels(i, pixels); } @@ -128,7 +132,7 @@ void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color colo this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); } - if (y >= this->get_height_internal() || y < 0) // If pixel is outside display then dont draw + if ((y >= this->get_height_internal()) || (y < 0) || (x < 0)) // If pixel is outside display then dont draw return; uint16_t pos = x; // X is starting at 0 top left diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index dfd61e84e5..ddefe976a2 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -52,6 +52,7 @@ class MAX7219Component : public PollingComponent, void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; void set_scroll(bool on_off) { this->scroll_ = on_off; }; void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; + void set_reverse(bool on_off) { this->reverse_ = on_off; }; void send_char(byte chip, byte data); void send64pixels(byte chip, const byte pixels[8]); @@ -87,6 +88,7 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) uint8_t num_chips_; bool scroll_; + bool reverse_; bool update_{false}; uint16_t scroll_speed_; uint16_t scroll_delay_; diff --git a/esphome/components/mcp23s08/__init__.py b/esphome/components/mcp23s08/__init__.py new file mode 100644 index 0000000000..1440f74f56 --- /dev/null +++ b/esphome/components/mcp23s08/__init__.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +CODEOWNERS = ['@SenexCrenshaw'] + +DEPENDENCIES = ['spi'] +MULTI_CONF = True + +CONF_DEVICEADDRESS = "deviceaddress" + +mcp23S08_ns = cg.esphome_ns.namespace('mcp23s08') +mcp23S08GPIOMode = mcp23S08_ns.enum('MCP23S08GPIOMode') +mcp23S08_GPIO_MODES = { + 'INPUT': mcp23S08GPIOMode.MCP23S08_INPUT, + 'INPUT_PULLUP': mcp23S08GPIOMode.MCP23S08_INPUT_PULLUP, + 'OUTPUT': mcp23S08GPIOMode.MCP23S08_OUTPUT, +} + +mcp23S08 = mcp23S08_ns.class_('MCP23S08', cg.Component, spi.SPIDevice) +mcp23S08GPIOPin = mcp23S08_ns.class_('MCP23S08GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(mcp23S08), + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + +CONF_MCP23S08 = 'mcp23s08' + +mcp23S08_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_MCP23S08): cv.use_id(mcp23S08), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(mcp23S08_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +mcp23S08_INPUT_PIN_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_MCP23S08): cv.use_id(mcp23S08), + cv.Required(CONF_NUMBER): cv.int_range(0, 7), + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(mcp23S08_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23S08, + (mcp23S08_OUTPUT_PIN_SCHEMA, mcp23S08_INPUT_PIN_SCHEMA)) +def mcp23S08_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23S08]) + yield mcp23S08GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23s08/mcp23s08.cpp b/esphome/components/mcp23s08/mcp23s08.cpp new file mode 100644 index 0000000000..07e1808485 --- /dev/null +++ b/esphome/components/mcp23s08/mcp23s08.cpp @@ -0,0 +1,121 @@ +#include "mcp23s08.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23s08 { + +static const char *TAG = "mcp23s08"; + +void MCP23S08::set_device_address(uint8_t device_addr) { + if (device_addr != 0) { + this->device_opcode_ |= ((device_addr & 0x03) << 1); + } +} + +void MCP23S08::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23S08..."); + this->spi_setup(); + this->enable(); + + this->transfer_byte(MCP23S08_IODIR); + this->transfer_byte(0xFF); + for (uint8_t i = 0; i < MCP23S08_OLAT; i++) { + this->transfer_byte(0x00); + } + this->disable(); +} + +void MCP23S08::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23S08:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +float MCP23S08::get_setup_priority() const { return setup_priority::HARDWARE; } + +bool MCP23S08::digital_read(uint8_t pin) { + if (pin > 7) { + return false; + } + uint8_t bit = pin % 8; + uint8_t reg_addr = MCP23S08_GPIO; + uint8_t value = 0; + this->read_reg(reg_addr, &value); + return value & (1 << bit); +} + +void MCP23S08::digital_write(uint8_t pin, bool value) { + if (pin > 7) { + return; + } + uint8_t reg_addr = MCP23S08_OLAT; + this->update_reg(pin, value, reg_addr); +} + +void MCP23S08::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = MCP23S08_IODIR; + uint8_t gppu = MCP23S08_GPPU; + switch (mode) { + case MCP23S08_INPUT: + this->update_reg(pin, true, iodir); + break; + case MCP23S08_INPUT_PULLUP: + this->update_reg(pin, true, iodir); + this->update_reg(pin, true, gppu); + break; + case MCP23S08_OUTPUT: + this->update_reg(pin, false, iodir); + break; + default: + break; + } +} + +void MCP23S08::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23S08_OLAT) { + reg_value = this->olat_; + } else { + this->read_reg(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg(reg_addr, reg_value); + + if (reg_addr == MCP23S08_OLAT) { + this->olat_ = reg_value; + } +} + +bool MCP23S08::write_reg(uint8_t reg, uint8_t value) { + this->enable(); + this->transfer_byte(this->device_opcode_); + this->transfer_byte(reg); + this->transfer_byte(value); + this->disable(); + return true; +} + +bool MCP23S08::read_reg(uint8_t reg, uint8_t *value) { + uint8_t data; + this->enable(); + this->transfer_byte(this->device_opcode_ | 1); + this->transfer_byte(reg); + *value = this->transfer_byte(0); + this->disable(); + return true; +} + +MCP23S08GPIOPin::MCP23S08GPIOPin(MCP23S08 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23S08GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23S08GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23S08GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23S08GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23s08 +} // namespace esphome diff --git a/esphome/components/mcp23s08/mcp23s08.h b/esphome/components/mcp23s08/mcp23s08.h new file mode 100644 index 0000000000..a90f89ba23 --- /dev/null +++ b/esphome/components/mcp23s08/mcp23s08.h @@ -0,0 +1,74 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp23s08 { + +/// Modes for MCP23S08 pins +enum MCP23S08GPIOMode : uint8_t { + MCP23S08_INPUT = INPUT, // 0x00 + MCP23S08_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + MCP23S08_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23S08GPIORegisters { + // A side + MCP23S08_IODIR = 0x00, + MCP23S08_IPOL = 0x01, + MCP23S08_GPINTEN = 0x02, + MCP23S08_DEFVAL = 0x03, + MCP23S08_INTCON = 0x04, + MCP23S08_IOCON = 0x05, + MCP23S08_GPPU = 0x06, + MCP23S08_INTF = 0x07, + MCP23S08_INTCAP = 0x08, + MCP23S08_GPIO = 0x09, + MCP23S08_OLAT = 0x0A, +}; + +class MCP23S08 : public Component, + public spi::SPIDevice { + public: + MCP23S08() = default; + + void setup() override; + void dump_config() override; + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + void set_device_address(uint8_t device_addr); + + float get_setup_priority() const override; + + // read a given register + bool read_reg(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a); + + protected: + uint8_t device_opcode_ = 0x40; + uint8_t olat_{0x00}; +}; + +class MCP23S08GPIOPin : public GPIOPin { + public: + MCP23S08GPIOPin(MCP23S08 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23S08 *parent_; +}; + +} // namespace mcp23s08 +} // namespace esphome diff --git a/esphome/components/mcp23s17/__init__.py b/esphome/components/mcp23s17/__init__.py new file mode 100644 index 0000000000..c0c06d495c --- /dev/null +++ b/esphome/components/mcp23s17/__init__.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +CODEOWNERS = ['@SenexCrenshaw'] + +DEPENDENCIES = ['spi'] +MULTI_CONF = True + +CONF_DEVICEADDRESS = "deviceaddress" + +mcp23S17_ns = cg.esphome_ns.namespace('mcp23s17') +mcp23S17GPIOMode = mcp23S17_ns.enum('MCP23S17GPIOMode') +mcp23S17_GPIO_MODES = { + 'INPUT': mcp23S17GPIOMode.MCP23S17_INPUT, + 'INPUT_PULLUP': mcp23S17GPIOMode.MCP23S17_INPUT_PULLUP, + 'OUTPUT': mcp23S17GPIOMode.MCP23S17_OUTPUT, +} + +mcp23S17 = mcp23S17_ns.class_('MCP23S17', cg.Component, spi.SPIDevice) +mcp23S17GPIOPin = mcp23S17_ns.class_('MCP23S17GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(mcp23S17), + cv.Optional(CONF_DEVICEADDRESS, default=0): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_device_address(config[CONF_DEVICEADDRESS])) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + +CONF_MCP23S17 = 'mcp23s17' + +mcp23S17_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_MCP23S17): cv.use_id(mcp23S17), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(mcp23S17_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +mcp23S17_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23S17): cv.use_id(mcp23S17), + cv.Required(CONF_NUMBER): cv.int_range(0, 15), + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(mcp23S17_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23S17, + (mcp23S17_OUTPUT_PIN_SCHEMA, mcp23S17_INPUT_PIN_SCHEMA)) +def mcp23S17_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23S17]) + yield mcp23S17GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23s17/mcp23s17.cpp b/esphome/components/mcp23s17/mcp23s17.cpp new file mode 100644 index 0000000000..30e4f63953 --- /dev/null +++ b/esphome/components/mcp23s17/mcp23s17.cpp @@ -0,0 +1,126 @@ +#include "mcp23s17.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23s17 { + +static const char *TAG = "mcp23s17"; + +void MCP23S17::set_device_address(uint8_t device_addr) { + if (device_addr != 0) { + this->device_opcode_ |= ((device_addr & 0b111) << 1); + } +} + +void MCP23S17::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23S17..."); + this->spi_setup(); + + this->enable(); + uint8_t cmd = 0b01000000; + this->transfer_byte(cmd); + this->transfer_byte(0x18); + this->transfer_byte(0x0A); + this->transfer_byte(this->device_opcode_); + this->transfer_byte(0); + this->transfer_byte(0xFF); + this->transfer_byte(0xFF); + + for (uint8_t i = 0; i < 20; i++) { + this->transfer_byte(0); + } + this->disable(); +} + +void MCP23S17::dump_config() { + ESP_LOGCONFIG(TAG, "MCP23S17:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +float MCP23S17::get_setup_priority() const { return setup_priority::HARDWARE; } + +bool MCP23S17::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? MCP23S17_GPIOA : MCP23S17_GPIOB; + uint8_t value = 0; + this->read_reg(reg_addr, &value); + return value & (1 << bit); +} + +void MCP23S17::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? MCP23S17_OLATA : MCP23S17_OLATB; + this->update_reg(pin, value, reg_addr); +} + +void MCP23S17::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = pin < 8 ? MCP23S17_IODIRA : MCP23S17_IODIRB; + uint8_t gppu = pin < 8 ? MCP23S17_GPPUA : MCP23S17_GPPUB; + switch (mode) { + case MCP23S17_INPUT: + this->update_reg(pin, true, iodir); + break; + case MCP23S17_INPUT_PULLUP: + this->update_reg(pin, true, iodir); + this->update_reg(pin, true, gppu); + break; + case MCP23S17_OUTPUT: + this->update_reg(pin, false, iodir); + break; + default: + break; + } +} + +void MCP23S17::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23S17_OLATA) { + reg_value = this->olat_a_; + } else if (reg_addr == MCP23S17_OLATB) { + reg_value = this->olat_b_; + } else { + this->read_reg(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg(reg_addr, reg_value); + + if (reg_addr == MCP23S17_OLATA) { + this->olat_a_ = reg_value; + } else if (reg_addr == MCP23S17_OLATB) { + this->olat_b_ = reg_value; + } +} + +bool MCP23S17::read_reg(uint8_t reg, uint8_t *value) { + this->enable(); + this->transfer_byte(this->device_opcode_ | 1); + this->transfer_byte(reg); + *value = this->transfer_byte(0xFF); + this->disable(); + return true; +} + +bool MCP23S17::write_reg(uint8_t reg, uint8_t value) { + this->enable(); + this->transfer_byte(this->device_opcode_); + this->transfer_byte(reg); + this->transfer_byte(value); + + this->disable(); + return true; +} + +MCP23S17GPIOPin::MCP23S17GPIOPin(MCP23S17 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23S17GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23S17GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23S17GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23S17GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23s17 +} // namespace esphome diff --git a/esphome/components/mcp23s17/mcp23s17.h b/esphome/components/mcp23s17/mcp23s17.h new file mode 100644 index 0000000000..8e27ff447f --- /dev/null +++ b/esphome/components/mcp23s17/mcp23s17.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp23s17 { + +/// Modes for MCP23S17 pins +enum MCP23S17GPIOMode : uint8_t { + MCP23S17_INPUT = INPUT, // 0x00 + MCP23S17_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + MCP23S17_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23S17GPIORegisters { + // A side + MCP23S17_IODIRA = 0x00, + MCP23S17_IPOLA = 0x02, + MCP23S17_GPINTENA = 0x04, + MCP23S17_DEFVALA = 0x06, + MCP23S17_INTCONA = 0x08, + MCP23S17_IOCONA = 0x0A, + MCP23S17_GPPUA = 0x0C, + MCP23S17_INTFA = 0x0E, + MCP23S17_INTCAPA = 0x10, + MCP23S17_GPIOA = 0x12, + MCP23S17_OLATA = 0x14, + // B side + MCP23S17_IODIRB = 0x01, + MCP23S17_IPOLB = 0x03, + MCP23S17_GPINTENB = 0x05, + MCP23S17_DEFVALB = 0x07, + MCP23S17_INTCONB = 0x09, + MCP23S17_IOCONB = 0x0B, + MCP23S17_GPPUB = 0x0D, + MCP23S17_INTFB = 0x0F, + MCP23S17_INTCAPB = 0x11, + MCP23S17_GPIOB = 0x13, + MCP23S17_OLATB = 0x15, +}; + +class MCP23S17 : public Component, + public spi::SPIDevice { + public: + MCP23S17() = default; + + void setup() override; + void dump_config() override; + void set_device_address(uint8_t device_addr); + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + float get_setup_priority() const override; + + // read a given register + bool read_reg(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a); + + protected: + uint8_t device_opcode_ = 0x40; + uint8_t olat_a_{0x00}; + uint8_t olat_b_{0x00}; +}; + +class MCP23S17GPIOPin : public GPIOPin { + public: + MCP23S17GPIOPin(MCP23S17 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23S17 *parent_; +}; + +} // namespace mcp23s17 +} // namespace esphome diff --git a/esphome/components/mcp2515/__init__.py b/esphome/components/mcp2515/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp2515/canbus.py b/esphome/components/mcp2515/canbus.py new file mode 100644 index 0000000000..a877507e36 --- /dev/null +++ b/esphome/components/mcp2515/canbus.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, canbus +from esphome.const import CONF_ID, CONF_MODE +from esphome.components.canbus import CanbusComponent + +CODEOWNERS = ['@mvturnho', '@danielschramm'] +DEPENDENCIES = ['spi'] + +CONF_CLOCK = 'clock' + +mcp2515_ns = cg.esphome_ns.namespace('mcp2515') +mcp2515 = mcp2515_ns.class_('MCP2515', CanbusComponent, spi.SPIDevice) +CanClock = mcp2515_ns.enum('CAN_CLOCK') +McpMode = mcp2515_ns.enum('CANCTRL_REQOP_MODE') + +CAN_CLOCK = { + '8MHZ': CanClock.MCP_8MHZ, + '16MHZ': CanClock.MCP_16MHZ, + '20MHZ': CanClock.MCP_20MHZ, +} + +MCP_MODE = { + 'NORMAL': McpMode.CANCTRL_REQOP_NORMAL, + 'LOOPBACK': McpMode.CANCTRL_REQOP_LOOPBACK, + 'LISTENONLY': McpMode.CANCTRL_REQOP_LISTENONLY, +} + +CONFIG_SCHEMA = canbus.CONFIG_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(mcp2515), + cv.Optional(CONF_CLOCK, default='8MHZ'): cv.enum(CAN_CLOCK, upper=True), + cv.Optional(CONF_MODE, default='NORMAL'): cv.enum(MCP_MODE, upper=True), +}).extend(spi.spi_device_schema(True)) + + +def to_code(config): + rhs = mcp2515.new() + var = cg.Pvariable(config[CONF_ID], rhs) + yield canbus.register_canbus(var, config) + if CONF_CLOCK in config: + canclock = CAN_CLOCK[config[CONF_CLOCK]] + cg.add(var.set_mcp_clock(canclock)) + if CONF_MODE in config: + mode = MCP_MODE[config[CONF_MODE]] + cg.add(var.set_mcp_mode(mode)) + + yield spi.register_spi_device(var, config) diff --git a/esphome/components/mcp2515/mcp2515.cpp b/esphome/components/mcp2515/mcp2515.cpp new file mode 100644 index 0000000000..228a951702 --- /dev/null +++ b/esphome/components/mcp2515/mcp2515.cpp @@ -0,0 +1,612 @@ +#include "mcp2515.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp2515 { + +static const char *TAG = "mcp2515"; + +const struct MCP2515::TxBnRegs MCP2515::TXB[N_TXBUFFERS] = {{MCP_TXB0CTRL, MCP_TXB0SIDH, MCP_TXB0DATA}, + {MCP_TXB1CTRL, MCP_TXB1SIDH, MCP_TXB1DATA}, + {MCP_TXB2CTRL, MCP_TXB2SIDH, MCP_TXB2DATA}}; + +const struct MCP2515::RxBnRegs MCP2515::RXB[N_RXBUFFERS] = {{MCP_RXB0CTRL, MCP_RXB0SIDH, MCP_RXB0DATA, CANINTF_RX0IF}, + {MCP_RXB1CTRL, MCP_RXB1SIDH, MCP_RXB1DATA, CANINTF_RX1IF}}; + +bool MCP2515::setup_internal() { + this->spi_setup(); + + if (this->reset_() == canbus::ERROR_FAIL) + return false; + this->set_bitrate_(this->bit_rate_, this->mcp_clock_); + this->set_mode_(this->mcp_mode_); + ESP_LOGV(TAG, "setup done"); + return true; +} + +canbus::Error MCP2515::reset_() { + this->enable(); + this->transfer_byte(INSTRUCTION_RESET); + this->disable(); + ESP_LOGV(TAG, "reset_()"); + delay(10); + + ESP_LOGV(TAG, "reset() CLEAR ALL TXB registers"); + + uint8_t zeros[14]; + memset(zeros, 0, sizeof(zeros)); + set_registers_(MCP_TXB0CTRL, zeros, 14); + set_registers_(MCP_TXB1CTRL, zeros, 14); + set_registers_(MCP_TXB2CTRL, zeros, 14); + ESP_LOGD(TAG, "reset() CLEARED TXB registers"); + + set_register_(MCP_RXB0CTRL, 0); + set_register_(MCP_RXB1CTRL, 0); + + set_register_(MCP_CANINTE, CANINTF_RX0IF | CANINTF_RX1IF | CANINTF_ERRIF | CANINTF_MERRF); + + modify_register_(MCP_RXB0CTRL, RXB_CTRL_RXM_MASK | RXB_0_CTRL_BUKT, RXB_CTRL_RXM_STDEXT | RXB_0_CTRL_BUKT); + modify_register_(MCP_RXB1CTRL, RXB_CTRL_RXM_MASK, RXB_CTRL_RXM_STDEXT); + + return canbus::ERROR_OK; +} + +uint8_t MCP2515::read_register_(const REGISTER reg) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + uint8_t ret = this->transfer_byte(0x00); + this->disable(); + + return ret; +} + +void MCP2515::read_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_READ); + this->transfer_byte(reg); + // this->transfer_array(values, n); + // mcp2515 has auto - increment of address - pointer + for (uint8_t i = 0; i < n; i++) { + values[i] = this->transfer_byte(0x00); + } + this->disable(); +} + +void MCP2515::set_register_(const REGISTER reg, const uint8_t value) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + this->transfer_byte(value); + this->disable(); +} + +void MCP2515::set_registers_(const REGISTER reg, uint8_t values[], const uint8_t n) { + this->enable(); + this->transfer_byte(INSTRUCTION_WRITE); + this->transfer_byte(reg); + // this->transfer_array(values, n); + for (uint8_t i = 0; i < n; i++) { + this->transfer_byte(values[i]); + } + this->disable(); +} + +void MCP2515::modify_register_(const REGISTER reg, const uint8_t mask, const uint8_t data) { + this->enable(); + this->transfer_byte(INSTRUCTION_BITMOD); + this->transfer_byte(reg); + this->transfer_byte(mask); + this->transfer_byte(data); + this->disable(); +} + +uint8_t MCP2515::get_status_() { + this->enable(); + this->transfer_byte(INSTRUCTION_READ_STATUS); + uint8_t i = this->transfer_byte(0x00); + this->disable(); + + return i; +} + +canbus::Error MCP2515::set_mode_(const CanctrlReqopMode mode) { + modify_register_(MCP_CANCTRL, CANCTRL_REQOP, mode); + + unsigned long end_time = millis() + 10; + bool mode_match = false; + while (millis() < end_time) { + uint8_t new_mode = read_register_(MCP_CANSTAT); + new_mode &= CANSTAT_OPMOD; + mode_match = new_mode == mode; + if (mode_match) { + break; + } + } + return mode_match ? canbus::ERROR_OK : canbus::ERROR_FAIL; +} + +canbus::Error MCP2515::set_clk_out_(const CanClkOut divisor) { + canbus::Error res; + uint8_t cfg3; + + if (divisor == CLKOUT_DISABLE) { + /* Turn off CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, 0x00); + + /* Turn on CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, CNF3_SOF); + return canbus::ERROR_OK; + } + + /* Set the prescaler (CLKPRE) */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKPRE, divisor); + + /* Turn on CLKEN */ + modify_register_(MCP_CANCTRL, CANCTRL_CLKEN, CANCTRL_CLKEN); + + /* Turn off CLKOUT for SOF */ + modify_register_(MCP_CNF3, CNF3_SOF, 0x00); + return canbus::ERROR_OK; +} + +void MCP2515::prepare_id_(uint8_t *buffer, const bool extended, const uint32_t id) { + uint16_t canid = (uint16_t)(id & 0x0FFFF); + + if (extended) { + buffer[MCP_EID0] = (uint8_t)(canid & 0xFF); + buffer[MCP_EID8] = (uint8_t)(canid >> 8); + canid = (uint16_t)(id >> 16); + buffer[MCP_SIDL] = (uint8_t)(canid & 0x03); + buffer[MCP_SIDL] += (uint8_t)((canid & 0x1C) << 3); + buffer[MCP_SIDL] |= TXB_EXIDE_MASK; + buffer[MCP_SIDH] = (uint8_t)(canid >> 5); + } else { + buffer[MCP_SIDH] = (uint8_t)(canid >> 3); + buffer[MCP_SIDL] = (uint8_t)((canid & 0x07) << 5); + buffer[MCP_EID0] = 0; + buffer[MCP_EID8] = 0; + } +} + +canbus::Error MCP2515::set_filter_mask_(const MASK mask, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + + REGISTER reg; + switch (mask) { + case MASK0: + reg = MCP_RXM0SIDH; + break; + case MASK1: + reg = MCP_RXM1SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::set_filter_(const RXF num, const bool extended, const uint32_t ul_data) { + canbus::Error res = set_mode_(CANCTRL_REQOP_CONFIG); + if (res != canbus::ERROR_OK) { + return res; + } + + REGISTER reg; + + switch (num) { + case RXF0: + reg = MCP_RXF0SIDH; + break; + case RXF1: + reg = MCP_RXF1SIDH; + break; + case RXF2: + reg = MCP_RXF2SIDH; + break; + case RXF3: + reg = MCP_RXF3SIDH; + break; + case RXF4: + reg = MCP_RXF4SIDH; + break; + case RXF5: + reg = MCP_RXF5SIDH; + break; + default: + return canbus::ERROR_FAIL; + } + + uint8_t tbufdata[4]; + prepare_id_(tbufdata, extended, ul_data); + set_registers_(reg, tbufdata, 4); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message_(TXBn txbn, struct canbus::CanFrame *frame) { + const struct TxBnRegs *txbuf = &TXB[txbn]; + + uint8_t data[13]; + + prepare_id_(data, frame->use_extended_id, frame->can_id); + data[MCP_DLC] = + frame->remote_transmission_request ? (frame->can_data_length_code | RTR_MASK) : frame->can_data_length_code; + memcpy(&data[MCP_DATA], frame->data, frame->can_data_length_code); + set_registers_(txbuf->SIDH, data, 5 + frame->can_data_length_code); + modify_register_(txbuf->CTRL, TXB_TXREQ, TXB_TXREQ); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::send_message(struct canbus::CanFrame *frame) { + if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAILTX; + } + TXBn tx_buffers[N_TXBUFFERS] = {TXB0, TXB1, TXB2}; + + for (auto &tx_buffer : tx_buffers) { + const struct TxBnRegs *txbuf = &TXB[tx_buffer]; + uint8_t ctrlval = read_register_(txbuf->CTRL); + if ((ctrlval & TXB_TXREQ) == 0) { + return send_message_(tx_buffer, frame); + } + } + + return canbus::ERROR_FAILTX; +} + +canbus::Error MCP2515::read_message_(RXBn rxbn, struct canbus::CanFrame *frame) { + const struct RxBnRegs *rxb = &RXB[rxbn]; + + uint8_t tbufdata[5]; + + read_registers_(rxb->SIDH, tbufdata, 5); + + uint32_t id = (tbufdata[MCP_SIDH] << 3) + (tbufdata[MCP_SIDL] >> 5); + bool use_extended_id = false; + bool remote_transmission_request = false; + + if ((tbufdata[MCP_SIDL] & TXB_EXIDE_MASK) == TXB_EXIDE_MASK) { + id = (id << 2) + (tbufdata[MCP_SIDL] & 0x03); + id = (id << 8) + tbufdata[MCP_EID8]; + id = (id << 8) + tbufdata[MCP_EID0]; + // id |= canbus::CAN_EFF_FLAG; + use_extended_id = true; + } + + uint8_t dlc = (tbufdata[MCP_DLC] & DLC_MASK); + if (dlc > canbus::CAN_MAX_DATA_LENGTH) { + return canbus::ERROR_FAIL; + } + + uint8_t ctrl = read_register_(rxb->CTRL); + if (ctrl & RXB_CTRL_RTR) { + // id |= canbus::CAN_RTR_FLAG; + remote_transmission_request = true; + } + + frame->can_id = id; + frame->can_data_length_code = dlc; + frame->use_extended_id = use_extended_id; + frame->remote_transmission_request = remote_transmission_request; + + read_registers_(rxb->DATA, frame->data, dlc); + + modify_register_(MCP_CANINTF, rxb->CANINTF_RXnIF, 0); + + return canbus::ERROR_OK; +} + +canbus::Error MCP2515::read_message(struct canbus::CanFrame *frame) { + canbus::Error rc; + uint8_t stat = get_status_(); + + if (stat & STAT_RX0IF) { + rc = read_message_(RXB0, frame); + } else if (stat & STAT_RX1IF) { + rc = read_message_(RXB1, frame); + } else { + rc = canbus::ERROR_NOMSG; + } + + return rc; +} + +bool MCP2515::check_receive_() { + uint8_t res = get_status_(); + return (res & STAT_RXIF_MASK) != 0; +} + +bool MCP2515::check_error_() { + uint8_t eflg = get_error_flags_(); + return (eflg & EFLG_ERRORMASK) != 0; +} + +uint8_t MCP2515::get_error_flags_() { return read_register_(MCP_EFLG); } + +void MCP2515::clear_rx_n_ovr_flags_() { modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); } + +uint8_t MCP2515::get_int_() { return read_register_(MCP_CANINTF); } + +void MCP2515::clear_int_() { set_register_(MCP_CANINTF, 0); } + +uint8_t MCP2515::get_int_mask_() { return read_register_(MCP_CANINTE); } + +void MCP2515::clear_tx_int_() { modify_register_(MCP_CANINTF, (CANINTF_TX0IF | CANINTF_TX1IF | CANINTF_TX2IF), 0); } + +void MCP2515::clear_rx_n_ovr_() { + uint8_t eflg = get_error_flags_(); + if (eflg != 0) { + clear_rx_n_ovr_flags_(); + clear_int_(); + // modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); + } +} + +void MCP2515::clear_merr_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_MERRF, 0); +} + +void MCP2515::clear_errif_() { + // modify_register_(MCP_EFLG, EFLG_RX0OVR | EFLG_RX1OVR, 0); + // clear_int_(); + modify_register_(MCP_CANINTF, CANINTF_ERRIF, 0); +} + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed) { return this->set_bitrate_(can_speed, MCP_16MHZ); } + +canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock) { + canbus::Error error = set_mode_(CANCTRL_REQOP_CONFIG); + if (error != canbus::ERROR_OK) { + return error; + } + + uint8_t set, cfg1, cfg2, cfg3; + set = 1; + switch (can_clock) { + case (MCP_8MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5KBPS + cfg1 = MCP_8MHZ_5KBPS_CFG1; + cfg2 = MCP_8MHZ_5KBPS_CFG2; + cfg3 = MCP_8MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10KBPS + cfg1 = MCP_8MHZ_10KBPS_CFG1; + cfg2 = MCP_8MHZ_10KBPS_CFG2; + cfg3 = MCP_8MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20KBPS + cfg1 = MCP_8MHZ_20KBPS_CFG1; + cfg2 = MCP_8MHZ_20KBPS_CFG2; + cfg3 = MCP_8MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_31K25BPS): // 31.25KBPS + cfg1 = MCP_8MHZ_31K25BPS_CFG1; + cfg2 = MCP_8MHZ_31K25BPS_CFG2; + cfg3 = MCP_8MHZ_31K25BPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333KBPS + cfg1 = MCP_8MHZ_33K3BPS_CFG1; + cfg2 = MCP_8MHZ_33K3BPS_CFG2; + cfg3 = MCP_8MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_8MHZ_40KBPS_CFG1; + cfg2 = MCP_8MHZ_40KBPS_CFG2; + cfg3 = MCP_8MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_8MHZ_50KBPS_CFG1; + cfg2 = MCP_8MHZ_50KBPS_CFG2; + cfg3 = MCP_8MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_8MHZ_80KBPS_CFG1; + cfg2 = MCP_8MHZ_80KBPS_CFG2; + cfg3 = MCP_8MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_8MHZ_100KBPS_CFG1; + cfg2 = MCP_8MHZ_100KBPS_CFG2; + cfg3 = MCP_8MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_8MHZ_125KBPS_CFG1; + cfg2 = MCP_8MHZ_125KBPS_CFG2; + cfg3 = MCP_8MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_8MHZ_200KBPS_CFG1; + cfg2 = MCP_8MHZ_200KBPS_CFG2; + cfg3 = MCP_8MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_8MHZ_250KBPS_CFG1; + cfg2 = MCP_8MHZ_250KBPS_CFG2; + cfg3 = MCP_8MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_8MHZ_500KBPS_CFG1; + cfg2 = MCP_8MHZ_500KBPS_CFG2; + cfg3 = MCP_8MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_8MHZ_1000KBPS_CFG1; + cfg2 = MCP_8MHZ_1000KBPS_CFG2; + cfg3 = MCP_8MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_16MHZ): + switch (can_speed) { + case (canbus::CAN_5KBPS): // 5Kbps + cfg1 = MCP_16MHZ_5KBPS_CFG1; + cfg2 = MCP_16MHZ_5KBPS_CFG2; + cfg3 = MCP_16MHZ_5KBPS_CFG3; + break; + case (canbus::CAN_10KBPS): // 10Kbps + cfg1 = MCP_16MHZ_10KBPS_CFG1; + cfg2 = MCP_16MHZ_10KBPS_CFG2; + cfg3 = MCP_16MHZ_10KBPS_CFG3; + break; + case (canbus::CAN_20KBPS): // 20Kbps + cfg1 = MCP_16MHZ_20KBPS_CFG1; + cfg2 = MCP_16MHZ_20KBPS_CFG2; + cfg3 = MCP_16MHZ_20KBPS_CFG3; + break; + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_16MHZ_33K3BPS_CFG1; + cfg2 = MCP_16MHZ_33K3BPS_CFG2; + cfg3 = MCP_16MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_16MHZ_40KBPS_CFG1; + cfg2 = MCP_16MHZ_40KBPS_CFG2; + cfg3 = MCP_16MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg2 = MCP_16MHZ_50KBPS_CFG2; + cfg3 = MCP_16MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_16MHZ_80KBPS_CFG1; + cfg2 = MCP_16MHZ_80KBPS_CFG2; + cfg3 = MCP_16MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_16MHZ_83K3BPS_CFG1; + cfg2 = MCP_16MHZ_83K3BPS_CFG2; + cfg3 = MCP_16MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_16MHZ_100KBPS_CFG1; + cfg2 = MCP_16MHZ_100KBPS_CFG2; + cfg3 = MCP_16MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_16MHZ_125KBPS_CFG1; + cfg2 = MCP_16MHZ_125KBPS_CFG2; + cfg3 = MCP_16MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_16MHZ_200KBPS_CFG1; + cfg2 = MCP_16MHZ_200KBPS_CFG2; + cfg3 = MCP_16MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_16MHZ_250KBPS_CFG1; + cfg2 = MCP_16MHZ_250KBPS_CFG2; + cfg3 = MCP_16MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_16MHZ_500KBPS_CFG1; + cfg2 = MCP_16MHZ_500KBPS_CFG2; + cfg3 = MCP_16MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_16MHZ_1000KBPS_CFG1; + cfg2 = MCP_16MHZ_1000KBPS_CFG2; + cfg3 = MCP_16MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + case (MCP_20MHZ): + switch (can_speed) { + case (canbus::CAN_33KBPS): // 33.333Kbps + cfg1 = MCP_20MHZ_33K3BPS_CFG1; + cfg2 = MCP_20MHZ_33K3BPS_CFG2; + cfg3 = MCP_20MHZ_33K3BPS_CFG3; + break; + case (canbus::CAN_40KBPS): // 40Kbps + cfg1 = MCP_20MHZ_40KBPS_CFG1; + cfg2 = MCP_20MHZ_40KBPS_CFG2; + cfg3 = MCP_20MHZ_40KBPS_CFG3; + break; + case (canbus::CAN_50KBPS): // 50Kbps + cfg1 = MCP_20MHZ_50KBPS_CFG1; + cfg2 = MCP_20MHZ_50KBPS_CFG2; + cfg3 = MCP_20MHZ_50KBPS_CFG3; + break; + case (canbus::CAN_80KBPS): // 80Kbps + cfg1 = MCP_20MHZ_80KBPS_CFG1; + cfg2 = MCP_20MHZ_80KBPS_CFG2; + cfg3 = MCP_20MHZ_80KBPS_CFG3; + break; + case (canbus::CAN_83K3BPS): // 83.333Kbps + cfg1 = MCP_20MHZ_83K3BPS_CFG1; + cfg2 = MCP_20MHZ_83K3BPS_CFG2; + cfg3 = MCP_20MHZ_83K3BPS_CFG3; + break; + case (canbus::CAN_100KBPS): // 100Kbps + cfg1 = MCP_20MHZ_100KBPS_CFG1; + cfg2 = MCP_20MHZ_100KBPS_CFG2; + cfg3 = MCP_20MHZ_100KBPS_CFG3; + break; + case (canbus::CAN_125KBPS): // 125Kbps + cfg1 = MCP_20MHZ_125KBPS_CFG1; + cfg2 = MCP_20MHZ_125KBPS_CFG2; + cfg3 = MCP_20MHZ_125KBPS_CFG3; + break; + case (canbus::CAN_200KBPS): // 200Kbps + cfg1 = MCP_20MHZ_200KBPS_CFG1; + cfg2 = MCP_20MHZ_200KBPS_CFG2; + cfg3 = MCP_20MHZ_200KBPS_CFG3; + break; + case (canbus::CAN_250KBPS): // 250Kbps + cfg1 = MCP_20MHZ_250KBPS_CFG1; + cfg2 = MCP_20MHZ_250KBPS_CFG2; + cfg3 = MCP_20MHZ_250KBPS_CFG3; + break; + case (canbus::CAN_500KBPS): // 500Kbps + cfg1 = MCP_20MHZ_500KBPS_CFG1; + cfg2 = MCP_20MHZ_500KBPS_CFG2; + cfg3 = MCP_20MHZ_500KBPS_CFG3; + break; + case (canbus::CAN_1000KBPS): // 1Mbps + cfg1 = MCP_20MHZ_1000KBPS_CFG1; + cfg2 = MCP_20MHZ_1000KBPS_CFG2; + cfg3 = MCP_20MHZ_1000KBPS_CFG3; + break; + default: + set = 0; + break; + } + break; + + default: + set = 0; + break; + } + + if (set) { + set_register_(MCP_CNF1, cfg1); + set_register_(MCP_CNF2, cfg2); + set_register_(MCP_CNF3, cfg3); + return canbus::ERROR_OK; + } else { + return canbus::ERROR_FAIL; + } +} +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp2515/mcp2515.h b/esphome/components/mcp2515/mcp2515.h new file mode 100644 index 0000000000..3b9797a78a --- /dev/null +++ b/esphome/components/mcp2515/mcp2515.h @@ -0,0 +1,112 @@ +#pragma once + +#include "esphome/components/canbus/canbus.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" +#include "mcp2515_defs.h" + +namespace esphome { +namespace mcp2515 { +static const uint32_t SPI_CLOCK = 10000000; // 10MHz + +static const int N_TXBUFFERS = 3; +static const int N_RXBUFFERS = 2; +enum CanClock { MCP_20MHZ, MCP_16MHZ, MCP_8MHZ }; +enum MASK { MASK0, MASK1 }; +enum RXF { RXF0 = 0, RXF1 = 1, RXF2 = 2, RXF3 = 3, RXF4 = 4, RXF5 = 5 }; +enum RXBn { RXB0 = 0, RXB1 = 1 }; +enum TXBn { TXB0 = 0, TXB1 = 1, TXB2 = 2 }; + +enum CanClkOut { + CLKOUT_DISABLE = -1, + CLKOUT_DIV1 = 0x0, + CLKOUT_DIV2 = 0x1, + CLKOUT_DIV4 = 0x2, + CLKOUT_DIV8 = 0x3, +}; + +enum CANINTF : uint8_t { + CANINTF_RX0IF = 0x01, + CANINTF_RX1IF = 0x02, + CANINTF_TX0IF = 0x04, + CANINTF_TX1IF = 0x08, + CANINTF_TX2IF = 0x10, + CANINTF_ERRIF = 0x20, + CANINTF_WAKIF = 0x40, + CANINTF_MERRF = 0x80 +}; + +enum EFLG : uint8_t { + EFLG_RX1OVR = (1 << 7), + EFLG_RX0OVR = (1 << 6), + EFLG_TXBO = (1 << 5), + EFLG_TXEP = (1 << 4), + EFLG_RXEP = (1 << 3), + EFLG_TXWAR = (1 << 2), + EFLG_RXWAR = (1 << 1), + EFLG_EWARN = (1 << 0) +}; + +enum STAT : uint8_t { STAT_RX0IF = (1 << 0), STAT_RX1IF = (1 << 1) }; + +static const uint8_t STAT_RXIF_MASK = STAT_RX0IF | STAT_RX1IF; +static const uint8_t EFLG_ERRORMASK = EFLG_RX1OVR | EFLG_RX0OVR | EFLG_TXBO | EFLG_TXEP | EFLG_RXEP; + +class MCP2515 : public canbus::Canbus, + public spi::SPIDevice { + public: + MCP2515(){}; + void set_mcp_clock(CanClock clock) { this->mcp_clock_ = clock; }; + void set_mcp_mode(const CanctrlReqopMode mode) { this->mcp_mode_ = mode; } + static const struct TxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + } TXB[N_TXBUFFERS]; + + static const struct RxBnRegs { + REGISTER CTRL; + REGISTER SIDH; + REGISTER DATA; + CANINTF CANINTF_RXnIF; + } RXB[N_RXBUFFERS]; + + protected: + CanClock mcp_clock_{MCP_8MHZ}; + CanctrlReqopMode mcp_mode_ = CANCTRL_REQOP_NORMAL; + bool setup_internal() override; + canbus::Error set_mode_(CanctrlReqopMode mode); + + uint8_t read_register_(REGISTER reg); + void read_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void set_register_(REGISTER reg, uint8_t value); + void set_registers_(REGISTER reg, uint8_t values[], uint8_t n); + void modify_register_(REGISTER reg, uint8_t mask, uint8_t data); + + void prepare_id_(uint8_t *buffer, bool extended, uint32_t id); + canbus::Error reset_(); + canbus::Error set_clk_out_(CanClkOut divisor); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed); + canbus::Error set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clock); + canbus::Error set_filter_mask_(MASK mask, bool extended, uint32_t ul_data); + canbus::Error set_filter_(RXF num, bool extended, uint32_t ul_data); + canbus::Error send_message_(TXBn txbn, struct canbus::CanFrame *frame); + canbus::Error send_message(struct canbus::CanFrame *frame) override; + canbus::Error read_message_(RXBn rxbn, struct canbus::CanFrame *frame); + canbus::Error read_message(struct canbus::CanFrame *frame) override; + bool check_receive_(); + bool check_error_(); + uint8_t get_error_flags_(); + void clear_rx_n_ovr_flags_(); + uint8_t get_int_(); + uint8_t get_int_mask_(); + void clear_int_(); + void clear_tx_int_(); + uint8_t get_status_(); + void clear_rx_n_ovr_(); + void clear_merr_(); + void clear_errif_(); +}; +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp2515/mcp2515_defs.h b/esphome/components/mcp2515/mcp2515_defs.h new file mode 100644 index 0000000000..454c760c6d --- /dev/null +++ b/esphome/components/mcp2515/mcp2515_defs.h @@ -0,0 +1,317 @@ +#pragma once + +namespace esphome { +namespace mcp2515 { + +static const uint8_t CANCTRL_REQOP = 0xE0; +static const uint8_t CANCTRL_ABAT = 0x10; +static const uint8_t CANCTRL_OSM = 0x08; +static const uint8_t CANCTRL_CLKEN = 0x04; +static const uint8_t CANCTRL_CLKPRE = 0x03; + +enum CanctrlReqopMode : uint8_t { + CANCTRL_REQOP_NORMAL = 0x00, + CANCTRL_REQOP_SLEEP = 0x20, + CANCTRL_REQOP_LOOPBACK = 0x40, + CANCTRL_REQOP_LISTENONLY = 0x60, + CANCTRL_REQOP_CONFIG = 0x80, + CANCTRL_REQOP_POWERUP = 0xE0 +}; + +enum TxbNCtrl : uint8_t { + TXB_ABTF = 0x40, + TXB_MLOA = 0x20, + TXB_TXERR = 0x10, + TXB_TXREQ = 0x08, + TXB_TXIE = 0x04, + TXB_TXP = 0x03 +}; + +enum INSTRUCTION : uint8_t { + INSTRUCTION_WRITE = 0x02, + INSTRUCTION_READ = 0x03, + INSTRUCTION_BITMOD = 0x05, + INSTRUCTION_LOAD_TX0 = 0x40, + INSTRUCTION_LOAD_TX1 = 0x42, + INSTRUCTION_LOAD_TX2 = 0x44, + INSTRUCTION_RTS_TX0 = 0x81, + INSTRUCTION_RTS_TX1 = 0x82, + INSTRUCTION_RTS_TX2 = 0x84, + INSTRUCTION_RTS_ALL = 0x87, + INSTRUCTION_READ_RX0 = 0x90, + INSTRUCTION_READ_RX1 = 0x94, + INSTRUCTION_READ_STATUS = 0xA0, + INSTRUCTION_RX_STATUS = 0xB0, + INSTRUCTION_RESET = 0xC0 +}; + +enum REGISTER : uint8_t { + MCP_RXF0SIDH = 0x00, + MCP_RXF0SIDL = 0x01, + MCP_RXF0EID8 = 0x02, + MCP_RXF0EID0 = 0x03, + MCP_RXF1SIDH = 0x04, + MCP_RXF1SIDL = 0x05, + MCP_RXF1EID8 = 0x06, + MCP_RXF1EID0 = 0x07, + MCP_RXF2SIDH = 0x08, + MCP_RXF2SIDL = 0x09, + MCP_RXF2EID8 = 0x0A, + MCP_RXF2EID0 = 0x0B, + MCP_CANSTAT = 0x0E, + MCP_CANCTRL = 0x0F, + MCP_RXF3SIDH = 0x10, + MCP_RXF3SIDL = 0x11, + MCP_RXF3EID8 = 0x12, + MCP_RXF3EID0 = 0x13, + MCP_RXF4SIDH = 0x14, + MCP_RXF4SIDL = 0x15, + MCP_RXF4EID8 = 0x16, + MCP_RXF4EID0 = 0x17, + MCP_RXF5SIDH = 0x18, + MCP_RXF5SIDL = 0x19, + MCP_RXF5EID8 = 0x1A, + MCP_RXF5EID0 = 0x1B, + MCP_TEC = 0x1C, + MCP_REC = 0x1D, + MCP_RXM0SIDH = 0x20, + MCP_RXM0SIDL = 0x21, + MCP_RXM0EID8 = 0x22, + MCP_RXM0EID0 = 0x23, + MCP_RXM1SIDH = 0x24, + MCP_RXM1SIDL = 0x25, + MCP_RXM1EID8 = 0x26, + MCP_RXM1EID0 = 0x27, + MCP_CNF3 = 0x28, + MCP_CNF2 = 0x29, + MCP_CNF1 = 0x2A, + MCP_CANINTE = 0x2B, + MCP_CANINTF = 0x2C, + MCP_EFLG = 0x2D, + MCP_TXB0CTRL = 0x30, + MCP_TXB0SIDH = 0x31, + MCP_TXB0SIDL = 0x32, + MCP_TXB0EID8 = 0x33, + MCP_TXB0EID0 = 0x34, + MCP_TXB0DLC = 0x35, + MCP_TXB0DATA = 0x36, + MCP_TXB1CTRL = 0x40, + MCP_TXB1SIDH = 0x41, + MCP_TXB1SIDL = 0x42, + MCP_TXB1EID8 = 0x43, + MCP_TXB1EID0 = 0x44, + MCP_TXB1DLC = 0x45, + MCP_TXB1DATA = 0x46, + MCP_TXB2CTRL = 0x50, + MCP_TXB2SIDH = 0x51, + MCP_TXB2SIDL = 0x52, + MCP_TXB2EID8 = 0x53, + MCP_TXB2EID0 = 0x54, + MCP_TXB2DLC = 0x55, + MCP_TXB2DATA = 0x56, + MCP_RXB0CTRL = 0x60, + MCP_RXB0SIDH = 0x61, + MCP_RXB0SIDL = 0x62, + MCP_RXB0EID8 = 0x63, + MCP_RXB0EID0 = 0x64, + MCP_RXB0DLC = 0x65, + MCP_RXB0DATA = 0x66, + MCP_RXB1CTRL = 0x70, + MCP_RXB1SIDH = 0x71, + MCP_RXB1SIDL = 0x72, + MCP_RXB1EID8 = 0x73, + MCP_RXB1EID0 = 0x74, + MCP_RXB1DLC = 0x75, + MCP_RXB1DATA = 0x76 +}; + +static const uint8_t CANSTAT_OPMOD = 0xE0; +static const uint8_t CANSTAT_ICOD = 0x0E; + +static const uint8_t CNF3_SOF = 0x80; + +static const uint8_t TXB_EXIDE_MASK = 0x08; +static const uint8_t DLC_MASK = 0x0F; +static const uint8_t RTR_MASK = 0x40; + +static const uint8_t RXB_CTRL_RXM_STD = 0x20; +static const uint8_t RXB_CTRL_RXM_EXT = 0x40; +static const uint8_t RXB_CTRL_RXM_STDEXT = 0x00; +static const uint8_t RXB_CTRL_RXM_MASK = 0x60; +static const uint8_t RXB_CTRL_RTR = 0x08; +static const uint8_t RXB_0_CTRL_BUKT = 0x04; + +static const uint8_t MCP_SIDH = 0; +static const uint8_t MCP_SIDL = 1; +static const uint8_t MCP_EID8 = 2; +static const uint8_t MCP_EID0 = 3; +static const uint8_t MCP_DLC = 4; +static const uint8_t MCP_DATA = 5; + +/* + * Speed 8M + */ +static const uint8_t MCP_8MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_1000KBPS_CFG2 = 0x80; +static const uint8_t MCP_8MHZ_1000KBPS_CFG3 = 0x80; + +static const uint8_t MCP_8MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_500KBPS_CFG2 = 0x90; +static const uint8_t MCP_8MHZ_500KBPS_CFG3 = 0x82; + +static const uint8_t MCP_8MHZ_250KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_250KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_200KBPS_CFG1 = 0x00; +static const uint8_t MCP_8MHZ_200KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_200KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_125KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_125KBPS_CFG2 = 0xB1; +static const uint8_t MCP_8MHZ_125KBPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_100KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_100KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_100KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_80KBPS_CFG1 = 0x01; +static const uint8_t MCP_8MHZ_80KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_50KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_50KBPS_CFG2 = 0xB4; +static const uint8_t MCP_8MHZ_50KBPS_CFG3 = 0x86; + +static const uint8_t MCP_8MHZ_40KBPS_CFG1 = 0x03; +static const uint8_t MCP_8MHZ_40KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_33K3BPS_CFG1 = 0x47; +static const uint8_t MCP_8MHZ_33K3BPS_CFG2 = 0xE2; +static const uint8_t MCP_8MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_8MHZ_31K25BPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_31K25BPS_CFG2 = 0xA4; +static const uint8_t MCP_8MHZ_31K25BPS_CFG3 = 0x84; + +static const uint8_t MCP_8MHZ_20KBPS_CFG1 = 0x07; +static const uint8_t MCP_8MHZ_20KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_10KBPS_CFG1 = 0x0F; +static const uint8_t MCP_8MHZ_10KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_8MHZ_5KBPS_CFG1 = 0x1F; +static const uint8_t MCP_8MHZ_5KBPS_CFG2 = 0xBF; +static const uint8_t MCP_8MHZ_5KBPS_CFG3 = 0x87; + +/* + * speed 16M + */ +static const uint8_t MCP_16MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_1000KBPS_CFG2 = 0xD0; +static const uint8_t MCP_16MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_16MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_16MHZ_500KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_500KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_16MHZ_250KBPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_250KBPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_16MHZ_200KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_125KBPS_CFG2 = 0xF0; +static const uint8_t MCP_16MHZ_125KBPS_CFG3 = 0x86; + +static const uint8_t MCP_16MHZ_100KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_80KBPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_83K3BPS_CFG1 = 0x03; +static const uint8_t MCP_16MHZ_83K3BPS_CFG2 = 0xBE; +static const uint8_t MCP_16MHZ_83K3BPS_CFG3 = 0x07; + +static const uint8_t MCP_16MHZ_50KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_16MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_40KBPS_CFG1 = 0x07; +static const uint8_t MCP_16MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_33K3BPS_CFG1 = 0x4E; +static const uint8_t MCP_16MHZ_33K3BPS_CFG2 = 0xF1; +static const uint8_t MCP_16MHZ_33K3BPS_CFG3 = 0x85; + +static const uint8_t MCP_16MHZ_20KBPS_CFG1 = 0x0F; +static const uint8_t MCP_16MHZ_20KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_20KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_10KBPS_CFG1 = 0x1F; +static const uint8_t MCP_16MHZ_10KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_10KBPS_CFG3 = 0x87; + +static const uint8_t MCP_16MHZ_5KBPS_CFG1 = 0x3F; +static const uint8_t MCP_16MHZ_5KBPS_CFG2 = 0xFF; +static const uint8_t MCP_16MHZ_5KBPS_CFG3 = 0x87; + +/* + * speed 20M + */ +static const uint8_t MCP_20MHZ_1000KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_1000KBPS_CFG2 = 0xD9; +static const uint8_t MCP_20MHZ_1000KBPS_CFG3 = 0x82; + +static const uint8_t MCP_20MHZ_500KBPS_CFG1 = 0x00; +static const uint8_t MCP_20MHZ_500KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_500KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_250KBPS_CFG1 = 0x41; +static const uint8_t MCP_20MHZ_250KBPS_CFG2 = 0xFB; +static const uint8_t MCP_20MHZ_250KBPS_CFG3 = 0x86; + +static const uint8_t MCP_20MHZ_200KBPS_CFG1 = 0x01; +static const uint8_t MCP_20MHZ_200KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_200KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_125KBPS_CFG1 = 0x03; +static const uint8_t MCP_20MHZ_125KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_125KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_100KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_100KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_100KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_83K3BPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_83K3BPS_CFG2 = 0xFE; +static const uint8_t MCP_20MHZ_83K3BPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_80KBPS_CFG1 = 0x04; +static const uint8_t MCP_20MHZ_80KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_80KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_50KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_50KBPS_CFG2 = 0xFA; +static const uint8_t MCP_20MHZ_50KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_40KBPS_CFG1 = 0x09; +static const uint8_t MCP_20MHZ_40KBPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_40KBPS_CFG3 = 0x87; + +static const uint8_t MCP_20MHZ_33K3BPS_CFG1 = 0x0B; +static const uint8_t MCP_20MHZ_33K3BPS_CFG2 = 0xFF; +static const uint8_t MCP_20MHZ_33K3BPS_CFG3 = 0x87; + +} // namespace mcp2515 +} // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index a4d019ab8f..d09c2e4e92 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -15,19 +15,18 @@ void MCP3008::setup() { void MCP3008::dump_config() { ESP_LOGCONFIG(TAG, "MCP3008:"); - LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" CS Pin:", this->cs_); } -float MCP3008::read_data_(uint8_t pin) { - uint8_t data_msb = 0; - uint8_t data_lsb = 0; +float MCP3008::read_data(uint8_t pin) { + uint8_t data_msb, data_lsb = 0; uint8_t command = ((0x01 << 7) | // start bit ((pin & 0x07) << 4)); // channel number this->enable(); - this->transfer_byte(0x01); + data_msb = this->transfer_byte(command) & 0x03; data_lsb = this->transfer_byte(0x00); @@ -35,18 +34,29 @@ float MCP3008::read_data_(uint8_t pin) { int data = data_msb << 8 | data_lsb; - return data / 1024.0f; + return data / 1023.0f; } -MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin) +MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin, float reference_voltage) : PollingComponent(1000), parent_(parent), pin_(pin) { this->set_name(name); + this->reference_voltage_ = reference_voltage; } + +float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } + void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } -void MCP3008Sensor::update() { - float value_v = this->parent_->read_data_(pin_); - this->publish_state(value_v); +void MCP3008Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008Sensor:"); + ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_); + ESP_LOGCONFIG(TAG, " Reference Voltage: %.2fV", this->reference_voltage_); } +float MCP3008Sensor::sample() { + float value_v = this->parent_->read_data(pin_); + value_v = (value_v * this->reference_voltage_); + return value_v; +} +void MCP3008Sensor::update() { this->publish_state(this->sample()); } } // namespace mcp3008 } // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h index 594bdc4204..129f299a14 100644 --- a/esphome/components/mcp3008/mcp3008.h +++ b/esphome/components/mcp3008/mcp3008.h @@ -4,38 +4,41 @@ #include "esphome/core/esphal.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/spi/spi.h" +#include "esphome/components/voltage_sampler/voltage_sampler.h" namespace esphome { namespace mcp3008 { -class MCP3008Sensor; - class MCP3008 : public Component, public spi::SPIDevice { // At 3.3V 2MHz is too fast 1.35MHz is about right + spi::DATA_RATE_75KHZ> { // Running at the slowest max speed supported by the + // mcp3008. 2.7v = 75ksps public: MCP3008() = default; void setup() override; void dump_config() override; float get_setup_priority() const override; + float read_data(uint8_t pin); protected: - float read_data_(uint8_t pin); - - friend class MCP3008Sensor; }; -class MCP3008Sensor : public PollingComponent, public sensor::Sensor { +class MCP3008Sensor : public PollingComponent, public sensor::Sensor, public voltage_sampler::VoltageSampler { public: - MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin); + MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin, float reference_voltage); + void set_reference_voltage(float reference_voltage) { reference_voltage_ = reference_voltage; } void setup() override; void update() override; + void dump_config() override; + float get_setup_priority() const override; + float sample() override; protected: MCP3008 *parent_; uint8_t pin_; + float reference_voltage_; }; } // namespace mcp3008 diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py index ec7df0429a..9248d51a42 100644 --- a/esphome/components/mcp3008/sensor.py +++ b/esphome/components/mcp3008/sensor.py @@ -1,23 +1,29 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor +from esphome.components import sensor, voltage_sampler from esphome.const import CONF_ID, CONF_NUMBER, CONF_NAME from . import mcp3008_ns, MCP3008 +AUTO_LOAD = ['voltage_sampler'] + DEPENDENCIES = ['mcp3008'] -MCP3008Sensor = mcp3008_ns.class_('MCP3008Sensor', sensor.Sensor, cg.PollingComponent) - +MCP3008Sensor = mcp3008_ns.class_('MCP3008Sensor', sensor.Sensor, cg.PollingComponent, + voltage_sampler.VoltageSampler) +CONF_REFERENCE_VOLTAGE = 'reference_voltage' CONF_MCP3008_ID = 'mcp3008_id' CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(MCP3008Sensor), cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_REFERENCE_VOLTAGE, default='3.3V'): cv.voltage, }).extend(cv.polling_component_schema('1s')) def to_code(config): parent = yield cg.get_variable(config[CONF_MCP3008_ID]) - var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_NAME], config[CONF_NUMBER]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_NAME], + config[CONF_NUMBER], config[CONF_REFERENCE_VOLTAGE]) yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) diff --git a/esphome/components/mcp9808/__init__.py b/esphome/components/mcp9808/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp new file mode 100644 index 0000000000..38870b3b00 --- /dev/null +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -0,0 +1,81 @@ +#include "mcp9808.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp9808 { + +static const uint8_t MCP9808_REG_AMBIENT_TEMP = 0x05; +static const uint8_t MCP9808_REG_MANUF_ID = 0x06; +static const uint8_t MCP9808_REG_DEVICE_ID = 0x07; + +static const uint16_t MCP9808_MANUF_ID = 0x0054; +static const uint16_t MCP9808_DEV_ID = 0x0400; + +static const uint8_t MCP9808_AMBIENT_CLEAR_FLAGS = 0x1F; +static const uint8_t MCP9808_AMBIENT_CLEAR_SIGN = 0x0F; +static const uint8_t MCP9808_AMBIENT_TEMP_NEGATIVE = 0x10; + +static const char *TAG = "mcp9808"; + +void MCP9808Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up %s...", this->name_.c_str()); + + uint16_t manu; + if (!this->read_byte_16(MCP9808_REG_MANUF_ID, &manu, 0) || manu != MCP9808_MANUF_ID) { + this->mark_failed(); + ESP_LOGE(TAG, "%s manufacuturer id failed, device returned %X", this->name_.c_str(), manu); + return; + } + uint16_t dev_id; + if (!this->read_byte_16(MCP9808_REG_DEVICE_ID, &dev_id, 0) || dev_id != MCP9808_DEV_ID) { + this->mark_failed(); + ESP_LOGE(TAG, "%s device id failed, device returned %X", this->name_.c_str(), dev_id); + return; + } +} +void MCP9808Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "%s:", this->name_.c_str()); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with %s failed!", this->name_.c_str()); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} +void MCP9808Sensor::update() { + uint16_t raw_temp; + if (!this->read_byte_16(MCP9808_REG_AMBIENT_TEMP, &raw_temp)) { + this->status_set_warning(); + return; + } + if (raw_temp == 0xFFFF) { + this->status_set_warning(); + return; + } + + float temp = NAN; + uint8_t msb = (uint8_t)((raw_temp & 0xff00) >> 8); + uint8_t lsb = raw_temp & 0x00ff; + + msb = msb & MCP9808_AMBIENT_CLEAR_FLAGS; + + if ((msb & MCP9808_AMBIENT_TEMP_NEGATIVE) == MCP9808_AMBIENT_TEMP_NEGATIVE) { + msb = msb & MCP9808_AMBIENT_CLEAR_SIGN; + temp = (256 - ((uint16_t)(msb) *16 + lsb / 16.0f)) * -1; + } else { + temp = (uint16_t)(msb) *16 + lsb / 16.0f; + } + + if (isnan(temp)) { + this->status_set_warning(); + return; + } + + ESP_LOGD(TAG, "%s: Got temperature=%.4f°C", this->name_.c_str(), temp); + this->publish_state(temp); + this->status_clear_warning(); +} +float MCP9808Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace mcp9808 +} // namespace esphome diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h new file mode 100644 index 0000000000..19aa3117c3 --- /dev/null +++ b/esphome/components/mcp9808/mcp9808.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp9808 { + +class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void update() override; +}; + +} // namespace mcp9808 +} // namespace esphome diff --git a/esphome/components/mcp9808/sensor.py b/esphome/components/mcp9808/sensor.py new file mode 100644 index 0000000000..f1ccfadc54 --- /dev/null +++ b/esphome/components/mcp9808/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS + +CODEOWNERS = ['@k7hpn'] +DEPENDENCIES = ['i2c'] + +mcp9808_ns = cg.esphome_ns.namespace('mcp9808') +MCP9808Sensor = mcp9808_ns.class_('MCP9808Sensor', sensor.Sensor, cg.PollingComponent, + i2c.I2CDevice) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(MCP9808Sensor), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x18)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) diff --git a/esphome/components/nfc/__init__.py b/esphome/components/nfc/__init__.py new file mode 100644 index 0000000000..ae3c9a4c0a --- /dev/null +++ b/esphome/components/nfc/__init__.py @@ -0,0 +1,7 @@ +import esphome.codegen as cg + +CODEOWNERS = ['@jesserockz'] + +nfc_ns = cg.esphome_ns.namespace('nfc') + +NfcTag = nfc_ns.class_('NfcTag') diff --git a/esphome/components/nfc/ndef_message.cpp b/esphome/components/nfc/ndef_message.cpp new file mode 100644 index 0000000000..caba833932 --- /dev/null +++ b/esphome/components/nfc/ndef_message.cpp @@ -0,0 +1,106 @@ +#include "ndef_message.h" + +namespace esphome { +namespace nfc { + +static const char *TAG = "nfc.ndef_message"; + +NdefMessage::NdefMessage(std::vector &data) { + ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size()); + uint8_t index = 0; + while (index <= data.size()) { + uint8_t tnf_byte = data[index++]; + bool me = tnf_byte & 0x40; + bool sr = tnf_byte & 0x10; + bool il = tnf_byte & 0x08; + uint8_t tnf = tnf_byte & 0x07; + + ESP_LOGVV(TAG, "me=%s, sr=%s, il=%s, tnf=%d", YESNO(me), YESNO(sr), YESNO(il), tnf); + + auto record = new NdefRecord(); + record->set_tnf(tnf); + + uint8_t type_length = data[index++]; + uint32_t payload_length = 0; + if (sr) { + payload_length = data[index++]; + } else { + payload_length = (static_cast(data[index]) << 24) | (static_cast(data[index + 1]) << 16) | + (static_cast(data[index + 2]) << 8) | static_cast(data[index + 3]); + index += 4; + } + + uint8_t id_length = 0; + if (il) { + id_length = data[index++]; + } + + ESP_LOGVV(TAG, "Lengths: type=%d, payload=%d, id=%d", type_length, payload_length, id_length); + + std::string type_str(data.begin() + index, data.begin() + index + type_length); + record->set_type(type_str); + index += type_length; + + if (il) { + std::string id_str(data.begin() + index, data.begin() + index + id_length); + record->set_id(id_str); + index += id_length; + } + + uint8_t payload_identifier = 0x00; + if (type_str == "U") { + payload_identifier = data[index++]; + payload_length -= 1; + } + + std::string payload_str(data.begin() + index, data.begin() + index + payload_length); + + if (payload_identifier > 0x00 && payload_identifier <= PAYLOAD_IDENTIFIERS_COUNT) { + payload_str.insert(0, PAYLOAD_IDENTIFIERS[payload_identifier]); + } + + record->set_payload(payload_str); + index += payload_length; + + this->add_record(record); + ESP_LOGV(TAG, "Adding record type %s = %s", record->get_type().c_str(), record->get_payload().c_str()); + + if (me) + break; + } +} + +bool NdefMessage::add_record(NdefRecord *record) { + if (this->records_.size() >= MAX_NDEF_RECORDS) { + ESP_LOGE(TAG, "Too many records. Max: %d", MAX_NDEF_RECORDS); + return false; + } + this->records_.push_back(record); + return true; +} + +bool NdefMessage::add_text_record(const std::string &text) { return this->add_text_record(text, "en"); }; + +bool NdefMessage::add_text_record(const std::string &text, const std::string &encoding) { + std::string payload = to_string(text.length()) + encoding + text; + auto r = new NdefRecord(TNF_WELL_KNOWN, "T", payload); + return this->add_record(r); +} + +bool NdefMessage::add_uri_record(const std::string &uri) { + auto r = new NdefRecord(TNF_WELL_KNOWN, "U", uri); + return this->add_record(r); +} + +std::vector NdefMessage::encode() { + std::vector data; + + for (uint8_t i = 0; i < this->records_.size(); i++) { + auto encoded_record = this->records_[i]->encode(i == 0, (i + 1) == this->records_.size()); + data.insert(data.end(), encoded_record.begin(), encoded_record.end()); + } + return data; +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_message.h b/esphome/components/nfc/ndef_message.h new file mode 100644 index 0000000000..e47a7b992a --- /dev/null +++ b/esphome/components/nfc/ndef_message.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +static const uint8_t MAX_NDEF_RECORDS = 4; + +class NdefMessage { + public: + NdefMessage(){}; + NdefMessage(std::vector &data); + + std::vector get_records() { return this->records_; }; + + bool add_record(NdefRecord *record); + bool add_text_record(const std::string &text); + bool add_text_record(const std::string &text, const std::string &encoding); + bool add_uri_record(const std::string &uri); + + std::vector encode(); + + protected: + std::vector records_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record.cpp b/esphome/components/nfc/ndef_record.cpp new file mode 100644 index 0000000000..53fa221e41 --- /dev/null +++ b/esphome/components/nfc/ndef_record.cpp @@ -0,0 +1,85 @@ +#include "ndef_record.h" + +namespace esphome { +namespace nfc { + +static const char* TAG = "nfc.ndef_record"; + +uint32_t NdefRecord::get_encoded_size() { + uint32_t size = 2; + if (this->payload_.length() > 255) { + size += 4; + } else { + size += 1; + } + if (this->id_.length()) { + size += 1; + } + size += (this->type_.length() + this->payload_.length() + this->id_.length()); + return size; +} + +std::vector NdefRecord::encode(bool first, bool last) { + std::vector data; + + data.push_back(this->get_tnf_byte(first, last)); + + data.push_back(this->type_.length()); + + uint8_t payload_prefix = 0x00; + uint8_t payload_prefix_length = 0x00; + for (uint8_t i = 1; i < PAYLOAD_IDENTIFIERS_COUNT; i++) { + std::string prefix = PAYLOAD_IDENTIFIERS[i]; + if (this->payload_.substr(0, prefix.length()).find(prefix) != std::string::npos) { + payload_prefix = i; + payload_prefix_length = prefix.length(); + break; + } + } + + uint32_t payload_length = this->payload_.length() - payload_prefix_length + 1; + + if (payload_length <= 255) { + data.push_back(payload_length); + } else { + data.push_back(0); + data.push_back(0); + data.push_back((payload_length >> 8) & 0xFF); + data.push_back(payload_length & 0xFF); + } + + if (this->id_.length()) { + data.push_back(this->id_.length()); + } + + data.insert(data.end(), this->type_.begin(), this->type_.end()); + + if (this->id_.length()) { + data.insert(data.end(), this->id_.begin(), this->id_.end()); + } + + data.push_back(payload_prefix); + + data.insert(data.end(), this->payload_.begin() + payload_prefix_length, this->payload_.end()); + return data; +} + +uint8_t NdefRecord::get_tnf_byte(bool first, bool last) { + uint8_t value = this->tnf_; + if (first) { + value = value | 0x80; + } + if (last) { + value = value | 0x40; + } + if (this->payload_.length() <= 255) { + value = value | 0x10; + } + if (this->id_.length()) { + value = value | 0x08; + } + return value; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/ndef_record.h b/esphome/components/nfc/ndef_record.h new file mode 100644 index 0000000000..bc03ccf093 --- /dev/null +++ b/esphome/components/nfc/ndef_record.h @@ -0,0 +1,101 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace nfc { + +static const uint8_t TNF_EMPTY = 0x00; +static const uint8_t TNF_WELL_KNOWN = 0x01; +static const uint8_t TNF_MIME_MEDIA = 0x02; +static const uint8_t TNF_ABSOLUTE_URI = 0x03; +static const uint8_t TNF_EXTERNAL_TYPE = 0x04; +static const uint8_t TNF_UNKNOWN = 0x05; +static const uint8_t TNF_UNCHANGED = 0x06; +static const uint8_t TNF_RESERVED = 0x07; + +static const uint8_t PAYLOAD_IDENTIFIERS_COUNT = 0x23; +static const char *PAYLOAD_IDENTIFIERS[] = {"", + "http://www.", + "https://www.", + "http://", + "https://", + "tel:", + "mailto:", + "ftp://anonymous:anonymous@", + "ftp://ftp.", + "ftps://", + "sftp://", + "smb://", + "nfs://", + "ftp://", + "dav://", + "news:", + "telnet://", + "imap:", + "rtsp://", + "urn:", + "pop:", + "sip:", + "sips:", + "tftp:", + "btspp://", + "btl2cap://", + "btgoep://", + "tcpobex://", + "irdaobex://", + "file://", + "urn:epc:id:", + "urn:epc:tag:", + "urn:epc:pat:", + "urn:epc:raw:", + "urn:epc:", + "urn:nfc:"}; + +class NdefRecord { + public: + NdefRecord(){}; + NdefRecord(uint8_t tnf, const std::string &type, const std::string &payload) { + this->tnf_ = tnf; + this->type_ = type; + this->set_payload(payload); + }; + NdefRecord(uint8_t tnf, const std::string &type, const std::string &payload, const std::string &id) { + this->tnf_ = tnf; + this->type_ = type; + this->set_payload(payload); + this->id_ = id; + }; + NdefRecord(const NdefRecord &rhs) { + this->tnf_ = rhs.tnf_; + this->type_ = rhs.type_; + this->payload_ = rhs.payload_; + this->payload_identifier_ = rhs.payload_identifier_; + this->id_ = rhs.id_; + }; + void set_tnf(uint8_t tnf) { this->tnf_ = tnf; }; + void set_type(const std::string &type) { this->type_ = type; }; + void set_payload_identifier(uint8_t payload_identifier) { this->payload_identifier_ = payload_identifier; }; + void set_payload(const std::string &payload) { this->payload_ = payload; }; + void set_id(const std::string &id) { this->id_ = id; }; + + uint32_t get_encoded_size(); + + std::vector encode(bool first, bool last); + uint8_t get_tnf_byte(bool first, bool last); + + const std::string &get_type() { return this->type_; }; + const std::string &get_id() { return this->id_; }; + const std::string &get_payload() { return this->payload_; }; + + protected: + uint8_t tnf_; + std::string type_; + uint8_t payload_identifier_; + std::string payload_; + std::string id_; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp new file mode 100644 index 0000000000..d1f7cbe15f --- /dev/null +++ b/esphome/components/nfc/nfc.cpp @@ -0,0 +1,107 @@ +#include "nfc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace nfc { + +static const char *TAG = "nfc"; + +std::string format_uid(std::vector &uid) { + char buf[(uid.size() * 2) + uid.size() - 1]; + int offset = 0; + for (uint8_t i = 0; i < uid.size(); i++) { + const char *format = "%02X"; + if (i + 1 < uid.size()) + format = "%02X-"; + offset += sprintf(buf + offset, format, uid[i]); + } + return std::string(buf); +} + +std::string format_bytes(std::vector &bytes) { + char buf[(bytes.size() * 2) + bytes.size() - 1]; + int offset = 0; + for (uint8_t i = 0; i < bytes.size(); i++) { + const char *format = "%02X"; + if (i + 1 < bytes.size()) + format = "%02X "; + offset += sprintf(buf + offset, format, bytes[i]); + } + return std::string(buf); +} + +uint8_t guess_tag_type(uint8_t uid_length) { + if (uid_length == 4) { + return TAG_TYPE_MIFARE_CLASSIC; + } else { + return TAG_TYPE_2; + } +} + +uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { + for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { + if (data[i] == 0x00) { + // Do nothing, skip + } else if (data[i] == 0x03) { + return i; + } else { + return -2; + } + } + return -1; +} + +bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { + uint8_t i = get_mifare_classic_ndef_start_index(data); + if (i < 0 || data[i] != 0x03) { + ESP_LOGE(TAG, "Error, Can't decode message length."); + return false; + } + if (data[i + 1] == 0xFF) { + message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); + message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else { + message_length = data[i + 1]; + message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + } + return true; +} + +uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length) { + uint32_t buffer_size = message_length + 2 + 1; + if (buffer_size % MIFARE_ULTRALIGHT_READ_SIZE != 0) + buffer_size = ((buffer_size / MIFARE_ULTRALIGHT_READ_SIZE) + 1) * MIFARE_ULTRALIGHT_READ_SIZE; + return buffer_size; +} + +uint32_t get_mifare_classic_buffer_size(uint32_t message_length) { + uint32_t buffer_size = message_length; + if (message_length < 255) { + buffer_size += MIFARE_CLASSIC_SHORT_TLV_SIZE + 1; + } else { + buffer_size += MIFARE_CLASSIC_LONG_TLV_SIZE + 1; + } + if (buffer_size % MIFARE_CLASSIC_BLOCK_SIZE != 0) { + buffer_size = ((buffer_size / MIFARE_CLASSIC_BLOCK_SIZE) + 1) * MIFARE_CLASSIC_BLOCK_SIZE; + } + return buffer_size; +} + +bool mifare_classic_is_first_block(uint8_t block_num) { + if (block_num < 128) { + return (block_num % 4 == 0); + } else { + return (block_num % 16 == 0); + } +} + +bool mifare_classic_is_trailer_block(uint8_t block_num) { + if (block_num < 128) { + return ((block_num + 1) % 4 == 0); + } else { + return ((block_num + 1) % 16 == 0); + } +} + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h new file mode 100644 index 0000000000..cf56f9f4de --- /dev/null +++ b/esphome/components/nfc/nfc.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "ndef_record.h" +#include "ndef_message.h" +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const uint8_t MIFARE_CLASSIC_BLOCK_SIZE = 16; +static const uint8_t MIFARE_CLASSIC_LONG_TLV_SIZE = 4; +static const uint8_t MIFARE_CLASSIC_SHORT_TLV_SIZE = 2; + +static const uint8_t MIFARE_ULTRALIGHT_PAGE_SIZE = 4; +static const uint8_t MIFARE_ULTRALIGHT_READ_SIZE = 4; +static const uint8_t MIFARE_ULTRALIGHT_DATA_START_PAGE = 4; +static const uint8_t MIFARE_ULTRALIGHT_MAX_PAGE = 63; + +static const uint8_t TAG_TYPE_MIFARE_CLASSIC = 0; +static const uint8_t TAG_TYPE_1 = 1; +static const uint8_t TAG_TYPE_2 = 2; +static const uint8_t TAG_TYPE_3 = 3; +static const uint8_t TAG_TYPE_4 = 4; +static const uint8_t TAG_TYPE_UNKNOWN = 99; + +// Mifare Commands +static const uint8_t MIFARE_CMD_AUTH_A = 0x60; +static const uint8_t MIFARE_CMD_AUTH_B = 0x61; +static const uint8_t MIFARE_CMD_READ = 0x30; +static const uint8_t MIFARE_CMD_WRITE = 0xA0; +static const uint8_t MIFARE_CMD_WRITE_ULTRALIGHT = 0xA2; + +static const char *MIFARE_CLASSIC = "Mifare Classic"; +static const char *NFC_FORUM_TYPE_2 = "NFC Forum Type 2"; +static const char *ERROR = "Error"; + +static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; +static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; + +std::string format_uid(std::vector &uid); +std::string format_bytes(std::vector &bytes); + +uint8_t guess_tag_type(uint8_t uid_length); +uint8_t get_mifare_classic_ndef_start_index(std::vector &data); +bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index); +uint32_t get_mifare_classic_buffer_size(uint32_t message_length); + +bool mifare_classic_is_first_block(uint8_t block_num); +bool mifare_classic_is_trailer_block(uint8_t block_num); + +uint32_t get_mifare_ultralight_buffer_size(uint32_t message_length); + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_tag.cpp b/esphome/components/nfc/nfc_tag.cpp new file mode 100644 index 0000000000..39e84e2032 --- /dev/null +++ b/esphome/components/nfc/nfc_tag.cpp @@ -0,0 +1,9 @@ +#include "nfc_tag.h" + +namespace esphome { +namespace nfc { + +static const char *TAG = "nfc.tag"; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/nfc/nfc_tag.h b/esphome/components/nfc/nfc_tag.h new file mode 100644 index 0000000000..ff0d1c39b4 --- /dev/null +++ b/esphome/components/nfc/nfc_tag.h @@ -0,0 +1,47 @@ +#pragma once + +#include "esphome/core/log.h" +#include "ndef_message.h" + +namespace esphome { +namespace nfc { + +class NfcTag { + public: + NfcTag() { + this->uid_ = {}; + this->tag_type_ = "Unknown"; + }; + NfcTag(std::vector &uid) { + this->uid_ = uid; + this->tag_type_ = "Unknown"; + }; + NfcTag(std::vector &uid, const std::string &tag_type) { + this->uid_ = uid; + this->tag_type_ = tag_type; + }; + NfcTag(std::vector &uid, const std::string &tag_type, nfc::NdefMessage *ndef_message) { + this->uid_ = uid; + this->tag_type_ = tag_type; + this->ndef_message_ = ndef_message; + }; + NfcTag(std::vector &uid, const std::string &tag_type, std::vector &ndef_data) { + this->uid_ = uid; + this->tag_type_ = tag_type; + this->ndef_message_ = new NdefMessage(ndef_data); + }; + + std::vector &get_uid() { return this->uid_; }; + const std::string &get_tag_type() { return this->tag_type_; }; + bool has_ndef_message() { return this->ndef_message_ != nullptr; }; + NdefMessage *get_ndef_message() { return this->ndef_message_; }; + void set_ndef_message(NdefMessage *ndef_message) { this->ndef_message_ = ndef_message; }; + + protected: + std::vector uid_; + std::string tag_type_; + NdefMessage *ndef_message_{nullptr}; +}; + +} // namespace nfc +} // namespace esphome diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index e6bcff045f..e4b6946116 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -1,6 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE +from esphome.const import ( + CONF_ID, CONF_NUM_ATTEMPTS, CONF_PASSWORD, + CONF_PORT, CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE +) from esphome.core import CORE, coroutine_with_priority CODEOWNERS = ['@esphome/core'] @@ -14,6 +17,8 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232): cv.port, cv.Optional(CONF_PASSWORD, default=''): cv.string, + cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_NUM_ATTEMPTS, default='10'): cv.positive_not_null_int }).extend(cv.COMPONENT_SCHEMA) @@ -26,7 +31,7 @@ def to_code(config): yield cg.register_component(var, config) if config[CONF_SAFE_MODE]: - cg.add(var.start_safe_mode()) + cg.add(var.start_safe_mode(config[CONF_NUM_ATTEMPTS], config[CONF_REBOOT_TIMEOUT])) if CORE.is_esp8266: cg.add_library('Update', None) diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index e37cb7160c..65d44482b8 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -47,7 +47,7 @@ class OTAComponent : public Component { /// Manually set the port OTA should listen on. void set_port(uint16_t port); - void start_safe_mode(uint8_t num_attempts = 10, uint32_t enable_time = 120000); + void start_safe_mode(uint8_t num_attempts, uint32_t enable_time); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/output/float_output.cpp b/esphome/components/output/float_output.cpp index 0d536d0946..d9f2db559c 100644 --- a/esphome/components/output/float_output.cpp +++ b/esphome/components/output/float_output.cpp @@ -29,10 +29,9 @@ void FloatOutput::set_level(float state) { this->power_.unrequest(); } #endif - - float adjusted_value = (state * (this->max_power_ - this->min_power_)) + this->min_power_; if (this->is_inverted()) - adjusted_value = 1.0f - adjusted_value; + state = 1.0f - state; + float adjusted_value = (state * (this->max_power_ - this->min_power_)) + this->min_power_; this->write_state(adjusted_value); } diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index 8cc92065ec..f4b625fe8b 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import display, spi from esphome.const import ( - CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, CONF_CS_PIN, + CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, CONF_CS_PIN, CONF_CONTRAST ) DEPENDENCIES = ['spi'] @@ -17,8 +17,9 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE + cv.Optional(CONF_CONTRAST, default=0x7f): cv.int_, }).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) def to_code(config): @@ -33,6 +34,8 @@ def to_code(config): reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + cg.add(var.set_contrast(config[CONF_CONTRAST])) + if CONF_LAMBDA in config: lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index e47d71e8af..85614874ee 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -35,8 +35,7 @@ void PCD8544::initialize() { this->command(this->PCD8544_SETBIAS | 0x04); // contrast - // TODO: in future version we may add a user a control over contrast - this->command(this->PCD8544_SETVOP | 0x7f); // Experimentally determined + this->command(this->PCD8544_SETVOP | this->contrast_); // normal mode this->command(this->PCD8544_FUNCTIONSET); diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index 4c590b402c..b57662bbd9 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -29,9 +29,11 @@ class PCD8544 : public PollingComponent, const uint8_t PCD8544_SETTEMP = 0x04; const uint8_t PCD8544_SETBIAS = 0x10; const uint8_t PCD8544_SETVOP = 0x80; + uint8_t contrast_; void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void set_contrast(uint8_t contrast) { this->contrast_ = contrast; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } void command(uint8_t value); diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index cbd41d11cc..c96ebe2b4d 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -1,32 +1,63 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.components import spi +from esphome.components import nfc from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID +from esphome.core import coroutine -CODEOWNERS = ['@OttoWinter'] -DEPENDENCIES = ['spi'] -AUTO_LOAD = ['binary_sensor'] +CODEOWNERS = ['@OttoWinter', '@jesserockz'] +AUTO_LOAD = ['binary_sensor', 'nfc'] MULTI_CONF = True -pn532_ns = cg.esphome_ns.namespace('pn532') -PN532 = pn532_ns.class_('PN532', cg.PollingComponent, spi.SPIDevice) -PN532Trigger = pn532_ns.class_('PN532Trigger', automation.Trigger.template(cg.std_string)) +CONF_PN532_ID = 'pn532_id' +CONF_ON_FINISHED_WRITE = 'on_finished_write' -CONFIG_SCHEMA = cv.Schema({ +pn532_ns = cg.esphome_ns.namespace('pn532') +PN532 = pn532_ns.class_('PN532', cg.PollingComponent) + +PN532OnTagTrigger = pn532_ns.class_('PN532OnTagTrigger', + automation.Trigger.template(cg.std_string, nfc.NfcTag)) +PN532OnFinishedWriteTrigger = pn532_ns.class_('PN532OnFinishedWriteTrigger', + automation.Trigger.template()) + +PN532IsWritingCondition = pn532_ns.class_('PN532IsWritingCondition', automation.Condition) + +PN532_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(PN532), cv.Optional(CONF_ON_TAG): automation.validate_automation({ - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532Trigger), + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnTagTrigger), }), -}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) + cv.Optional(CONF_ON_FINISHED_WRITE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532OnFinishedWriteTrigger), + }), +}).extend(cv.polling_component_schema('1s')) -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) +def CONFIG_SCHEMA(conf): + if conf: + raise cv.Invalid("This component has been moved in 1.16, please see the docs for updated " + "instructions. https://esphome.io/components/binary_sensor/pn532.html") + + +@coroutine +def setup_pn532(var, config): yield cg.register_component(var, config) - yield spi.register_spi_device(var, config) for conf in config.get(CONF_ON_TAG, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) cg.add(var.register_trigger(trigger)) - yield automation.build_automation(trigger, [(cg.std_string, 'x')], conf) + yield automation.build_automation(trigger, [(cg.std_string, 'x'), (nfc.NfcTag, 'tag')], + conf) + + for conf in config.get(CONF_ON_FINISHED_WRITE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_condition('pn532.is_writing', PN532IsWritingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(PN532), +})) +def pn532_is_writing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/pn532/binary_sensor.py b/esphome/components/pn532/binary_sensor.py index 1c5e220fa6..2404bc9e99 100644 --- a/esphome/components/pn532/binary_sensor.py +++ b/esphome/components/pn532/binary_sensor.py @@ -3,12 +3,10 @@ import esphome.config_validation as cv from esphome.components import binary_sensor from esphome.const import CONF_UID, CONF_ID from esphome.core import HexInt -from . import pn532_ns, PN532 +from . import pn532_ns, PN532, CONF_PN532_ID DEPENDENCIES = ['pn532'] -CONF_PN532_ID = 'pn532_id' - def validate_uid(value): value = cv.string_strict(value) @@ -18,8 +16,8 @@ def validate_uid(value): "long.") try: x = int(x, 16) - except ValueError: - raise cv.Invalid("Valid characters for parts of a UID are 0123456789ABCDEF.") + except ValueError as err: + raise cv.Invalid("Valid characters for parts of a UID are 0123456789ABCDEF.") from err if x < 0 or x > 255: raise cv.Invalid("Valid values for UID parts (separated by '-') are 00 to FF") return value diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 792d92a6ac..e2511648b6 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -11,51 +11,38 @@ namespace pn532 { static const char *TAG = "pn532"; -void format_uid(char *buf, const uint8_t *uid, uint8_t uid_length) { - int offset = 0; - for (uint8_t i = 0; i < uid_length; i++) { - const char *format = "%02X"; - if (i + 1 < uid_length) - format = "%02X-"; - offset += sprintf(buf + offset, format, uid[i]); - } -} - void PN532::setup() { ESP_LOGCONFIG(TAG, "Setting up PN532..."); - this->spi_setup(); - // Wake the chip up from power down - // 1. Enable the SS line for at least 2ms - // 2. Send a dummy command to get the protocol synced up - // (this may time out, but that's ok) - // 3. Send SAM config command with normal mode without waiting for ready bit (IRQ not initialized yet) - // 4. Probably optional, send SAM config again, this time checking ACK and return value - this->cs_->digital_write(false); - delay(10); + // Get version data + if (!this->write_command_({PN532_COMMAND_VERSION_DATA})) { + ESP_LOGE(TAG, "Error sending version command"); + this->mark_failed(); + return; + } - // send dummy firmware version command to get synced up - this->pn532_write_command_check_ack_({0x02}); // get firmware version command - // do not actually read any data, this should be OK according to datasheet + std::vector version_data; + if (!this->read_response_(PN532_COMMAND_VERSION_DATA, version_data)) { + ESP_LOGE(TAG, "Error getting version"); + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Found chip PN5%02X", version_data[0]); + ESP_LOGD(TAG, "Firmware ver. %d.%d", version_data[1], version_data[2]); - this->pn532_write_command_({ - 0x14, // SAM config command - 0x01, // normal mode - 0x14, // zero timeout (not in virtual card mode) - 0x01, - }); + if (!this->write_command_({ + PN532_COMMAND_SAMCONFIGURATION, + 0x01, // normal mode + 0x14, // zero timeout (not in virtual card mode) + 0x01, + })) { + ESP_LOGE(TAG, "No wakeup ack"); + this->mark_failed(); + return; + } - // do not wait for ready bit, this is a dummy command - delay(2); - - // Try to read ACK, if it fails it might be because there's data from a previous power cycle left - this->read_ack_(); - // do not wait for ready bit for return data - delay(5); - - // read data packet for wakeup result - auto wakeup_result = this->pn532_read_data_(); - if (wakeup_result.size() != 1) { + std::vector wakeup_result; + if (!this->read_response_(PN532_COMMAND_SAMCONFIGURATION, wakeup_result)) { this->error_code_ = WAKEUP_FAILED; this->mark_failed(); return; @@ -63,23 +50,21 @@ void PN532::setup() { // Set up SAM (secure access module) uint8_t sam_timeout = std::min(255u, this->update_interval_ / 50); - bool ret = this->pn532_write_command_check_ack_({ - 0x14, // SAM config command - 0x01, // normal mode - sam_timeout, // timeout as multiple of 50ms (actually only for virtual card mode, but shouldn't matter) - 0x01, // Enable IRQ - }); - - if (!ret) { + if (!this->write_command_({ + PN532_COMMAND_SAMCONFIGURATION, + 0x01, // normal mode + sam_timeout, // timeout as multiple of 50ms (actually only for virtual card mode, but shouldn't matter) + 0x01, // Enable IRQ + })) { this->error_code_ = SAM_COMMAND_FAILED; this->mark_failed(); return; } - auto sam_result = this->pn532_read_data_(); - if (sam_result.size() != 1) { + std::vector sam_result; + if (!this->read_response_(PN532_COMMAND_SAMCONFIGURATION, sam_result)) { ESP_LOGV(TAG, "Invalid SAM result: (%u)", sam_result.size()); // NOLINT - for (auto dat : sam_result) { + for (uint8_t dat : sam_result) { ESP_LOGV(TAG, " 0x%02X", dat); } this->error_code_ = SAM_COMMAND_FAILED; @@ -94,12 +79,11 @@ void PN532::update() { for (auto *obj : this->binary_sensors_) obj->on_scan_end(); - bool success = this->pn532_write_command_check_ack_({ - 0x4A, // INLISTPASSIVETARGET - 0x01, // max 1 card - 0x00, // baud rate ISO14443A (106 kbit/s) - }); - if (!success) { + if (!this->write_command_({ + PN532_COMMAND_INLISTPASSIVETARGET, + 0x01, // max 1 card + 0x00, // baud rate ISO14443A (106 kbit/s) + })) { ESP_LOGW(TAG, "Requesting tag read failed!"); this->status_set_warning(); return; @@ -107,250 +91,323 @@ void PN532::update() { this->status_clear_warning(); this->requested_read_ = true; } + void PN532::loop() { - if (!this->requested_read_ || !this->is_ready_()) + if (!this->requested_read_) return; - auto read = this->pn532_read_data_(); + std::vector read; + bool success = this->read_response_(PN532_COMMAND_INLISTPASSIVETARGET, read); + this->requested_read_ = false; - if (read.size() <= 2 || read[0] != 0x4B) { + if (!success) { // Something failed + this->current_uid_ = {}; this->turn_off_rf_(); return; } - uint8_t num_targets = read[1]; + uint8_t num_targets = read[0]; if (num_targets != 1) { // no tags found or too many + this->current_uid_ = {}; this->turn_off_rf_(); return; } - // const uint8_t target_number = read[2]; - // const uint16_t sens_res = uint16_t(read[3] << 8) | read[4]; - // const uint8_t sel_res = read[5]; - const uint8_t nfcid_length = read[6]; - const uint8_t *nfcid = &read[7]; - if (read.size() < 7U + nfcid_length) { + uint8_t nfcid_length = read[5]; + std::vector nfcid(read.begin() + 6, read.begin() + 6 + nfcid_length); + if (read.size() < 6U + nfcid_length) { // oops, pn532 returned invalid data return; } bool report = true; - // 1. Go through all triggers - for (auto *trigger : this->triggers_) - trigger->process(nfcid, nfcid_length); - - // 2. Find a binary sensor - for (auto *tag : this->binary_sensors_) { - if (tag->process(nfcid, nfcid_length)) { - // 2.1 if found, do not dump + for (auto *bin_sens : this->binary_sensors_) { + if (bin_sens->process(nfcid)) { report = false; } } - if (report) { - char buf[32]; - format_uid(buf, nfcid, nfcid_length); - ESP_LOGD(TAG, "Found new tag '%s'", buf); + if (nfcid.size() == this->current_uid_.size()) { + bool same_uid = false; + for (uint8_t i = 0; i < nfcid.size(); i++) + same_uid |= nfcid[i] == this->current_uid_[i]; + if (same_uid) + return; } + this->current_uid_ = nfcid; + + if (next_task_ == READ) { + auto tag = this->read_tag_(nfcid); + for (auto *trigger : this->triggers_) + trigger->process(tag); + + if (report) { + ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid(nfcid).c_str()); + if (tag->has_ndef_message()) { + auto message = tag->get_ndef_message(); + auto records = message->get_records(); + ESP_LOGD(TAG, " NDEF formatted records:"); + for (auto &record : records) { + ESP_LOGD(TAG, " %s - %s", record->get_type().c_str(), record->get_payload().c_str()); + } + } + } + } else if (next_task_ == CLEAN) { + ESP_LOGD(TAG, " Tag cleaning..."); + if (!this->clean_tag_(nfcid)) { + ESP_LOGE(TAG, " Tag was not fully cleaned successfully"); + } + ESP_LOGD(TAG, " Tag cleaned!"); + } else if (next_task_ == FORMAT) { + ESP_LOGD(TAG, " Tag formatting..."); + if (!this->format_tag_(nfcid)) { + ESP_LOGE(TAG, "Error formatting tag as NDEF"); + } + ESP_LOGD(TAG, " Tag formatted!"); + } else if (next_task_ == WRITE) { + if (this->next_task_message_to_write_ != nullptr) { + ESP_LOGD(TAG, " Tag writing..."); + ESP_LOGD(TAG, " Tag formatting..."); + if (!this->format_tag_(nfcid)) { + ESP_LOGE(TAG, " Tag could not be formatted for writing"); + } else { + ESP_LOGD(TAG, " Writing NDEF data"); + if (!this->write_tag_(nfcid, this->next_task_message_to_write_)) { + ESP_LOGE(TAG, " Failed to write message to tag"); + } + ESP_LOGD(TAG, " Finished writing NDEF data"); + delete this->next_task_message_to_write_; + this->next_task_message_to_write_ = nullptr; + this->on_finished_write_callback_.call(); + } + } + } + + this->read_mode(); + this->turn_off_rf_(); } -void PN532::turn_off_rf_() { - ESP_LOGVV(TAG, "Turning RF field OFF"); - this->pn532_write_command_check_ack_({ - 0x32, // RFConfiguration - 0x1, // RF Field - 0x0 // Off - }); -} - -float PN532::get_setup_priority() const { return setup_priority::DATA; } - -void PN532::pn532_write_command_(const std::vector &data) { - this->enable(); - delay(2); - // First byte, communication mode: Write data - this->write_byte(0x01); - +bool PN532::write_command_(const std::vector &data) { + std::vector write_data; // Preamble - this->write_byte(0x00); + write_data.push_back(0x00); // Start code - this->write_byte(0x00); - this->write_byte(0xFF); + write_data.push_back(0x00); + write_data.push_back(0xFF); // Length of message, TFI + data bytes const uint8_t real_length = data.size() + 1; // LEN - this->write_byte(real_length); + write_data.push_back(real_length); // LCS (Length checksum) - this->write_byte(~real_length + 1); + write_data.push_back(~real_length + 1); // TFI (Frame Identifier, 0xD4 means to PN532, 0xD5 means from PN532) - this->write_byte(0xD4); + write_data.push_back(0xD4); // calculate checksum, TFI is part of checksum uint8_t checksum = 0xD4; // DATA for (uint8_t dat : data) { - this->write_byte(dat); + write_data.push_back(dat); checksum += dat; } // DCS (Data checksum) - this->write_byte(~checksum + 1); + write_data.push_back(~checksum + 1); // Postamble - this->write_byte(0x00); + write_data.push_back(0x00); - this->disable(); + this->write_data(write_data); + + return this->read_ack_(); } -bool PN532::pn532_write_command_check_ack_(const std::vector &data) { - // 1. write command - this->pn532_write_command_(data); +bool PN532::read_ack_() { + ESP_LOGVV(TAG, "Reading ACK..."); - // 2. wait for readiness - if (!this->wait_ready_()) - return false; - - // 3. read ack - if (!this->read_ack_()) { - ESP_LOGV(TAG, "Invalid ACK frame received from PN532!"); + std::vector data; + if (!this->read_data(data, 6)) { return false; } + bool matches = (data[1] == 0x00 && // preamble + data[2] == 0x00 && // start of packet + data[3] == 0xFF && data[4] == 0x00 && // ACK packet code + data[5] == 0xFF && data[6] == 0x00); // postamble + ESP_LOGVV(TAG, "ACK valid: %s", YESNO(matches)); + return matches; +} + +bool PN532::read_response_(uint8_t command, std::vector &data) { + ESP_LOGV(TAG, "Reading response"); + uint8_t len = this->read_response_length_(); + if (len == 0) { + return false; + } + + ESP_LOGV(TAG, "Reading response of length %d", len); + if (!this->read_data(data, 6 + len + 2)) { + ESP_LOGD(TAG, "No response data"); + return false; + } + + if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + // invalid packet + ESP_LOGV(TAG, "read data invalid preamble!"); + return false; + } + + bool valid_header = (static_cast(data[4] + data[5]) == 0 && // LCS, len + lcs = 0 + data[6] == 0xD5 && // TFI - frame from PN532 to system controller + data[7] == command + 1); // Correct command response + + if (!valid_header) { + ESP_LOGV(TAG, "read data invalid header!"); + return false; + } + + data.erase(data.begin(), data.begin() + 6); // Remove headers + + uint8_t checksum = 0; + for (int i = 0; i < len + 1; i++) { + uint8_t dat = data[i]; + checksum += dat; + } + checksum = ~checksum + 1; + + if (data[len + 1] != checksum) { + ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", data[len], checksum); + return false; + } + + if (data[len + 2] != 0x00) { + ESP_LOGV(TAG, "read data invalid postamble!"); + return false; + } + + data.erase(data.begin(), data.begin() + 2); // Remove TFI and command code + data.erase(data.end() - 2, data.end()); // Remove checksum and postamble + return true; } -std::vector PN532::pn532_read_data_() { - this->enable(); - delay(2); - // Read data (transmission from the PN532 to the host) - this->write_byte(0x03); +uint8_t PN532::read_response_length_() { + std::vector data; + if (!this->read_data(data, 6)) { + return 0; + } - // sometimes preamble is not transmitted for whatever reason - // mostly happens during startup. - // just read the first two bytes and check if that is the case - uint8_t header[6]; - this->read_array(header, 2); - if (header[0] == 0x00 && header[1] == 0x00) { - // normal packet, preamble included - this->read_array(header + 2, 4); - } else if (header[0] == 0x00 && header[1] == 0xFF) { - // weird packet, preamble skipped; make it look like a normal packet - header[0] = 0x00; - header[1] = 0x00; - header[2] = 0xFF; - this->read_array(header + 3, 3); - } else { + if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { // invalid packet - this->disable(); ESP_LOGV(TAG, "read data invalid preamble!"); - return {}; + return 0; } - bool valid_header = (header[0] == 0x00 && // preamble - header[1] == 0x00 && // start code - header[2] == 0xFF && static_cast(header[3] + header[4]) == 0 && // LCS, len + lcs = 0 - header[5] == 0xD5 // TFI - frame from PN532 to system controller - ); + bool valid_header = (static_cast(data[4] + data[5]) == 0 && // LCS, len + lcs = 0 + data[6] == 0xD5); // TFI - frame from PN532 to system controller + if (!valid_header) { - this->disable(); ESP_LOGV(TAG, "read data invalid header!"); - return {}; + return 0; } - std::vector ret; + this->write_data({0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00}); // NACK - Retransmit last message + // full length of message, including TFI - const uint8_t full_len = header[3]; + uint8_t full_len = data[4]; // length of data, excluding TFI uint8_t len = full_len - 1; if (full_len == 0) len = 0; - - ret.resize(len); - this->read_array(ret.data(), len); - - uint8_t checksum = 0xD5; - for (uint8_t dat : ret) - checksum += dat; - checksum = ~checksum + 1; - - uint8_t dcs = this->read_byte(); - if (dcs != checksum) { - this->disable(); - ESP_LOGV(TAG, "read data invalid checksum! %02X != %02X", dcs, checksum); - return {}; - } - - if (this->read_byte() != 0x00) { - this->disable(); - ESP_LOGV(TAG, "read data invalid postamble!"); - return {}; - } - this->disable(); - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - ESP_LOGVV(TAG, "PN532 Data Frame: (%u)", ret.size()); // NOLINT - for (uint8_t dat : ret) { - ESP_LOGVV(TAG, " 0x%02X", dat); - } -#endif - - return ret; + return len; } -bool PN532::is_ready_() { - this->enable(); - // First byte, communication mode: Read state - this->write_byte(0x02); - // PN532 returns a single data byte, - // "After having sent a command, the host controller must wait for bit 0 of Status byte equals 1 - // before reading the data from the PN532." - bool ret = this->read_byte() == 0x01; - this->disable(); - if (ret) { - ESP_LOGVV(TAG, "Chip is ready!"); +void PN532::turn_off_rf_() { + ESP_LOGVV(TAG, "Turning RF field OFF"); + this->write_command_({ + PN532_COMMAND_RFCONFIGURATION, + 0x01, // RF Field + 0x00, // Off + }); +} + +nfc::NfcTag *PN532::read_tag_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + ESP_LOGD(TAG, "Mifare classic"); + return this->read_mifare_classic_tag_(uid); + } else if (type == nfc::TAG_TYPE_2) { + ESP_LOGD(TAG, "Mifare ultralight"); + return this->read_mifare_ultralight_tag_(uid); + } else if (type == nfc::TAG_TYPE_UNKNOWN) { + ESP_LOGV(TAG, "Cannot determine tag type"); + return new nfc::NfcTag(uid); + } else { + return new nfc::NfcTag(uid); } - return ret; } -bool PN532::read_ack_() { - ESP_LOGVV(TAG, "Reading ACK..."); - this->enable(); - delay(2); - // "Read data (transmission from the PN532 to the host) " - this->write_byte(0x03); - uint8_t ack[6]; - memset(ack, 0, sizeof(ack)); - - this->read_array(ack, 6); - this->disable(); - - bool matches = (ack[0] == 0x00 && // preamble - ack[1] == 0x00 && // start of packet - ack[2] == 0xFF && ack[3] == 0x00 && // ACK packet code - ack[4] == 0xFF && ack[5] == 0x00 // postamble - ); - ESP_LOGVV(TAG, "ACK valid: %s", YESNO(matches)); - return matches; +void PN532::read_mode() { + this->next_task_ = READ; + ESP_LOGD(TAG, "Waiting to read next tag"); } -bool PN532::wait_ready_() { - uint32_t start_time = millis(); - while (!this->is_ready_()) { - if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Timed out waiting for readiness from PN532!"); - return false; - } - yield(); +void PN532::clean_mode() { + this->next_task_ = CLEAN; + ESP_LOGD(TAG, "Waiting to clean next tag"); +} +void PN532::format_mode() { + this->next_task_ = FORMAT; + ESP_LOGD(TAG, "Waiting to format next tag"); +} +void PN532::write_mode(nfc::NdefMessage *message) { + this->next_task_ = WRITE; + this->next_task_message_to_write_ = message; + ESP_LOGD(TAG, "Waiting to write next tag"); +} + +bool PN532::clean_tag_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->format_mifare_classic_mifare_(uid); + } else if (type == nfc::TAG_TYPE_2) { + return this->clean_mifare_ultralight_(); } - return true; + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; } +bool PN532::format_tag_(std::vector &uid) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->format_mifare_classic_ndef_(uid); + } else if (type == nfc::TAG_TYPE_2) { + return this->clean_mifare_ultralight_(); + } + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; +} + +bool PN532::write_tag_(std::vector &uid, nfc::NdefMessage *message) { + uint8_t type = nfc::guess_tag_type(uid.size()); + if (type == nfc::TAG_TYPE_MIFARE_CLASSIC) { + return this->write_mifare_classic_tag_(uid, message); + } else if (type == nfc::TAG_TYPE_2) { + return this->write_mifare_ultralight_tag_(uid, message); + } + ESP_LOGE(TAG, "Unsupported Tag for formatting"); + return false; +} + +float PN532::get_setup_priority() const { return setup_priority::DATA; } + void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { @@ -364,7 +421,6 @@ void PN532::dump_config() { break; } - LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); for (auto *child : this->binary_sensors_) { @@ -372,11 +428,11 @@ void PN532::dump_config() { } } -bool PN532BinarySensor::process(const uint8_t *data, uint8_t len) { - if (len != this->uid_.size()) +bool PN532BinarySensor::process(std::vector &data) { + if (data.size() != this->uid_.size()) return false; - for (uint8_t i = 0; i < len; i++) { + for (uint8_t i = 0; i < data.size(); i++) { if (data[i] != this->uid_[i]) return false; } @@ -385,11 +441,7 @@ bool PN532BinarySensor::process(const uint8_t *data, uint8_t len) { this->found_ = true; return true; } -void PN532Trigger::process(const uint8_t *uid, uint8_t uid_length) { - char buf[32]; - format_uid(buf, uid, uid_length); - this->trigger(std::string(buf)); -} +void PN532OnTagTrigger::process(nfc::NfcTag *tag) { this->trigger(nfc::format_uid(tag->get_uid()), *tag); } } // namespace pn532 } // namespace esphome diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 3a734b7ba2..6b3bef2918 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -3,17 +3,22 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" #include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/spi/spi.h" +#include "esphome/components/nfc/nfc_tag.h" +#include "esphome/components/nfc/nfc.h" namespace esphome { namespace pn532 { -class PN532BinarySensor; -class PN532Trigger; +static const uint8_t PN532_COMMAND_VERSION_DATA = 0x02; +static const uint8_t PN532_COMMAND_SAMCONFIGURATION = 0x14; +static const uint8_t PN532_COMMAND_RFCONFIGURATION = 0x32; +static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; +static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; -class PN532 : public PollingComponent, - public spi::SPIDevice { +class PN532BinarySensor; +class PN532OnTagTrigger; + +class PN532 : public PollingComponent { public: void setup() override; @@ -25,53 +30,76 @@ class PN532 : public PollingComponent, void loop() override; void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } - void register_trigger(PN532Trigger *trig) { this->triggers_.push_back(trig); } + void register_trigger(PN532OnTagTrigger *trig) { this->triggers_.push_back(trig); } + + void add_on_finished_write_callback(std::function callback) { + this->on_finished_write_callback_.add(std::move(callback)); + } + + bool is_writing() { return this->next_task_ != READ; }; + + void read_mode(); + void clean_mode(); + void format_mode(); + void write_mode(nfc::NdefMessage *message); protected: - /// Write the full command given in data to the PN532 - void pn532_write_command_(const std::vector &data); - bool pn532_write_command_check_ack_(const std::vector &data); - - /** Read a data frame from the PN532 and return the result as a vector. - * - * Note that is_ready needs to be checked first before requesting this method. - * - * On failure, an empty vector is returned. - */ - std::vector pn532_read_data_(); - - /** Checks if the PN532 has set its ready status flag. - * - * Procedure goes as follows: - * - Host sends command to PN532 "write data" - * - Wait for readiness (until PN532 has processed command) by polling "read status"/is_ready_ - * - Parse ACK/NACK frame with "read data" byte - * - * - If data required, wait until device reports readiness again - * - Then call "read data" and read certain number of bytes (length is given at offset 4 of frame) - */ - bool is_ready_(); - bool wait_ready_(); - - bool read_ack_(); - void turn_off_rf_(); + bool write_command_(const std::vector &data); + bool read_response_(uint8_t command, std::vector &data); + bool read_ack_(); + uint8_t read_response_length_(); + + virtual bool write_data(const std::vector &data) = 0; + virtual bool read_data(std::vector &data, uint8_t len) = 0; + + nfc::NfcTag *read_tag_(std::vector &uid); + + bool format_tag_(std::vector &uid); + bool clean_tag_(std::vector &uid); + bool write_tag_(std::vector &uid, nfc::NdefMessage *message); + + nfc::NfcTag *read_mifare_classic_tag_(std::vector &uid); + bool read_mifare_classic_block_(uint8_t block_num, std::vector &data); + bool write_mifare_classic_block_(uint8_t block_num, std::vector &data); + bool auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, const uint8_t *key); + bool format_mifare_classic_mifare_(std::vector &uid); + bool format_mifare_classic_ndef_(std::vector &uid); + bool write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message); + + nfc::NfcTag *read_mifare_ultralight_tag_(std::vector &uid); + bool read_mifare_ultralight_page_(uint8_t page_num, std::vector &data); + bool is_mifare_ultralight_formatted_(); + uint16_t read_mifare_ultralight_capacity_(); + bool find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index); + bool write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data); + bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); + bool clean_mifare_ultralight_(); bool requested_read_{false}; std::vector binary_sensors_; - std::vector triggers_; + std::vector triggers_; + std::vector current_uid_; + nfc::NdefMessage *next_task_message_to_write_; + enum NfcTask { + READ = 0, + CLEAN, + FORMAT, + WRITE, + } next_task_{READ}; enum PN532Error { NONE = 0, WAKEUP_FAILED, SAM_COMMAND_FAILED, } error_code_{NONE}; + CallbackManager on_finished_write_callback_; }; class PN532BinarySensor : public binary_sensor::BinarySensor { public: void set_uid(const std::vector &uid) { uid_ = uid; } - bool process(const uint8_t *data, uint8_t len); + bool process(std::vector &data); void on_scan_end() { if (!this->found_) { @@ -85,9 +113,21 @@ class PN532BinarySensor : public binary_sensor::BinarySensor { bool found_{false}; }; -class PN532Trigger : public Trigger { +class PN532OnTagTrigger : public Trigger { public: - void process(const uint8_t *uid, uint8_t uid_length); + void process(nfc::NfcTag *tag); +}; + +class PN532OnFinishedWriteTrigger : public Trigger<> { + public: + explicit PN532OnFinishedWriteTrigger(PN532 *parent) { + parent->add_on_finished_write_callback([this]() { this->trigger(); }); + } +}; + +template class PN532IsWritingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_writing(); } }; } // namespace pn532 diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp new file mode 100644 index 0000000000..4e8c255755 --- /dev/null +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -0,0 +1,249 @@ +#include "pn532.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn532 { + +static const char *TAG = "pn532.mifare_classic"; + +nfc::NfcTag *PN532::read_mifare_classic_tag_(std::vector &uid) { + uint8_t current_block = 4; + uint8_t message_start_index = 0; + uint32_t message_length = 0; + + if (this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + std::vector data; + if (this->read_mifare_classic_block_(current_block, data)) { + if (!nfc::decode_mifare_classic_tlv(data, message_length, message_start_index)) { + return new nfc::NfcTag(uid, nfc::ERROR); + } + } else { + ESP_LOGE(TAG, "Failed to read block %d", current_block); + return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC); + } + } else { + ESP_LOGV(TAG, "Tag is not NDEF formatted"); + return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC); + } + + uint32_t index = 0; + uint32_t buffer_size = nfc::get_mifare_classic_buffer_size(message_length); + std::vector buffer; + + while (index < buffer_size) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (!this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + ESP_LOGE(TAG, "Error, Block authentication failed for %d", current_block); + } + } + std::vector block_data; + if (this->read_mifare_classic_block_(current_block, block_data)) { + buffer.insert(buffer.end(), block_data.begin(), block_data.end()); + } else { + ESP_LOGE(TAG, "Error reading block %d", current_block); + } + + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + current_block++; + } + } + buffer.erase(buffer.begin(), buffer.begin() + message_start_index); + return new nfc::NfcTag(uid, nfc::MIFARE_CLASSIC, buffer); +} + +bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { + if (!this->write_command_({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + block_num, + })) { + return false; + } + + if (!this->read_response_(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { + return false; + } + data.erase(data.begin()); + + ESP_LOGVV(TAG, " Block %d: %s", block_num, nfc::format_bytes(data).c_str()); + return true; +} + +bool PN532::auth_mifare_classic_block_(std::vector &uid, uint8_t block_num, uint8_t key_num, + const uint8_t *key) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + key_num, // Mifare Key slot + block_num, // Block number + }); + data.insert(data.end(), key, key + 6); + data.insert(data.end(), uid.begin(), uid.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Authentication failed - Block %d", block_num); + return false; + } + + std::vector response; + if (!this->read_response_(PN532_COMMAND_INDATAEXCHANGE, response) || response[0] != 0x00) { + ESP_LOGE(TAG, "Authentication failed - Block 0x%02x", block_num); + return false; + } + + return true; +} + +bool PN532::format_mifare_classic_mifare_(std::vector &uid) { + std::vector blank_buffer( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector trailer_buffer( + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x07, 0x80, 0x69, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + bool error = false; + + for (int block = 0; block < 64; block += 4) { + if (!this->auth_mifare_classic_block_(uid, block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + continue; + } + if (block != 0) { + if (!this->write_mifare_classic_block_(block, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block); + error = true; + } + } + if (!this->write_mifare_classic_block_(block + 1, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 1); + error = true; + } + if (!this->write_mifare_classic_block_(block + 2, blank_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 2); + error = true; + } + if (!this->write_mifare_classic_block_(block + 3, trailer_buffer)) { + ESP_LOGE(TAG, "Unable to write block %d", block + 3); + error = true; + } + } + + return !error; +} + +bool PN532::format_mifare_classic_ndef_(std::vector &uid) { + std::vector empty_ndef_message( + {0x03, 0x03, 0xD0, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector blank_block( + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + std::vector block_1_data( + {0x14, 0x01, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_2_data( + {0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1, 0x03, 0xE1}); + std::vector block_3_trailer( + {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0x78, 0x77, 0x88, 0xC1, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + std::vector ndef_trailer( + {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7, 0x7F, 0x07, 0x88, 0x40, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}); + + if (!this->auth_mifare_classic_block_(uid, 0, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + ESP_LOGE(TAG, "Unable to authenticate block 0 for formatting!"); + return false; + } + if (!this->write_mifare_classic_block_(1, block_1_data)) + return false; + if (!this->write_mifare_classic_block_(2, block_2_data)) + return false; + if (!this->write_mifare_classic_block_(3, block_3_trailer)) + return false; + + ESP_LOGD(TAG, "Sector 0 formatted to NDEF"); + + for (int block = 4; block < 64; block += 4) { + if (!this->auth_mifare_classic_block_(uid, block + 3, nfc::MIFARE_CMD_AUTH_B, nfc::DEFAULT_KEY)) { + return false; + } + if (block == 4) { + if (!this->write_mifare_classic_block_(block, empty_ndef_message)) + ESP_LOGE(TAG, "Unable to write block %d", block); + } else { + if (!this->write_mifare_classic_block_(block, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block); + } + if (!this->write_mifare_classic_block_(block + 1, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block + 1); + if (!this->write_mifare_classic_block_(block + 2, blank_block)) + ESP_LOGE(TAG, "Unable to write block %d", block + 2); + if (!this->write_mifare_classic_block_(block + 3, ndef_trailer)) + ESP_LOGE(TAG, "Unable to write trailer block %d", block + 3); + } + return true; +} + +bool PN532::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_WRITE, + block_num, + }); + data.insert(data.end(), write_data.begin(), write_data.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Error writing block %d", block_num); + return false; + } + + std::vector response; + if (!this->read_response_(PN532_COMMAND_INDATAEXCHANGE, response)) { + ESP_LOGE(TAG, "Error writing block %d", block_num); + return false; + } + + return true; +} + +bool PN532::write_mifare_classic_tag_(std::vector &uid, nfc::NdefMessage *message) { + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_classic_buffer_size(message_length); + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 3, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_block = 4; + + while (index < buffer_length) { + if (nfc::mifare_classic_is_first_block(current_block)) { + if (!this->auth_mifare_classic_block_(uid, current_block, nfc::MIFARE_CMD_AUTH_A, nfc::NDEF_KEY)) { + return false; + } + } + + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_CLASSIC_BLOCK_SIZE); + if (!this->write_mifare_classic_block_(current_block, data)) { + return false; + } + index += nfc::MIFARE_CLASSIC_BLOCK_SIZE; + current_block++; + + if (nfc::mifare_classic_is_trailer_block(current_block)) { + // Skipping as cannot write to trailer + current_block++; + } + } + return true; +} + +} // namespace pn532 +} // namespace esphome diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp new file mode 100644 index 0000000000..00cb18aacd --- /dev/null +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -0,0 +1,180 @@ +#include "pn532.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pn532 { + +static const char *TAG = "pn532.mifare_ultralight"; + +nfc::NfcTag *PN532::read_mifare_ultralight_tag_(std::vector &uid) { + if (!this->is_mifare_ultralight_formatted_()) { + ESP_LOGD(TAG, "Not NDEF formatted"); + return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + } + + uint8_t message_length; + uint8_t message_start_index; + if (!this->find_mifare_ultralight_ndef_(message_length, message_start_index)) { + return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + } + + if (message_length == 0) { + return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + } + std::vector data; + uint8_t index = 0; + for (uint8_t page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; page < nfc::MIFARE_ULTRALIGHT_MAX_PAGE; page++) { + std::vector page_data; + if (!this->read_mifare_ultralight_page_(page, page_data)) { + ESP_LOGE(TAG, "Error reading page %d", page); + return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2); + } + data.insert(data.end(), page_data.begin(), page_data.end()); + + if (index >= (message_length + message_start_index)) + break; + + index += page_data.size(); + } + + data.erase(data.begin(), data.begin() + message_start_index); + + return new nfc::NfcTag(uid, nfc::NFC_FORUM_TYPE_2, data); +} + +bool PN532::read_mifare_ultralight_page_(uint8_t page_num, std::vector &data) { + if (!this->write_command_({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_READ, + page_num, + })) { + return false; + } + + if (!this->read_response_(PN532_COMMAND_INDATAEXCHANGE, data) || data[0] != 0x00) { + return false; + } + data.erase(data.begin()); + // We only want 1 page of data but the PN532 returns 4 at once. + data.erase(data.begin() + 4, data.end()); + + ESP_LOGVV(TAG, "Pages %d-%d: %s", page_num, page_num + 4, nfc::format_bytes(data).c_str()); + + return true; +} + +bool PN532::is_mifare_ultralight_formatted_() { + std::vector data; + if (this->read_mifare_ultralight_page_(4, data)) { + return !(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[3] == 0xFF); + } + return true; +} + +uint16_t PN532::read_mifare_ultralight_capacity_() { + std::vector data; + if (this->read_mifare_ultralight_page_(3, data)) { + return data[2] * 8U; + } + return 0; +} + +bool PN532::find_mifare_ultralight_ndef_(uint8_t &message_length, uint8_t &message_start_index) { + std::vector data; + for (int page = 4; page < 6; page++) { + std::vector page_data; + if (!this->read_mifare_ultralight_page_(page, page_data)) { + return false; + } + data.insert(data.end(), page_data.begin(), page_data.end()); + } + if (data[0] == 0x03) { + message_length = data[1]; + message_start_index = 2; + return true; + } else if (data[5] == 0x03) { + message_length = data[6]; + message_start_index = 7; + return true; + } + return false; +} + +bool PN532::write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message) { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + + auto encoded = message->encode(); + + uint32_t message_length = encoded.size(); + uint32_t buffer_length = nfc::get_mifare_ultralight_buffer_size(message_length); + + if (buffer_length > capacity) { + ESP_LOGE(TAG, "Message length exceeds tag capacity %d > %d", buffer_length, capacity); + return false; + } + + encoded.insert(encoded.begin(), 0x03); + if (message_length < 255) { + encoded.insert(encoded.begin() + 1, message_length); + } else { + encoded.insert(encoded.begin() + 1, 0xFF); + encoded.insert(encoded.begin() + 2, (message_length >> 8) & 0xFF); + encoded.insert(encoded.begin() + 2, message_length & 0xFF); + } + encoded.push_back(0xFE); + + encoded.resize(buffer_length, 0); + + uint32_t index = 0; + uint8_t current_page = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + while (index < buffer_length) { + std::vector data(encoded.begin() + index, encoded.begin() + index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE); + if (!this->write_mifare_ultralight_page_(current_page, data)) { + return false; + } + index += nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; + current_page++; + } + return true; +} + +bool PN532::clean_mifare_ultralight_() { + uint32_t capacity = this->read_mifare_ultralight_capacity_(); + uint8_t pages = (capacity / nfc::MIFARE_ULTRALIGHT_PAGE_SIZE) + nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; + + std::vector blank_data = {0x00, 0x00, 0x00, 0x00}; + + for (int i = nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE; i < pages; i++) { + if (!this->write_mifare_ultralight_page_(i, blank_data)) { + return false; + } + } + return true; +} + +bool PN532::write_mifare_ultralight_page_(uint8_t page_num, std::vector &write_data) { + std::vector data({ + PN532_COMMAND_INDATAEXCHANGE, + 0x01, // One card + nfc::MIFARE_CMD_WRITE_ULTRALIGHT, + page_num, + }); + data.insert(data.end(), write_data.begin(), write_data.end()); + if (!this->write_command_(data)) { + ESP_LOGE(TAG, "Error writing page %d", page_num); + return false; + } + + std::vector response; + if (!this->read_response_(PN532_COMMAND_INDATAEXCHANGE, response)) { + ESP_LOGE(TAG, "Error writing page %d", page_num); + return false; + } + + return true; +} + +} // namespace pn532 +} // namespace esphome diff --git a/esphome/components/pn532_i2c/__init__.py b/esphome/components/pn532_i2c/__init__.py new file mode 100644 index 0000000000..f1c50adf45 --- /dev/null +++ b/esphome/components/pn532_i2c/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, pn532 +from esphome.const import CONF_ID + +AUTO_LOAD = ['pn532'] +CODEOWNERS = ['@OttoWinter', '@jesserockz'] +DEPENDENCIES = ['i2c'] + +pn532_i2c_ns = cg.esphome_ns.namespace('pn532_i2c') +PN532I2C = pn532_i2c_ns.class_('PN532I2C', pn532.PN532, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All(pn532.PN532_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PN532I2C), +}).extend(i2c.i2c_device_schema(0x24))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield pn532.setup_pn532(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp new file mode 100644 index 0000000000..162487de58 --- /dev/null +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -0,0 +1,45 @@ +#include "pn532_i2c.h" +#include "esphome/core/log.h" + +// Based on: +// - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf + +namespace esphome { +namespace pn532_i2c { + +static const char *TAG = "pn532_i2c"; + +bool PN532I2C::write_data(const std::vector &data) { return this->write_bytes_raw(data.data(), data.size()); } + +bool PN532I2C::read_data(std::vector &data, uint8_t len) { + delay(1); + + std::vector ready; + ready.resize(1); + uint32_t start_time = millis(); + while (true) { + if (this->read_bytes_raw(ready.data(), 1)) { + if (ready[0] == 0x01) + break; + } + + if (millis() - start_time > 100) { + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + return false; + } + } + + data.resize(len + 1); + this->read_bytes_raw(data.data(), len + 1); + return true; +} + +void PN532I2C::dump_config() { + PN532::dump_config(); + LOG_I2C_DEVICE(this); +} + +} // namespace pn532_i2c +} // namespace esphome diff --git a/esphome/components/pn532_i2c/pn532_i2c.h b/esphome/components/pn532_i2c/pn532_i2c.h new file mode 100644 index 0000000000..23cb00bb10 --- /dev/null +++ b/esphome/components/pn532_i2c/pn532_i2c.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn532/pn532.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pn532_i2c { + +class PN532I2C : public pn532::PN532, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + bool write_data(const std::vector &data) override; + bool read_data(std::vector &data, uint8_t len) override; +}; + +} // namespace pn532_i2c +} // namespace esphome diff --git a/esphome/components/pn532_spi/__init__.py b/esphome/components/pn532_spi/__init__.py new file mode 100644 index 0000000000..e378b96c2d --- /dev/null +++ b/esphome/components/pn532_spi/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, pn532 +from esphome.const import CONF_ID + +AUTO_LOAD = ['pn532'] +CODEOWNERS = ['@OttoWinter', '@jesserockz'] +DEPENDENCIES = ['spi'] + +pn532_spi_ns = cg.esphome_ns.namespace('pn532_spi') +PN532Spi = pn532_spi_ns.class_('PN532Spi', pn532.PN532, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(pn532.PN532_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PN532Spi), +}).extend(spi.spi_device_schema(cs_pin_required=True))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield pn532.setup_pn532(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/pn532_spi/pn532_spi.cpp b/esphome/components/pn532_spi/pn532_spi.cpp new file mode 100644 index 0000000000..3da799fb24 --- /dev/null +++ b/esphome/components/pn532_spi/pn532_spi.cpp @@ -0,0 +1,69 @@ +#include "pn532_spi.h" +#include "esphome/core/log.h" + +// Based on: +// - https://cdn-shop.adafruit.com/datasheets/PN532C106_Application+Note_v1.2.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/AN133910.pdf +// - https://www.nxp.com/docs/en/nxp/application-notes/153710.pdf + +namespace esphome { +namespace pn532_spi { + +static const char *TAG = "pn532_spi"; + +void PN532Spi::setup() { + ESP_LOGI(TAG, "PN532Spi setup started!"); + this->spi_setup(); + + this->cs_->digital_write(false); + delay(10); + ESP_LOGI(TAG, "SPI setup finished!"); + PN532::setup(); +} + +bool PN532Spi::write_data(const std::vector &data) { + this->enable(); + delay(2); + // First byte, communication mode: Write data + this->write_byte(0x01); + + this->write_array(data.data(), data.size()); + this->disable(); + + return true; +} + +bool PN532Spi::read_data(std::vector &data, uint8_t len) { + this->enable(); + // First byte, communication mode: Read state + this->write_byte(0x02); + + uint32_t start_time = millis(); + while (true) { + if (this->read_byte() & 0x01) + break; + + if (millis() - start_time > 100) { + this->disable(); + ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!"); + return false; + } + } + + // Read data (transmission from the PN532 to the host) + this->write_byte(0x03); + + data.resize(len); + this->read_array(data.data(), len); + this->disable(); + data.insert(data.begin(), 0x01); + return true; +}; + +void PN532Spi::dump_config() { + PN532::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +} // namespace pn532_spi +} // namespace esphome diff --git a/esphome/components/pn532_spi/pn532_spi.h b/esphome/components/pn532_spi/pn532_spi.h new file mode 100644 index 0000000000..967b8a66cf --- /dev/null +++ b/esphome/components/pn532_spi/pn532_spi.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pn532/pn532.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace pn532_spi { + +class PN532Spi : public pn532::PN532, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + bool write_data(const std::vector &data) override; + bool read_data(std::vector &data, uint8_t len) override; +}; + +} // namespace pn532_spi +} // namespace esphome diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py index d015af9f78..9c3deef73d 100644 --- a/esphome/components/prometheus/__init__.py +++ b/esphome/components/prometheus/__init__.py @@ -18,5 +18,7 @@ CONFIG_SCHEMA = cv.Schema({ def to_code(config): paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + cg.add_define('USE_PROMETHEUS') + var = cg.new_Pvariable(config[CONF_ID], paren) yield cg.register_component(var, config) diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index c71e51eb32..46b50a3021 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -158,6 +158,12 @@ void PulseCounterSensor::update() { ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value); this->publish_state(value); + + if (this->total_sensor_ != nullptr) { + current_total_ += raw; + ESP_LOGD(TAG, "'%s': Total : %i pulses", this->get_name().c_str(), current_total_); + this->total_sensor_->publish_state(current_total_); + } } #ifdef ARDUINO_ARCH_ESP32 diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 483036ac34..b3e3f42c01 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -54,6 +54,7 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { void set_rising_edge_mode(PulseCounterCountMode mode) { storage_.rising_edge_mode = mode; } void set_falling_edge_mode(PulseCounterCountMode mode) { storage_.falling_edge_mode = mode; } void set_filter_us(uint32_t filter) { storage_.filter_us = filter; } + void set_total_sensor(sensor::Sensor *total_sensor) { total_sensor_ = total_sensor; } /// Unit of measurement is "pulses/min". void setup() override; @@ -64,6 +65,8 @@ class PulseCounterSensor : public sensor::Sensor, public PollingComponent { protected: GPIOPin *pin_; PulseCounterStorage storage_; + uint32_t current_total_ = 0; + sensor::Sensor *total_sensor_; }; #ifdef ARDUINO_ARCH_ESP32 diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 61d3f3d5b5..7550d5693a 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -3,8 +3,8 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \ - CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, \ - ICON_PULSE, UNIT_PULSES_PER_MINUTE + CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, CONF_TOTAL, \ + ICON_PULSE, UNIT_PULSES_PER_MINUTE, UNIT_PULSES from esphome.core import CORE pulse_counter_ns = cg.esphome_ns.namespace('pulse_counter') @@ -58,6 +58,8 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).exte cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, }), validate_count_mode), cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, + cv.Optional(CONF_TOTAL): sensor.sensor_schema(UNIT_PULSES, ICON_PULSE, 0), + }).extend(cv.polling_component_schema('60s')) @@ -72,3 +74,7 @@ def to_code(config): cg.add(var.set_rising_edge_mode(count[CONF_RISING_EDGE])) cg.add(var.set_falling_edge_mode(count[CONF_FALLING_EDGE])) cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER])) + + if CONF_TOTAL in config: + sens = yield sensor.new_sensor(config[CONF_TOTAL]) + cg.add(var.set_total_sensor(sens)) diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py new file mode 100644 index 0000000000..7b4df37ce2 --- /dev/null +++ b/esphome/components/rc522/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation, pins +from esphome.components import i2c +from esphome.const import CONF_ON_TAG, CONF_TRIGGER_ID, CONF_RESET_PIN +from esphome.core import coroutine + +CODEOWNERS = ['@glmnet'] +AUTO_LOAD = ['binary_sensor'] +MULTI_CONF = True + +CONF_RC522_ID = 'rc522_id' + +rc522_ns = cg.esphome_ns.namespace('rc522') +RC522 = rc522_ns.class_('RC522', cg.PollingComponent, i2c.I2CDevice) +RC522Trigger = rc522_ns.class_('RC522Trigger', automation.Trigger.template(cg.std_string)) + +RC522_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(RC522), + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_ON_TAG): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), + }), +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_rc522(var, config): + yield cg.register_component(var, config) + + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + for conf in config.get(CONF_ON_TAG, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_trigger(trigger)) + yield automation.build_automation(trigger, [(cg.std_string, 'x')], conf) diff --git a/esphome/components/rc522/binary_sensor.py b/esphome/components/rc522/binary_sensor.py new file mode 100644 index 0000000000..675db2f130 --- /dev/null +++ b/esphome/components/rc522/binary_sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_UID, CONF_ID +from esphome.core import HexInt, coroutine +from . import rc522_ns, RC522, CONF_RC522_ID + +DEPENDENCIES = ['rc522'] + + +def validate_uid(value): + value = cv.string_strict(value) + for x in value.split('-'): + if len(x) != 2: + raise cv.Invalid("Each part (separated by '-') of the UID must be two characters " + "long.") + try: + x = int(x, 16) + except ValueError as err: + raise cv.Invalid("Valid characters for parts of a UID are 0123456789ABCDEF.") from err + if x < 0 or x > 255: + raise cv.Invalid("Valid values for UID parts (separated by '-') are 00 to FF") + return value + + +RC522BinarySensor = rc522_ns.class_('RC522BinarySensor', binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(RC522BinarySensor), + cv.GenerateID(CONF_RC522_ID): cv.use_id(RC522), + cv.Required(CONF_UID): validate_uid, +}) + + +@coroutine +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield binary_sensor.register_binary_sensor(var, config) + + hub = yield cg.get_variable(config[CONF_RC522_ID]) + cg.add(hub.register_tag(var)) + addr = [HexInt(int(x, 16)) for x in config[CONF_UID].split('-')] + cg.add(var.set_uid(addr)) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp new file mode 100644 index 0000000000..8182c1e8c9 --- /dev/null +++ b/esphome/components/rc522/rc522.cpp @@ -0,0 +1,758 @@ +#include "rc522.h" +#include "esphome/core/log.h" + +// Based on: +// - https://github.com/miguelbalboa/rfid + +namespace esphome { +namespace rc522 { + +static const char *TAG = "rc522"; + +static const uint8_t RESET_COUNT = 5; + +void format_uid(char *buf, const uint8_t *uid, uint8_t uid_length) { + int offset = 0; + for (uint8_t i = 0; i < uid_length; i++) { + const char *format = "%02X"; + if (i + 1 < uid_length) + format = "%02X-"; + offset += sprintf(buf + offset, format, uid[i]); + } +} + +void RC522::setup() { + initialize_pending_ = true; + // Pull device out of power down / reset state. + + // First set the resetPowerDownPin as digital input, to check the MFRC522 power down mode. + if (reset_pin_ != nullptr) { + reset_pin_->pin_mode(INPUT); + + if (reset_pin_->digital_read() == LOW) { // The MFRC522 chip is in power down mode. + ESP_LOGV(TAG, "Power down mode detected. Hard resetting..."); + reset_pin_->pin_mode(OUTPUT); // Now set the resetPowerDownPin as digital output. + reset_pin_->digital_write(LOW); // Make sure we have a clean LOW state. + delayMicroseconds(2); // 8.8.1 Reset timing requirements says about 100ns. Let us be generous: 2μsl + reset_pin_->digital_write(HIGH); // Exit power down mode. This triggers a hard reset. + // Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. + // Let us be generous: 50ms. + reset_timeout_ = millis(); + return; + } + } + + // Setup a soft reset + reset_count_ = RESET_COUNT; + reset_timeout_ = millis(); +} + +void RC522::initialize_() { + // Per originall code, wait 50 ms + if (millis() - reset_timeout_ < 50) + return; + + // Reset baud rates + ESP_LOGV(TAG, "Initialize"); + + pcd_write_register(TX_MODE_REG, 0x00); + pcd_write_register(RX_MODE_REG, 0x00); + // Reset ModWidthReg + pcd_write_register(MOD_WIDTH_REG, 0x26); + + // When communicating with a PICC we need a timeout if something goes wrong. + // f_timer = 13.56 MHz / (2*TPreScaler+1) where TPreScaler = [TPrescaler_Hi:TPrescaler_Lo]. + // TPrescaler_Hi are the four low bits in TModeReg. TPrescaler_Lo is TPrescalerReg. + pcd_write_register(T_MODE_REG, 0x80); // TAuto=1; timer starts automatically at the end of the transmission in all + // communication modes at all speeds + + // TPreScaler = TModeReg[3..0]:TPrescalerReg, ie 0x0A9 = 169 => f_timer=40kHz, ie a timer period of 25μs. + pcd_write_register(T_PRESCALER_REG, 0xA9); + pcd_write_register(T_RELOAD_REG_H, 0x03); // Reload timer with 0x3E8 = 1000, ie 25ms before timeout. + pcd_write_register(T_RELOAD_REG_L, 0xE8); + + // Default 0x00. Force a 100 % ASK modulation independent of the ModGsPReg register setting + pcd_write_register(TX_ASK_REG, 0x40); + pcd_write_register(MODE_REG, 0x3D); // Default 0x3F. Set the preset value for the CRC coprocessor for the CalcCRC + // command to 0x6363 (ISO 14443-3 part 6.2.4) + pcd_antenna_on_(); // Enable the antenna driver pins TX1 and TX2 (they were disabled by the reset) + + initialize_pending_ = false; +} + +void RC522::dump_config() { + ESP_LOGCONFIG(TAG, "RC522:"); + switch (this->error_code_) { + case NONE: + break; + case RESET_FAILED: + ESP_LOGE(TAG, "Reset command failed!"); + break; + } + + LOG_PIN(" RESET Pin: ", this->reset_pin_); + + LOG_UPDATE_INTERVAL(this); + + for (auto *child : this->binary_sensors_) { + LOG_BINARY_SENSOR(" ", "Tag", child); + } +} + +void RC522::loop() { + // First check reset is needed + if (reset_count_ > 0) { + pcd_reset_(); + return; + } + if (initialize_pending_) { + initialize_(); + return; + } + + if (millis() - update_wait_ < this->update_interval_) + return; + + auto status = picc_is_new_card_present_(); + + static StatusCode LAST_STATUS = StatusCode::STATUS_OK; + + if (status != LAST_STATUS) { + ESP_LOGD(TAG, "Status is now: %d", status); + LAST_STATUS = status; + } + + if (status == STATUS_ERROR) // No card + { + // ESP_LOGE(TAG, "Error"); + // mark_failed(); + return; + } + + if (status != STATUS_OK) // We can receive STATUS_TIMEOUT when no card, or unexpected status. + return; + + // Try process card + if (!picc_read_card_serial_()) { + ESP_LOGW(TAG, "Requesting tag read failed!"); + return; + }; + + if (uid_.size < 4) { + return; + ESP_LOGW(TAG, "Read serial size: %d", uid_.size); + } + + update_wait_ = millis(); + + bool report = true; + // 1. Go through all triggers + for (auto *trigger : this->triggers_) + trigger->process(uid_.uiduint8_t, uid_.size); + + // 2. Find a binary sensor + for (auto *tag : this->binary_sensors_) { + if (tag->process(uid_.uiduint8_t, uid_.size)) { + // 2.1 if found, do not dump + report = false; + } + } + + if (report) { + char buf[32]; + format_uid(buf, uid_.uiduint8_t, uid_.size); + ESP_LOGD(TAG, "Found new tag '%s'", buf); + } +} + +void RC522::update() { + for (auto *obj : this->binary_sensors_) + obj->on_scan_end(); +} + +/** + * Performs a soft reset on the MFRC522 chip and waits for it to be ready again. + */ +void RC522::pcd_reset_() { + // The datasheet does not mention how long the SoftRest command takes to complete. + // But the MFRC522 might have been in soft power-down mode (triggered by bit 4 of CommandReg) + // Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. Let + // us be generous: 50ms. + + if (millis() - reset_timeout_ < 50) + return; + + if (reset_count_ == RESET_COUNT) { + ESP_LOGV(TAG, "Soft reset..."); + // Issue the SoftReset command. + pcd_write_register(COMMAND_REG, PCD_SOFT_RESET); + } + + // Expect the PowerDown bit in CommandReg to be cleared (max 3x50ms) + if ((pcd_read_register(COMMAND_REG) & (1 << 4)) == 0) { + reset_count_ = 0; + ESP_LOGI(TAG, "Device online."); + // Wait for initialize + reset_timeout_ = millis(); + return; + } + + if (--reset_count_ == 0) { + ESP_LOGE(TAG, "Unable to reset RC522."); + mark_failed(); + } +} + +/** + * Turns the antenna on by enabling pins TX1 and TX2. + * After a reset these pins are disabled. + */ +void RC522::pcd_antenna_on_() { + uint8_t value = pcd_read_register(TX_CONTROL_REG); + if ((value & 0x03) != 0x03) { + pcd_write_register(TX_CONTROL_REG, value | 0x03); + } +} + +/** + * Transmits a REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for anticollision or + * selection. 7 bit frame. Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT - + * probably due do bad antenna design. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +RC522::StatusCode RC522::picc_request_a_( + uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in + uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. +) { + return picc_reqa_or_wupa_(PICC_CMD_REQA, buffer_atqa, buffer_size); +} + +/** + * Transmits REQA or WUPA commands. + * Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT - probably due do bad antenna + * design. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +RC522::StatusCode RC522::picc_reqa_or_wupa_( + uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA + uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in + uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. +) { + uint8_t valid_bits; + RC522::StatusCode status; + + if (buffer_atqa == nullptr || *buffer_size < 2) { // The ATQA response is 2 uint8_ts long. + return STATUS_NO_ROOM; + } + pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. + valid_bits = 7; // For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only) + // uint8_t. TxLastBits = BitFramingReg[2..0] + status = pcd_transceive_data_(&command, 1, buffer_atqa, buffer_size, &valid_bits); + if (status != STATUS_OK) + return status; + if (*buffer_size != 2 || valid_bits != 0) { // ATQA must be exactly 16 bits. + ESP_LOGVV(TAG, "picc_reqa_or_wupa_() -> STATUS_ERROR"); + return STATUS_ERROR; + } + + return STATUS_OK; +} + +/** + * Sets the bits given in mask in register reg. + */ +void RC522::pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to set. +) { + uint8_t tmp = pcd_read_register(reg); + pcd_write_register(reg, tmp | mask); // set bit mask +} + +/** + * Clears the bits given in mask from register reg. + */ +void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to clear. +) { + uint8_t tmp = pcd_read_register(reg); + pcd_write_register(reg, tmp & (~mask)); // clear bit mask +} + +/** + * Executes the Transceive command. + * CRC validation can only be done if backData and backLen are specified. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +RC522::StatusCode RC522::pcd_transceive_data_( + uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO. + uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO. + uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command. + uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. + uint8_t + *valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits. Default nullptr. + uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0. + bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be + ///< validated. +) { + uint8_t wait_i_rq = 0x30; // RxIRq and IdleIRq + auto ret = pcd_communicate_with_picc_(PCD_TRANSCEIVE, wait_i_rq, send_data, send_len, back_data, back_len, valid_bits, + rx_align, check_crc); + + if (ret == STATUS_OK && *back_len == 5) + ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ) -> %d [%x, %x, %x, %x, %x]", send_len, ret, back_data[0], + back_data[1], back_data[2], back_data[3], back_data[4]); + else + ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ... ) -> %d", send_len, ret); + return ret; +} + +/** + * Transfers data to the MFRC522 FIFO, executes a command, waits for completion and transfers data back from the FIFO. + * CRC validation can only be done if backData and backLen are specified. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +RC522::StatusCode RC522::pcd_communicate_with_picc_( + uint8_t command, ///< The command to execute. One of the PCD_Command enums. + uint8_t wait_i_rq, ///< The bits in the ComIrqReg register that signals successful completion of the command. + uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO. + uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO. + uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command. + uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. + uint8_t *valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits. + uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0. + bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be + ///< validated. +) { + ESP_LOGVV(TAG, "pcd_communicate_with_picc_(%d, %d,... %d)", command, wait_i_rq, check_crc); + + // Prepare values for BitFramingReg + uint8_t tx_last_bits = valid_bits ? *valid_bits : 0; + uint8_t bit_framing = + (rx_align << 4) + tx_last_bits; // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0] + + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. + pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits + pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization + pcd_write_register(FIFO_DATA_REG, send_len, send_data); // Write sendData to the FIFO + pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments + pcd_write_register(COMMAND_REG, command); // Execute the command + if (command == PCD_TRANSCEIVE) { + pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts + } + + // Wait for the command to complete. + // In PCD_Init() we set the TAuto flag in TModeReg. This means the timer automatically starts when the PCD stops + // transmitting. Each iteration of the do-while-loop takes 17.86μs. + // TODO check/modify for other architectures than Arduino Uno 16bit + uint16_t i; + for (i = 2000; i > 0; i--) { + uint8_t n = pcd_read_register( + COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq + if (n & wait_i_rq) { // One of the interrupts that signal success has been set. + break; + } + if (n & 0x01) { // Timer interrupt - nothing received in 25ms + return STATUS_TIMEOUT; + } + } + // 35.7ms and nothing happend. Communication with the MFRC522 might be down. + if (i == 0) { + return STATUS_TIMEOUT; + } + + // Stop now if any errors except collisions were detected. + uint8_t error_reg_value = pcd_read_register( + ERROR_REG); // ErrorReg[7..0] bits are: WrErr TempErr reserved BufferOvfl CollErr CRCErr ParityErr ProtocolErr + if (error_reg_value & 0x13) { // BufferOvfl ParityErr ProtocolErr + return STATUS_ERROR; + } + + uint8_t valid_bits_local = 0; + + // If the caller wants data back, get it from the MFRC522. + if (back_data && back_len) { + uint8_t n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO + if (n > *back_len) { + return STATUS_NO_ROOM; + } + *back_len = n; // Number of uint8_ts returned + pcd_read_register(FIFO_DATA_REG, n, back_data, rx_align); // Get received data from FIFO + valid_bits_local = + pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last + // received uint8_t. If this value is 000b, the whole uint8_t is valid. + if (valid_bits) { + *valid_bits = valid_bits_local; + } + } + + // Tell about collisions + if (error_reg_value & 0x08) { // CollErr + return STATUS_COLLISION; + } + + // Perform CRC_A validation if requested. + if (back_data && back_len && check_crc) { + // In this case a MIFARE Classic NAK is not OK. + if (*back_len == 1 && valid_bits_local == 4) { + return STATUS_MIFARE_NACK; + } + // We need at least the CRC_A value and all 8 bits of the last uint8_t must be received. + if (*back_len < 2 || valid_bits_local != 0) { + return STATUS_CRC_WRONG; + } + // Verify CRC_A - do our own calculation and store the control in controlBuffer. + uint8_t control_buffer[2]; + RC522::StatusCode status = pcd_calculate_crc_(&back_data[0], *back_len - 2, &control_buffer[0]); + if (status != STATUS_OK) { + return status; + } + if ((back_data[*back_len - 2] != control_buffer[0]) || (back_data[*back_len - 1] != control_buffer[1])) { + return STATUS_CRC_WRONG; + } + } + + return STATUS_OK; +} + +/** + * Use the CRC coprocessor in the MFRC522 to calculate a CRC_A. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ + +RC522::StatusCode RC522::pcd_calculate_crc_( + uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length, ///< In: The number of uint8_ts to transfer. + uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first. +) { + ESP_LOGVV(TAG, "pcd_calculate_crc_(..., %d, ...)", length); + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. + pcd_write_register(DIV_IRQ_REG, 0x04); // Clear the CRCIRq interrupt request bit + pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization + pcd_write_register(FIFO_DATA_REG, length, data); // Write data to the FIFO + pcd_write_register(COMMAND_REG, PCD_CALC_CRC); // Start the calculation + + // Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73μs. + // TODO check/modify for other architectures than Arduino Uno 16bit + + // Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73us. + for (uint16_t i = 5000; i > 0; i--) { + // DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved + uint8_t n = pcd_read_register(DIV_IRQ_REG); + if (n & 0x04) { // CRCIRq bit set - calculation done + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO. + // Transfer the result from the registers to the result buffer + result[0] = pcd_read_register(CRC_RESULT_REG_L); + result[1] = pcd_read_register(CRC_RESULT_REG_H); + + ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK"); + return STATUS_OK; + } + } + ESP_LOGVV(TAG, "pcd_calculate_crc_() TIMEOUT"); + // 89ms passed and nothing happend. Communication with the MFRC522 might be down. + return STATUS_TIMEOUT; +} +/** + * Returns STATUS_OK if a PICC responds to PICC_CMD_REQA. + * Only "new" cards in state IDLE are invited. Sleeping cards in state HALT are ignored. + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ + +RC522::StatusCode RC522::picc_is_new_card_present_() { + uint8_t buffer_atqa[2]; + uint8_t buffer_size = sizeof(buffer_atqa); + + // Reset baud rates + pcd_write_register(TX_MODE_REG, 0x00); + pcd_write_register(RX_MODE_REG, 0x00); + // Reset ModWidthReg + pcd_write_register(MOD_WIDTH_REG, 0x26); + + auto result = picc_request_a_(buffer_atqa, &buffer_size); + + ESP_LOGV(TAG, "picc_is_new_card_present_() -> %d", result); + return result; +} + +/** + * Simple wrapper around PICC_Select. + * Returns true if a UID could be read. + * Remember to call PICC_IsNewCardPresent(), PICC_RequestA() or PICC_WakeupA() first. + * The read UID is available in the class variable uid. + * + * @return bool + */ +bool RC522::picc_read_card_serial_() { + RC522::StatusCode result = picc_select_(&this->uid_); + ESP_LOGVV(TAG, "picc_select_(...) -> %d", result); + return (result == STATUS_OK); +} + +/** + * Transmits SELECT/ANTICOLLISION commands to select a single PICC. + * Before calling this function the PICCs must be placed in the READY(*) state by calling PICC_RequestA() or + * PICC_WakeupA(). On success: + * - The chosen PICC is in state ACTIVE(*) and all other PICCs have returned to state IDLE/HALT. (Figure 7 of the + * ISO/IEC 14443-3 draft.) + * - The UID size and value of the chosen PICC is returned in *uid along with the SAK. + * + * A PICC UID consists of 4, 7 or 10 uint8_ts. + * Only 4 uint8_ts can be specified in a SELECT command, so for the longer UIDs two or three iterations are used: + * UID size Number of UID uint8_ts Cascade levels Example of PICC + * ======== =================== ============== =============== + * single 4 1 MIFARE Classic + * double 7 2 MIFARE Ultralight + * triple 10 3 Not currently in use? + * + * @return STATUS_OK on success, STATUS_??? otherwise. + */ +RC522::StatusCode RC522::picc_select_( + Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID. + uint8_t valid_bits ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also supply + ///< uid->size. +) { + bool uid_complete; + bool select_done; + bool use_cascade_tag; + uint8_t cascade_level = 1; + RC522::StatusCode result; + uint8_t count; + uint8_t check_bit; + uint8_t index; + uint8_t uid_index; // The first index in uid->uiduint8_t[] that is used in the current Cascade Level. + int8_t current_level_known_bits; // The number of known UID bits in the current Cascade Level. + uint8_t buffer[9]; // The SELECT/ANTICOLLISION commands uses a 7 uint8_t standard frame + 2 uint8_ts CRC_A + uint8_t buffer_used; // The number of uint8_ts used in the buffer, ie the number of uint8_ts to transfer to the FIFO. + uint8_t rx_align; // Used in BitFramingReg. Defines the bit position for the first bit received. + uint8_t tx_last_bits; // Used in BitFramingReg. The number of valid bits in the last transmitted uint8_t. + uint8_t *response_buffer; + uint8_t response_length; + + // Description of buffer structure: + // uint8_t 0: SEL Indicates the Cascade Level: PICC_CMD_SEL_CL1, PICC_CMD_SEL_CL2 or PICC_CMD_SEL_CL3 + // uint8_t 1: NVB Number of Valid Bits (in complete command, not just the UID): High nibble: complete + // uint8_ts, + // Low nibble: Extra bits. uint8_t 2: UID-data or CT See explanation below. CT means Cascade Tag. uint8_t + // 3: UID-data uint8_t 4: UID-data uint8_t 5: UID-data uint8_t 6: BCC Block Check Character - XOR of + // uint8_ts 2-5 uint8_t 7: CRC_A uint8_t 8: CRC_A The BCC and CRC_A are only transmitted if we know all the UID bits + // of the current Cascade Level. + // + // Description of uint8_ts 2-5: (Section 6.5.4 of the ISO/IEC 14443-3 draft: UID contents and cascade levels) + // UID size Cascade level uint8_t2 uint8_t3 uint8_t4 uint8_t5 + // ======== ============= ===== ===== ===== ===== + // 4 uint8_ts 1 uid0 uid1 uid2 uid3 + // 7 uint8_ts 1 CT uid0 uid1 uid2 + // 2 uid3 uid4 uid5 uid6 + // 10 uint8_ts 1 CT uid0 uid1 uid2 + // 2 CT uid3 uid4 uid5 + // 3 uid6 uid7 uid8 uid9 + + // Sanity checks + if (valid_bits > 80) { + return STATUS_INVALID; + } + + ESP_LOGVV(TAG, "picc_select_(&, %d)", valid_bits); + + // Prepare MFRC522 + pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. + + // Repeat Cascade Level loop until we have a complete UID. + uid_complete = false; + while (!uid_complete) { + // Set the Cascade Level in the SEL uint8_t, find out if we need to use the Cascade Tag in uint8_t 2. + switch (cascade_level) { + case 1: + buffer[0] = PICC_CMD_SEL_CL1; + uid_index = 0; + use_cascade_tag = valid_bits && uid->size > 4; // When we know that the UID has more than 4 uint8_ts + break; + + case 2: + buffer[0] = PICC_CMD_SEL_CL2; + uid_index = 3; + use_cascade_tag = valid_bits && uid->size > 7; // When we know that the UID has more than 7 uint8_ts + break; + + case 3: + buffer[0] = PICC_CMD_SEL_CL3; + uid_index = 6; + use_cascade_tag = false; // Never used in CL3. + break; + + default: + return STATUS_INTERNAL_ERROR; + break; + } + + // How many UID bits are known in this Cascade Level? + current_level_known_bits = valid_bits - (8 * uid_index); + if (current_level_known_bits < 0) { + current_level_known_bits = 0; + } + // Copy the known bits from uid->uiduint8_t[] to buffer[] + index = 2; // destination index in buffer[] + if (use_cascade_tag) { + buffer[index++] = PICC_CMD_CT; + } + uint8_t uint8_ts_to_copy = current_level_known_bits / 8 + + (current_level_known_bits % 8 + ? 1 + : 0); // The number of uint8_ts needed to represent the known bits for this level. + if (uint8_ts_to_copy) { + uint8_t maxuint8_ts = + use_cascade_tag ? 3 : 4; // Max 4 uint8_ts in each Cascade Level. Only 3 left if we use the Cascade Tag + if (uint8_ts_to_copy > maxuint8_ts) { + uint8_ts_to_copy = maxuint8_ts; + } + for (count = 0; count < uint8_ts_to_copy; count++) { + buffer[index++] = uid->uiduint8_t[uid_index + count]; + } + } + // Now that the data has been copied we need to include the 8 bits in CT in currentLevelKnownBits + if (use_cascade_tag) { + current_level_known_bits += 8; + } + + // Repeat anti collision loop until we can transmit all UID bits + BCC and receive a SAK - max 32 iterations. + select_done = false; + while (!select_done) { + // Find out how many bits and uint8_ts to send and receive. + if (current_level_known_bits >= 32) { // All UID bits in this Cascade Level are known. This is a SELECT. + + if (response_length < 4) { + ESP_LOGW(TAG, "Not enough data received."); + return STATUS_INVALID; + } + + // Serial.print(F("SELECT: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC); + buffer[1] = 0x70; // NVB - Number of Valid Bits: Seven whole uint8_ts + // Calculate BCC - Block Check Character + buffer[6] = buffer[2] ^ buffer[3] ^ buffer[4] ^ buffer[5]; + // Calculate CRC_A + result = pcd_calculate_crc_(buffer, 7, &buffer[7]); + if (result != STATUS_OK) { + return result; + } + tx_last_bits = 0; // 0 => All 8 bits are valid. + buffer_used = 9; + // Store response in the last 3 uint8_ts of buffer (BCC and CRC_A - not needed after tx) + response_buffer = &buffer[6]; + response_length = 3; + } else { // This is an ANTICOLLISION. + // Serial.print(F("ANTICOLLISION: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC); + tx_last_bits = current_level_known_bits % 8; + count = current_level_known_bits / 8; // Number of whole uint8_ts in the UID part. + index = 2 + count; // Number of whole uint8_ts: SEL + NVB + UIDs + buffer[1] = (index << 4) + tx_last_bits; // NVB - Number of Valid Bits + buffer_used = index + (tx_last_bits ? 1 : 0); + // Store response in the unused part of buffer + response_buffer = &buffer[index]; + response_length = sizeof(buffer) - index; + } + + // Set bit adjustments + rx_align = tx_last_bits; // Having a separate variable is overkill. But it makes the next line easier to read. + pcd_write_register( + BIT_FRAMING_REG, + (rx_align << 4) + tx_last_bits); // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0] + + // Transmit the buffer and receive the response. + result = pcd_transceive_data_(buffer, buffer_used, response_buffer, &response_length, &tx_last_bits, rx_align); + if (result == STATUS_COLLISION) { // More than one PICC in the field => collision. + uint8_t value_of_coll_reg = pcd_read_register( + COLL_REG); // CollReg[7..0] bits are: ValuesAfterColl reserved CollPosNotValid CollPos[4:0] + if (value_of_coll_reg & 0x20) { // CollPosNotValid + return STATUS_COLLISION; // Without a valid collision position we cannot continue + } + uint8_t collision_pos = value_of_coll_reg & 0x1F; // Values 0-31, 0 means bit 32. + if (collision_pos == 0) { + collision_pos = 32; + } + if (collision_pos <= current_level_known_bits) { // No progress - should not happen + return STATUS_INTERNAL_ERROR; + } + // Choose the PICC with the bit set. + current_level_known_bits = collision_pos; + count = current_level_known_bits % 8; // The bit to modify + check_bit = (current_level_known_bits - 1) % 8; + index = 1 + (current_level_known_bits / 8) + (count ? 1 : 0); // First uint8_t is index 0. + if (response_length > 2) // Note: Otherwise buffer[index] might be not initialized + buffer[index] |= (1 << check_bit); + } else if (result != STATUS_OK) { + return result; + } else { // STATUS_OK + if (current_level_known_bits >= 32) { // This was a SELECT. + select_done = true; // No more anticollision + // We continue below outside the while. + } else { // This was an ANTICOLLISION. + // We now have all 32 bits of the UID in this Cascade Level + current_level_known_bits = 32; + // Run loop again to do the SELECT. + } + } + } // End of while (!selectDone) + + // We do not check the CBB - it was constructed by us above. + + // Copy the found UID uint8_ts from buffer[] to uid->uiduint8_t[] + index = (buffer[2] == PICC_CMD_CT) ? 3 : 2; // source index in buffer[] + uint8_ts_to_copy = (buffer[2] == PICC_CMD_CT) ? 3 : 4; + for (count = 0; count < uint8_ts_to_copy; count++) { + uid->uiduint8_t[uid_index + count] = buffer[index++]; + } + + // Check response SAK (Select Acknowledge) + if (response_length != 3 || tx_last_bits != 0) { // SAK must be exactly 24 bits (1 uint8_t + CRC_A). + return STATUS_ERROR; + } + // Verify CRC_A - do our own calculation and store the control in buffer[2..3] - those uint8_ts are not needed + // anymore. + result = pcd_calculate_crc_(response_buffer, 1, &buffer[2]); + if (result != STATUS_OK) { + return result; + } + if ((buffer[2] != response_buffer[1]) || (buffer[3] != response_buffer[2])) { + return STATUS_CRC_WRONG; + } + if (response_buffer[0] & 0x04) { // Cascade bit set - UID not complete yes + cascade_level++; + } else { + uid_complete = true; + uid->sak = response_buffer[0]; + } + } // End of while (!uidComplete) + + // Set correct uid->size + uid->size = 3 * cascade_level + 1; + + return STATUS_OK; +} + +bool RC522BinarySensor::process(const uint8_t *data, uint8_t len) { + if (len != this->uid_.size()) + return false; + + for (uint8_t i = 0; i < len; i++) { + if (data[i] != this->uid_[i]) + return false; + } + + this->publish_state(true); + this->found_ = true; + return true; +} +void RC522Trigger::process(const uint8_t *uid, uint8_t uid_length) { + char buf[32]; + format_uid(buf, uid, uid_length); + this->trigger(std::string(buf)); +} + +} // namespace rc522 +} // namespace esphome diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h new file mode 100644 index 0000000000..cabcf8db0b --- /dev/null +++ b/esphome/components/rc522/rc522.h @@ -0,0 +1,284 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace rc522 { + +class RC522BinarySensor; +class RC522Trigger; +class RC522 : public PollingComponent { + public: + void setup() override; + + void dump_config() override; + + void update() override; + float get_setup_priority() const override { return setup_priority::DATA; }; + + void loop() override; + + void register_tag(RC522BinarySensor *tag) { this->binary_sensors_.push_back(tag); } + void register_trigger(RC522Trigger *trig) { this->triggers_.push_back(trig); } + + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + + protected: + enum PcdRegister : uint8_t { + // Page 0: Command and status + // 0x00 // reserved for future use + COMMAND_REG = 0x01 << 1, // starts and stops command execution + COM_I_EN_REG = 0x02 << 1, // enable and disable interrupt request control bits + DIV_I_EN_REG = 0x03 << 1, // enable and disable interrupt request control bits + COM_IRQ_REG = 0x04 << 1, // interrupt request bits + DIV_IRQ_REG = 0x05 << 1, // interrupt request bits + ERROR_REG = 0x06 << 1, // error bits showing the error status of the last command executed + STATUS1_REG = 0x07 << 1, // communication status bits + STATUS2_REG = 0x08 << 1, // receiver and transmitter status bits + FIFO_DATA_REG = 0x09 << 1, // input and output of 64 uint8_t FIFO buffer + FIFO_LEVEL_REG = 0x0A << 1, // number of uint8_ts stored in the FIFO buffer + WATER_LEVEL_REG = 0x0B << 1, // level for FIFO underflow and overflow warning + CONTROL_REG = 0x0C << 1, // miscellaneous control registers + BIT_FRAMING_REG = 0x0D << 1, // adjustments for bit-oriented frames + COLL_REG = 0x0E << 1, // bit position of the first bit-collision detected on the RF interface + // 0x0F // reserved for future use + + // Page 1: Command + // 0x10 // reserved for future use + MODE_REG = 0x11 << 1, // defines general modes for transmitting and receiving + TX_MODE_REG = 0x12 << 1, // defines transmission data rate and framing + RX_MODE_REG = 0x13 << 1, // defines reception data rate and framing + TX_CONTROL_REG = 0x14 << 1, // controls the logical behavior of the antenna driver pins TX1 and TX2 + TX_ASK_REG = 0x15 << 1, // controls the setting of the transmission modulation + TX_SEL_REG = 0x16 << 1, // selects the internal sources for the antenna driver + RX_SEL_REG = 0x17 << 1, // selects internal receiver settings + RX_THRESHOLD_REG = 0x18 << 1, // selects thresholds for the bit decoder + DEMOD_REG = 0x19 << 1, // defines demodulator settings + // 0x1A // reserved for future use + // 0x1B // reserved for future use + MF_TX_REG = 0x1C << 1, // controls some MIFARE communication transmit parameters + MF_RX_REG = 0x1D << 1, // controls some MIFARE communication receive parameters + // 0x1E // reserved for future use + SERIAL_SPEED_REG = 0x1F << 1, // selects the speed of the serial UART interface + + // Page 2: Configuration + // 0x20 // reserved for future use + CRC_RESULT_REG_H = 0x21 << 1, // shows the MSB and LSB values of the CRC calculation + CRC_RESULT_REG_L = 0x22 << 1, + // 0x23 // reserved for future use + MOD_WIDTH_REG = 0x24 << 1, // controls the ModWidth setting? + // 0x25 // reserved for future use + RF_CFG_REG = 0x26 << 1, // configures the receiver gain + GS_N_REG = 0x27 << 1, // selects the conductance of the antenna driver pins TX1 and TX2 for modulation + CW_GS_P_REG = 0x28 << 1, // defines the conductance of the p-driver output during periods of no modulation + MOD_GS_P_REG = 0x29 << 1, // defines the conductance of the p-driver output during periods of modulation + T_MODE_REG = 0x2A << 1, // defines settings for the internal timer + T_PRESCALER_REG = 0x2B << 1, // the lower 8 bits of the TPrescaler value. The 4 high bits are in TModeReg. + T_RELOAD_REG_H = 0x2C << 1, // defines the 16-bit timer reload value + T_RELOAD_REG_L = 0x2D << 1, + T_COUNTER_VALUE_REG_H = 0x2E << 1, // shows the 16-bit timer value + T_COUNTER_VALUE_REG_L = 0x2F << 1, + + // Page 3: Test Registers + // 0x30 // reserved for future use + TEST_SEL1_REG = 0x31 << 1, // general test signal configuration + TEST_SEL2_REG = 0x32 << 1, // general test signal configuration + TEST_PIN_EN_REG = 0x33 << 1, // enables pin output driver on pins D1 to D7 + TEST_PIN_VALUE_REG = 0x34 << 1, // defines the values for D1 to D7 when it is used as an I/O bus + TEST_BUS_REG = 0x35 << 1, // shows the status of the internal test bus + AUTO_TEST_REG = 0x36 << 1, // controls the digital self-test + VERSION_REG = 0x37 << 1, // shows the software version + ANALOG_TEST_REG = 0x38 << 1, // controls the pins AUX1 and AUX2 + TEST_DA_C1_REG = 0x39 << 1, // defines the test value for TestDAC1 + TEST_DA_C2_REG = 0x3A << 1, // defines the test value for TestDAC2 + TEST_ADC_REG = 0x3B << 1 // shows the value of ADC I and Q channels + // 0x3C // reserved for production tests + // 0x3D // reserved for production tests + // 0x3E // reserved for production tests + // 0x3F // reserved for production tests + }; + + // MFRC522 commands. Described in chapter 10 of the datasheet. + enum PcdCommand : uint8_t { + PCD_IDLE = 0x00, // no action, cancels current command execution + PCD_MEM = 0x01, // stores 25 uint8_ts into the internal buffer + PCD_GENERATE_RANDOM_ID = 0x02, // generates a 10-uint8_t random ID number + PCD_CALC_CRC = 0x03, // activates the CRC coprocessor or performs a self-test + PCD_TRANSMIT = 0x04, // transmits data from the FIFO buffer + PCD_NO_CMD_CHANGE = 0x07, // no command change, can be used to modify the CommandReg register bits without + // affecting the command, for example, the PowerDown bit + PCD_RECEIVE = 0x08, // activates the receiver circuits + PCD_TRANSCEIVE = + 0x0C, // transmits data from FIFO buffer to antenna and automatically activates the receiver after transmission + PCD_MF_AUTHENT = 0x0E, // performs the MIFARE standard authentication as a reader + PCD_SOFT_RESET = 0x0F // resets the MFRC522 + }; + + // Commands sent to the PICC. + enum PiccCommand : uint8_t { + // The commands used by the PCD to manage communication with several PICCs (ISO 14443-3, Type A, section 6.4) + PICC_CMD_REQA = 0x26, // REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for + // anticollision or selection. 7 bit frame. + PICC_CMD_WUPA = 0x52, // Wake-UP command, Type A. Invites PICCs in state IDLE and HALT to go to READY(*) and + // prepare for anticollision or selection. 7 bit frame. + PICC_CMD_CT = 0x88, // Cascade Tag. Not really a command, but used during anti collision. + PICC_CMD_SEL_CL1 = 0x93, // Anti collision/Select, Cascade Level 1 + PICC_CMD_SEL_CL2 = 0x95, // Anti collision/Select, Cascade Level 2 + PICC_CMD_SEL_CL3 = 0x97, // Anti collision/Select, Cascade Level 3 + PICC_CMD_HLTA = 0x50, // HaLT command, Type A. Instructs an ACTIVE PICC to go to state HALT. + PICC_CMD_RATS = 0xE0, // Request command for Answer To Reset. + // The commands used for MIFARE Classic (from http://www.mouser.com/ds/2/302/MF1S503x-89574.pdf, Section 9) + // Use PCD_MFAuthent to authenticate access to a sector, then use these commands to read/write/modify the blocks on + // the sector. + // The read/write commands can also be used for MIFARE Ultralight. + PICC_CMD_MF_AUTH_KEY_A = 0x60, // Perform authentication with Key A + PICC_CMD_MF_AUTH_KEY_B = 0x61, // Perform authentication with Key B + PICC_CMD_MF_READ = + 0x30, // Reads one 16 uint8_t block from the authenticated sector of the PICC. Also used for MIFARE Ultralight. + PICC_CMD_MF_WRITE = 0xA0, // Writes one 16 uint8_t block to the authenticated sector of the PICC. Called + // "COMPATIBILITY WRITE" for MIFARE Ultralight. + PICC_CMD_MF_DECREMENT = + 0xC0, // Decrements the contents of a block and stores the result in the internal data register. + PICC_CMD_MF_INCREMENT = + 0xC1, // Increments the contents of a block and stores the result in the internal data register. + PICC_CMD_MF_RESTORE = 0xC2, // Reads the contents of a block into the internal data register. + PICC_CMD_MF_TRANSFER = 0xB0, // Writes the contents of the internal data register to a block. + // The commands used for MIFARE Ultralight (from http://www.nxp.com/documents/data_sheet/MF0ICU1.pdf, Section 8.6) + // The PICC_CMD_MF_READ and PICC_CMD_MF_WRITE can also be used for MIFARE Ultralight. + PICC_CMD_UL_WRITE = 0xA2 // Writes one 4 uint8_t page to the PICC. + }; + + // Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more. + // last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered + enum StatusCode : uint8_t { + STATUS_OK, // Success + STATUS_ERROR, // Error in communication + STATUS_COLLISION, // Collission 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 ;-) + STATUS_INVALID, // Invalid argument. + STATUS_CRC_WRONG, // The CRC_A does not match + STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK. + }; + + // A struct used for passing the UID of a PICC. + using Uid = struct { + uint8_t size; // Number of uint8_ts in the UID. 4, 7 or 10. + uint8_t uiduint8_t[10]; + uint8_t sak; // The SAK (Select acknowledge) uint8_t returned from the PICC after successful selection. + }; + + Uid uid_; + uint32_t update_wait_{0}; + + void pcd_reset_(); + void initialize_(); + void pcd_antenna_on_(); + virtual uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) = 0; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + virtual void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) = 0; + virtual void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) = 0; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + virtual void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) = 0; + + StatusCode picc_request_a_( + uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in + uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. + ); + StatusCode picc_reqa_or_wupa_( + uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA + uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in + uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. + ); + void pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to set. + ); + void pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. + uint8_t mask ///< The bits to clear. + ); + + StatusCode pcd_transceive_data_(uint8_t *send_data, uint8_t send_len, uint8_t *back_data, uint8_t *back_len, + uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false); + StatusCode pcd_communicate_with_picc_(uint8_t command, uint8_t wait_i_rq, uint8_t *send_data, uint8_t send_len, + uint8_t *back_data = nullptr, uint8_t *back_len = nullptr, + uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false); + StatusCode pcd_calculate_crc_( + uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length, ///< In: The number of uint8_ts to transfer. + uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first. + ); + RC522::StatusCode picc_is_new_card_present_(); + bool picc_read_card_serial_(); + StatusCode picc_select_( + Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID. + uint8_t valid_bits = 0 ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also + ///< supply uid->size. + ); + + /** Read a data frame from the RC522 and return the result as a vector. + * + * Note that is_ready needs to be checked first before requesting this method. + * + * On failure, an empty vector is returned. + */ + std::vector r_c522_read_data_(); + + GPIOPin *reset_pin_{nullptr}; + uint8_t reset_count_{0}; + uint32_t reset_timeout_{0}; + bool initialize_pending_{false}; + std::vector binary_sensors_; + std::vector triggers_; + + enum RC522Error { + NONE = 0, + RESET_FAILED, + } error_code_{NONE}; +}; + +class RC522BinarySensor : public binary_sensor::BinarySensor { + public: + void set_uid(const std::vector &uid) { uid_ = uid; } + + bool process(const uint8_t *data, uint8_t len); + + void on_scan_end() { + if (!this->found_) { + this->publish_state(false); + } + this->found_ = false; + } + + protected: + std::vector uid_; + bool found_{false}; +}; + +class RC522Trigger : public Trigger { + public: + void process(const uint8_t *uid, uint8_t uid_length); +}; + +} // namespace rc522 +} // namespace esphome diff --git a/esphome/components/rc522_i2c/__init__.py b/esphome/components/rc522_i2c/__init__.py new file mode 100644 index 0000000000..c5bb72ee53 --- /dev/null +++ b/esphome/components/rc522_i2c/__init__.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, rc522 +from esphome.const import CONF_ID + +CODEOWNERS = ['@glmnet'] +DEPENDENCIES = ['i2c'] +AUTO_LOAD = ['rc522'] + + +rc522_i2c_ns = cg.esphome_ns.namespace('rc522_i2c') +RC522I2C = rc522_i2c_ns.class_('RC522I2C', rc522.RC522, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All(rc522.RC522_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(RC522I2C), +}).extend(i2c.i2c_device_schema(0x2c))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield rc522.setup_rc522(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/rc522_i2c/rc522_i2c.cpp b/esphome/components/rc522_i2c/rc522_i2c.cpp new file mode 100644 index 0000000000..8248e79b50 --- /dev/null +++ b/esphome/components/rc522_i2c/rc522_i2c.cpp @@ -0,0 +1,99 @@ +#include "rc522_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rc522_i2c { + +static const char *TAG = "rc522_i2c"; + +void RC522I2C::dump_config() { + RC522::dump_config(); + LOG_I2C_DEVICE(this); +} + +/** + * Reads a uint8_t from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +uint8_t RC522I2C::pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. +) { + uint8_t value; + read_byte(reg >> 1, &value); + ESP_LOGVV(TAG, "read_register_(%x) -> %x", reg, value); + return value; +} + +/** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522I2C::pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. +) { + if (count == 0) { + return; + } + + std::string buf; + buf = "Rx"; + char cstrb[20]; + + uint8_t b = values[0]; + read_bytes(reg >> 1, values, count); + + if (rx_align) // Only update bit positions rxAlign..7 in values[0] + { + // Create bit mask for bit positions rxAlign..7 + uint8_t mask = 0xFF << rx_align; + // Apply mask to both current value of values[0] and the new data in values array. + values[0] = (b & ~mask) | (values[0] & mask); + } +} + +void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. +) { + this->write_byte(reg >> 1, value); +} + +/** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. +) { + write_bytes(reg >> 1, values, count); +} + +// bool RC522I2C::write_data(const std::vector &data) { +// return this->write_bytes_raw(data.data(), data.size()); } + +// bool RC522I2C::read_data(std::vector &data, uint8_t len) { +// delay(5); + +// std::vector ready; +// ready.resize(1); +// uint32_t start_time = millis(); +// while (true) { +// if (this->read_bytes_raw(ready.data(), 1)) { +// if (ready[0] == 0x01) +// break; +// } + +// if (millis() - start_time > 100) { +// ESP_LOGV(TAG, "Timed out waiting for readiness from RC522!"); +// return false; +// } +// } + +// data.resize(len + 1); +// this->read_bytes_raw(data.data(), len + 1); +// return true; +// } + +} // namespace rc522_i2c +} // namespace esphome diff --git a/esphome/components/rc522_i2c/rc522_i2c.h b/esphome/components/rc522_i2c/rc522_i2c.h new file mode 100644 index 0000000000..8d8b0a0716 --- /dev/null +++ b/esphome/components/rc522_i2c/rc522_i2c.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/rc522/rc522.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace rc522_i2c { + +class RC522I2C : public rc522::RC522, public i2c::I2CDevice { + public: + void dump_config() override; + + protected: + uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) override; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) override; + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) override; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) override; +}; + +} // namespace rc522_i2c +} // namespace esphome diff --git a/esphome/components/rc522_spi/__init__.py b/esphome/components/rc522_spi/__init__.py new file mode 100644 index 0000000000..37f78b66d0 --- /dev/null +++ b/esphome/components/rc522_spi/__init__.py @@ -0,0 +1,21 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi, rc522 +from esphome.const import CONF_ID + +CODEOWNERS = ['@glmnet'] +DEPENDENCIES = ['spi'] +AUTO_LOAD = ['rc522'] + +rc522_spi_ns = cg.esphome_ns.namespace('rc522_spi') +RC522Spi = rc522_spi_ns.class_('RC522Spi', rc522.RC522, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(rc522.RC522_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(RC522Spi), +}).extend(spi.spi_device_schema(cs_pin_required=True))) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield rc522.setup_rc522(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/rc522_spi/binary_sensor.py b/esphome/components/rc522_spi/binary_sensor.py new file mode 100644 index 0000000000..1f0eafe6af --- /dev/null +++ b/esphome/components/rc522_spi/binary_sensor.py @@ -0,0 +1,9 @@ +import esphome.components.rc522.binary_sensor as rc522_binary_sensor + +DEPENDENCIES = ['rc522'] + +CONFIG_SCHEMA = rc522_binary_sensor.CONFIG_SCHEMA + + +def to_code(config): + yield rc522_binary_sensor.to_code(config) diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp new file mode 100644 index 0000000000..61236393e4 --- /dev/null +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -0,0 +1,129 @@ +#include "rc522_spi.h" +#include "esphome/core/log.h" + +// Based on: +// - https://github.com/miguelbalboa/rfid + +namespace esphome { +namespace rc522_spi { + +static const char *TAG = "rc522_spi"; + +void RC522Spi::setup() { + ESP_LOGI(TAG, "SPI Setup"); + this->spi_setup(); + + RC522::setup(); +} + +void RC522Spi::dump_config() { + RC522::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); +} + +/** + * Reads a uint8_t from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +uint8_t RC522Spi::pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. +) { + uint8_t value; + enable(); + transfer_byte(0x80 | reg); + value = read_byte(); + disable(); + ESP_LOGV(TAG, "read_register_(%x) -> %x", reg, value); + return value; +} + +/** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. +) { + std::string buf; + buf = "Rx"; + char cstrb[20]; + if (count == 0) { + return; + } + + // Serial.print(F("Reading ")); Serial.print(count); Serial.println(F(" uint8_ts from register.")); + uint8_t address = 0x80 | reg; // MSB == 1 is for reading. LSB is not used in address. Datasheet section 8.1.2.3. + uint8_t index = 0; // Index in values array. + enable(); + count--; // One read is performed outside of the loop + + write_byte(address); // Tell MFRC522 which address we want to read + if (rx_align) { // Only update bit positions rxAlign..7 in values[0] + // Create bit mask for bit positions rxAlign..7 + uint8_t mask = 0xFF << rx_align; + // Read value and tell that we want to read the same address again. + uint8_t value = transfer_byte(address); + // Apply mask to both current value of values[0] and the new data in value. + values[0] = (values[0] & ~mask) | (value & mask); + index++; + + sprintf(cstrb, " %x", values[0]); + buf.append(cstrb); + } + while (index < count) { + values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again. + + sprintf(cstrb, " %x", values[index]); + buf.append(cstrb); + + index++; + } + values[index] = transfer_byte(0); // Read the final uint8_t. Send 0 to stop reading. + + buf = buf + " "; + sprintf(cstrb, "%x", values[index]); + buf.append(cstrb); + + ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str()); + + disable(); +} + +void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. +) { + enable(); + // MSB == 0 is for writing. LSB is not used in address. Datasheet section 8.1.2.3. + transfer_byte(reg); + transfer_byte(value); + disable(); +} + +/** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ +void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. +) { + std::string buf; + buf = "Tx"; + + enable(); + transfer_byte(reg); + char cstrb[20]; + + for (uint8_t index = 0; index < count; index++) { + transfer_byte(values[index]); + + sprintf(cstrb, " %x", values[index]); + buf.append(cstrb); + } + disable(); + ESP_LOGVV(TAG, "write_register_(%x, %d) -> %s", reg, count, buf.c_str()); +} + +} // namespace rc522_spi +} // namespace esphome diff --git a/esphome/components/rc522_spi/rc522_spi.h b/esphome/components/rc522_spi/rc522_spi.h new file mode 100644 index 0000000000..58edbbed4f --- /dev/null +++ b/esphome/components/rc522_spi/rc522_spi.h @@ -0,0 +1,55 @@ +/** + * Library based on https://github.com/miguelbalboa/rfid + * and adapted to ESPHome by @glmnet + * + * original authors Dr.Leong, Miguel Balboa, Søren Thing Andersen, Tom Clement, many more! See GitLog. + * + * + */ + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/rc522/rc522.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace rc522_spi { + +class RC522Spi : public rc522::RC522, + public spi::SPIDevice { + public: + void setup() override; + + void dump_config() override; + + protected: + uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. + ) override; + + /** + * Reads a number of uint8_ts from the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_read_register(PcdRegister reg, ///< The register to read from. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to read + uint8_t *values, ///< uint8_t array to store the values in. + uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. + ) override; + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t value ///< The value to write. + ) override; + + /** + * Writes a number of uint8_ts to the specified register in the MFRC522 chip. + * The interface is described in the datasheet section 8.1.2. + */ + void pcd_write_register(PcdRegister reg, ///< The register to write to. One of the PCD_Register enums. + uint8_t count, ///< The number of uint8_ts to write to the register + uint8_t *values ///< The values to write. uint8_t array. + ) override; +}; + +} // namespace rc522_spi +} // namespace esphome diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index 6c6f0c0311..2d2ba61824 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -46,8 +46,7 @@ void rdm6300::RDM6300Component::loop() { } else { // Valid data this->status_clear_warning(); - const uint32_t result = (uint32_t(this->buffer_[1]) << 24) | (uint32_t(this->buffer_[2]) << 16) | - (uint32_t(this->buffer_[3]) << 8) | this->buffer_[4]; + const uint32_t result = encode_uint32(this->buffer_[1], this->buffer_[2], this->buffer_[3], this->buffer_[4]); bool report = result != last_id_; for (auto *card : this->cards_) { if (card->process(result)) { diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 91b22500e6..dc4438a84e 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -55,13 +55,13 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { } void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); + this->sync(dst); for (int16_t i = len - 1; i >= 0; i--) { if (code & ((uint64_t) 1 << i)) this->one(dst); else this->zero(dst); } - this->sync(dst); } bool RCSwitchBase::expect_one(RemoteReceiveData &src) const { diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 885e5765dd..a0a910118b 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -1,8 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SYNC, CONF_HIGH from esphome.components import uart +from esphome.const import ( + CONF_CODE, + CONF_HIGH, + CONF_ID, + CONF_LENGTH, + CONF_LOW, + CONF_PROTOCOL, + CONF_RAW, + CONF_SYNC, + CONF_TRIGGER_ID, +) DEPENDENCIES = ['uart'] CODEOWNERS = ['@jesserockz'] @@ -11,21 +21,39 @@ rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) RFBridgeData = rf_bridge_ns.struct('RFBridgeData') +RFBridgeAdvancedData = rf_bridge_ns.struct('RFBridgeAdvancedData') RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_('RFBridgeReceivedCodeTrigger', automation.Trigger.template(RFBridgeData)) +RFBridgeReceivedAdvancedCodeTrigger = rf_bridge_ns.class_( + 'RFBridgeReceivedAdvancedCodeTrigger', + automation.Trigger.template(RFBridgeAdvancedData), +) RFBridgeSendCodeAction = rf_bridge_ns.class_('RFBridgeSendCodeAction', automation.Action) +RFBridgeSendAdvancedCodeAction = rf_bridge_ns.class_( + 'RFBridgeSendAdvancedCodeAction', automation.Action) + RFBridgeLearnAction = rf_bridge_ns.class_('RFBridgeLearnAction', automation.Action) +RFBridgeStartAdvancedSniffingAction = rf_bridge_ns.class_( + 'RFBridgeStartAdvancedSniffingAction', automation.Action) +RFBridgeStopAdvancedSniffingAction = rf_bridge_ns.class_( + 'RFBridgeStopAdvancedSniffingAction', automation.Action) + +RFBridgeSendRawAction = rf_bridge_ns.class_('RFBridgeSendRawAction', automation.Action) CONF_ON_CODE_RECEIVED = 'on_code_received' +CONF_ON_ADVANCED_CODE_RECEIVED = 'on_advanced_code_received' CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(RFBridgeComponent), cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RFBridgeReceivedCodeTrigger), }), + cv.Optional(CONF_ON_ADVANCED_CODE_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RFBridgeReceivedAdvancedCodeTrigger), + }), }).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) @@ -38,6 +66,12 @@ def to_code(config): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) yield automation.build_automation(trigger, [(RFBridgeData, 'data')], conf) + for conf in config.get(CONF_ON_ADVANCED_CODE_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation( + trigger, [(RFBridgeAdvancedData, 'data')], conf + ) + RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(RFBridgeComponent), @@ -64,13 +98,81 @@ def rf_bridge_send_code_to_code(config, action_id, template_args, args): yield var -RFBRIDGE_LEARN_SCHEMA = cv.Schema({ +RFBRIDGE_ID_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(RFBridgeComponent) }) -@automation.register_action('rf_bridge.learn', RFBridgeLearnAction, RFBRIDGE_LEARN_SCHEMA) +@automation.register_action('rf_bridge.learn', RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA) def rf_bridge_learnx_to_code(config, action_id, template_args, args): paren = yield cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_args, paren) yield var + + +@automation.register_action( + 'rf_bridge.start_advanced_sniffing', + RFBridgeStartAdvancedSniffingAction, + RFBRIDGE_ID_SCHEMA, +) +def rf_bridge_start_advanced_sniffing_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + yield var + + +@automation.register_action( + 'rf_bridge.stop_advanced_sniffing', + RFBridgeStopAdvancedSniffingAction, + RFBRIDGE_ID_SCHEMA, +) +def rf_bridge_stop_advanced_sniffing_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + yield var + + +RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_LENGTH): cv.templatable(cv.hex_uint8_t), + cv.Required(CONF_PROTOCOL): cv.templatable(cv.hex_uint8_t), + cv.Required(CONF_CODE): cv.templatable(cv.string), +}) + + +@automation.register_action( + 'rf_bridge.send_advanced_code', + RFBridgeSendAdvancedCodeAction, + RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA +) +def rf_bridge_send_advanced_code_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = yield cg.templatable(config[CONF_LENGTH], args, cg.uint16) + cg.add(var.set_length(template_)) + template_ = yield cg.templatable(config[CONF_PROTOCOL], args, cg.uint16) + cg.add(var.set_protocol(template_)) + template_ = yield cg.templatable(config[CONF_CODE], args, cg.std_string) + cg.add(var.set_code(template_)) + yield var + + +RFBRIDGE_SEND_RAW_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_RAW): cv.templatable(cv.string), + } +) + + +@automation.register_action( + 'rf_bridge.send_raw', + RFBridgeSendRawAction, + RFBRIDGE_SEND_RAW_SCHEMA +) +def rf_bridge_send_raw_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = yield cg.templatable(config[CONF_RAW], args, cg.std_string) + cg.add(var.set_raw(template_)) + yield var diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 42689ecf82..32e72453e0 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -20,13 +20,15 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; + ESP_LOGVV(TAG, "Processing byte: 0x%02X", byte); + // Byte 0: Start if (at == 0) return byte == RF_CODE_START; // Byte 1: Action if (at == 1) - return byte >= RF_CODE_ACK && byte <= RF_CODE_RFOUT; + return byte >= RF_CODE_ACK && byte <= RF_CODE_RFIN_BUCKET; uint8_t action = raw[1]; switch (action) { @@ -37,8 +39,8 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { ESP_LOGD(TAG, "Learning timeout"); break; case RF_CODE_LEARN_OK: - case RF_CODE_RFIN: - if (at < RF_MESSAGE_SIZE + 2) + case RF_CODE_RFIN: { + if (byte != RF_CODE_STOP || at < RF_MESSAGE_SIZE + 2) return true; RFBridgeData data; @@ -52,8 +54,52 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, data.code); - this->callback_.call(data); + this->data_callback_.call(data); break; + } + case RF_CODE_LEARN_OK_NEW: + case RF_CODE_ADVANCED_RFIN: { + if (byte != RF_CODE_STOP) { + return at < (raw[2] + 3); + } + + RFBridgeAdvancedData data{}; + + data.length = raw[2]; + data.protocol = raw[3]; + char next_byte[2]; + for (uint8_t i = 0; i < data.length - 1; i++) { + sprintf(next_byte, "%02X", raw[4 + i]); + data.code += next_byte; + } + + ESP_LOGD(TAG, "Received RFBridge Advanced Code: length=0x%02X protocol=0x%02X code=0x%s", data.length, + data.protocol, data.code.c_str()); + this->advanced_data_callback_.call(data); + break; + } + case RF_CODE_RFIN_BUCKET: { + if (byte != RF_CODE_STOP) { + return true; + } + + uint8_t buckets = raw[2] << 1; + std::string str; + char next_byte[2]; + + for (uint32_t i = 0; i <= at; i++) { + sprintf(next_byte, "%02X", raw[i]); + str += next_byte; + if ((i > 3) && buckets) { + buckets--; + } + if ((i < 3) || (buckets % 2) || (i == at - 1)) { + str += " "; + } + } + ESP_LOGD(TAG, "Received RFBridge Bucket: %s", str.c_str()); + break; + } default: ESP_LOGW(TAG, "Unknown action: 0x%02X", action); break; @@ -68,6 +114,15 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { return false; } +void RFBridgeComponent::write_byte_str_(std::string codes) { + uint8_t code; + int size = codes.length(); + for (int i = 0; i < size; i += 2) { + code = strtol(codes.substr(i, 2).c_str(), nullptr, 16); + this->write(code); + } +} + void RFBridgeComponent::loop() { const uint32_t now = millis(); if (now - this->last_bridge_byte_ > 50) { @@ -105,6 +160,18 @@ void RFBridgeComponent::send_code(RFBridgeData data) { this->flush(); } +void RFBridgeComponent::send_advanced_code(RFBridgeAdvancedData data) { + ESP_LOGD(TAG, "Sending advanced code: length=0x%02X protocol=0x%02X code=0x%s", data.length, data.protocol, + data.code.c_str()); + this->write(RF_CODE_START); + this->write(RF_CODE_RFOUT_NEW); + this->write(data.length & 0xFF); + this->write(data.protocol & 0xFF); + this->write_byte_str_(data.code); + this->write(RF_CODE_STOP); + this->flush(); +} + void RFBridgeComponent::learn() { ESP_LOGD(TAG, "Learning mode"); this->write(RF_CODE_START); @@ -118,5 +185,28 @@ void RFBridgeComponent::dump_config() { this->check_uart_settings(19200); } +void RFBridgeComponent::start_advanced_sniffing() { + ESP_LOGD(TAG, "Advanced Sniffing on"); + this->write(RF_CODE_START); + this->write(RF_CODE_SNIFFING_ON); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::stop_advanced_sniffing() { + ESP_LOGD(TAG, "Advanced Sniffing off"); + this->write(RF_CODE_START); + this->write(RF_CODE_SNIFFING_OFF); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::send_raw(std::string raw_code) { + ESP_LOGD(TAG, "Sending Raw Code: %s", raw_code.c_str()); + + this->write_byte_str_(raw_code); + this->flush(); +} + } // namespace rf_bridge } // namespace esphome diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 7ae84a032f..b850140b75 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -15,6 +15,7 @@ static const uint8_t RF_CODE_LEARN_KO = 0xA2; static const uint8_t RF_CODE_LEARN_OK = 0xA3; static const uint8_t RF_CODE_RFIN = 0xA4; static const uint8_t RF_CODE_RFOUT = 0xA5; +static const uint8_t RF_CODE_ADVANCED_RFIN = 0xA6; static const uint8_t RF_CODE_SNIFFING_ON = 0xA6; static const uint8_t RF_CODE_SNIFFING_OFF = 0xA7; static const uint8_t RF_CODE_RFOUT_NEW = 0xA8; @@ -22,6 +23,7 @@ static const uint8_t RF_CODE_LEARN_NEW = 0xA9; static const uint8_t RF_CODE_LEARN_KO_NEW = 0xAA; static const uint8_t RF_CODE_LEARN_OK_NEW = 0xAB; static const uint8_t RF_CODE_RFOUT_BUCKET = 0xB0; +static const uint8_t RF_CODE_RFIN_BUCKET = 0xB1; static const uint8_t RF_CODE_STOP = 0x55; static const uint8_t RF_DEBOUNCE = 200; @@ -32,25 +34,40 @@ struct RFBridgeData { uint32_t code; }; +struct RFBridgeAdvancedData { + uint8_t length; + uint8_t protocol; + std::string code; +}; + class RFBridgeComponent : public uart::UARTDevice, public Component { public: void loop() override; void dump_config() override; void add_on_code_received_callback(std::function callback) { - this->callback_.add(std::move(callback)); + this->data_callback_.add(std::move(callback)); + } + void add_on_advanced_code_received_callback(std::function callback) { + this->advanced_data_callback_.add(std::move(callback)); } void send_code(RFBridgeData data); + void send_advanced_code(RFBridgeAdvancedData data); void learn(); + void start_advanced_sniffing(); + void stop_advanced_sniffing(); + void send_raw(std::string code); protected: void ack_(); void decode_(); bool parse_bridge_byte_(uint8_t byte); + void write_byte_str_(std::string codes); std::vector rx_buffer_; uint32_t last_bridge_byte_{0}; - CallbackManager callback_; + CallbackManager data_callback_; + CallbackManager advanced_data_callback_; }; class RFBridgeReceivedCodeTrigger : public Trigger { @@ -60,6 +77,13 @@ class RFBridgeReceivedCodeTrigger : public Trigger { } }; +class RFBridgeReceivedAdvancedCodeTrigger : public Trigger { + public: + explicit RFBridgeReceivedAdvancedCodeTrigger(RFBridgeComponent *parent) { + parent->add_on_advanced_code_received_callback([this](RFBridgeAdvancedData data) { this->trigger(data); }); + } +}; + template class RFBridgeSendCodeAction : public Action { public: RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} @@ -81,6 +105,25 @@ template class RFBridgeSendCodeAction : public Action { RFBridgeComponent *parent_; }; +template class RFBridgeSendAdvancedCodeAction : public Action { + public: + RFBridgeSendAdvancedCodeAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint8_t, length) + TEMPLATABLE_VALUE(uint8_t, protocol) + TEMPLATABLE_VALUE(std::string, code) + + void play(Ts... x) { + RFBridgeAdvancedData data{}; + data.length = this->length_.value(x...); + data.protocol = this->protocol_.value(x...); + data.code = this->code_.value(x...); + this->parent_->send_advanced_code(data); + } + + protected: + RFBridgeComponent *parent_; +}; + template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} @@ -91,5 +134,36 @@ template class RFBridgeLearnAction : public Action { RFBridgeComponent *parent_; }; +template class RFBridgeStartAdvancedSniffingAction : public Action { + public: + RFBridgeStartAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->start_advanced_sniffing(); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeStopAdvancedSniffingAction : public Action { + public: + RFBridgeStopAdvancedSniffingAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->stop_advanced_sniffing(); } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeSendRawAction : public Action { + public: + RFBridgeSendRawAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, raw) + + void play(Ts... x) { this->parent_->send_raw(this->raw_.value(x...)); } + + protected: + RFBridgeComponent *parent_; +}; + } // namespace rf_bridge } // namespace esphome diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 769d3e1103..0398489dca 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -90,14 +90,34 @@ void ICACHE_RAM_ATTR HOT RotaryEncoderSensorStore::gpio_intr(RotaryEncoderSensor if (arg->pin_b->digital_read()) input_state |= STATE_PIN_B_HIGH; + int8_t rotation_dir = 0; uint16_t new_state = STATE_LOOKUP_TABLE[input_state]; if ((new_state & arg->resolution & STATE_HAS_INCREMENTED) != 0) { if (arg->counter < arg->max_value) arg->counter++; + rotation_dir = 1; } if ((new_state & arg->resolution & STATE_HAS_DECREMENTED) != 0) { if (arg->counter > arg->min_value) arg->counter--; + rotation_dir = -1; + } + + if (rotation_dir != 0) { + auto first_zero = std::find(arg->rotation_events.begin(), arg->rotation_events.end(), 0); // find first zero + if (first_zero == arg->rotation_events.begin() // are we at the start (first event this loop iteration) + || std::signbit(*std::prev(first_zero)) != + std::signbit(rotation_dir) // or is the last stored event the wrong direction + || *std::prev(first_zero) == std::numeric_limits::lowest() // or the last event slot is full (negative) + || *std::prev(first_zero) == std::numeric_limits::max()) { // or the last event slot is full (positive) + if (first_zero != arg->rotation_events.end()) { // we have a free rotation slot + *first_zero += rotation_dir; // store the rotation into a new slot + } else { + arg->rotation_events_overflow = true; + } + } else { + *std::prev(first_zero) += rotation_dir; // store the rotation into the previous slot + } } arg->state = new_state; @@ -135,6 +155,35 @@ void RotaryEncoderSensor::dump_config() { } } void RotaryEncoderSensor::loop() { + std::array rotation_events; + bool rotation_events_overflow; + ets_intr_lock(); + rotation_events = this->store_.rotation_events; + rotation_events_overflow = this->store_.rotation_events_overflow; + + this->store_.rotation_events.fill(0); + this->store_.rotation_events_overflow = false; + ets_intr_unlock(); + + if (rotation_events_overflow) { + ESP_LOGW(TAG, "Captured more rotation events than expected"); + } + + for (auto events : rotation_events) { + if (events == 0) // we are at the end of the recorded events + break; + + if (events > 0) { + while (events--) { + this->on_clockwise_callback_.call(); + } + } else { + while (events++) { + this->on_anticlockwise_callback_.call(); + } + } + } + if (this->pin_i_ != nullptr && this->pin_i_->digital_read()) { this->store_.counter = 0; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index f0e47dfe0a..000350d66c 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "esphome/core/component.h" #include "esphome/core/esphal.h" #include "esphome/core/automation.h" @@ -27,6 +29,9 @@ struct RotaryEncoderSensorStore { int32_t last_read{0}; uint8_t state{0}; + std::array rotation_events{}; + bool rotation_events_overflow{false}; + static void gpio_intr(RotaryEncoderSensorStore *arg); }; @@ -62,12 +67,23 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { float get_setup_priority() const override; + void add_on_clockwise_callback(std::function callback) { + this->on_clockwise_callback_.add(std::move(callback)); + } + + void add_on_anticlockwise_callback(std::function callback) { + this->on_anticlockwise_callback_.add(std::move(callback)); + } + protected: GPIOPin *pin_a_; GPIOPin *pin_b_; GPIOPin *pin_i_{nullptr}; /// Index pin, if this is not nullptr, the counter will reset to 0 once this pin is HIGH. RotaryEncoderSensorStore store_{}; + + CallbackManager on_clockwise_callback_; + CallbackManager on_anticlockwise_callback_; }; template class RotaryEncoderSetValueAction : public Action { @@ -81,5 +97,19 @@ template class RotaryEncoderSetValueAction : public Action { + public: + explicit RotaryEncoderClockwiseTrigger(RotaryEncoderSensor *parent) { + parent->add_on_clockwise_callback([this]() { this->trigger(); }); + } +}; + +class RotaryEncoderAnticlockwiseTrigger : public Trigger<> { + public: + explicit RotaryEncoderAnticlockwiseTrigger(RotaryEncoderSensor *parent) { + parent->add_on_anticlockwise_callback([this]() { this->trigger(); }); + } +}; + } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 214ccbd056..c518982bc6 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins, automation from esphome.components import sensor from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B + ICON_ROTATE_RIGHT, CONF_VALUE, CONF_PIN_A, CONF_PIN_B, CONF_TRIGGER_ID rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -14,11 +14,18 @@ RESOLUTIONS = { } CONF_PIN_RESET = 'pin_reset' +CONF_ON_CLOCKWISE = 'on_clockwise' +CONF_ON_ANTICLOCKWISE = 'on_anticlockwise' RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction', automation.Action) +RotaryEncoderClockwiseTrigger = rotary_encoder_ns.class_('RotaryEncoderClockwiseTrigger', + automation.Trigger) +RotaryEncoderAnticlockwiseTrigger = rotary_encoder_ns.class_('RotaryEncoderAnticlockwiseTrigger', + automation.Trigger) + def validate_min_max_value(config): if CONF_MIN_VALUE in config and CONF_MAX_VALUE in config: @@ -40,6 +47,12 @@ CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_STEPS, ICON_ROTATE_RIGHT, 0).ex cv.Optional(CONF_RESOLUTION, default=1): cv.enum(RESOLUTIONS, int=True), cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, + cv.Optional(CONF_ON_CLOCKWISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RotaryEncoderClockwiseTrigger), + }), + cv.Optional(CONF_ON_ANTICLOCKWISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RotaryEncoderAnticlockwiseTrigger), + }), }).extend(cv.COMPONENT_SCHEMA), validate_min_max_value) @@ -61,6 +74,13 @@ def to_code(config): if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + for conf in config.get(CONF_ON_CLOCKWISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_ANTICLOCKWISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + @automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction, cv.Schema({ diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 195dfef5f6..443deda3cc 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -44,13 +44,21 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); /// Sensor initialization - if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, 0)) { + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, this->ambient_pressure_compensation_)) { ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } + if (this->temperature_offset_ != 0) { + if (!this->write_command_(SCD30_CMD_TEMPERATURE_OFFSET, (uint16_t)(temperature_offset_ * 100.0))) { + ESP_LOGE(TAG, "Sensor SCD30 error setting temperature offset."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on if (this->altitude_compensation_ != 0xFFFF) { if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { @@ -94,6 +102,8 @@ void SCD30Component::dump_config() { ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); } ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); + ESP_LOGCONFIG(TAG, " Ambient pressure compensation: %dmBar", this->ambient_pressure_compensation_); + ESP_LOGCONFIG(TAG, " Temperature offset: %.2f °C", this->temperature_offset_); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 2c4ee51f8a..f11b7cc1f4 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -15,6 +15,10 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } + void set_ambient_pressure_compensation(float pressure) { + ambient_pressure_compensation_ = (uint16_t)(pressure * 1000); + } + void set_temperature_offset(float offset) { temperature_offset_ = offset; } void setup() override; void update() override; @@ -35,6 +39,8 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { } error_code_{UNKNOWN}; bool enable_asc_{true}; uint16_t altitude_compensation_{0xFFFF}; + uint16_t ambient_pressure_compensation_{0x0000}; + float temperature_offset_{0.0}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index ca1a254e05..eef6f44390 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -13,6 +13,8 @@ SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CD CONF_AUTOMATIC_SELF_CALIBRATION = 'automatic_self_calibration' CONF_ALTITUDE_COMPENSATION = 'altitude_compensation' +CONF_AMBIENT_PRESSURE_COMPENSATION = 'ambient_pressure_compensation' +CONF_TEMPERATURE_OFFSET = 'temperature_offset' def remove_altitude_suffix(value): @@ -21,14 +23,16 @@ def remove_altitude_suffix(value): CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SCD30Component), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), - cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(remove_altitude_suffix, cv.int_range(min=0, max=0xFFFF, max_included=False)), + cv.Optional(CONF_AMBIENT_PRESSURE_COMPENSATION, default=0): cv.pressure, + cv.Optional(CONF_TEMPERATURE_OFFSET): cv.temperature, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) @@ -41,6 +45,12 @@ def to_code(config): if CONF_ALTITUDE_COMPENSATION in config: cg.add(var.set_altitude_compensation(config[CONF_ALTITUDE_COMPENSATION])) + if CONF_AMBIENT_PRESSURE_COMPENSATION in config: + cg.add(var.set_ambient_pressure_compensation(config[CONF_AMBIENT_PRESSURE_COMPENSATION])) + + if CONF_TEMPERATURE_OFFSET in config: + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + if CONF_CO2 in config: sens = yield sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/servo/__init__.py b/esphome/components/servo/__init__.py index 9b06159c13..76690dbcf3 100644 --- a/esphome/components/servo/__init__.py +++ b/esphome/components/servo/__init__.py @@ -4,13 +4,14 @@ from esphome import automation from esphome.automation import maybe_simple_id from esphome.components.output import FloatOutput from esphome.const import CONF_ID, CONF_IDLE_LEVEL, CONF_MAX_LEVEL, CONF_MIN_LEVEL, CONF_OUTPUT, \ - CONF_LEVEL, CONF_RESTORE + CONF_LEVEL, CONF_RESTORE, CONF_TRANSITION_LENGTH servo_ns = cg.esphome_ns.namespace('servo') Servo = servo_ns.class_('Servo', cg.Component) ServoWriteAction = servo_ns.class_('ServoWriteAction', automation.Action) ServoDetachAction = servo_ns.class_('ServoDetachAction', automation.Action) +CONF_AUTO_DETACH_TIME = 'auto_detach_time' MULTI_CONF = True CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_ID): cv.declare_id(Servo), @@ -19,6 +20,8 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_IDLE_LEVEL, default='7.5%'): cv.percentage, cv.Optional(CONF_MAX_LEVEL, default='12%'): cv.percentage, cv.Optional(CONF_RESTORE, default=False): cv.boolean, + cv.Optional(CONF_AUTO_DETACH_TIME, default='0s'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds }).extend(cv.COMPONENT_SCHEMA) @@ -32,6 +35,8 @@ def to_code(config): cg.add(var.set_idle_level(config[CONF_IDLE_LEVEL])) cg.add(var.set_max_level(config[CONF_MAX_LEVEL])) cg.add(var.set_restore(config[CONF_RESTORE])) + cg.add(var.set_auto_detach_time(config[CONF_AUTO_DETACH_TIME])) + cg.add(var.set_transition_length(config[CONF_TRANSITION_LENGTH])) @automation.register_action('servo.write', ServoWriteAction, cv.Schema({ diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index 2fade280ee..5d2a9ee236 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -13,6 +13,64 @@ void Servo::dump_config() { ESP_LOGCONFIG(TAG, " Idle Level: %.1f%%", this->idle_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Min Level: %.1f%%", this->min_level_ * 100.0f); ESP_LOGCONFIG(TAG, " Max Level: %.1f%%", this->max_level_ * 100.0f); + ESP_LOGCONFIG(TAG, " auto detach time: %d ms", this->auto_detach_time_); + ESP_LOGCONFIG(TAG, " run duration: %d ms", this->transition_length_); +} + +void Servo::loop() { + // check if auto_detach_time_ is set and servo reached target + if (this->auto_detach_time_ && this->state_ == STATE_TARGET_REACHED) { + if (millis() - this->start_millis_ > this->auto_detach_time_) { + this->detach(); + this->start_millis_ = 0; + this->state_ = STATE_DETACHED; + ESP_LOGD(TAG, "Servo detached on auto_detach_time"); + } + } + if (this->target_value_ != this->current_value_ && this->state_ == STATE_ATTACHED) { + if (this->transition_length_) { + float new_value; + float travel_diff = this->target_value_ - this->source_value_; + uint32_t target_runtime = target_runtime = abs((int) ((travel_diff) * this->transition_length_ * 1.0f / 2.0f)); + uint32_t current_runtime = millis() - this->start_millis_; + float percentage_run = current_runtime * 1.0f / target_runtime * 1.0f; + if (percentage_run > 1.0f) { + percentage_run = 1.0f; + } + new_value = this->target_value_ - (1.0f - percentage_run) * (this->target_value_ - this->source_value_); + this->internal_write(new_value); + } else { + this->internal_write(this->target_value_); + } + } + if (this->target_value_ == this->current_value_ && this->state_ == STATE_ATTACHED) { + this->state_ = STATE_TARGET_REACHED; + this->start_millis_ = millis(); // set current stamp for potential auto_detach_time_ check + ESP_LOGD(TAG, "Servo reached target"); + } +} + +void Servo::write(float value) { + value = clamp(value, -1.0f, 1.0f); + this->target_value_ = value; + this->source_value_ = this->current_value_; + this->state_ = STATE_ATTACHED; + this->start_millis_ = millis(); + ESP_LOGD(TAG, "Servo new target: %f", value); +} + +void Servo::internal_write(float value) { + value = clamp(value, -1.0f, 1.0f); + float level; + if (value < 0.0) + level = lerp(-value, this->idle_level_, this->min_level_); + else + level = lerp(value, this->idle_level_, this->max_level_); + this->output_->set_level(level); + if (this->target_value_ == this->current_value_) { + this->save_level_(level); + } + this->current_value_ = value; } } // namespace servo diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index b864efc877..937706d44d 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -14,18 +14,9 @@ extern uint32_t global_servo_id; class Servo : public Component { public: void set_output(output::FloatOutput *output) { output_ = output; } - void write(float value) { - value = clamp(value, -1.0f, 1.0f); - - float level; - if (value < 0.0) - level = lerp(-value, this->idle_level_, this->min_level_); - else - level = lerp(value, this->idle_level_, this->max_level_); - - this->output_->set_level(level); - this->save_level_(level); - } + void loop() override; + void write(float value); + void internal_write(float value); void detach() { this->output_->set_level(0.0f); this->save_level_(0.0f); @@ -48,6 +39,8 @@ class Servo : public Component { void set_idle_level(float idle_level) { idle_level_ = idle_level; } void set_max_level(float max_level) { max_level_ = max_level; } void set_restore(bool restore) { restore_ = restore; } + void set_auto_detach_time(uint32_t auto_detach_time) { auto_detach_time_ = auto_detach_time; } + void set_transition_length(uint32_t transition_length) { transition_length_ = transition_length; } protected: void save_level_(float v) { this->rtc_.save(&v); } @@ -57,7 +50,19 @@ class Servo : public Component { float idle_level_ = 0.0750f; float max_level_ = 0.1200f; bool restore_{false}; + uint32_t auto_detach_time_ = 0; + uint32_t transition_length_ = 0; ESPPreferenceObject rtc_; + uint8_t state_; + float target_value_ = 0; + float source_value_ = 0; + float current_value_ = 0; + uint32_t start_millis_ = 0; + enum State { + STATE_ATTACHED = 0, + STATE_DETACHED = 1, + STATE_TARGET_REACHED = 2, + }; }; template class ServoWriteAction : public Action { diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py index de80c38214..369fc41fac 100644 --- a/esphome/components/sn74hc595/__init__.py +++ b/esphome/components/sn74hc595/__init__.py @@ -34,8 +34,9 @@ def to_code(config): cg.add(var.set_clock_pin(clock_pin)) latch_pin = yield cg.gpio_pin_expression(config[CONF_LATCH_PIN]) cg.add(var.set_latch_pin(latch_pin)) - oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) - cg.add(var.set_oe_pin(oe_pin)) + if CONF_OE_PIN in config: + oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe_pin)) cg.add(var.set_sr_count(config[CONF_SR_COUNT])) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 253f3ba2c1..4050609dd8 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -42,6 +42,7 @@ void SNTPComponent::dump_config() { ESP_LOGCONFIG(TAG, " Server 3: '%s'", this->server_3_.c_str()); ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str()); } +void SNTPComponent::update() {} void SNTPComponent::loop() { if (this->has_time_) return; @@ -53,6 +54,7 @@ void SNTPComponent::loop() { char buf[128]; time.strftime(buf, sizeof(buf), "%c"); ESP_LOGD(TAG, "Synchronized time: %s", buf); + this->time_sync_callback_.call(); this->has_time_ = true; } diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index 785f458d6c..4c70a6b09f 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -24,6 +24,7 @@ class SNTPComponent : public time::RealTimeClock { } float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + void update() override; void loop() override; protected: diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index ba726f8052..64364d70c9 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -50,11 +50,15 @@ enum SPIClockPhase { */ enum SPIDataRate : uint32_t { DATA_RATE_1KHZ = 1000, + DATA_RATE_75KHZ = 75000, DATA_RATE_200KHZ = 200000, DATA_RATE_1MHZ = 1000000, DATA_RATE_2MHZ = 2000000, DATA_RATE_4MHZ = 4000000, DATA_RATE_8MHZ = 8000000, + DATA_RATE_10MHZ = 10000000, + DATA_RATE_20MHZ = 20000000, + DATA_RATE_40MHZ = 40000000, }; class SPIComponent : public Component { diff --git a/esphome/components/ssd1322_base/__init__.py b/esphome/components/ssd1322_base/__init__.py new file mode 100644 index 0000000000..addfec4153 --- /dev/null +++ b/esphome/components/ssd1322_base/__init__.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, \ + CONF_RESET_PIN +from esphome.core import coroutine + +CODEOWNERS = ['@kbx81'] + +ssd1322_base_ns = cg.esphome_ns.namespace('ssd1322_base') +SSD1322 = ssd1322_base_ns.class_('SSD1322', cg.PollingComponent, display.DisplayBuffer) +SSD1322Model = ssd1322_base_ns.enum('SSD1322Model') + +MODELS = { + 'SSD1322_256X64': SSD1322Model.SSD1322_MODEL_256_64, +} + +SSD1322_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1322_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1322_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1322(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_EXTERNAL_VCC in config: + cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1322_base/ssd1322_base.cpp b/esphome/components/ssd1322_base/ssd1322_base.cpp new file mode 100644 index 0000000000..9f382190a6 --- /dev/null +++ b/esphome/components/ssd1322_base/ssd1322_base.cpp @@ -0,0 +1,204 @@ +#include "ssd1322_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1322_base { + +static const char *TAG = "ssd1322"; + +static const uint8_t SSD1322_MAX_CONTRAST = 255; +static const uint8_t SSD1322_COLORMASK = 0x0f; +static const uint8_t SSD1322_COLORSHIFT = 4; +static const uint8_t SSD1322_PIXELSPERBYTE = 2; + +static const uint8_t SSD1322_ENABLEGRAYSCALETABLE = 0x00; +static const uint8_t SSD1322_SETCOLUMNADDRESS = 0x15; +static const uint8_t SSD1322_WRITERAM = 0x5C; +static const uint8_t SSD1322_READRAM = 0x5D; +static const uint8_t SSD1322_SETROWADDRESS = 0x75; +static const uint8_t SSD1322_SETREMAP = 0xA0; +static const uint8_t SSD1322_SETSTARTLINE = 0xA1; +static const uint8_t SSD1322_SETOFFSET = 0xA2; +static const uint8_t SSD1322_SETMODEALLOFF = 0xA4; +static const uint8_t SSD1322_SETMODEALLON = 0xA5; +static const uint8_t SSD1322_SETMODENORMAL = 0xA6; +static const uint8_t SSD1322_SETMODEINVERTED = 0xA7; +static const uint8_t SSD1322_ENABLEPARTIALDISPLAY = 0xA8; +static const uint8_t SSD1322_EXITPARTIALDISPLAY = 0xA9; +static const uint8_t SSD1322_SETFUNCTIONSELECTION = 0xAB; +static const uint8_t SSD1322_SETDISPLAYOFF = 0xAE; +static const uint8_t SSD1322_SETDISPLAYON = 0xAF; +static const uint8_t SSD1322_SETPHASELENGTH = 0xB1; +static const uint8_t SSD1322_SETFRONTCLOCKDIVIDER = 0xB3; +static const uint8_t SSD1322_DISPLAYENHANCEMENTA = 0xB4; +static const uint8_t SSD1322_SETGPIO = 0xB5; +static const uint8_t SSD1322_SETSECONDPRECHARGEPERIOD = 0xB6; +static const uint8_t SSD1322_SETGRAYSCALETABLE = 0xB8; +static const uint8_t SSD1322_SELECTDEFAULTLINEARGRAYSCALETABLE = 0xB9; +static const uint8_t SSD1322_SETPRECHARGEVOLTAGE = 0xBB; +static const uint8_t SSD1322_SETVCOMHVOLTAGE = 0xBE; +static const uint8_t SSD1322_SETCONTRAST = 0xC1; +static const uint8_t SSD1322_MASTERCURRENTCONTROL = 0xC7; +static const uint8_t SSD1322_SETMULTIPLEXRATIO = 0xCA; +static const uint8_t SSD1322_DISPLAYENHANCEMENTB = 0xD1; +static const uint8_t SSD1322_SETCOMMANDLOCK = 0xFD; + +static const uint8_t SSD1322_SETCOMMANDLOCK_UNLOCK = 0x12; +static const uint8_t SSD1322_SETCOMMANDLOCK_LOCK = 0x16; + +void SSD1322::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1322_SETCOMMANDLOCK); + this->data(SSD1322_SETCOMMANDLOCK_UNLOCK); + this->turn_off(); + this->command(SSD1322_SETFRONTCLOCKDIVIDER); + this->data(0x91); + this->command(SSD1322_SETMULTIPLEXRATIO); + this->data(0x3F); + this->command(SSD1322_SETOFFSET); + this->data(0x00); + this->command(SSD1322_SETSTARTLINE); + this->data(0x00); + this->command(SSD1322_SETREMAP); + this->data(0x14); + this->data(0x11); + this->command(SSD1322_SETGPIO); + this->data(0x00); + this->command(SSD1322_SETFUNCTIONSELECTION); + this->data(0x01); + this->command(SSD1322_DISPLAYENHANCEMENTA); + this->data(0xA0); + this->data(0xFD); + this->command(SSD1322_MASTERCURRENTCONTROL); + this->data(0x0F); + this->command(SSD1322_SETPHASELENGTH); + this->data(0xE2); + this->command(SSD1322_DISPLAYENHANCEMENTB); + this->data(0x82); + this->data(0x20); + this->command(SSD1322_SETPRECHARGEVOLTAGE); + this->data(0x1F); + this->command(SSD1322_SETSECONDPRECHARGEPERIOD); + this->data(0x08); + this->command(SSD1322_SETVCOMHVOLTAGE); + this->data(0x07); + this->command(SSD1322_SETMODENORMAL); + this->command(SSD1322_EXITPARTIALDISPLAY); + // this->command(SSD1322_SELECTDEFAULTLINEARGRAYSCALETABLE); + this->command(SSD1322_SETGRAYSCALETABLE); + // gamma ~2.2 + this->data(24); + this->data(29); + this->data(36); + this->data(43); + this->data(51); + this->data(60); + this->data(70); + this->data(81); + this->data(93); + this->data(105); + this->data(118); + this->data(132); + this->data(147); + this->data(163); + this->data(180); + this->command(SSD1322_ENABLEGRAYSCALETABLE); + set_brightness(this->brightness_); + this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1322::display() { + this->command(SSD1322_SETCOLUMNADDRESS); // set column address + this->data(0x1C); // set column start address + this->data(0x5B); // set column end address + this->command(SSD1322_SETROWADDRESS); // set row address + this->data(0x00); // set row start address + this->data(0x3F); // set last row + this->command(SSD1322_WRITERAM); // write + + this->write_display_data(); +} +void SSD1322::update() { + this->do_update_(); + this->display(); +} +void SSD1322::set_brightness(float brightness) { + this->brightness_ = clamp(brightness, 0, 1); + // now write the new brightness level to the display + this->command(SSD1322_SETCONTRAST); + this->data(int(SSD1322_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1322::is_on() { return this->is_on_; } +void SSD1322::turn_on() { + this->command(SSD1322_SETDISPLAYON); + this->is_on_ = true; +} +void SSD1322::turn_off() { + this->command(SSD1322_SETDISPLAYOFF); + this->is_on_ = false; +} +int SSD1322::get_height_internal() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return 64; + default: + return 0; + } +} +int SSD1322::get_width_internal() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return 256; + default: + return 0; + } +} +size_t SSD1322::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1322_PIXELSPERBYTE; +} +void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + uint32_t color4 = color.to_grayscale4(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1322_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1322_PIXELSPERBYTE); + uint8_t shift = (1u - (x % SSD1322_PIXELSPERBYTE)) * SSD1322_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1322_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; +} +void SSD1322::fill(Color color) { + const uint32_t color4 = color.to_grayscale4(); + uint8_t fill = (color4 & SSD1322_COLORMASK) | ((color4 & SSD1322_COLORMASK) << SSD1322_COLORSHIFT); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1322::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1322::model_str_() { + switch (this->model_) { + case SSD1322_MODEL_256_64: + return "SSD1322 256x64"; + default: + return "Unknown"; + } +} + +} // namespace ssd1322_base +} // namespace esphome diff --git a/esphome/components/ssd1322_base/ssd1322_base.h b/esphome/components/ssd1322_base/ssd1322_base.h new file mode 100644 index 0000000000..125e374246 --- /dev/null +++ b/esphome/components/ssd1322_base/ssd1322_base.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1322_base { + +enum SSD1322Model { + SSD1322_MODEL_256_64 = 0, +}; + +class SSD1322 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1322Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1322Model model_{SSD1322_MODEL_256_64}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1322_base +} // namespace esphome diff --git a/esphome/components/ssd1322_spi/__init__.py b/esphome/components/ssd1322_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1322_spi/display.py b/esphome/components/ssd1322_spi/display.py new file mode 100644 index 0000000000..cf900e9e2d --- /dev/null +++ b/esphome/components/ssd1322_spi/display.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1322_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ['@kbx81'] + +AUTO_LOAD = ['ssd1322_base'] +DEPENDENCIES = ['spi'] + +ssd1322_spi = cg.esphome_ns.namespace('ssd1322_spi') +SPISSD1322 = ssd1322_spi.class_('SPISSD1322', ssd1322_base.SSD1322, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1322_base.SSD1322_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1322), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1322_base.setup_ssd1322(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.cpp b/esphome/components/ssd1322_spi/ssd1322_spi.cpp new file mode 100644 index 0000000000..561a2e7a71 --- /dev/null +++ b/esphome/components/ssd1322_spi/ssd1322_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1322_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1322_spi { + +static const char *TAG = "ssd1322_spi"; + +void SPISSD1322::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1322..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1322::setup(); +} +void SPISSD1322::dump_config() { + LOG_DISPLAY("", "SPI SSD1322", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1322::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1322::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1322::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1322_spi +} // namespace esphome diff --git a/esphome/components/ssd1322_spi/ssd1322_spi.h b/esphome/components/ssd1322_spi/ssd1322_spi.h new file mode 100644 index 0000000000..316742706e --- /dev/null +++ b/esphome/components/ssd1322_spi/ssd1322_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1322_base/ssd1322_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1322_spi { + +class SPISSD1322 : public ssd1322_base::SSD1322, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1322_spi +} // namespace esphome diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 6cb0dafe54..e51f67e8b4 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -6,6 +6,8 @@ from esphome.const import CONF_BRIGHTNESS, CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_ CONF_RESET_PIN from esphome.core import coroutine +CODEOWNERS = ['@kbx81'] + ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer) SSD1325Model = ssd1325_base_ns.enum('SSD1325Model') diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index 2d8e91c3df..9471cf9c76 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import spi, ssd1325_base from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES +CODEOWNERS = ['@kbx81'] + AUTO_LOAD = ['ssd1325_base'] DEPENDENCIES = ['spi'] diff --git a/esphome/components/ssd1327_base/__init__.py b/esphome/components/ssd1327_base/__init__.py new file mode 100644 index 0000000000..ee282f215e --- /dev/null +++ b/esphome/components/ssd1327_base/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +CODEOWNERS = ['@kbx81'] + +ssd1327_base_ns = cg.esphome_ns.namespace('ssd1327_base') +SSD1327 = ssd1327_base_ns.class_('SSD1327', cg.PollingComponent, display.DisplayBuffer) +SSD1327Model = ssd1327_base_ns.enum('SSD1327Model') + +MODELS = { + 'SSD1327_128X128': SSD1327Model.SSD1327_MODEL_128_128, +} + +SSD1327_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1327_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1327_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1327(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1327_base/ssd1327_base.cpp b/esphome/components/ssd1327_base/ssd1327_base.cpp new file mode 100644 index 0000000000..debe2455ff --- /dev/null +++ b/esphome/components/ssd1327_base/ssd1327_base.cpp @@ -0,0 +1,178 @@ +#include "ssd1327_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1327_base { + +static const char *TAG = "ssd1327"; + +static const uint8_t SSD1327_MAX_CONTRAST = 127; +static const uint8_t SSD1327_COLORMASK = 0x0f; +static const uint8_t SSD1327_COLORSHIFT = 4; +static const uint8_t SSD1327_PIXELSPERBYTE = 2; + +static const uint8_t SSD1327_SETCOLUMNADDRESS = 0x15; +static const uint8_t SSD1327_SETROWADDRESS = 0x75; +static const uint8_t SSD1327_SETCONTRAST = 0x81; +static const uint8_t SSD1327_SETREMAP = 0xA0; +static const uint8_t SSD1327_SETSTARTLINE = 0xA1; +static const uint8_t SSD1327_SETOFFSET = 0xA2; +static const uint8_t SSD1327_NORMALDISPLAY = 0xA4; +static const uint8_t SSD1327_DISPLAYALLON = 0xA5; +static const uint8_t SSD1327_DISPLAYALLOFF = 0xA6; +static const uint8_t SSD1327_INVERTDISPLAY = 0xA7; +static const uint8_t SSD1327_SETMULTIPLEX = 0xA8; +static const uint8_t SSD1327_FUNCTIONSELECTIONA = 0xAB; +static const uint8_t SSD1327_DISPLAYOFF = 0xAE; +static const uint8_t SSD1327_DISPLAYON = 0xAF; +static const uint8_t SSD1327_SETPHASELENGTH = 0xB1; +static const uint8_t SSD1327_SETFRONTCLOCKDIVIDER = 0xB3; +static const uint8_t SSD1327_SETGPIO = 0xB5; +static const uint8_t SSD1327_SETSECONDPRECHARGEPERIOD = 0xB6; +static const uint8_t SSD1327_SETGRAYSCALETABLE = 0xB8; +static const uint8_t SSD1327_SELECTDEFAULTLINEARGRAYSCALETABLE = 0xB9; +static const uint8_t SSD1327_SETPRECHARGEVOLTAGE = 0xBC; +static const uint8_t SSD1327_SETVCOMHVOLTAGE = 0xBE; +static const uint8_t SSD1327_FUNCTIONSELECTIONB = 0xD5; +static const uint8_t SSD1327_SETCOMMANDLOCK = 0xFD; +static const uint8_t SSD1327_HORIZONTALSCROLLRIGHTSETUP = 0x26; +static const uint8_t SSD1327_HORIZONTALSCROLLLEFTSETUP = 0x27; +static const uint8_t SSD1327_DEACTIVATESCROLL = 0x2E; +static const uint8_t SSD1327_ACTIVATESCROLL = 0x2F; + +void SSD1327::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->turn_off(); // display OFF + this->command(SSD1327_SETFRONTCLOCKDIVIDER); // set osc division + this->command(0xF1); // 145 + this->command(SSD1327_SETMULTIPLEX); // multiplex ratio + this->command(0x7f); // duty = height - 1 + this->command(SSD1327_SETOFFSET); // set display offset + this->command(0x00); // 0 + this->command(SSD1327_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1327_SETREMAP); // set segment remapping + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping + this->command(SSD1327_SETGRAYSCALETABLE); + // gamma ~2.2 + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + this->command(SSD1327_SETPHASELENGTH); + this->command(0x55); + this->command(SSD1327_SETVCOMHVOLTAGE); // Set High Voltage Level of COM Pin + this->command(0x1C); + this->command(SSD1327_NORMALDISPLAY); // set display mode + set_brightness(this->brightness_); + this->fill(COLOR_BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1327::display() { + this->command(SSD1327_SETCOLUMNADDRESS); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1327_SETROWADDRESS); // set row address + this->command(0x00); // set row start address + this->command(127); // set last row + + this->write_display_data(); +} +void SSD1327::update() { + if (!this->is_failed()) { + this->do_update_(); + this->display(); + } +} +void SSD1327::set_brightness(float brightness) { + // validation + this->brightness_ = clamp(brightness, 0, 1); + // now write the new brightness level to the display + this->command(SSD1327_SETCONTRAST); + this->command(int(SSD1327_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1327::is_on() { return this->is_on_; } +void SSD1327::turn_on() { + this->command(SSD1327_DISPLAYON); + this->is_on_ = true; +} +void SSD1327::turn_off() { + this->command(SSD1327_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1327::get_height_internal() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1327::get_width_internal() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1327::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1327_PIXELSPERBYTE; +} +void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + uint32_t color4 = color.to_grayscale4(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1327_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1327_PIXELSPERBYTE); + uint8_t shift = (x % SSD1327_PIXELSPERBYTE) * SSD1327_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1327_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; +} +void SSD1327::fill(Color color) { + const uint32_t color4 = color.to_grayscale4(); + uint8_t fill = (color4 & SSD1327_COLORMASK) | ((color4 & SSD1327_COLORMASK) << SSD1327_COLORSHIFT); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1327::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1327::model_str_() { + switch (this->model_) { + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1327_base +} // namespace esphome diff --git a/esphome/components/ssd1327_base/ssd1327_base.h b/esphome/components/ssd1327_base/ssd1327_base.h new file mode 100644 index 0000000000..03f360b258 --- /dev/null +++ b/esphome/components/ssd1327_base/ssd1327_base.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1327_base { + +enum SSD1327Model { + SSD1327_MODEL_128_128 = 0, +}; + +class SSD1327 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1327Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1327Model model_{SSD1327_MODEL_128_128}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1327_base +} // namespace esphome diff --git a/esphome/components/ssd1327_i2c/__init__.py b/esphome/components/ssd1327_i2c/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1327_i2c/display.py b/esphome/components/ssd1327_i2c/display.py new file mode 100644 index 0000000000..9caa0ce031 --- /dev/null +++ b/esphome/components/ssd1327_i2c/display.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import ssd1327_base, i2c +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ['@kbx81'] + +AUTO_LOAD = ['ssd1327_base'] +DEPENDENCIES = ['i2c'] + +ssd1327_i2c = cg.esphome_ns.namespace('ssd1327_i2c') +I2CSSD1327 = ssd1327_i2c.class_('I2CSSD1327', ssd1327_base.SSD1327, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.All(ssd1327_base.SSD1327_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(I2CSSD1327), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3D)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1327_base.setup_ssd1327(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp new file mode 100644 index 0000000000..f256c9df77 --- /dev/null +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.cpp @@ -0,0 +1,44 @@ +#include "ssd1327_i2c.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ssd1327_i2c { + +static const char *TAG = "ssd1327_i2c"; + +void I2CSSD1327::setup() { + ESP_LOGCONFIG(TAG, "Setting up I2C SSD1327..."); + this->init_reset_(); + + this->parent_->raw_begin_transmission(this->address_); + if (!this->parent_->raw_end_transmission(this->address_)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + SSD1327::setup(); +} +void I2CSSD1327::dump_config() { + LOG_DISPLAY("", "I2C SSD1327", this); + LOG_I2C_DEVICE(this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); + + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with SSD1327 failed!"); + } +} +void I2CSSD1327::command(uint8_t value) { this->write_byte(0x00, value); } +void HOT I2CSSD1327::write_display_data() { + for (uint32_t i = 0; i < this->get_buffer_length_();) { + uint8_t data[16]; + for (uint8_t &j : data) + j = this->buffer_[i++]; + this->write_bytes(0x40, data, sizeof(data)); + } +} + +} // namespace ssd1327_i2c +} // namespace esphome diff --git a/esphome/components/ssd1327_i2c/ssd1327_i2c.h b/esphome/components/ssd1327_i2c/ssd1327_i2c.h new file mode 100644 index 0000000000..dd292f9936 --- /dev/null +++ b/esphome/components/ssd1327_i2c/ssd1327_i2c.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1327_base/ssd1327_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ssd1327_i2c { + +class I2CSSD1327 : public ssd1327_base::SSD1327, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + protected: + void command(uint8_t value) override; + void write_display_data() override; + + enum ErrorCode { NONE = 0, COMMUNICATION_FAILED } error_code_{NONE}; +}; + +} // namespace ssd1327_i2c +} // namespace esphome diff --git a/esphome/components/ssd1327_spi/__init__.py b/esphome/components/ssd1327_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1327_spi/display.py b/esphome/components/ssd1327_spi/display.py new file mode 100644 index 0000000000..5e3d21dae5 --- /dev/null +++ b/esphome/components/ssd1327_spi/display.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1327_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ['@kbx81'] + +AUTO_LOAD = ['ssd1327_base'] +DEPENDENCIES = ['spi'] + +ssd1327_spi = cg.esphome_ns.namespace('ssd1327_spi') +SPISSD1327 = ssd1327_spi.class_('SPISSD1327', ssd1327_base.SSD1327, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1327_base.SSD1327_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1327), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=False)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1327_base.setup_ssd1327(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.cpp b/esphome/components/ssd1327_spi/ssd1327_spi.cpp new file mode 100644 index 0000000000..c10ce6e9c8 --- /dev/null +++ b/esphome/components/ssd1327_spi/ssd1327_spi.cpp @@ -0,0 +1,59 @@ +#include "ssd1327_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1327_spi { + +static const char *TAG = "ssd1327_spi"; + +void SPISSD1327::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1327..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1327::setup(); +} +void SPISSD1327::dump_config() { + LOG_DISPLAY("", "SPI SSD1327", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1327::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1327::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1327_spi +} // namespace esphome diff --git a/esphome/components/ssd1327_spi/ssd1327_spi.h b/esphome/components/ssd1327_spi/ssd1327_spi.h new file mode 100644 index 0000000000..6f7abea96f --- /dev/null +++ b/esphome/components/ssd1327_spi/ssd1327_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1327_base/ssd1327_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1327_spi { + +class SPISSD1327 : public ssd1327_base::SSD1327, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1327_spi +} // namespace esphome diff --git a/esphome/components/ssd1331_base/__init__.py b/esphome/components/ssd1331_base/__init__.py new file mode 100644 index 0000000000..f6423f4aaf --- /dev/null +++ b/esphome/components/ssd1331_base/__init__.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_RESET_PIN +from esphome.core import coroutine + +CODEOWNERS = ['@kbx81'] + +ssd1331_base_ns = cg.esphome_ns.namespace('ssd1331_base') +SSD1331 = ssd1331_base_ns.class_('SSD1331', cg.PollingComponent, display.DisplayBuffer) + +SSD1331_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1331(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1331_base/ssd1331_base.cpp b/esphome/components/ssd1331_base/ssd1331_base.cpp new file mode 100644 index 0000000000..1405184177 --- /dev/null +++ b/esphome/components/ssd1331_base/ssd1331_base.cpp @@ -0,0 +1,155 @@ +#include "ssd1331_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1331_base { + +static const char *TAG = "ssd1331"; + +static const uint16_t BLACK = 0; +static const uint16_t WHITE = 0xffff; +static const uint16_t SSD1331_COLORMASK = 0xffff; +static const uint8_t SSD1331_MAX_CONTRASTA = 0x91; +static const uint8_t SSD1331_MAX_CONTRASTB = 0x50; +static const uint8_t SSD1331_MAX_CONTRASTC = 0x7D; +static const uint8_t SSD1331_BYTESPERPIXEL = 2; +// SSD1331 Commands +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_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 +static const uint8_t SSD1331_MASTERCURRENT = 0x87; // Master current control +static const uint8_t SSD1331_SETREMAP = 0xA0; // Set re-map & data format +static const uint8_t SSD1331_STARTLINE = 0xA1; // Set display start line +static const uint8_t SSD1331_DISPLAYOFFSET = 0xA2; // Set display offset +static const uint8_t SSD1331_NORMALDISPLAY = 0xA4; // Set display to normal mode +static const uint8_t SSD1331_DISPLAYALLON = 0xA5; // Set entire display ON +static const uint8_t SSD1331_DISPLAYALLOFF = 0xA6; // Set entire display OFF +static const uint8_t SSD1331_INVERTDISPLAY = 0xA7; // Invert display +static const uint8_t SSD1331_SETMULTIPLEX = 0xA8; // Set multiplex ratio +static const uint8_t SSD1331_SETMASTER = 0xAD; // Set master configuration +static const uint8_t SSD1331_DISPLAYOFF = 0xAE; // Display OFF (sleep mode) +static const uint8_t SSD1331_DISPLAYON = 0xAF; // Normal Brightness Display ON +static const uint8_t SSD1331_POWERMODE = 0xB0; // Power save mode +static const uint8_t SSD1331_PRECHARGE = 0xB1; // Phase 1 and 2 period adjustment +static const uint8_t SSD1331_CLOCKDIV = 0xB3; // Set display clock divide ratio/oscillator frequency +static const uint8_t SSD1331_PRECHARGEA = 0x8A; // Set second pre-charge speed for color A +static const uint8_t SSD1331_PRECHARGEB = 0x8B; // Set second pre-charge speed for color B +static const uint8_t SSD1331_PRECHARGEC = 0x8C; // Set second pre-charge speed for color C +static const uint8_t SSD1331_PRECHARGELEVEL = 0xBB; // Set pre-charge voltage +static const uint8_t SSD1331_VCOMH = 0xBE; // Set Vcomh voltge + +void SSD1331::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1331_DISPLAYOFF); // 0xAE + this->command(SSD1331_SETREMAP); // 0xA0 + this->command(0x72); // RGB Color + this->command(SSD1331_STARTLINE); // 0xA1 + this->command(0x0); + this->command(SSD1331_DISPLAYOFFSET); // 0xA2 + this->command(0x0); + this->command(SSD1331_NORMALDISPLAY); // 0xA4 + this->command(SSD1331_SETMULTIPLEX); // 0xA8 + this->command(0x3F); // 0x3F 1/64 duty + this->command(SSD1331_SETMASTER); // 0xAD + this->command(0x8E); + this->command(SSD1331_POWERMODE); // 0xB0 + this->command(0x0B); + this->command(SSD1331_PRECHARGE); // 0xB1 + this->command(0x31); + this->command(SSD1331_CLOCKDIV); // 0xB3 + this->command(0xF0); // 7:4 = Oscillator Frequency, 3:0 = CLK Div Ratio, (A[3:0]+1 = 1..16) + this->command(SSD1331_PRECHARGEA); // 0x8A + this->command(0x64); + this->command(SSD1331_PRECHARGEB); // 0x8B + this->command(0x78); + this->command(SSD1331_PRECHARGEC); // 0x8C + this->command(0x64); + this->command(SSD1331_PRECHARGELEVEL); // 0xBB + this->command(0x3A); + this->command(SSD1331_VCOMH); // 0xBE + this->command(0x3E); + this->command(SSD1331_MASTERCURRENT); // 0x87 + this->command(0x06); + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1331::display() { + this->command(SSD1331_SETCOLUMN); // set column address + this->command(0x00); // set column start address + this->command(0x5F); // set column end address + this->command(SSD1331_SETROW); // set row address + this->command(0x00); // set row start address + this->command(0x3F); // set last row + this->write_display_data(); +} +void SSD1331::update() { + this->do_update_(); + this->display(); +} +void SSD1331::set_brightness(float brightness) { + // validation + this->brightness_ = clamp(brightness, 0, 1); + // now write the new brightness level to the display + this->command(SSD1331_CONTRASTA); // 0x81 + this->command(int(SSD1331_MAX_CONTRASTA * (this->brightness_))); + this->command(SSD1331_CONTRASTB); // 0x82 + this->command(int(SSD1331_MAX_CONTRASTB * (this->brightness_))); + this->command(SSD1331_CONTRASTC); // 0x83 + this->command(int(SSD1331_MAX_CONTRASTC * (this->brightness_))); +} +bool SSD1331::is_on() { return this->is_on_; } +void SSD1331::turn_on() { + this->command(SSD1331_DISPLAYON); + this->is_on_ = true; +} +void SSD1331::turn_off() { + this->command(SSD1331_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1331::get_height_internal() { return 64; } +int SSD1331::get_width_internal() { return 96; } +size_t SSD1331::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1331_BYTESPERPIXEL); +} +void HOT SSD1331::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = color.to_rgb_565(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1331_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1331::fill(Color color) { + const uint32_t color565 = color.to_rgb_565(); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1331::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +} // namespace ssd1331_base +} // namespace esphome diff --git a/esphome/components/ssd1331_base/ssd1331_base.h b/esphome/components/ssd1331_base/ssd1331_base.h new file mode 100644 index 0000000000..8d2bca5de0 --- /dev/null +++ b/esphome/components/ssd1331_base/ssd1331_base.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1331_base { + +class SSD1331 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1331_base +} // namespace esphome diff --git a/esphome/components/ssd1331_spi/__init__.py b/esphome/components/ssd1331_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1331_spi/display.py b/esphome/components/ssd1331_spi/display.py new file mode 100644 index 0000000000..c10d34539e --- /dev/null +++ b/esphome/components/ssd1331_spi/display.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1331_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +CODEOWNERS = ['@kbx81'] + +AUTO_LOAD = ['ssd1331_base'] +DEPENDENCIES = ['spi'] + +ssd1331_spi = cg.esphome_ns.namespace('ssd1331_spi') +SPISSD1331 = ssd1331_spi.class_('SPISSD1331', ssd1331_base.SSD1331, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1331_base.SSD1331_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1331), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1331_base.setup_ssd1331(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.cpp b/esphome/components/ssd1331_spi/ssd1331_spi.cpp new file mode 100644 index 0000000000..f618c6d368 --- /dev/null +++ b/esphome/components/ssd1331_spi/ssd1331_spi.cpp @@ -0,0 +1,58 @@ +#include "ssd1331_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1331_spi { + +static const char *TAG = "ssd1331_spi"; + +void SPISSD1331::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1331..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1331::setup(); +} +void SPISSD1331::dump_config() { + LOG_DISPLAY("", "SPI SSD1331", this); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1331::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1331::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1331_spi +} // namespace esphome diff --git a/esphome/components/ssd1331_spi/ssd1331_spi.h b/esphome/components/ssd1331_spi/ssd1331_spi.h new file mode 100644 index 0000000000..93b2e228b1 --- /dev/null +++ b/esphome/components/ssd1331_spi/ssd1331_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1331_base/ssd1331_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1331_spi { + +class SPISSD1331 : public ssd1331_base::SSD1331, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1331_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py index 198f81668e..3bff245b82 100644 --- a/esphome/components/ssd1351_base/__init__.py +++ b/esphome/components/ssd1351_base/__init__.py @@ -5,6 +5,8 @@ from esphome.components import display from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN from esphome.core import coroutine +CODEOWNERS = ['@kbx81'] + ssd1351_base_ns = cg.esphome_ns.namespace('ssd1351_base') SSD1351 = ssd1351_base_ns.class_('SSD1351', cg.PollingComponent, display.DisplayBuffer) SSD1351Model = ssd1351_base_ns.enum('SSD1351Model') diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py index 16b0d4387a..30b4c73085 100644 --- a/esphome/components/ssd1351_spi/display.py +++ b/esphome/components/ssd1351_spi/display.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import spi, ssd1351_base from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES +CODEOWNERS = ['@kbx81'] + AUTO_LOAD = ['ssd1351_base'] DEPENDENCIES = ['spi'] diff --git a/esphome/components/st7735/__init__.py b/esphome/components/st7735/__init__.py new file mode 100644 index 0000000000..9f1d58b671 --- /dev/null +++ b/esphome/components/st7735/__init__.py @@ -0,0 +1,2 @@ +import esphome.codegen as cg +st7735_ns = cg.esphome_ns.namespace('st7735') diff --git a/esphome/components/st7735/display.py b/esphome/components/st7735/display.py new file mode 100644 index 0000000000..902f9c8beb --- /dev/null +++ b/esphome/components/st7735/display.py @@ -0,0 +1,75 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi +from esphome.components import display +from esphome.core import coroutine +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, CONF_PAGES +from . import st7735_ns + +CODEOWNERS = ['@SenexCrenshaw'] + +DEPENDENCIES = ['spi'] + +CONF_DEVICEWIDTH = 'devicewidth' +CONF_DEVICEHEIGHT = 'deviceheight' +CONF_ROWSTART = 'rowstart' +CONF_COLSTART = 'colstart' +CONF_EIGHTBITCOLOR = 'eightbitcolor' +CONF_USEBGR = 'usebgr' + +SPIST7735 = st7735_ns.class_('ST7735', cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice) +ST7735Model = st7735_ns.enum('ST7735Model') + +MODELS = { + 'INITR_GREENTAB': ST7735Model.ST7735_INITR_GREENTAB, + 'INITR_REDTAB': ST7735Model.ST7735_INITR_REDTAB, + 'INITR_BLACKTAB': ST7735Model.ST7735_INITR_BLACKTAB, + 'INITR_MINI160X80': ST7735Model.ST7735_INITR_MINI_160X80, + 'INITR_18BLACKTAB': ST7735Model.ST7735_INITR_18BLACKTAB, + 'INITR_18REDTAB': ST7735Model.ST7735_INITR_18REDTAB +} +ST7735_MODEL = cv.enum(MODELS, upper=True, space="_") + + +ST7735_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): ST7735_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema +}).extend(cv.polling_component_schema('1s')) + +CONFIG_SCHEMA = cv.All(ST7735_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPIST7735), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DEVICEWIDTH): cv.int_, + cv.Required(CONF_DEVICEHEIGHT): cv.int_, + cv.Required(CONF_COLSTART): cv.int_, + cv.Required(CONF_ROWSTART): cv.int_, + cv.Optional(CONF_EIGHTBITCOLOR, default=False): cv.boolean, + cv.Optional(CONF_USEBGR, default=False): cv.boolean, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +@coroutine +def setup_st7735(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID], config[CONF_MODEL], config[CONF_DEVICEWIDTH], + config[CONF_DEVICEHEIGHT], config[CONF_COLSTART], config[CONF_ROWSTART], + config[CONF_EIGHTBITCOLOR], config[CONF_USEBGR]) + yield setup_st7735(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/st7735/st7735.cpp b/esphome/components/st7735/st7735.cpp new file mode 100644 index 0000000000..2a3d8fc903 --- /dev/null +++ b/esphome/components/st7735/st7735.cpp @@ -0,0 +1,484 @@ +#include "st7735.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace st7735 { + +static const uint8_t ST_CMD_DELAY = 0x80; // special signifier for command lists + +static const uint8_t ST77XX_NOP = 0x00; +static const uint8_t ST77XX_SWRESET = 0x01; +static const uint8_t ST77XX_RDDID = 0x04; +static const uint8_t ST77XX_RDDST = 0x09; + +static const uint8_t ST77XX_SLPIN = 0x10; +static const uint8_t ST77XX_SLPOUT = 0x11; +static const uint8_t ST77XX_PTLON = 0x12; +static const uint8_t ST77XX_NORON = 0x13; + +static const uint8_t ST77XX_INVOFF = 0x20; +static const uint8_t ST77XX_INVON = 0x21; +static const uint8_t ST77XX_DISPOFF = 0x28; +static const uint8_t ST77XX_DISPON = 0x29; +static const uint8_t ST77XX_CASET = 0x2A; +static const uint8_t ST77XX_RASET = 0x2B; +static const uint8_t ST77XX_RAMWR = 0x2C; +static const uint8_t ST77XX_RAMRD = 0x2E; + +static const uint8_t ST77XX_PTLAR = 0x30; +static const uint8_t ST77XX_TEOFF = 0x34; +static const uint8_t ST77XX_TEON = 0x35; +static const uint8_t ST77XX_MADCTL = 0x36; +static const uint8_t ST77XX_COLMOD = 0x3A; + +static const uint8_t ST77XX_MADCTL_MY = 0x80; +static const uint8_t ST77XX_MADCTL_MX = 0x40; +static const uint8_t ST77XX_MADCTL_MV = 0x20; +static const uint8_t ST77XX_MADCTL_ML = 0x10; +static const uint8_t ST77XX_MADCTL_RGB = 0x00; + +static const uint8_t ST77XX_RDID1 = 0xDA; +static const uint8_t ST77XX_RDID2 = 0xDB; +static const uint8_t ST77XX_RDID3 = 0xDC; +static const uint8_t ST77XX_RDID4 = 0xDD; + +// Some register settings +static const uint8_t ST7735_MADCTL_BGR = 0x08; + +static const uint8_t ST7735_MADCTL_MH = 0x04; + +static const uint8_t ST7735_FRMCTR1 = 0xB1; +static const uint8_t ST7735_FRMCTR2 = 0xB2; +static const uint8_t ST7735_FRMCTR3 = 0xB3; +static const uint8_t ST7735_INVCTR = 0xB4; +static const uint8_t ST7735_DISSET5 = 0xB6; + +static const uint8_t ST7735_PWCTR1 = 0xC0; +static const uint8_t ST7735_PWCTR2 = 0xC1; +static const uint8_t ST7735_PWCTR3 = 0xC2; +static const uint8_t ST7735_PWCTR4 = 0xC3; +static const uint8_t ST7735_PWCTR5 = 0xC4; +static const uint8_t ST7735_VMCTR1 = 0xC5; + +static const uint8_t ST7735_PWCTR6 = 0xFC; + +static const uint8_t ST7735_GMCTRP1 = 0xE0; +static const uint8_t ST7735_GMCTRN1 = 0xE1; + +// clang-format off +static const uint8_t PROGMEM + BCMD[] = { // Init commands for 7735B screens + 18, // 18 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, no args, w/delay + 50, // 50 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, no args, w/delay + 255, // 255 = max (500 ms) delay + ST77XX_COLMOD, 1+ST_CMD_DELAY, // 3: Set color mode, 1 arg + delay: + 0x05, // 16-bit color + 10, // 10 ms delay + ST7735_FRMCTR1, 3+ST_CMD_DELAY, // 4: Frame rate control, 3 args + delay: + 0x00, // fastest refresh + 0x06, // 6 lines front porch + 0x03, // 3 lines back porch + 10, // 10 ms delay + ST77XX_MADCTL, 1, // 5: Mem access ctl (directions), 1 arg: + 0x08, // Row/col addr, bottom-top refresh + ST7735_DISSET5, 2, // 6: Display settings #5, 2 args: + 0x15, // 1 clk cycle nonoverlap, 2 cycle gate + // rise, 3 cycle osc equalize + 0x02, // Fix on VTL + ST7735_INVCTR, 1, // 7: Display inversion control, 1 arg: + 0x0, // Line inversion + ST7735_PWCTR1, 2+ST_CMD_DELAY, // 8: Power control, 2 args + delay: + 0x02, // GVDD = 4.7V + 0x70, // 1.0uA + 10, // 10 ms delay + ST7735_PWCTR2, 1, // 9: Power control, 1 arg, no delay: + 0x05, // VGH = 14.7V, VGL = -7.35V + ST7735_PWCTR3, 2, // 10: Power control, 2 args, no delay: + 0x01, // Opamp current small + 0x02, // Boost frequency + ST7735_VMCTR1, 2+ST_CMD_DELAY, // 11: Power control, 2 args + delay: + 0x3C, // VCOMH = 4V + 0x38, // VCOML = -1.1V + 10, // 10 ms delay + ST7735_PWCTR6, 2, // 12: Power control, 2 args, no delay: + 0x11, 0x15, + ST7735_GMCTRP1,16, // 13: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x09, 0x16, 0x09, 0x20, // (Not entirely necessary, but provides + 0x21, 0x1B, 0x13, 0x19, // accurate colors) + 0x17, 0x15, 0x1E, 0x2B, + 0x04, 0x05, 0x02, 0x0E, + ST7735_GMCTRN1,16+ST_CMD_DELAY, // 14: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x0B, 0x14, 0x08, 0x1E, // (Not entirely necessary, but provides + 0x22, 0x1D, 0x18, 0x1E, // accurate colors) + 0x1B, 0x1A, 0x24, 0x2B, + 0x06, 0x06, 0x02, 0x0F, + 10, // 10 ms delay + ST77XX_CASET, 4, // 15: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 2 + 0x00, 0x81, // XEND = 129 + ST77XX_RASET, 4, // 16: Row addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 1 + 0x00, 0x81, // XEND = 160 + ST77XX_NORON, ST_CMD_DELAY, // 17: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 18: Main screen turn on, no args, delay + 255 }, // 255 = max (500 ms) delay + + RCMD1[] = { // 7735R init, part 1 (red or green tab) + 15, // 15 commands in list: + ST77XX_SWRESET, ST_CMD_DELAY, // 1: Software reset, 0 args, w/delay + 150, // 150 ms delay + ST77XX_SLPOUT, ST_CMD_DELAY, // 2: Out of sleep mode, 0 args, w/delay + 255, // 500 ms delay + ST7735_FRMCTR1, 3, // 3: Framerate ctrl - normal mode, 3 arg: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR2, 3, // 4: Framerate ctrl - idle mode, 3 args: + 0x01, 0x2C, 0x2D, // Rate = fosc/(1x2+40) * (LINE+2C+2D) + ST7735_FRMCTR3, 6, // 5: Framerate - partial mode, 6 args: + 0x01, 0x2C, 0x2D, // Dot inversion mode + 0x01, 0x2C, 0x2D, // Line inversion mode + ST7735_INVCTR, 1, // 6: Display inversion ctrl, 1 arg: + 0x07, // No inversion + ST7735_PWCTR1, 3, // 7: Power control, 3 args, no delay: + 0xA2, + 0x02, // -4.6V + 0x84, // AUTO mode + ST7735_PWCTR2, 1, // 8: Power control, 1 arg, no delay: + 0xC5, // VGH25=2.4C VGSEL=-10 VGH=3 * AVDD + ST7735_PWCTR3, 2, // 9: Power control, 2 args, no delay: + 0x0A, // Opamp current small + 0x00, // Boost frequency + ST7735_PWCTR4, 2, // 10: Power control, 2 args, no delay: + 0x8A, // BCLK/2, + 0x2A, // opamp current small & medium low + ST7735_PWCTR5, 2, // 11: Power control, 2 args, no delay: + 0x8A, 0xEE, + ST7735_VMCTR1, 1, // 12: Power control, 1 arg, no delay: + 0x0E, + ST77XX_INVOFF, 0, // 13: Don't invert display, no args + ST77XX_MADCTL, 1, // 14: Mem access ctl (directions), 1 arg: + 0xC8, // row/col addr, bottom-top refresh + ST77XX_COLMOD, 1, // 15: set color mode, 1 arg, no delay: + 0x05 }, // 16-bit color + + RCMD2GREEN[] = { // 7735R init, part 2 (green tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x02, // XSTART = 0 + 0x00, 0x7F+0x02, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x01, // XSTART = 0 + 0x00, 0x9F+0x01 }, // XEND = 159 + + RCMD2RED[] = { // 7735R init, part 2 (red tab only) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + RCMD2GREEN144[] = { // 7735R init, part 2 (green 1.44 tab) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F, // XEND = 127 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x7F }, // XEND = 127 + + RCMD2GREEN160X80[] = { // 7735R init, part 2 (mini 160x80) + 2, // 2 commands in list: + ST77XX_CASET, 4, // 1: Column addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x4F, // XEND = 79 + ST77XX_RASET, 4, // 2: Row addr set, 4 args, no delay: + 0x00, 0x00, // XSTART = 0 + 0x00, 0x9F }, // XEND = 159 + + RCMD3[] = { // 7735R init, part 3 (red or green tab) + 4, // 4 commands in list: + ST7735_GMCTRP1, 16 , // 1: Gamma Adjustments (pos. polarity), 16 args + delay: + 0x02, 0x1c, 0x07, 0x12, // (Not entirely necessary, but provides + 0x37, 0x32, 0x29, 0x2d, // accurate colors) + 0x29, 0x25, 0x2B, 0x39, + 0x00, 0x01, 0x03, 0x10, + ST7735_GMCTRN1, 16 , // 2: Gamma Adjustments (neg. polarity), 16 args + delay: + 0x03, 0x1d, 0x07, 0x06, // (Not entirely necessary, but provides + 0x2E, 0x2C, 0x29, 0x2D, // accurate colors) + 0x2E, 0x2E, 0x37, 0x3F, + 0x00, 0x00, 0x02, 0x10, + ST77XX_NORON, ST_CMD_DELAY, // 3: Normal display on, no args, w/delay + 10, // 10 ms delay + ST77XX_DISPON, ST_CMD_DELAY, // 4: Main screen turn on, no args w/delay + 100 }; // 100 ms delay + +// clang-format on +static const char *TAG = "st7735"; + +ST7735::ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, + boolean usebgr) { + model_ = model; + this->width_ = width; + this->height_ = height; + this->colstart_ = colstart; + this->rowstart_ = rowstart; + this->eightbitcolor_ = eightbitcolor; + this->usebgr_ = usebgr; +} + +void ST7735::setup() { + ESP_LOGCONFIG(TAG, "Setting up ST7735..."); + this->spi_setup(); + + this->dc_pin_->setup(); // OUTPUT + this->cs_->setup(); // OUTPUT + + this->dc_pin_->digital_write(true); + this->cs_->digital_write(true); + + this->init_reset_(); + delay(100); // NOLINT + + ESP_LOGD(TAG, " START"); + dump_config(); + ESP_LOGD(TAG, " END"); + + display_init_(RCMD1); + + if (this->model_ == INITR_GREENTAB) { + display_init_(RCMD2GREEN); + colstart_ == 0 ? colstart_ = 2 : colstart_; + rowstart_ == 0 ? rowstart_ = 1 : rowstart_; + } else if ((this->model_ == INITR_144GREENTAB) || (this->model_ == INITR_HALLOWING)) { + height_ == 0 ? height_ = ST7735_TFTHEIGHT_128 : height_; + width_ == 0 ? width_ = ST7735_TFTWIDTH_128 : width_; + display_init_(RCMD2GREEN144); + colstart_ == 0 ? colstart_ = 2 : colstart_; + rowstart_ == 0 ? rowstart_ = 3 : rowstart_; + } else if (this->model_ == INITR_MINI_160X80) { + height_ == 0 ? height_ = ST7735_TFTHEIGHT_160 : height_; + width_ == 0 ? width_ = ST7735_TFTWIDTH_80 : width_; + display_init_(RCMD2GREEN160X80); + colstart_ = 24; + rowstart_ = 0; // For default rotation 0 + } else { + // colstart, rowstart left at default '0' values + display_init_(RCMD2RED); + } + display_init_(RCMD3); + + uint8_t data = 0; + if (this->model_ != INITR_HALLOWING) { + uint8_t data = ST77XX_MADCTL_MX | ST77XX_MADCTL_MY; + } + if (this->usebgr_) { + data = data | ST7735_MADCTL_BGR; + } else { + data = data | ST77XX_MADCTL_RGB; + } + sendcommand_(ST77XX_MADCTL, &data, 1); + + this->init_internal_(this->get_buffer_length()); + memset(this->buffer_, 0x00, this->get_buffer_length()); +} + +void ST7735::update() { + this->do_update_(); + this->write_display_data_(); +} + +int ST7735::get_height_internal() { return height_; } + +int ST7735::get_width_internal() { return width_; } + +size_t ST7735::get_buffer_length() { + if (this->eightbitcolor_) { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()); + } + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +void HOT ST7735::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + if (this->eightbitcolor_) { + const uint32_t color332 = color.to_332(); + uint16_t pos = (x + y * this->get_width_internal()); + this->buffer_[pos] = color332; + } else { + const uint32_t color565 = color.to_565(); + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; + } +} + +void ST7735::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *ST7735::model_str_() { + switch (this->model_) { + case INITR_GREENTAB: + return "ST7735 GREENTAB"; + case INITR_REDTAB: + return "ST7735 REDTAB"; + case INITR_BLACKTAB: + return "ST7735 BLACKTAB"; + case INITR_MINI_160X80: + return "ST7735 MINI160x80"; + default: + return "Unknown"; + } +} + +void ST7735::display_init_(const uint8_t *addr) { + uint8_t num_commands, cmd, num_args; + uint16_t ms; + + num_commands = pgm_read_byte(addr++); // Number of commands to follow + while (num_commands--) { // For each command... + cmd = pgm_read_byte(addr++); // Read command + num_args = pgm_read_byte(addr++); // Number of args to follow + ms = num_args & ST_CMD_DELAY; // If hibit set, delay follows args + num_args &= ~ST_CMD_DELAY; // Mask out delay bit + this->sendcommand_(cmd, addr, num_args); + addr += num_args; + + if (ms) { + ms = pgm_read_byte(addr++); // Read post-command delay time (ms) + if (ms == 255) + ms = 500; // If 255, delay for 500 ms + delay(ms); + } + } +} + +void ST7735::dump_config() { + LOG_DISPLAY("", "ST7735", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGD(TAG, " Buffer Size: %zu", this->get_buffer_length()); + ESP_LOGD(TAG, " Height: %d", this->height_); + ESP_LOGD(TAG, " Width: %d", this->width_); + ESP_LOGD(TAG, " ColStart: %d", this->colstart_); + ESP_LOGD(TAG, " RowStart: %d", this->rowstart_); + LOG_UPDATE_INTERVAL(this); +} + +void HOT ST7735::writecommand_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void HOT ST7735::writedata_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +void HOT ST7735::sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->writecommand_(cmd); + this->senddata_(data_bytes, num_data_bytes); +} + +void HOT ST7735::senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes) { + this->dc_pin_->digital_write(true); // pull DC high to indicate data + this->cs_->digital_write(false); + this->enable(); + for (uint8_t i = 0; i < num_data_bytes; i++) { + this->transfer_byte(pgm_read_byte(data_bytes++)); // write byte - SPI library + } + this->cs_->digital_write(true); + this->disable(); +} + +void HOT ST7735::write_display_data_() { + uint16_t offsetx = colstart_; + uint16_t offsety = rowstart_; + + uint16_t x1 = offsetx; + uint16_t x2 = x1 + get_width_internal() - 1; + uint16_t y1 = offsety; + uint16_t y2 = y1 + get_height_internal() - 1; + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_CASET); + this->dc_pin_->digital_write(true); + this->spi_master_write_addr_(x1, x2); + + // set Page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_RASET); + this->dc_pin_->digital_write(true); + this->spi_master_write_addr_(y1, y2); + + // Memory Write + this->dc_pin_->digital_write(false); + this->write_byte(ST77XX_RAMWR); + this->dc_pin_->digital_write(true); + + if (this->eightbitcolor_) { + for (int line = 0; line < this->get_buffer_length(); line = line + this->get_width_internal()) { + for (int index = 0; index < this->get_width_internal(); ++index) { + auto color = Color(this->buffer_[index + line], Color::ColorOrder::COLOR_ORDER_RGB, + Color::ColorBitness::COLOR_BITNESS_332, true) + .to_565(); + this->write_byte((color >> 8) & 0xff); + this->write_byte(color & 0xff); + } + } + } else { + this->write_array(this->buffer_, this->get_buffer_length()); + } + this->disable(); +} + +void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) { + static uint8_t BYTE[4]; + BYTE[0] = (addr1 >> 8) & 0xFF; + BYTE[1] = addr1 & 0xFF; + BYTE[2] = (addr2 >> 8) & 0xFF; + BYTE[3] = addr2 & 0xFF; + + this->dc_pin_->digital_write(true); + this->write_array(BYTE, 4); +} + +void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) { + static uint8_t BYTE[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + BYTE[index++] = (color >> 8) & 0xFF; + BYTE[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(BYTE, size * 2); +} + +} // namespace st7735 +} // namespace esphome diff --git a/esphome/components/st7735/st7735.h b/esphome/components/st7735/st7735.h new file mode 100644 index 0000000000..11bcc746f0 --- /dev/null +++ b/esphome/components/st7735/st7735.h @@ -0,0 +1,87 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7735 { + +static const uint8_t ST7735_TFTWIDTH_128 = 128; // for 1.44 and mini^M +static const uint8_t ST7735_TFTWIDTH_80 = 80; // for mini^M +static const uint8_t ST7735_TFTHEIGHT_128 = 128; // for 1.44" display^M +static const uint8_t ST7735_TFTHEIGHT_160 = 160; // for 1.8" and mini display^M + +// some flags for initR() :( +static const uint8_t INITR_GREENTAB = 0x00; +static const uint8_t INITR_REDTAB = 0x01; +static const uint8_t INITR_BLACKTAB = 0x02; +static const uint8_t INITR_144GREENTAB = 0x01; +static const uint8_t INITR_MINI_160X80 = 0x04; +static const uint8_t INITR_HALLOWING = 0x05; +static const uint8_t INITR_18GREENTAB = INITR_GREENTAB; +static const uint8_t INITR_18REDTAB = INITR_REDTAB; +static const uint8_t INITR_18BLACKTAB = INITR_BLACKTAB; + +enum ST7735Model { + ST7735_INITR_GREENTAB = INITR_GREENTAB, + ST7735_INITR_REDTAB = INITR_REDTAB, + ST7735_INITR_BLACKTAB = INITR_BLACKTAB, + ST7735_INITR_MINI_160X80 = INITR_MINI_160X80, + ST7735_INITR_18BLACKTAB = INITR_18BLACKTAB, + ST7735_INITR_18REDTAB = INITR_18REDTAB +}; + +class ST7735 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + ST7735(ST7735Model model, int width, int height, int colstart, int rowstart, boolean eightbitcolor, boolean usebgr); + void dump_config() override; + void setup() override; + + void display(); + + void update() override; + + void set_model(ST7735Model model) { this->model_ = model; } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + void set_reset_pin(GPIOPin *value) { this->reset_pin_ = value; } + void set_dc_pin(GPIOPin *value) { dc_pin_ = value; } + size_t get_buffer_length(); + + protected: + void sendcommand_(uint8_t cmd, const uint8_t *data_bytes, uint8_t num_data_bytes); + void senddata_(const uint8_t *data_bytes, uint8_t num_data_bytes); + + void writecommand_(uint8_t value); + void writedata_(uint8_t value); + + void write_display_data_(); + + void init_reset_(); + void display_init_(const uint8_t *addr); + void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); + void draw_absolute_pixel_internal(int x, int y, Color color) override; + void spi_master_write_addr_(uint16_t addr1, uint16_t addr2); + void spi_master_write_color_(uint16_t color, uint16_t size); + + int get_width_internal() override; + int get_height_internal() override; + + const char *model_str_(); + + ST7735Model model_{ST7735_INITR_18BLACKTAB}; + uint8_t colstart_ = 0, rowstart_ = 0; + boolean eightbitcolor_ = false; + boolean usebgr_ = false; + int16_t width_ = 80, height_ = 80; // Watch heap size + + GPIOPin *reset_pin_{nullptr}; + GPIOPin *dc_pin_{nullptr}; +}; + +} // namespace st7735 +} // namespace esphome diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 36e5acaa72..5815ae599c 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -6,6 +6,8 @@ from esphome.const import CONF_BACKLIGHT_PIN, CONF_BRIGHTNESS, CONF_CS_PIN, CONF CONF_LAMBDA, CONF_RESET_PIN from . import st7789v_ns +CODEOWNERS = ['@kbx81'] + DEPENDENCIES = ['spi'] ST7789V = st7789v_ns.class_('ST7789V', cg.PollingComponent, spi.SPIDevice, diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index e8d6acbd1c..c61aaa7fc9 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -28,6 +28,7 @@ def validate_acceleration(value): try: value = float(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") if value <= 0: @@ -48,6 +49,7 @@ def validate_speed(value): try: value = float(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(f"Expected speed as floating point number, got {value}") if value <= 0: diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py new file mode 100644 index 0000000000..00ca592272 --- /dev/null +++ b/esphome/components/teleinfo/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@0hax'] diff --git a/esphome/components/teleinfo/sensor.py b/esphome/components/teleinfo/sensor.py new file mode 100644 index 0000000000..54b50a9921 --- /dev/null +++ b/esphome/components/teleinfo/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_ID, CONF_SENSOR, ICON_FLASH, UNIT_WATT_HOURS + +DEPENDENCIES = ['uart'] + +teleinfo_ns = cg.esphome_ns.namespace('teleinfo') +TeleInfo = teleinfo_ns.class_('TeleInfo', cg.PollingComponent, uart.UARTDevice) + +CONF_TAG_NAME = "tag_name" +TELEINFO_TAG_SCHEMA = cv.Schema({ + cv.Required(CONF_TAG_NAME): cv.string, + cv.Required(CONF_SENSOR): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_FLASH, 0) +}) + +CONF_TAGS = "tags" +CONF_HISTORICAL_MODE = "historical_mode" +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TeleInfo), + cv.Optional(CONF_HISTORICAL_MODE, default=False): cv.boolean, + cv.Optional(CONF_TAGS): cv.ensure_list(TELEINFO_TAG_SCHEMA), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +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) + + if CONF_TAGS in config: + for tag in config[CONF_TAGS]: + sens = yield sensor.new_sensor(tag[CONF_SENSOR]) + cg.add(var.register_teleinfo_sensor(tag[CONF_TAG_NAME], sens)) diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp new file mode 100644 index 0000000000..7c0a83d103 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -0,0 +1,184 @@ +#include "teleinfo.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace teleinfo { + +static const char *TAG = "teleinfo"; + +/* Helpers */ +static int get_field(char *dest, char *buf_start, char *buf_end, int sep) { + char *field_end; + int len; + + field_end = static_cast(memchr(buf_start, sep, buf_end - buf_start)); + if (!field_end) + return 0; + len = field_end - buf_start; + strncpy(dest, buf_start, len); + dest[len] = '\0'; + + return len; +} +/* TeleInfo methods */ +bool TeleInfo::check_crc_(const char *grp, const char *grp_end) { + int grp_len = grp_end - grp; + uint8_t raw_crc = grp[grp_len - 1]; + uint8_t crc_tmp = 0; + int i; + + for (i = 0; i < grp_len - checksum_area_end_; i++) + crc_tmp += grp[i]; + + crc_tmp &= 0x3F; + crc_tmp += 0x20; + if (raw_crc != crc_tmp) { + ESP_LOGE(TAG, "bad crc: got %d except %d", raw_crc, crc_tmp); + return false; + } + + return true; +} +bool TeleInfo::read_chars_until_(bool drop, uint8_t c) { + uint8_t received; + int j = 0; + + while (available() > 0 && j < 128) { + j++; + received = read(); + if (received == c) + return true; + if (drop) + continue; + /* + * Internal buffer is full, switch to OFF mode. + * Data will be retrieved on next update. + */ + if (buf_index_ >= (MAX_BUF_SIZE - 1)) { + ESP_LOGW(TAG, "Internal buffer full"); + state_ = OFF; + return false; + } + buf_[buf_index_++] = received; + } + + return false; +} +void TeleInfo::setup() { state_ = OFF; } +void TeleInfo::update() { + if (state_ == OFF) { + buf_index_ = 0; + state_ = ON; + } +} +void TeleInfo::loop() { + switch (state_) { + case OFF: + break; + case ON: + /* Dequeue chars until start frame (0x2) */ + if (read_chars_until_(true, 0x2)) + state_ = START_FRAME_RECEIVED; + break; + case START_FRAME_RECEIVED: + /* Dequeue chars until end frame (0x3) */ + if (read_chars_until_(false, 0x3)) + state_ = END_FRAME_RECEIVED; + break; + case END_FRAME_RECEIVED: + char *buf_finger; + char *grp_end; + char *buf_end; + int field_len; + + buf_finger = buf_; + buf_end = buf_ + buf_index_; + + /* Each frame is composed of multiple groups starting by 0xa(Line Feed) and ending by + * 0xd ('\r'). + * + * Historical mode: each group contains tag, data and a CRC separated by 0x20 (Space) + * 0xa | Tag | 0x20 | Data | 0x20 | CRC | 0xd + * ^^^^^^^^^^^^^^^^^^^^ + * Checksum is computed on the above in historical mode. + * + * Standard mode: each group contains tag, data and a CRC separated by 0x9 (\t) + * 0xa | Tag | 0x9 | Data | 0x9 | CRC | 0xd + * ^^^^^^^^^^^^^^^^^^^^^^^^^ + * Checksum is computed on the above in standard mode. + */ + while ((buf_finger = static_cast(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) && + ((buf_finger - buf_) < buf_index_)) { + /* Point to the first char of the group after 0xa */ + buf_finger += 1; + + /* Group len */ + grp_end = static_cast(memchr(buf_finger, 0xd, buf_end - buf_finger)); + if (!grp_end) { + ESP_LOGE(TAG, "No group found"); + break; + } + + if (!check_crc_(buf_finger, grp_end)) + break; + + /* Get tag */ + field_len = get_field(tag_, buf_finger, grp_end, separator_); + if (!field_len || field_len >= MAX_TAG_SIZE) { + ESP_LOGE(TAG, "Invalid tag."); + break; + } + + /* Advance buf_finger to after the tag and the separator. */ + buf_finger += field_len + 1; + + /* Get value (after next separator) */ + field_len = get_field(val_, buf_finger, grp_end, separator_); + if (!field_len || field_len >= MAX_VAL_SIZE) { + ESP_LOGE(TAG, "Invalid Value"); + break; + } + + /* Advance buf_finger to end of group */ + buf_finger += field_len + 1 + 1 + 1; + + publish_value_(std::string(tag_), std::string(val_)); + } + state_ = OFF; + break; + } +} +void TeleInfo::publish_value_(std::string tag, std::string val) { + /* It will return 0 if tag is not a float. */ + auto newval = parse_float(val); + for (auto element : teleinfo_sensors_) + if (tag == element->tag) + element->sensor->publish_state(*newval); +} +void TeleInfo::dump_config() { + ESP_LOGCONFIG(TAG, "TeleInfo:"); + for (auto element : teleinfo_sensors_) + LOG_SENSOR(" ", element->tag, element->sensor); + this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7); +} +TeleInfo::TeleInfo(bool historical_mode) { + if (historical_mode) { + /* + * Historical mode doesn't contain last separator between checksum and data. + */ + checksum_area_end_ = 2; + separator_ = 0x20; + baud_rate_ = 1200; + } else { + checksum_area_end_ = 1; + separator_ = 0x9; + baud_rate_ = 9600; + } +} +void TeleInfo::register_teleinfo_sensor(const char *tag, sensor::Sensor *sensor) { + const TeleinfoSensorElement *teleinfo_sensor = new TeleinfoSensorElement{tag, sensor}; + teleinfo_sensors_.push_back(teleinfo_sensor); +} + +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h new file mode 100644 index 0000000000..de9cf646c4 --- /dev/null +++ b/esphome/components/teleinfo/teleinfo.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace teleinfo { +/* + * 198 bytes should be enough to contain a full session in historical mode with + * three phases. But go with 1024 just to be sure. + */ +static const uint8_t MAX_TAG_SIZE = 64; +static const uint16_t MAX_VAL_SIZE = 256; +static const uint16_t MAX_BUF_SIZE = 1024; + +struct TeleinfoSensorElement { + const char *tag; + sensor::Sensor *sensor; +}; + +class TeleInfo : public PollingComponent, public uart::UARTDevice { + public: + TeleInfo(bool historical_mode); + void register_teleinfo_sensor(const char *tag, sensor::Sensor *sensors); + void loop() override; + void setup() override; + void update() override; + void dump_config() override; + std::vector teleinfo_sensors_{}; + + protected: + uint32_t baud_rate_; + int checksum_area_end_; + int separator_; + char buf_[MAX_BUF_SIZE]; + uint32_t buf_index_{0}; + char tag_[MAX_TAG_SIZE]; + char val_[MAX_VAL_SIZE]; + enum State { + OFF, + ON, + START_FRAME_RECEIVED, + END_FRAME_RECEIVED, + } state_{OFF}; + bool read_chars_until_(bool drop, uint8_t c); + bool check_crc_(const char *grp, const char *grp_end); + void publish_value_(std::string tag, std::string val); +}; +} // namespace teleinfo +} // namespace esphome diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index c9cb81194c..bd8633ee1c 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -11,6 +11,8 @@ from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CO CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION +CODEOWNERS = ['@kbx81'] + thermostat_ns = cg.esphome_ns.namespace('thermostat') ThermostatClimate = thermostat_ns.class_('ThermostatClimate', climate.Climate, cg.Component) ThermostatClimateTargetTempConfig = thermostat_ns.struct('ThermostatClimateTargetTempConfig') diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 49ed53c47e..5f30a8f2ee 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -10,10 +10,11 @@ import tzlocal import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation -from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOURS, \ - CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ - CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE +from esphome.const import CONF_ID, CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOURS, \ + CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_ON_TIME_SYNC, CONF_SECONDS, CONF_TIMEZONE, \ + CONF_TRIGGER_ID, CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE from esphome.core import coroutine, coroutine_with_priority +from esphome.automation import Condition _LOGGER = logging.getLogger(__name__) @@ -21,9 +22,11 @@ CODEOWNERS = ['@OttoWinter'] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace('time') -RealTimeClock = time_ns.class_('RealTimeClock', cg.Component) +RealTimeClock = time_ns.class_('RealTimeClock', cg.PollingComponent) CronTrigger = time_ns.class_('CronTrigger', automation.Trigger.template(), cg.Component) +SyncTrigger = time_ns.class_('SyncTrigger', automation.Trigger.template(), cg.Component) ESPTime = time_ns.struct('ESPTime') +TimeHasTimeCondition = time_ns.class_('TimeHasTimeCondition', Condition) def _tz_timedelta(td): @@ -138,6 +141,7 @@ def _parse_cron_int(value, special_mapping, message): try: return int(value) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid(message.format(value)) @@ -158,6 +162,7 @@ def _parse_cron_part(part, min_value, max_value, special_mapping): try: repeat_n = int(repeat) except ValueError: + # pylint: disable=raise-missing-from raise cv.Invalid("Repeat for '/' time expression must be an integer, got {}" .format(repeat)) return set(range(offset_n, max_value + 1, repeat_n)) @@ -290,7 +295,10 @@ TIME_SCHEMA = cv.Schema({ cv.Optional(CONF_CRON): validate_cron_raw, cv.Optional(CONF_AT): validate_time_at, }, validate_cron_keys), -}) + cv.Optional(CONF_ON_TIME_SYNC): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SyncTrigger), + }), +}).extend(cv.polling_component_schema('15min')) @coroutine @@ -316,6 +324,12 @@ def setup_time_core_(time_var, config): yield cg.register_component(trigger, conf) yield automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TIME_SYNC, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) + + yield cg.register_component(trigger, conf) + yield automation.build_automation(trigger, [], conf) + @coroutine def register_time(time_var, config): @@ -326,3 +340,11 @@ def register_time(time_var, config): def to_code(config): cg.add_define('USE_TIME') cg.add_global(time_ns.using) + + +@automation.register_condition('time.has_time', TimeHasTimeCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(RealTimeClock), +})) +def time_has_time_to_code(config, condition_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 1232e6f834..8e41fba0da 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -75,5 +75,9 @@ void CronTrigger::add_days_of_week(const std::vector &days_of_week) { } float CronTrigger::get_setup_priority() const { return setup_priority::HARDWARE; } +SyncTrigger::SyncTrigger(RealTimeClock *rtc) : rtc_(rtc) { + rtc->add_on_time_sync_callback([this]() { this->trigger(); }); +} + } // namespace time } // namespace esphome diff --git a/esphome/components/time/automation.h b/esphome/components/time/automation.h index 978d25fbd4..6167aac4f7 100644 --- a/esphome/components/time/automation.h +++ b/esphome/components/time/automation.h @@ -37,5 +37,12 @@ class CronTrigger : public Trigger<>, public Component { optional last_check_; }; +class SyncTrigger : public Trigger<>, public Component { + public: + explicit SyncTrigger(RealTimeClock *rtc); + + protected: + RealTimeClock *rtc_; +}; } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index cdcfcb14ad..44ff505ecc 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -15,7 +15,7 @@ RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { setenv("TZ", this->timezone_.c_str(), 1); tzset(); - this->setup(); + PollingComponent::call_setup(); } void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { @@ -38,6 +38,8 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { char buf[128]; time.strftime(buf, sizeof(buf), "%c"); ESP_LOGD(TAG, "Synchronized time: %s", buf); + + this->time_sync_callback_.call(); } size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 9f40fdc5b5..a809401c33 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" #include #include #include @@ -105,7 +106,7 @@ struct ESPTime { /// The C library (newlib) available on ESPs only supports TZ strings that specify an offset and DST info; /// you cannot specify zone names or paths to zoneinfo files. /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html -class RealTimeClock : public Component { +class RealTimeClock : public PollingComponent { public: explicit RealTimeClock(); @@ -126,11 +127,26 @@ class RealTimeClock : public Component { void call_setup() override; + void add_on_time_sync_callback(std::function callback) { + this->time_sync_callback_.add(std::move(callback)); + }; + protected: /// Report a unix epoch as current time. void synchronize_epoch_(uint32_t epoch); std::string timezone_{}; + + CallbackManager time_sync_callback_; +}; + +template class TimeHasTimeCondition : public Condition { + public: + TimeHasTimeCondition(RealTimeClock *parent) : parent_(parent) {} + bool check(Ts... x) override { return this->parent_->now().is_valid(); } + + protected: + RealTimeClock *parent_; }; } // namespace time diff --git a/esphome/components/tmp102/__init__.py b/esphome/components/tmp102/__init__.py new file mode 100644 index 0000000000..3e32a230f2 --- /dev/null +++ b/esphome/components/tmp102/__init__.py @@ -0,0 +1,9 @@ +""" +The TMP102 is a two-wire, serial output temperature +sensor available in a tiny SOT563 package. Requiring +no external components, the TMP102 is capable of +reading temperatures to a resolution of 0.0625°C. + +https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf + +""" diff --git a/esphome/components/tmp102/sensor.py b/esphome/components/tmp102/sensor.py new file mode 100644 index 0000000000..d60f0a8646 --- /dev/null +++ b/esphome/components/tmp102/sensor.py @@ -0,0 +1,31 @@ +""" +The TMP102 is a two-wire, serial output temperature +sensor available in a tiny SOT563 package. Requiring +no external components, the TMP102 is capable of +reading temperatures to a resolution of 0.0625°C. + +https://www.sparkfun.com/datasheets/Sensors/Temperature/tmp102.pdf + +""" +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_CELSIUS, ICON_THERMOMETER + +CODEOWNERS = ['@timsavage'] +DEPENDENCIES = ['i2c'] + +tmp102_ns = cg.esphome_ns.namespace('tmp102') +TMP102Component = tmp102_ns.class_("TMP102Component", cg.PollingComponent, i2c.I2CDevice, + sensor.Sensor) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(TMP102Component), +}).extend(cv.polling_component_schema("60s")).extend(i2c.i2c_device_schema(0x48)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp new file mode 100644 index 0000000000..204c4f0805 --- /dev/null +++ b/esphome/components/tmp102/tmp102.cpp @@ -0,0 +1,47 @@ +#include "tmp102.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tmp102 { + +static const char *TAG = "tmp102"; + +static const uint8_t TMP102_ADDRESS = 0x48; +static const uint8_t TMP102_REGISTER_TEMPERATURE = 0x00; +static const uint8_t TMP102_REGISTER_CONFIGURATION = 0x01; +static const uint8_t TMP102_REGISTER_LOW_LIMIT = 0x02; +static const uint8_t TMP102_REGISTER_HIGH_LIMIT = 0x03; + +static const float TMP102_CONVERSION_FACTOR = 0.0625; + +void TMP102Component::setup() { ESP_LOGCONFIG(TAG, "Setting up TMP102..."); } + +void TMP102Component::dump_config() { + ESP_LOGCONFIG(TAG, "TMP102:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TMP102 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Temperature", this); +} + +void TMP102Component::update() { + uint16_t raw_temperature; + if (!this->read_byte_16(TMP102_REGISTER_TEMPERATURE, &raw_temperature, 50)) { + this->status_set_warning(); + return; + } + + raw_temperature = raw_temperature >> 4; + float temperature = raw_temperature * TMP102_CONVERSION_FACTOR; + ESP_LOGD(TAG, "Got Temperature=%.1f°C", temperature); + + this->publish_state(temperature); + this->status_clear_warning(); +} + +float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace tmp102 +} // namespace esphome diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h new file mode 100644 index 0000000000..1bbb2d5ae3 --- /dev/null +++ b/esphome/components/tmp102/tmp102.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tmp102 { + +class TMP102Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + /// Setup (reset) the sensor and check connection. + void setup() override; + void dump_config() override; + /// Update the sensor values (temperature) + void update() override; + + float get_setup_priority() const override; +}; + +} // namespace tmp102 +} // namespace esphome diff --git a/esphome/components/tuya/__init__.py b/esphome/components/tuya/__init__.py index 541f10f862..83a4f733ca 100644 --- a/esphome/components/tuya/__init__.py +++ b/esphome/components/tuya/__init__.py @@ -1,16 +1,21 @@ +from esphome.components import time import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_TIME_ID DEPENDENCIES = ['uart'] +CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS = "ignore_mcu_update_on_datapoints" + tuya_ns = cg.esphome_ns.namespace('tuya') Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) CONF_TUYA_ID = 'tuya_id' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(Tuya), + cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Optional(CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS): cv.ensure_list(cv.uint8_t), }).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) @@ -18,3 +23,9 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_TIME_ID in config: + time_ = yield cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time_id(time_)) + if CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS in config: + for dp in config[CONF_IGNORE_MCU_UPDATE_ON_DATAPOINTS]: + cg.add(var.add_ignore_mcu_update_on_datapoints(dp)) diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index 79c6551c14..f0219de97b 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -10,18 +10,54 @@ CODEOWNERS = ['@jesserockz'] CONF_TARGET_TEMPERATURE_DATAPOINT = 'target_temperature_datapoint' CONF_CURRENT_TEMPERATURE_DATAPOINT = 'current_temperature_datapoint' CONF_TEMPERATURE_MULTIPLIER = 'temperature_multiplier' +CONF_CURRENT_TEMPERATURE_MULTIPLIER = 'current_temperature_multiplier' +CONF_TARGET_TEMPERATURE_MULTIPLIER = 'target_temperature_multiplier' TuyaClimate = tuya_ns.class_('TuyaClimate', climate.Climate, cg.Component) + +def validate_temperature_multipliers(value): + if CONF_TEMPERATURE_MULTIPLIER in value: + if ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + or CONF_TARGET_TEMPERATURE_MULTIPLIER in value + ): + raise cv.Invalid((f"Cannot have {CONF_TEMPERATURE_MULTIPLIER} at the same time as " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} and " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}")) + if ( + CONF_CURRENT_TEMPERATURE_MULTIPLIER in value + and CONF_TARGET_TEMPERATURE_MULTIPLIER not in value + ): + raise cv.Invalid((f"{CONF_TARGET_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER}")) + if ( + CONF_TARGET_TEMPERATURE_MULTIPLIER in value + and CONF_CURRENT_TEMPERATURE_MULTIPLIER not in value + ): + raise cv.Invalid((f"{CONF_CURRENT_TEMPERATURE_MULTIPLIER} required if using " + f"{CONF_TARGET_TEMPERATURE_MULTIPLIER}")) + keys = ( + CONF_TEMPERATURE_MULTIPLIER, + CONF_CURRENT_TEMPERATURE_MULTIPLIER, + CONF_TARGET_TEMPERATURE_MULTIPLIER + ) + if all(multiplier not in value for multiplier in keys): + value[CONF_TEMPERATURE_MULTIPLIER] = 1.0 + return value + + CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(TuyaClimate), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, - cv.Optional(CONF_TEMPERATURE_MULTIPLIER, default=1): cv.positive_float, + cv.Optional(CONF_TEMPERATURE_MULTIPLIER): cv.positive_float, + cv.Optional(CONF_CURRENT_TEMPERATURE_MULTIPLIER): cv.positive_float, + cv.Optional(CONF_TARGET_TEMPERATURE_MULTIPLIER): cv.positive_float, }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( - CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT)) + CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT), validate_temperature_multipliers) def to_code(config): @@ -38,4 +74,9 @@ def to_code(config): cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: cg.add(var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT])) - cg.add(var.set_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])) + if CONF_TEMPERATURE_MULTIPLIER in config: + cg.add(var.set_target_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])) + cg.add(var.set_current_temperature_multiplier(config[CONF_TEMPERATURE_MULTIPLIER])) + else: + cg.add(var.set_current_temperature_multiplier(config[CONF_CURRENT_TEMPERATURE_MULTIPLIER])) + cg.add(var.set_target_temperature_multiplier(config[CONF_TARGET_TEMPERATURE_MULTIPLIER])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp index 0b66a58af6..d1f6829e72 100644 --- a/esphome/components/tuya/climate/tuya_climate.cpp +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -21,7 +21,7 @@ void TuyaClimate::setup() { } if (this->target_temperature_id_.has_value()) { this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) { - this->target_temperature = datapoint.value_int * this->temperature_multiplier_; + this->target_temperature = datapoint.value_int * this->target_temperature_multiplier_; this->compute_state_(); this->publish_state(); ESP_LOGD(TAG, "MCU reported target temperature is: %.1f", this->target_temperature); @@ -29,7 +29,7 @@ void TuyaClimate::setup() { } if (this->current_temperature_id_.has_value()) { this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) { - this->current_temperature = datapoint.value_int * this->temperature_multiplier_; + this->current_temperature = datapoint.value_int * this->current_temperature_multiplier_; this->compute_state_(); this->publish_state(); ESP_LOGD(TAG, "MCU reported current temperature is: %.1f", this->current_temperature); @@ -55,7 +55,7 @@ void TuyaClimate::control(const climate::ClimateCall &call) { TuyaDatapoint datapoint{}; datapoint.id = *this->target_temperature_id_; datapoint.type = TuyaDatapointType::INTEGER; - datapoint.value_int = (int) (this->target_temperature / this->temperature_multiplier_); + datapoint.value_int = (int) (this->target_temperature / this->target_temperature_multiplier_); this->parent_->set_datapoint_value(datapoint); ESP_LOGD(TAG, "Setting target temperature: %.1f", this->target_temperature); } diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h index e09e110a35..e9c366e898 100644 --- a/esphome/components/tuya/climate/tuya_climate.h +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -18,8 +18,11 @@ class TuyaClimate : public climate::Climate, public Component { void set_current_temperature_id(uint8_t current_temperature_id) { this->current_temperature_id_ = current_temperature_id; } - void set_temperature_multiplier(float temperature_multiplier) { - this->temperature_multiplier_ = temperature_multiplier; + void set_current_temperature_multiplier(float temperature_multiplier) { + this->current_temperature_multiplier_ = temperature_multiplier; + } + void set_target_temperature_multiplier(float temperature_multiplier) { + this->target_temperature_multiplier_ = temperature_multiplier; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } @@ -40,7 +43,8 @@ class TuyaClimate : public climate::Climate, public Component { optional switch_id_{}; optional target_temperature_id_{}; optional current_temperature_id_{}; - float temperature_multiplier_{1.0f}; + float current_temperature_multiplier_{1.0f}; + float target_temperature_multiplier_{1.0f}; }; } // namespace tuya diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index d014f8a763..05605822cb 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -8,6 +8,7 @@ from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" +CONF_MIN_VALUE_DATAPOINT = "min_value_datapoint" TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.All(light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_MIN_VALUE_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, @@ -34,6 +36,8 @@ def to_code(config): if CONF_DIMMER_DATAPOINT in config: cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_MIN_VALUE_DATAPOINT in config: + cg.add(var.set_min_value_datapoint_id(config[CONF_MIN_VALUE_DATAPOINT])) if CONF_SWITCH_DATAPOINT in config: cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) if CONF_MIN_VALUE in config: diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 9696252049..e7b44882a1 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -21,6 +21,13 @@ void TuyaLight::setup() { call.perform(); }); } + if (min_value_datapoint_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->min_value_datapoint_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = this->min_value_; + parent_->set_datapoint_value(datapoint); + } } void TuyaLight::dump_config() { diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index 581512c29c..896c0cc7ef 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -12,6 +12,9 @@ class TuyaLight : public Component, public light::LightOutput { void setup() override; void dump_config() override; void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_min_value_datapoint_id(uint8_t min_value_datapoint_id) { + this->min_value_datapoint_id_ = min_value_datapoint_id; + } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void set_min_value(uint32_t min_value) { min_value_ = min_value; } @@ -26,6 +29,7 @@ class TuyaLight : public Component, public light::LightOutput { Tuya *parent_; optional dimmer_id_{}; + optional min_value_datapoint_id_{}; optional switch_id_{}; uint32_t min_value_ = 0; uint32_t max_value_ = 255; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 644babbdec..75514dde19 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -6,9 +6,10 @@ namespace esphome { namespace tuya { static const char *TAG = "tuya"; +static const int COMMAND_DELAY = 50; void Tuya::setup() { - this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); + this->set_interval("heartbeat", 1000, [this] { this->schedule_empty_command_(TuyaCommandType::HEARTBEAT); }); } void Tuya::loop() { @@ -19,6 +20,15 @@ void Tuya::loop() { } } +void Tuya::schedule_empty_command_(TuyaCommandType command) { + uint32_t delay = millis() - this->last_command_timestamp_; + if (delay > COMMAND_DELAY) { + send_empty_command_(command); + } else { + this->set_timeout(COMMAND_DELAY - delay, [this, command] { this->send_empty_command_(command); }); + } +} + void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { @@ -32,6 +42,8 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); else if (info.type == TuyaDatapointType::INTEGER) ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::STRING) + ESP_LOGCONFIG(TAG, " Datapoint %d: string value (value: %s)", info.id, info.value_string.c_str()); else if (info.type == TuyaDatapointType::ENUM) ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); else if (info.type == TuyaDatapointType::BITMASK) @@ -110,6 +122,7 @@ void Tuya::handle_char_(uint8_t c) { } void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { + this->last_command_timestamp_ = millis(); switch ((TuyaCommandType) command) { case TuyaCommandType::HEARTBEAT: ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); @@ -119,7 +132,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } if (this->init_state_ == TuyaInitState::INIT_HEARTBEAT) { this->init_state_ = TuyaInitState::INIT_PRODUCT; - this->send_empty_command_(TuyaCommandType::PRODUCT_QUERY); + this->schedule_empty_command_(TuyaCommandType::PRODUCT_QUERY); } break; case TuyaCommandType::PRODUCT_QUERY: { @@ -138,7 +151,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff } if (this->init_state_ == TuyaInitState::INIT_PRODUCT) { this->init_state_ = TuyaInitState::INIT_CONF; - this->send_empty_command_(TuyaCommandType::CONF_QUERY); + this->schedule_empty_command_(TuyaCommandType::CONF_QUERY); } break; } @@ -148,19 +161,27 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff gpio_reset_ = buffer[1]; } if (this->init_state_ == TuyaInitState::INIT_CONF) { - // If we were following the spec to the letter we would send - // state updates until connected to both WiFi and API/MQTT. - // Instead we just claim to be connected immediately and move on. - uint8_t c[] = {0x04}; - this->init_state_ = TuyaInitState::INIT_WIFI; - this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); + // If mcu returned status gpio, then we can ommit sending wifi state + if (this->gpio_status_ != -1) { + this->init_state_ = TuyaInitState::INIT_DATAPOINT; + this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + } else { + this->init_state_ = TuyaInitState::INIT_WIFI; + this->set_timeout(COMMAND_DELAY, [this] { + // If we were following the spec to the letter we would send + // state updates until connected to both WiFi and API/MQTT. + // Instead we just claim to be connected immediately and move on. + uint8_t c[] = {0x04}; + this->send_command_(TuyaCommandType::WIFI_STATE, c, 1); + }); + } } break; } case TuyaCommandType::WIFI_STATE: if (this->init_state_ == TuyaInitState::INIT_WIFI) { this->init_state_ = TuyaInitState::INIT_DATAPOINT; - this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); + this->schedule_empty_command_(TuyaCommandType::DATAPOINT_QUERY); } break; case TuyaCommandType::WIFI_RESET: @@ -185,6 +206,44 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->send_command_(TuyaCommandType::WIFI_TEST, c, 2); break; } + case TuyaCommandType::LOCAL_TIME_QUERY: { +#ifdef USE_TIME + if (this->time_id_.has_value()) { + auto time_id = *this->time_id_; + auto now = time_id->now(); + + if (now.is_valid()) { + this->set_timeout(COMMAND_DELAY, [this, now] { + uint8_t year = now.year - 2000; + uint8_t month = now.month; + uint8_t day_of_month = now.day_of_month; + uint8_t hour = now.hour; + uint8_t minute = now.minute; + uint8_t second = now.second; + // Tuya days starts from Monday, esphome uses Sunday as day 1 + uint8_t day_of_week = now.day_of_week - 1; + if (day_of_week == 0) { + day_of_week = 7; + } + uint8_t c[] = {0x01, year, month, day_of_month, hour, minute, second, day_of_week}; + this->send_command_(TuyaCommandType::LOCAL_TIME_QUERY, c, 8); + }); + } else { + ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not valid"); + // By spec we need to notify MCU that the time was not obtained + this->set_timeout(COMMAND_DELAY, [this] { + uint8_t c[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + this->send_command_(TuyaCommandType::LOCAL_TIME_QUERY, c, 8); + }); + } + } else { + ESP_LOGW(TAG, "TUYA_CMD_LOCAL_TIME_QUERY is not handled because time is not configured"); + } +#else + ESP_LOGE(TAG, "LOCAL_TIME_QUERY is not handled"); +#endif + break; + } default: ESP_LOGE(TAG, "invalid command (%02x) received", command); } @@ -199,6 +258,14 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { datapoint.type = (TuyaDatapointType) buffer[1]; datapoint.value_uint = 0; + // drop update if datapoint is in ignore_mcu_datapoint_update list + for (auto i : this->ignore_mcu_update_on_datapoints_) { + if (datapoint.id == i) { + ESP_LOGV(TAG, "Datapoint %u found in ignore_mcu_update_on_datapoints list, dropping MCU update", datapoint.id); + return; + } + } + size_t data_size = (buffer[2] << 8) + buffer[3]; const uint8_t *data = buffer + 4; size_t data_len = len - 4; @@ -216,8 +283,10 @@ void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { case TuyaDatapointType::INTEGER: if (data_len != 4) return; - datapoint.value_uint = - (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + datapoint.value_uint = encode_uint32(data[0], data[1], data[2], data[3]); + break; + case TuyaDatapointType::STRING: + datapoint.value_string = std::string(reinterpret_cast(data), data_len); break; case TuyaDatapointType::ENUM: if (data_len != 1) @@ -275,7 +344,13 @@ void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); for (auto &other : this->datapoints_) { if (other.id == datapoint.id) { - if (other.value_uint == datapoint.value_uint) { + // String value is stored outside the union; must be checked separately. + if (datapoint.type == TuyaDatapointType::STRING) { + if (other.value_string == datapoint.value_string) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + } else if (other.value_uint == datapoint.value_uint) { ESP_LOGV(TAG, "Not sending unchanged value"); return; } @@ -295,6 +370,11 @@ void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { data.push_back(datapoint.value_uint >> 8); data.push_back(datapoint.value_uint >> 0); break; + case TuyaDatapointType::STRING: + for (char const &c : datapoint.value_string) { + data.push_back(c); + } + break; case TuyaDatapointType::ENUM: data.push_back(datapoint.value_enum); break; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 2fc9a16d44..ddbbb48edf 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -1,8 +1,13 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/components/uart/uart.h" +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + namespace esphome { namespace tuya { @@ -25,6 +30,7 @@ struct TuyaDatapoint { uint8_t value_enum; uint16_t value_bitmask; }; + std::string value_string; }; struct TuyaDatapointListener { @@ -43,6 +49,7 @@ enum class TuyaCommandType : uint8_t { DATAPOINT_REPORT = 0x07, DATAPOINT_QUERY = 0x08, WIFI_TEST = 0x0E, + LOCAL_TIME_QUERY = 0x1C, }; enum class TuyaInitState : uint8_t { @@ -62,6 +69,12 @@ class Tuya : public Component, public uart::UARTDevice { void dump_config() override; void register_listener(uint8_t datapoint_id, const std::function &func); void set_datapoint_value(TuyaDatapoint datapoint); +#ifdef USE_TIME + void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } +#endif + void add_ignore_mcu_update_on_datapoints(uint8_t ignore_mcu_update_on_datapoints) { + this->ignore_mcu_update_on_datapoints_.push_back(ignore_mcu_update_on_datapoints); + } protected: void handle_char_(uint8_t c); @@ -71,14 +84,20 @@ class Tuya : public Component, public uart::UARTDevice { void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + void schedule_empty_command_(TuyaCommandType command); +#ifdef USE_TIME + optional time_id_{}; +#endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; int gpio_status_ = -1; int gpio_reset_ = -1; + uint32_t last_command_timestamp_ = 0; std::string product_ = ""; std::vector listeners_; std::vector datapoints_; std::vector rx_message_; + std::vector ignore_mcu_update_on_datapoints_{}; }; } // namespace tuya diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index b1a397ad6c..38eadc9dc8 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -70,14 +70,8 @@ void ULN2003::write_step_(int32_t step) { } case ULN2003_STEP_MODE_HALF_STEP: { // A, AB, B, BC, C, CD, D, DA - if (i == 0 || i == 2 || i == 7) - res |= 1 << 0; - if (i == 1 || i == 2 || i == 3) - res |= 1 << 1; - if (i == 3 || i == 4 || i == 5) - res |= 1 << 2; - if (i == 5 || i == 6 || i == 7) - res |= 1 << 3; + res |= 1 << (i >> 1); + res |= 1 << (((i + 1) >> 1) & 0x3); break; } case ULN2003_STEP_MODE_WAVE_DRIVE: { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2f0d179eba..069b0a3895 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -36,6 +36,7 @@ def to_code(config): yield cg.register_component(var, config) cg.add(paren.set_port(config[CONF_PORT])) + cg.add_define('WEBSERVER_PORT', config[CONF_PORT]) cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) if CONF_AUTH in config: diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 48a47080b2..9e0e881b1b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -31,6 +31,7 @@ void write_row(AsyncResponseStream *stream, Nameable *obj, const std::string &kl stream->print(""); stream->print(action.c_str()); stream->print(""); + stream->print(""); } UrlMatch match_url(const std::string &url, bool only_domain = false) { diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index d8db3b7353..eba08d5bbe 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -105,7 +105,7 @@ void WhirlpoolClimate::transmit_state() { } // Checksum - for (uint8_t i = 2; i < 12; i++) + for (uint8_t i = 2; i < 13; i++) remote_state[13] ^= remote_state[i]; for (uint8_t i = 14; i < 20; i++) remote_state[20] ^= remote_state[i]; @@ -184,7 +184,7 @@ bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { uint8_t checksum13 = 0; uint8_t checksum20 = 0; // Calculate checksum and compare with signal value. - for (uint8_t i = 2; i < 12; i++) + for (uint8_t i = 2; i < 13; i++) checksum13 ^= remote_state[i]; for (uint8_t i = 14; i < 20; i++) checksum20 ^= remote_state[i]; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 7e7ab468ff..4fe6929d75 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -60,7 +60,7 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) -EAP_AUTH_SCHEMA = cv.All(cv.only_on_esp32, cv.Schema({ +EAP_AUTH_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_IDENTITY): cv.string_strict, cv.Optional(CONF_USERNAME): cv.string_strict, cv.Optional(CONF_PASSWORD): cv.string_strict, diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index efffff0abc..dee3d5a4a5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -6,6 +6,9 @@ #include #include +#ifdef ESPHOME_WIFI_WPA2_EAP +#include +#endif extern "C" { #include "lwip/err.h" @@ -239,6 +242,52 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } + // setup enterprise authentication if required +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. + EAPAuth eap = ap.get_eap().value(); + ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", ret); + } + int ca_cert_len = strlen(eap.ca_cert); + int client_cert_len = strlen(eap.client_cert); + int client_key_len = strlen(eap.client_key); + if (ca_cert_len) { + ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", ret); + } + } + // workout what type of EAP this is + // validation is not required as the config tool has already validated it + if (client_cert_len && client_key_len) { + // if we have certs, this must be EAP-TLS + ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", ret); + } + } else { + // in the absence of certs, assume this is username/password based + ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", ret); + } + ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", ret); + } + } + ret = wifi_station_set_wpa2_enterprise_auth(true); + if (ret) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", ret); + } + } +#endif // ESPHOME_WIFI_WPA2_EAP + this->wifi_apply_hostname_(); ETS_UART_INTR_DISABLE(); diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 8bf50598b0..54195c852b 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -18,9 +18,9 @@ _LOGGER = logging.getLogger(__name__) def validate_cryptography_installed(): try: import cryptography - except ImportError: + except ImportError as err: raise cv.Invalid("This settings requires the cryptography python package. " - "Please install it with `pip install cryptography`") + "Please install it with `pip install cryptography`") from err if cryptography.__version__[0] < '2': raise cv.Invalid("Please update your python cryptography installation to least 2.x " diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 202620c06d..033269f66c 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -70,8 +70,7 @@ bool parse_xiaomi_value(uint8_t value_type, const uint8_t *data, uint8_t value_l } // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min else if ((value_type == 0x17) && (value_length == 4)) { - const uint32_t idle_time = - uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); + const uint32_t idle_time = encode_uint32(data[3], data[2], data[1], data[0]); result.idle_time = idle_time / 60.0f; result.has_motion = (idle_time) ? false : true; } else { @@ -195,6 +194,9 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service result.name = "MJYD02YLA"; if (raw.size() == 19) result.raw_offset -= 6; + } else if ((raw[2] == 0x87) && (raw[3] == 0x03)) { // square body, e-ink display + result.type = XiaomiParseResult::TYPE_MHOC401; + result.name = "MHOC401"; } else { ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); return {}; diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index ad73226159..3973dac80f 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -21,7 +21,8 @@ struct XiaomiParseResult { TYPE_JQJCY01YM, TYPE_MUE4094RT, TYPE_WX08ZM, - TYPE_MJYD02YLA + TYPE_MJYD02YLA, + TYPE_MHOC401 } type; std::string name; optional temperature; diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py index 8e4d59316b..97eff0cf79 100644 --- a/esphome/components/xiaomi_lywsd02/sensor.py +++ b/esphome/components/xiaomi_lywsd02/sensor.py @@ -1,8 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID DEPENDENCIES = ['esp32_ble_tracker'] AUTO_LOAD = ['xiaomi_ble'] @@ -16,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) @@ -32,3 +33,6 @@ def to_code(config): 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)) diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index 5ecd99047e..12590960e4 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -12,6 +12,7 @@ void XiaomiLYWSD02::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); } bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { @@ -44,6 +45,8 @@ bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { 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); success = true; } diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index f32506eb44..ec00464cb5 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -20,11 +20,13 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis 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; } protected: uint64_t address_; sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; }; } // namespace xiaomi_lywsd02 diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 9ecf3f64a9..a1983825fe 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -5,6 +5,8 @@ from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, C UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ CONF_BINDKEY +CODEOWNERS = ['@ahpohl'] + DEPENDENCIES = ['esp32_ble_tracker'] AUTO_LOAD = ['xiaomi_ble'] diff --git a/esphome/components/xiaomi_mhoc401/__init__.py b/esphome/components/xiaomi_mhoc401/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mhoc401/sensor.py b/esphome/components/xiaomi_mhoc401/sensor.py new file mode 100644 index 0000000000..579ca0619a --- /dev/null +++ b/esphome/components/xiaomi_mhoc401/sensor.py @@ -0,0 +1,43 @@ +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_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +CODEOWNERS = ['@vevsvevs'] +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mhoc401_ns = cg.esphome_ns.namespace('xiaomi_mhoc401') +XiaomiMHOC401 = xiaomi_mhoc401_ns.class_('XiaomiMHOC401', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiMHOC401), + 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_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).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)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + 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)) diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp new file mode 100644 index 0000000000..a41df3e6a1 --- /dev/null +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.cpp @@ -0,0 +1,81 @@ +#include "xiaomi_mhoc401.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mhoc401 { + +static const char *TAG = "xiaomi_mhoc401"; + +void XiaomiMHOC401::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MHOC401"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiMHOC401::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 = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_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); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiMHOC401::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_mhoc401 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h new file mode 100644 index 0000000000..e80916f855 --- /dev/null +++ b/esphome/components/xiaomi_mhoc401/xiaomi_mhoc401.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mhoc401 { + +class XiaomiMHOC401 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + 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; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_mhoc401 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 72cf57d22c..e13dd77d13 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, binary_sensor, esp32_ble_tracker from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY, \ CONF_DEVICE_CLASS, CONF_LIGHT, CONF_BATTERY_LEVEL, UNIT_PERCENT, ICON_BATTERY, \ - CONF_IDLE_TIME, UNIT_MINUTE, ICON_TIMELAPSE + CONF_IDLE_TIME, CONF_ILLUMINANCE, UNIT_MINUTE, UNIT_LUX, ICON_TIMELAPSE, ICON_BRIGHTNESS_5 DEPENDENCIES = ['esp32_ble_tracker'] AUTO_LOAD = ['xiaomi_ble'] @@ -19,6 +19,7 @@ CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(UNIT_MINUTE, ICON_TIMELAPSE, 0), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.Optional(CONF_DEVICE_CLASS, default='light'): binary_sensor.device_class, }), @@ -40,6 +41,9 @@ def to_code(config): if CONF_BATTERY_LEVEL in config: sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) if CONF_LIGHT in config: sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) cg.add(var.set_light(sens)) diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp index aaea3606ba..c7a2a75348 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -14,6 +14,7 @@ void XiaomiMJYD02YLA::dump_config() { LOG_BINARY_SENSOR(" ", "Light", this->is_light_); LOG_SENSOR(" ", "Idle Time", this->idle_time_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); } bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { @@ -47,6 +48,8 @@ bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) this->idle_time_->publish_state(*res->idle_time); if (res->battery_level.has_value() && this->battery_level_ != nullptr) this->battery_level_->publish_state(*res->battery_level); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); if (res->is_light.has_value() && this->is_light_ != nullptr) this->is_light_->publish_state(*res->is_light); if (res->has_motion.has_value()) diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h index d3fde4d6f8..973b19a372 100644 --- a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -24,6 +24,7 @@ class XiaomiMJYD02YLA : public Component, float get_setup_priority() const override { return setup_priority::DATA; } void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } void set_light(binary_sensor::BinarySensor *light) { is_light_ = light; } protected: @@ -31,6 +32,7 @@ class XiaomiMJYD02YLA : public Component, uint8_t bindkey_[16]; sensor::Sensor *idle_time_{nullptr}; sensor::Sensor *battery_level_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; binary_sensor::BinarySensor *is_light_{nullptr}; }; diff --git a/esphome/config.py b/esphome/config.py index 85f48c64b7..0484414929 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -675,7 +675,7 @@ def _load_config(command_line_substitutions): try: config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: - raise InvalidYAMLError(e) + raise InvalidYAMLError(e) from e CORE.raw_config = config try: @@ -693,7 +693,7 @@ def load_config(command_line_substitutions): try: return _load_config(command_line_substitutions) except vol.Invalid as err: - raise EsphomeError(f"Error while parsing config: {err}") + raise EsphomeError(f"Error while parsing config: {err}") from err def line_info(obj, highlight=True): diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 371ae0eea1..a5b1559f2f 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -11,7 +11,7 @@ from string import ascii_letters, digits import voluptuous as vol from esphome import core -from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, \ +from esphome.const import ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TOPIC, \ CONF_DISCOVERY, CONF_ID, CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, \ CONF_PAYLOAD_NOT_AVAILABLE, CONF_RETAIN, CONF_SETUP_PRIORITY, CONF_STATE_TOPIC, CONF_TOPIC, \ CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, \ @@ -41,8 +41,6 @@ ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid -ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' - RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword 'alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool', 'break', @@ -227,6 +225,7 @@ def int_(value): try: return int(value, base) except ValueError: + # pylint: disable=raise-missing-from raise Invalid(f"Expected integer, but cannot parse {value} as an integer") @@ -426,6 +425,7 @@ def time_period_str_colon(value): try: parsed = [int(x) for x in value.split(':')] except ValueError: + # pylint: disable=raise-missing-from raise Invalid(TIME_PERIOD_ERROR.format(value)) if len(parsed) == 2: @@ -530,6 +530,7 @@ def time_of_day(value): try: date = datetime.strptime(value, '%H:%M:%S %p') except ValueError: + # pylint: disable=raise-missing-from raise Invalid(f"Invalid time of day: {err}") return { @@ -551,6 +552,7 @@ def mac_address(value): try: parts_int.append(int(part, 16)) except ValueError: + # pylint: disable=raise-missing-from raise Invalid("MAC Address parts must be hexadecimal values from 00 to FF") return core.MACAddress(*parts_int) @@ -568,6 +570,7 @@ def bind_key(value): try: parts_int.append(int(part, 16)) except ValueError: + # pylint: disable=raise-missing-from raise Invalid("Bind key must be hex values from 00 to FF") return ''.join(f'{part:02X}' for part in parts_int) @@ -619,6 +622,7 @@ _temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") _temperature_k = float_with_unit("temperature", "(° K|° K|K)?") _temperature_f = float_with_unit("temperature", "(°F|° F|F)?") decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) +pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) def temperature(value): @@ -689,8 +693,8 @@ def domain(value): return value try: return str(ipv4(value)) - except Invalid: - raise Invalid(f"Invalid domain: {value}") + except Invalid as err: + raise Invalid(f"Invalid domain: {value}") from err def domain_name(value): @@ -742,8 +746,8 @@ def _valid_topic(value): value = string(value) try: raw_value = value.encode('utf-8') - except UnicodeError: - raise Invalid("MQTT topic name/filter must be valid UTF-8 string.") + except UnicodeError as err: + raise Invalid("MQTT topic name/filter must be valid UTF-8 string.") from err if not raw_value: raise Invalid("MQTT topic name/filter must not be empty.") if len(raw_value) > 65535: @@ -795,6 +799,7 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): + # pylint: disable=raise-missing-from raise Invalid(f"MQTT Quality of Service must be integer, got {value}") return one_of(0, 1, 2)(value) @@ -839,6 +844,7 @@ def possibly_negative_percentage(value): else: value = float(value) except ValueError: + # pylint: disable=raise-missing-from raise Invalid("invalid number") if value > 1: msg = "Percentage must not be higher than 100%." @@ -1009,6 +1015,7 @@ def dimensions(value): try: width, height = int(value[0]), int(value[1]) except ValueError: + # pylint: disable=raise-missing-from raise Invalid("Width and height dimensions must be integers") if width <= 0 or height <= 0: raise Invalid("Width and height must at least be 1") @@ -1170,8 +1177,8 @@ class OnlyWith(Optional): # pylint: disable=unsupported-membership-test if (self._component in CORE.raw_config or (CONF_PACKAGES in CORE.raw_config and - self._component in - {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()})): + self._component in + {list(x.keys())[0] for x in CORE.raw_config[CONF_PACKAGES].values()})): return self._default return vol.UNDEFINED diff --git a/esphome/const.py b/esphome/const.py index abe559089e..f0edcc71d4 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,8 +1,8 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 15 -PATCH_VERSION = '3' +MINOR_VERSION = 16 +PATCH_VERSION = '0' __short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' __version__ = f'{__short_version__}.{PATCH_VERSION}' @@ -10,7 +10,7 @@ ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_-' # Lookup table from ESP32 arduino framework version to latest platformio # package with that version # See also https://github.com/platformio/platform-espressif32/releases @@ -122,6 +122,7 @@ CONF_COMPONENTS = 'components' CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' +CONF_CONTRAST = 'contrast' CONF_COOL_ACTION = 'cool_action' CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' @@ -136,6 +137,7 @@ CONF_DALLAS_ID = 'dallas_id' CONF_DATA = 'data' CONF_DATA_PIN = 'data_pin' CONF_DATA_PINS = 'data_pins' +CONF_DATA_RATE = 'data_rate' CONF_DATA_TEMPLATE = 'data_template' CONF_DAYS_OF_MONTH = 'days_of_month' CONF_DAYS_OF_WEEK = 'days_of_week' @@ -158,6 +160,7 @@ CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' CONF_DISTANCE = 'distance' +CONF_DITHER = 'dither' CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' @@ -263,6 +266,7 @@ CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' CONF_KEY = 'key' CONF_LAMBDA = 'lambda' +CONF_LENGTH = 'length' CONF_LEVEL = 'level' CONF_LG = 'lg' CONF_LIBRARIES = 'libraries' @@ -346,6 +350,7 @@ CONF_ON_SHUTDOWN = 'on_shutdown' CONF_ON_STATE = 'on_state' CONF_ON_TAG = 'on_tag' CONF_ON_TIME = 'on_time' +CONF_ON_TIME_SYNC = 'on_time_sync' CONF_ON_TURN_OFF = 'on_turn_off' CONF_ON_TURN_ON = 'on_turn_on' CONF_ON_VALUE = 'on_value' @@ -525,6 +530,7 @@ CONF_TO = 'to' CONF_TOLERANCE = 'tolerance' CONF_TOPIC = 'topic' CONF_TOPIC_PREFIX = 'topic_prefix' +CONF_TOTAL = "total" CONF_TRANSITION_LENGTH = 'transition_length' CONF_TRIGGER_ID = 'trigger_id' CONF_TRIGGER_PIN = 'trigger_pin' @@ -568,6 +574,9 @@ CONF_WIND_SPEED = 'wind_speed' CONF_WINDOW_SIZE = 'window_size' CONF_ZERO = 'zero' +ENV_NOGITIGNORE = 'ESPHOME_NOGITIGNORE' +ENV_QUICKWIZARD = 'ESPHOME_QUICKWIZARD' + ICON_ACCELERATION = 'mdi:axis-arrow' ICON_ACCELERATION_X = 'mdi:axis-x-arrow' ICON_ACCELERATION_Y = 'mdi:axis-y-arrow' @@ -624,7 +633,7 @@ UNIT_DEGREES = '°' UNIT_EMPTY = '' UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' -UNIT_HERTZ = 'hz' +UNIT_HERTZ = 'Hz' UNIT_KELVIN = 'K' UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' @@ -641,6 +650,7 @@ UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' +UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = 'pulses/min' UNIT_SECOND = 's' UNIT_STEPS = 'steps' diff --git a/esphome/core/color.h b/esphome/core/color.h index b19340a1d3..3120e48064 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -6,6 +6,7 @@ namespace esphome { inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } +inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 255) { return (max_value * i / scale); } struct Color { union { @@ -30,6 +31,8 @@ struct Color { uint8_t raw[4]; uint32_t raw_32; }; + enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 }; + enum ColorBitness : uint8_t { COLOR_BITNESS_888 = 0, COLOR_BITNESS_565 = 1, COLOR_BITNESS_332 = 2 }; inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT inline Color(float red, float green, float blue) ALWAYS_INLINE : r(uint8_t(red * 255)), g(uint8_t(green * 255)), @@ -43,6 +46,60 @@ struct Color { g((colorcode >> 8) & 0xFF), b((colorcode >> 0) & 0xFF), w((colorcode >> 24) & 0xFF) {} + inline Color(uint32_t colorcode, ColorOrder color_order, ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, + bool right_bit_aligned = true) { + uint8_t first_color, second_color, third_color; + uint8_t first_bits = 0; + uint8_t second_bits = 0; + uint8_t third_bits = 0; + + switch (color_bitness) { + case COLOR_BITNESS_888: + first_bits = 8; + second_bits = 8; + third_bits = 8; + break; + case COLOR_BITNESS_565: + first_bits = 5; + second_bits = 6; + third_bits = 5; + break; + case COLOR_BITNESS_332: + first_bits = 3; + second_bits = 3; + third_bits = 2; + break; + } + + first_color = right_bit_aligned ? esp_scale(((colorcode >> (second_bits + third_bits)) & ((1 << first_bits) - 1)), + ((1 << first_bits) - 1)) + : esp_scale(((colorcode >> 16) & 0xFF), (1 << first_bits) - 1); + + second_color = right_bit_aligned + ? esp_scale(((colorcode >> third_bits) & ((1 << second_bits) - 1)), ((1 << second_bits) - 1)) + : esp_scale(((colorcode >> 8) & 0xFF), ((1 << second_bits) - 1)); + + third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & 0xFF), ((1 << third_bits) - 1)) + : esp_scale(((colorcode >> 0) & 0xFF), (1 << third_bits) - 1)); + + switch (color_order) { + case COLOR_ORDER_RGB: + this->r = first_color; + this->g = second_color; + this->b = third_color; + break; + case COLOR_ORDER_BGR: + this->b = first_color; + this->g = second_color; + this->r = third_color; + break; + case COLOR_ORDER_GRB: + this->g = first_color; + this->r = second_color; + this->b = third_color; + break; + } + } inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } inline Color &operator=(const Color &rhs) ALWAYS_INLINE { this->r = rhs.r; @@ -140,7 +197,40 @@ struct Color { Color fade_to_black(uint8_t amnt) { return *this * amnt; } Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } + uint8_t to_332(ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) const { + uint16_t red_color, green_color, blue_color; + red_color = esp_scale8(this->red, ((1 << 3) - 1)); + green_color = esp_scale8(this->green, ((1 << 3) - 1)); + blue_color = esp_scale8(this->blue, (1 << 2) - 1); + + switch (color_order) { + case COLOR_ORDER_RGB: + return red_color << 5 | green_color << 2 | blue_color; + case COLOR_ORDER_BGR: + return blue_color << 6 | green_color << 3 | red_color; + case COLOR_ORDER_GRB: + return green_color << 5 | red_color << 2 | blue_color; + } + return 0; + } + uint16_t to_565(ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) const { + uint16_t red_color, green_color, blue_color; + + red_color = esp_scale8(this->red, ((1 << 5) - 1)); + green_color = esp_scale8(this->green, ((1 << 6) - 1)); + blue_color = esp_scale8(this->blue, (1 << 5) - 1); + + switch (color_order) { + case COLOR_ORDER_RGB: + return red_color << 11 | green_color << 5 | blue_color; + case COLOR_ORDER_BGR: + return blue_color << 11 | green_color << 5 | red_color; + case COLOR_ORDER_GRB: + return green_color << 10 | red_color << 5 | blue_color; + } + return 0; + } uint32_t to_rgb_565() const { uint32_t color565 = (esp_scale8(this->red, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->blue, 31) << 0); diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 38b80d85fb..3e7472d2fe 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -171,15 +171,17 @@ uint8_t crc8(uint8_t *data, uint8_t len) { } return crc; } + void delay_microseconds_accurate(uint32_t usec) { if (usec == 0) return; - - if (usec <= 16383UL) { + if (usec < 5000UL) { delayMicroseconds(usec); - } else { - delay(usec / 1000UL); - delayMicroseconds(usec % 1000UL); + return; + } + uint32_t start = micros(); + while (micros() - start < usec) { + delay(0); } } @@ -299,6 +301,10 @@ std::array decode_uint16(uint16_t value) { return {msb, lsb}; } +uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb) { + return (uint32_t(msb) << 24) | (uint32_t(byte2) << 16) | (uint32_t(byte3) << 8) | uint32_t(lsb); +} + std::string hexencode(const uint8_t *data, uint32_t len) { char buf[20]; std::string res; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 0c660bdc8e..40e53e601e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -132,6 +132,8 @@ uint32_t reverse_bits_32(uint32_t x); uint16_t encode_uint16(uint8_t msb, uint8_t lsb); /// Decode a 16-bit unsigned integer into an array of two values: most significant byte, least significant byte. std::array decode_uint16(uint16_t value); +/// Encode a 32-bit unsigned integer given four bytes in MSB -> LSB order +uint32_t encode_uint32(uint8_t msb, uint8_t byte2, uint8_t byte3, uint8_t lsb); /*** * An interrupt helper class. diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index 867ba8421b..a701b8013c 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -43,6 +43,10 @@ bool network_is_connected() { bool mdns_setup; #endif +#ifndef WEBSERVER_PORT +static const uint8_t WEBSERVER_PORT = 80; +#endif + #ifdef ARDUINO_ARCH_ESP8266 void network_setup_mdns(IPAddress address, int interface) { // Latest arduino framework breaks mDNS for AP interface @@ -65,12 +69,15 @@ void network_setup_mdns(IPAddress address, int interface) { MDNS.addServiceTxt("esphomelib", "tcp", "mac", get_mac_address().c_str()); } else { #endif - // Publish "http" service if not using native API. + // Publish "http" service if not using native API nor the webserver component // This is just to have *some* mDNS service so that .local resolution works - MDNS.addService("http", "tcp", 80); + MDNS.addService("http", "tcp", WEBSERVER_PORT); MDNS.addServiceTxt("http", "tcp", "version", ESPHOME_VERSION); #ifdef USE_API } +#endif +#ifdef USE_PROMETHEUS + MDNS.addService("prometheus-http", "tcp", WEBSERVER_PORT); #endif } void network_tick_mdns() { diff --git a/esphome/core_config.py b/esphome/core_config.py index f167eb8d8b..f1bb18eef5 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -209,11 +209,8 @@ def _esp8266_add_lwip_type(): cg.add_build_flag('-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH') -@coroutine_with_priority(100.0) -def to_code(config): - cg.add_global(cg.global_ns.namespace('esphome').using) - cg.add(cg.App.pre_setup(config[CONF_NAME], cg.RawExpression('__DATE__ ", " __TIME__'))) - +@coroutine_with_priority(30.0) +def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) yield cg.register_component(trigger, conf) @@ -229,6 +226,14 @@ def to_code(config): yield cg.register_component(trigger, conf) yield automation.build_automation(trigger, [], conf) + +@coroutine_with_priority(100.0) +def to_code(config): + cg.add_global(cg.global_ns.namespace('esphome').using) + cg.add(cg.App.pre_setup(config[CONF_NAME], cg.RawExpression('__DATE__ ", " __TIME__'))) + + CORE.add_job(_add_automations, config) + # Set LWIP build constants for ESP8266 if CORE.is_esp8266: CORE.add_job(_esp8266_add_lwip_type) diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index d7ba9373be..142bc2cd6f 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -359,7 +359,7 @@

Names must be all lowercase and must not contain any spaces! Characters that are allowed are: a-z, - 0-9 and _. + 0-9, _ and -.

diff --git a/esphome/espota2.py b/esphome/espota2.py index edfa4e63e6..a1408a7d44 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -83,19 +83,19 @@ def receive_exactly(sock, amount, msg, expect, decode=True): try: data += recv_decode(sock, 1, decode=decode) except OSError as err: - raise OTAError(f"Error receiving acknowledge {msg}: {err}") + raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err try: check_error(data, expect) except OTAError as err: sock.close() - raise OTAError(f"Error {msg}: {err}") + raise OTAError(f"Error {msg}: {err}") from err while len(data) < amount: try: data += recv_decode(sock, amount - len(data), decode=decode) except OSError as err: - raise OTAError(f"Error receiving {msg}: {err}") + raise OTAError(f"Error receiving {msg}: {err}") from err return data @@ -151,7 +151,7 @@ def send_check(sock, data, msg): sock.sendall(data) except OSError as err: - raise OTAError(f"Error sending {msg}: {err}") + raise OTAError(f"Error sending {msg}: {err}") from err def perform_ota(sock, password, file_handle, filename): @@ -226,7 +226,7 @@ def perform_ota(sock, password, file_handle, filename): sock.sendall(chunk) except OSError as err: sys.stderr.write('\n') - raise OTAError(f"Error sending data: {err}") + raise OTAError(f"Error sending data: {err}") from err progress.update(offset / float(file_size)) progress.done() @@ -259,7 +259,7 @@ def run_ota_impl_(remote_host, remote_port, password, filename): remote_host) _LOGGER.error("(If this error persists, please set a static IP address: " "https://esphome.io/components/wifi.html#manual-ips)") - raise OTAError(err) + raise OTAError(err) from err _LOGGER.info(" -> %s", ip) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/esphome/helpers.py b/esphome/helpers.py index b30ace89d2..1389804fd9 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -89,7 +89,7 @@ def mkdir_p(path): pass else: from esphome.core import EsphomeError - raise EsphomeError(f"Error creating directories {path}: {err}") + raise EsphomeError(f"Error creating directories {path}: {err}") from err def is_ip_address(host): @@ -110,13 +110,13 @@ def _resolve_with_zeroconf(host): try: zc = Zeroconf() - except Exception: + except Exception as err: raise EsphomeError("Cannot start mDNS sockets, is this a docker container without " - "host network mode?") + "host network mode?") from err try: info = zc.resolve_host(host + '.') except Exception as err: - raise EsphomeError(f"Error resolving mDNS hostname: {err}") + raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err finally: zc.close() if info is None: @@ -142,7 +142,7 @@ def resolve_ip_address(host): except OSError as err: errs.append(str(err)) raise EsphomeError("Error resolving IP address: {}" - "".format(', '.join(errs))) + "".format(', '.join(errs))) from err def get_bool_env(var, default=False): @@ -165,10 +165,10 @@ def read_file(path): return f_handle.read() except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error reading file {path}: {err}") + raise EsphomeError(f"Error reading file {path}: {err}") from err except UnicodeDecodeError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error reading file {path}: {err}") + raise EsphomeError(f"Error reading file {path}: {err}") from err def _write_file(path: Union[Path, str], text: Union[str, bytes]): @@ -205,9 +205,9 @@ def _write_file(path: Union[Path, str], text: Union[str, bytes]): def write_file(path: Union[Path, str], text: str): try: _write_file(path, text) - except OSError: + except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Could not write file at {path}") + raise EsphomeError(f"Could not write file at {path}") from err def write_file_if_changed(path: Union[Path, str], text: str): @@ -230,7 +230,7 @@ def copy_file_if_changed(src, dst): shutil.copy(src, dst) except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(f"Error copying file {src} to {dst}: {err}") + raise EsphomeError(f"Error copying file {src} to {dst}: {err}") from err def list_starts_with(list_, sub): diff --git a/esphome/mqtt.py b/esphome/mqtt.py index cbcf067c44..499ccbe7f1 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -67,7 +67,7 @@ def initialize(config, subscriptions, on_message, username, password, client_id) port = int(config[CONF_MQTT][CONF_PORT]) client.connect(host, port) except OSError as err: - raise EsphomeError(f"Cannot connect to MQTT broker: {err}") + raise EsphomeError(f"Cannot connect to MQTT broker: {err}") from err try: client.loop_forever() diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 4866119902..1cda141a54 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -22,13 +22,13 @@ def patch_structhash(): from os import makedirs def patched_clean_build_dir(build_dir, *args): - from platformio import util + from platformio import fs from platformio.project.helpers import get_project_dir platformio_ini = join(get_project_dir(), "platformio.ini") # if project's config is modified if isdir(build_dir) and getmtime(platformio_ini) > getmtime(build_dir): - util.rmtree_(build_dir) + fs.rmtree(build_dir) if not isdir(build_dir): makedirs(build_dir) @@ -205,7 +205,7 @@ def process_stacktrace(config, line, backtrace_state): # ESP8266 Exception type match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line) if match is not None: - code = match.group(1) + code = int(match.group(1)) _LOGGER.warning("Exception type: %s", ESP8266_EXCEPTION_CODES.get(code, 'unknown')) # ESP8266 PC/EXCVADDR @@ -273,4 +273,9 @@ class IDEData: if cc_path is None: return None # replace gcc at end with addr2line + + # Windows + if cc_path.endswith('.exe'): + return cc_path[:-7] + 'addr2line.exe' + return cc_path[:-3] + 'addr2line' diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index ce13f0ceb0..d10801fb95 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -32,6 +32,7 @@ class _Schema(vol.Schema): try: res = extra(res) except vol.Invalid as err: + # pylint: disable=raise-missing-from raise ensure_multiple_invalid(err) return res diff --git a/esphome/wizard.py b/esphome/wizard.py index b1a0b17072..9b6237acf3 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -11,6 +11,7 @@ from esphome.helpers import color, get_bool_env, write_file from esphome.pins 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 CORE_BIG = r""" _____ ____ _____ ______ / ____/ __ \| __ \| ____| @@ -106,7 +107,7 @@ def wizard_write(path, **kwargs): storage.save(storage_path) -if get_bool_env('ESPHOME_QUICKWIZARD'): +if get_bool_env(ENV_QUICKWIZARD): def sleep(time): pass else: @@ -168,10 +169,11 @@ def wizard(path): name = cv.valid_name(name) break except vol.Invalid: - safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " - "numbers, lower-case letters and underscores.".format(name))) + safe_print(color("red", f"Oh noes, \"{name}\" isn't a valid name. Names can only " + f"include numbers, lower-case letters, underscores and " + f"hyphens.")) name = strip_accents(name).lower().replace(' ', '_') - name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) + name = ''.join(c for c in name if c in ALLOWED_NAME_CHARS) safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) sleep(0.5) name = default_input("(name [{}]): ", name) diff --git a/esphome/writer.py b/esphome/writer.py index faf086519b..237f0fb4b7 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -4,10 +4,11 @@ import re from esphome.config import iter_components from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \ - HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__, ARDUINO_VERSION_ESP8266 + HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__, ARDUINO_VERSION_ESP8266, \ + ENV_NOGITIGNORE from esphome.core import CORE, EsphomeError from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \ - copy_file_if_changed + copy_file_if_changed, get_bool_env from esphome.storage_json import StorageJSON, storage_path from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS @@ -284,7 +285,8 @@ def write_platformio_project(): mkdir_p(CORE.build_path) content = get_ini_content() - write_gitignore() + if not get_bool_env(ENV_NOGITIGNORE): + write_gitignore() write_platformio_ini(content) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 053fba6274..857a986538 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -126,6 +126,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors try: hash(key) except TypeError: + # pylint: disable=raise-missing-from raise yaml.constructor.ConstructorError( f'Invalid key "{key}" (not hashable)', key_node.start_mark) @@ -297,7 +298,7 @@ def _load_yaml_internal(fname): try: return loader.get_single_data() or OrderedDict() except yaml.YAMLError as exc: - raise EsphomeError(exc) + raise EsphomeError(exc) from exc finally: loader.dispose() diff --git a/requirements.txt b/requirements.txt index 5f15ec5aa0..78aa0442b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ -voluptuous==0.11.7 +voluptuous==0.12.0 PyYAML==5.3.1 -paho-mqtt==1.5.0 -colorlog==4.2.1 -tornado==6.0.4 -protobuf==3.12.2 +paho-mqtt==1.5.1 +colorama==0.4.4 +colorlog==4.6.2 +tornado==6.1 +protobuf==3.13.0 tzlocal==2.1 -pytz==2020.1 -pyserial==3.4 +pytz==2020.5 +pyserial==3.5 ifaddr==0.1.7 -platformio==4.3.4 +platformio==5.0.4 esptool==2.8 click==7.1.2 diff --git a/requirements_test.txt b/requirements_test.txt index e9846fb29c..864dfe1a72 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,12 +1,12 @@ -pylint==2.5.3 -flake8==3.8.3 +pylint==2.6.0 +flake8==3.8.4 pillow>4.0.0 cryptography>=2.0.0,<4 pexpect==4.8.0 # Unit tests -pytest==5.4.3 -pytest-cov==2.10.0 -pytest-mock==3.2.0 +pytest==6.2.1 +pytest-cov==2.10.1 +pytest-mock==3.5.1 asyncmock==0.4.2 hypothesis==5.21.0 diff --git a/script/build_codeowners.py b/script/build_codeowners.py index fce59b3b95..f21e9ca2a5 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -48,6 +48,10 @@ for path in components_dir.iterdir(): name = path.name comp = get_component(name) + if comp is None: + print(f'Cannot find component {name}. Make sure current path is pip installed ESPHome') + sys.exit(1) + codeowners[f'esphome/components/{name}/*'].extend(comp.codeowners) for platform_path in path.iterdir(): diff --git a/script/ci-custom.py b/script/ci-custom.py index 3954eea0de..ab2beadf85 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -7,9 +7,18 @@ import os.path import re import subprocess import sys +import time +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): + 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.splitlines()): column = 0 while True: @@ -20,15 +29,24 @@ def find_all(a_str, sub): column += len(sub) -command = ['git', 'ls-files', '-s'] -proc = subprocess.Popen(command, stdout=subprocess.PIPE) -output, err = proc.communicate() -lines = [x.split() for x in output.decode('utf-8').splitlines()] -EXECUTABLE_BIT = { - s[3].strip(): int(s[0]) for s in lines -} -files = [s[3].strip() for s in lines] -files = list(filter(os.path.exists, files)) +parser = argparse.ArgumentParser() +parser.add_argument('files', nargs='*', default=[], + help='files to be processed (regex on path)') +parser.add_argument('-c', '--changed', action='store_true', + help='Only run on changed files') +parser.add_argument('--print-slowest', action='store_true', + help='Print the slowest checks') +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)] + +if args.changed: + files = filter_changed(files) + files.sort() file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg', @@ -60,7 +78,14 @@ def run_check(lint_obj, fname, *args): def run_checks(lints, fname, *args): for lint in lints: - add_errors(fname, run_check(lint, fname, *args)) + start = time.process_time() + try: + add_errors(fname, run_check(lint, fname, *args)) + except Exception: + print(f"Check {lint['func'].__name__} on file {fname} failed:") + raise + duration = time.process_time() - start + lint.setdefault('durations', []).append(duration) def _add_check(checks, func, include=None, exclude=None): @@ -96,6 +121,7 @@ def lint_re_check(regex, **kwargs): decor = lint_content_check(**kwargs) def decorator(func): + @functools.wraps(func) def new_func(fname, content): errors = [] for match in prog.finditer(content): @@ -109,6 +135,7 @@ def lint_re_check(regex, **kwargs): continue errors.append((lineno, col+1, err)) return errors + return decor(new_func) return decorator @@ -117,6 +144,7 @@ def lint_content_find_check(find, **kwargs): decor = lint_content_check(**kwargs) def decorator(func): + @functools.wraps(func) def new_func(fname, content): find_ = find if callable(find): @@ -206,6 +234,10 @@ def lint_no_long_delays(fname, match): @lint_content_check(include=['esphome/const.py']) def lint_const_ordered(fname, content): + """Lint that value in const.py are ordered. + + Reason: Otherwise people add it to the end, and then that results in merge conflicts. + """ lines = content.splitlines() errors = [] for start in ['CONF_', 'ICON_', 'UNIT_']: @@ -217,10 +249,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((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))) + errors.append((mi, 1, + f"Constant {highlight(ml)} is not ordered, please make sure all " + f"constants are ordered. See line {mi} (should go to line {target}, " + f"{target_text})")) return errors @@ -302,7 +334,7 @@ def lint_no_arduino_framework_functions(fname, match): ) -@lint_re_check(r'[^\w\d]byte\s+[\w\d]+\s*=.*', include=cpp_include, exclude={ +@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): @@ -385,8 +417,8 @@ 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) +@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" @@ -471,4 +503,15 @@ for f, errs in sorted(errors.items()): print(f"ERROR {f}:{lineno}:{col} - {msg}") print() +if args.print_slowest: + lint_times = [] + for lint in LINT_FILE_CHECKS + LINT_CONTENT_CHECKS + LINT_POST_CHECKS: + durations = lint.get('durations', []) + lint_times.append((sum(durations), len(durations), lint['func'].__name__)) + lint_times.sort(key=lambda x: -x[0]) + for i in range(min(len(lint_times), 10)): + dur, invocations, name = lint_times[i] + print(f" - '{name}' took {dur:.2f}s total (ran on {invocations} files)") + print(f"Total time measured: {sum(x[0] for x in lint_times):.2f}s") + sys.exit(len(errors)) diff --git a/script/quicklint b/script/quicklint index e391ca3276..a4fae98195 100755 --- a/script/quicklint +++ b/script/quicklint @@ -6,6 +6,6 @@ cd "$(dirname "$0")/.." set -x -script/ci-custom.py +script/ci-custom.py -c script/lint-python -c script/lint-cpp -c diff --git a/tests/test1.yaml b/tests/test1.yaml index d53d113d01..5aed1dac44 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -34,12 +34,12 @@ packages: wifi: networks: - - ssid: 'MySSID' - password: 'password1' - - ssid: 'MySSID2' - password: '' - channel: 14 - bssid: 'A1:63:95:47:D3:1D' + - ssid: 'MySSID' + password: 'password1' + - ssid: 'MySSID2' + password: '' + channel: 14 + bssid: 'A1:63:95:47:D3:1D' manual_ip: static_ip: 192.168.178.230 gateway: 192.168.178.1 @@ -80,47 +80,47 @@ mqtt: ESP_LOGD("main", "Got message %s", x.c_str()); - topic: livingroom/ota_mode then: - - deep_sleep.prevent + - deep_sleep.prevent - topic: livingroom/ota_mode then: - - deep_sleep.enter: + - deep_sleep.enter: on_json_message: topic: the/topic then: - - if: - condition: - - wifi.connected: - - mqtt.connected: - - light.is_on: kitchen - - light.is_off: kitchen - then: - - lambda: |- - int data = x["my_data"]; - ESP_LOGD("main", "The data is: %d", data); - - light.turn_on: - id: living_room_lights - brightness: !lambda |- - float brightness = 1.0; - if (x.containsKey("brightness")) - brightness = x["brightness"]; - return brightness; - effect: !lambda |- - const char *effect = "None"; - if (x.containsKey("effect")) - effect = x["effect"]; - return effect; - - light.control: - id: living_room_lights - brightness: !lambda 'return id(living_room_lights).current_values.get_brightness() + 0.5;' - - light.dim_relative: - id: living_room_lights - relative_brightness: 5% - - uart.write: - id: uart0 - data: Hello World - - uart.write: [0x00, 0x20, 0x30] - - uart.write: !lambda |- - return {}; + - if: + condition: + - wifi.connected: + - mqtt.connected: + - light.is_on: kitchen + - light.is_off: kitchen + then: + - lambda: |- + int data = x["my_data"]; + ESP_LOGD("main", "The data is: %d", data); + - light.turn_on: + id: living_room_lights + brightness: !lambda |- + float brightness = 1.0; + if (x.containsKey("brightness")) + brightness = x["brightness"]; + return brightness; + effect: !lambda |- + const char *effect = "None"; + if (x.containsKey("effect")) + effect = x["effect"]; + return effect; + - light.control: + id: living_room_lights + brightness: !lambda 'return id(living_room_lights).current_values.get_brightness() + 0.5;' + - light.dim_relative: + id: living_room_lights + relative_brightness: 5% + - uart.write: + id: uart0 + data: Hello World + - uart.write: [0x00, 0x20, 0x30] + - uart.write: !lambda |- + return {}; i2c: sda: 21 @@ -154,6 +154,8 @@ ota: safe_mode: True password: 'superlongpasswordthatnoonewillknow' port: 3286 + reboot_timeout: 2min + num_attempts: 5 logger: baud_rate: 0 @@ -195,14 +197,29 @@ wled: adalight: +mcp3008: + - id: 'mcp3008_hub' + cs_pin: GPIO12 + +mcp23s08: + - id: 'mcp23s08_hub' + cs_pin: GPIO12 + deviceaddress: 0 + +mcp23s17: + - id: 'mcp23s17_hub' + cs_pin: GPIO12 + deviceaddress: 1 + + sensor: - platform: adc pin: A0 - name: "Living Room Brightness" + name: 'Living Room Brightness' update_interval: '1:01' attenuation: 2.5db - unit_of_measurement: "°C" - icon: "mdi:water-percent" + unit_of_measurement: '°C' + icon: 'mdi:water-percent' accuracy_decimals: 5 expire_after: 120s setup_priority: -100 @@ -211,9 +228,9 @@ sensor: - offset: 2.0 - multiply: 1.2 - calibrate_linear: - - 0.0 -> 0.0 - - 40.0 -> 45.0 - - 100.0 -> 102.5 + - 0.0 -> 0.0 + - 40.0 -> 45.0 + - 100.0 -> 102.5 - filter_out: 42.0 - filter_out: nan - median: @@ -232,8 +249,8 @@ sensor: - debounce: 0.1s - delta: 5.0 - or: - - throttle: 1s - - delta: 5.0 + - throttle: 1s + - delta: 5.0 - lambda: return x * (9.0/5.0) + 32.0; on_value: then: @@ -255,9 +272,9 @@ sensor: ESP_LOGD("main", "Got raw value %f", x); - logger.log: level: DEBUG - format: "Got raw value %f" + format: 'Got raw value %f' args: ['x'] - - logger.log: "Got raw value NAN" + - logger.log: 'Got raw value NAN' - mqtt.publish: topic: some/topic payload: Hello @@ -277,48 +294,48 @@ sensor: cs_pin: 5 phase_a: voltage: - name: "EMON Line Voltage A" + name: 'EMON Line Voltage A' current: - name: "EMON CT1 Current" + name: 'EMON CT1 Current' power: - name: "EMON Active Power CT1" + name: 'EMON Active Power CT1' reactive_power: - name: "EMON Reactive Power CT1" + name: 'EMON Reactive Power CT1' power_factor: - name: "EMON Power Factor CT1" + name: 'EMON Power Factor CT1' gain_voltage: 7305 gain_ct: 27961 phase_b: current: - name: "EMON CT2 Current" + name: 'EMON CT2 Current' power: - name: "EMON Active Power CT2" + name: 'EMON Active Power CT2' reactive_power: - name: "EMON Reactive Power CT2" + name: 'EMON Reactive Power CT2' power_factor: - name: "EMON Power Factor CT2" + name: 'EMON Power Factor CT2' gain_voltage: 7305 gain_ct: 27961 phase_c: current: - name: "EMON CT3 Current" + name: 'EMON CT3 Current' power: - name: "EMON Active Power CT3" + name: 'EMON Active Power CT3' reactive_power: - name: "EMON Reactive Power CT3" + name: 'EMON Reactive Power CT3' power_factor: - name: "EMON Power Factor CT3" + name: 'EMON Power Factor CT3' gain_voltage: 7305 gain_ct: 27961 frequency: - name: "EMON Line Frequency" + name: 'EMON Line Frequency' chip_temperature: - name: "EMON Chip Temp A" + name: 'EMON Chip Temp A' line_frequency: 60Hz current_phases: 3 gain_pga: 2X - platform: bh1750 - name: "Living Room Brightness 3" + name: 'Living Room Brightness 3' internal: true address: 0x23 resolution: 1.0 @@ -329,27 +346,27 @@ sensor: measurement_time: 31 - platform: bme280 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' oversampling: none humidity: - name: "Outside Humidity" + name: 'Outside Humidity' oversampling: 8x address: 0x77 iir_filter: 16x update_interval: 15s - platform: bme680 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' humidity: - name: "Outside Humidity" + name: 'Outside Humidity' gas_resistance: - name: "Outside Gas Sensor" + name: 'Outside Gas Sensor' address: 0x77 heater: temperature: 320 @@ -357,65 +374,65 @@ sensor: update_interval: 15s - platform: bmp085 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' pressure: - name: "Outside Pressure" + name: 'Outside Pressure' filters: - lambda: >- return x / powf(1.0 - (x / 44330.0), 5.255); update_interval: 15s - platform: bmp280 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' oversampling: 16x pressure: - name: "Outside Pressure" + name: 'Outside Pressure' address: 0x77 update_interval: 15s iir_filter: 16x - platform: dallas address: 0x1C0000031EDD2A28 - name: "Living Room Temperature" + name: 'Living Room Temperature' resolution: 9 - platform: dallas index: 1 - name: "Living Room Temperature 2" + name: 'Living Room Temperature 2' - platform: dht pin: GPIO26 temperature: - name: "Living Room Temperature 3" + name: 'Living Room Temperature 3' humidity: - name: "Living Room Humidity 3" + name: 'Living Room Humidity 3' model: AM2302 update_interval: 15s - platform: dht12 temperature: - name: "Living Room Temperature 4" + name: 'Living Room Temperature 4' humidity: - name: "Living Room Humidity 4" + name: 'Living Room Humidity 4' update_interval: 15s - platform: duty_cycle pin: GPIO25 name: Duty Cycle Sensor - platform: esp32_hall - name: "ESP32 Hall Sensor" + name: 'ESP32 Hall Sensor' update_interval: 15s - platform: hdc1080 temperature: - name: "Living Room Temperature 5" + name: 'Living Room Temperature 5' humidity: - name: "Living Room Pressure 5" + name: 'Living Room Pressure 5' update_interval: 15s - platform: hlw8012 sel_pin: 5 cf_pin: 14 cf1_pin: 13 current: - name: "HLW8012 Current" + name: 'HLW8012 Current' voltage: - name: "HLW8012 Voltage" + name: 'HLW8012 Voltage' power: - name: "HLW8012 Power" + name: 'HLW8012 Power' id: hlw8012_power update_interval: 15s current_resistor: 0.001 ohm @@ -424,39 +441,39 @@ sensor: initial_mode: VOLTAGE - platform: total_daily_energy power_id: hlw8012_power - name: "HLW8012 Total Daily Energy" + name: 'HLW8012 Total Daily Energy' - platform: integration sensor: hlw8012_power - name: "Integration Sensor" + name: 'Integration Sensor' time_unit: s - platform: hmc5883l address: 0x68 field_strength_x: - name: "HMC5883L Field Strength X" + name: 'HMC5883L Field Strength X' field_strength_y: - name: "HMC5883L Field Strength Y" + name: 'HMC5883L Field Strength Y' field_strength_z: - name: "HMC5883L Field Strength Z" + name: 'HMC5883L Field Strength Z' heading: - name: "HMC5883L Heading" + name: 'HMC5883L Heading' range: 130uT oversampling: 8x update_interval: 15s - platform: qmc5883l address: 0x0D field_strength_x: - name: "QMC5883L Field Strength X" + name: 'QMC5883L Field Strength X' field_strength_y: - name: "QMC5883L Field Strength Y" + name: 'QMC5883L Field Strength Y' field_strength_z: - name: "QMC5883L Field Strength Z" + name: 'QMC5883L Field Strength Z' heading: - name: "QMC5883L Heading" + name: 'QMC5883L Heading' range: 800uT oversampling: 256x update_interval: 15s - platform: hx711 - name: "HX711 Value" + name: 'HX711 Value' dout_pin: GPIO23 clk_pin: GPIO25 gain: 128 @@ -465,13 +482,13 @@ sensor: address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA219 Current" + name: 'INA219 Current' power: - name: "INA219 Power" + name: 'INA219 Power' bus_voltage: - name: "INA219 Bus Voltage" + name: 'INA219 Bus Voltage' shunt_voltage: - name: "INA219 Shunt Voltage" + name: 'INA219 Shunt Voltage' max_voltage: 32.0V max_current: 3.2A update_interval: 15s @@ -479,13 +496,13 @@ sensor: address: 0x40 shunt_resistance: 0.1 ohm current: - name: "INA226 Current" + name: 'INA226 Current' power: - name: "INA226 Power" + name: 'INA226 Power' bus_voltage: - name: "INA226 Bus Voltage" + name: 'INA226 Bus Voltage' shunt_voltage: - name: "INA226 Shunt Voltage" + name: 'INA226 Shunt Voltage' max_current: 3.2A update_interval: 15s - platform: ina3221 @@ -493,73 +510,73 @@ sensor: channel_1: shunt_resistance: 0.1 ohm current: - name: "INA3221 Channel 1 Current" + name: 'INA3221 Channel 1 Current' power: - name: "INA3221 Channel 1 Power" + name: 'INA3221 Channel 1 Power' bus_voltage: - name: "INA3221 Channel 1 Bus Voltage" + name: 'INA3221 Channel 1 Bus Voltage' shunt_voltage: - name: "INA3221 Channel 1 Shunt Voltage" + name: 'INA3221 Channel 1 Shunt Voltage' update_interval: 15s - platform: htu21d temperature: - name: "Living Room Temperature 6" + name: 'Living Room Temperature 6' humidity: - name: "Living Room Humidity 6" + name: 'Living Room Humidity 6' update_interval: 15s - platform: max6675 - name: "Living Room Temperature" + name: 'Living Room Temperature' cs_pin: GPIO23 update_interval: 15s - platform: max31855 - name: "Den Temperature" + name: 'Den Temperature' cs_pin: GPIO23 update_interval: 15s reference_temperature: - name: "MAX31855 Internal Temperature" + name: 'MAX31855 Internal Temperature' - platform: max31856 - name: "BBQ Temperature" + name: 'BBQ Temperature' cs_pin: GPIO17 update_interval: 15s mains_filter: 50Hz - platform: max31865 - name: "Water Tank Temperature" + name: 'Water Tank Temperature' cs_pin: GPIO23 update_interval: 15s - reference_resistance: "430 Ω" - rtd_nominal_resistance: "100 Ω" + reference_resistance: '430 Ω' + rtd_nominal_resistance: '100 Ω' - platform: mhz19 co2: - name: "MH-Z19 CO2 Value" + name: 'MH-Z19 CO2 Value' temperature: - name: "MH-Z19 Temperature" + name: 'MH-Z19 Temperature' update_interval: 15s automatic_baseline_calibration: false - platform: mpu6050 address: 0x68 accel_x: - name: "MPU6050 Accel X" + name: 'MPU6050 Accel X' accel_y: - name: "MPU6050 Accel Y" + name: 'MPU6050 Accel Y' accel_z: - name: "MPU6050 Accel z" + name: 'MPU6050 Accel z' gyro_x: - name: "MPU6050 Gyro X" + name: 'MPU6050 Gyro X' gyro_y: - name: "MPU6050 Gyro Y" + name: 'MPU6050 Gyro Y' gyro_z: - name: "MPU6050 Gyro z" + name: 'MPU6050 Gyro z' temperature: - name: "MPU6050 Temperature" + name: 'MPU6050 Temperature' - platform: ms5611 temperature: - name: "Outside Temperature" + name: 'Outside Temperature' pressure: - name: "Outside Pressure" + name: 'Outside Pressure' address: 0x77 update_interval: 15s - platform: pulse_counter - name: "Pulse Counter" + name: 'Pulse Counter' pin: GPIO12 count_mode: rising_edge: INCREMENT @@ -567,15 +584,15 @@ sensor: internal_filter: 13us update_interval: 15s - platform: rotary_encoder - name: "Rotary Encoder" + name: 'Rotary Encoder' id: rotary_encoder1 pin_a: GPIO23 pin_b: GPIO25 pin_reset: GPIO25 filters: - or: - - debounce: 0.1s - - delta: 10 + - debounce: 0.1s + - delta: 10 resolution: 4 min_value: -10 max_value: 30 @@ -586,82 +603,88 @@ sensor: - sensor.rotary_encoder.set_value: id: rotary_encoder1 value: !lambda 'return -1;' + on_clockwise: + - logger.log: 'Clockwise' + on_anticlockwise: + - logger.log: 'Anticlockwise' - platform: pulse_width name: Pulse Width pin: GPIO12 - platform: senseair co2: - name: "SenseAir CO2 Value" + name: 'SenseAir CO2 Value' update_interval: 15s - platform: sht3xd temperature: - name: "Living Room Temperature 8" + name: 'Living Room Temperature 8' humidity: - name: "Living Room Humidity 8" + name: 'Living Room Humidity 8' address: 0x44 update_interval: 15s - platform: sts3x - name: "Living Room Temperature 9" + name: 'Living Room Temperature 9' address: 0x4A - platform: scd30 co2: - name: "Living Room CO2 9" + name: 'Living Room CO2 9' temperature: - name: "Living Room Temperature 9" + name: 'Living Room Temperature 9' humidity: - name: "Living Room Humidity 9" + name: 'Living Room Humidity 9' address: 0x61 update_interval: 15s automatic_self_calibration: true altitude_compensation: 10m + ambient_pressure_compensation: 961mBar + temperature_offset: 4.2C - platform: sgp30 eco2: - name: "Workshop eCO2" + name: 'Workshop eCO2' accuracy_decimals: 1 tvoc: - name: "Workshop TVOC" + name: 'Workshop TVOC' accuracy_decimals: 1 address: 0x58 update_interval: 5s - platform: sps30 pm_1_0: - name: "Workshop PM <1µm Weight concentration" - id: "workshop_PM_1_0" + name: 'Workshop PM <1µm Weight concentration' + id: 'workshop_PM_1_0' pm_2_5: - name: "Workshop PM <2.5µm Weight concentration" - id: "workshop_PM_2_5" + name: 'Workshop PM <2.5µm Weight concentration' + id: 'workshop_PM_2_5' pm_4_0: - name: "Workshop PM <4µm Weight concentration" - id: "workshop_PM_4_0" + name: 'Workshop PM <4µm Weight concentration' + id: 'workshop_PM_4_0' pm_10_0: - name: "Workshop PM <10µm Weight concentration" - id: "workshop_PM_10_0" + name: 'Workshop PM <10µm Weight concentration' + id: 'workshop_PM_10_0' pmc_0_5: - name: "Workshop PM <0.5µm Number concentration" - id: "workshop_PMC_0_5" + name: 'Workshop PM <0.5µm Number concentration' + id: 'workshop_PMC_0_5' pmc_1_0: - name: "Workshop PM <1µm Number concentration" - id: "workshop_PMC_1_0" + name: 'Workshop PM <1µm Number concentration' + id: 'workshop_PMC_1_0' pmc_2_5: - name: "Workshop PM <2.5µm Number concentration" - id: "workshop_PMC_2_5" + name: 'Workshop PM <2.5µm Number concentration' + id: 'workshop_PMC_2_5' pmc_4_0: - name: "Workshop PM <4µm Number concentration" - id: "workshop_PMC_4_0" + name: 'Workshop PM <4µm Number concentration' + id: 'workshop_PMC_4_0' pmc_10_0: - name: "Workshop PM <10µm Number concentration" - id: "workshop_PMC_10_0" + name: 'Workshop PM <10µm Number concentration' + id: 'workshop_PMC_10_0' address: 0x69 update_interval: 10s - platform: shtcx temperature: - name: "Living Room Temperature 10" + name: 'Living Room Temperature 10' humidity: - name: "Living Room Humidity 10" + name: 'Living Room Humidity 10' address: 0x70 update_interval: 15s - platform: template - name: "Template Sensor" + name: 'Template Sensor' id: template_sensor lambda: |- if (id(ultrasonic_sensor1).state > 1) { @@ -678,7 +701,7 @@ sensor: id: template_sensor state: !lambda 'return NAN;' - platform: tsl2561 - name: "TSL2561 Ambient Light" + name: 'TSL2561 Ambient Light' address: 0x39 update_interval: 15s is_cs_package: true @@ -689,17 +712,17 @@ sensor: echo_pin: number: GPIO23 inverted: true - name: "Ultrasonic Sensor" + name: 'Ultrasonic Sensor' timeout: 5.5m id: ultrasonic_sensor1 - platform: uptime name: Uptime Sensor - platform: wifi_signal - name: "WiFi Signal Sensor" + name: 'WiFi Signal Sensor' update_interval: 15s - platform: mqtt_subscribe - name: "MQTT Subscribe Sensor 1" - topic: "mqtt/topic" + name: 'MQTT Subscribe Sensor 1' + topic: 'mqtt/topic' id: the_sensor qos: 2 on_value: @@ -710,9 +733,9 @@ sensor: root["greeting"] = "Hello World"; - platform: sds011 pm_2_5: - name: "SDS011 PM2.5" + name: 'SDS011 PM2.5' pm_10_0: - name: "SDS011 PM10.0" + name: 'SDS011 PM10.0' update_interval: 5min rx_only: false - platform: ccs811 @@ -724,9 +747,9 @@ sensor: baseline: 0x4242 - platform: tx20 wind_speed: - name: "Windspeed" + name: 'Windspeed' wind_direction_degrees: - name: "Winddirection Degrees" + name: 'Winddirection Degrees' pin: number: GPIO04 mode: INPUT @@ -734,29 +757,61 @@ sensor: clock_pin: GPIO5 data_pin: GPIO4 co2: - name: "ZyAura CO2" + name: 'ZyAura CO2' temperature: - name: "ZyAura Temperature" + name: 'ZyAura Temperature' humidity: - name: "ZyAura Humidity" + name: 'ZyAura Humidity' - platform: as3935 lightning_energy: - name: "Lightning Energy" + name: 'Lightning Energy' distance: - name: "Distance Storm" + name: 'Distance Storm' - platform: tmp117 - name: "TMP117 Temperature" + name: 'TMP117 Temperature' update_interval: 5s - platform: hm3301 pm_1_0: - name: "PM1.0" + name: 'PM1.0' pm_2_5: - name: "PM2.5" + name: 'PM2.5' pm_10_0: - name: "PM10.0" + name: 'PM10.0' aqi: - name: "AQI" - calculation_type: "CAQI" + name: 'AQI' + calculation_type: 'CAQI' + - platform: teleinfo + tags: + - tag_name: 'HCHC' + sensor: + name: 'hchc' + unit_of_measurement: 'Wh' + icon: mdi:flash + - tag_name: 'HCHP' + sensor: + name: 'hchp' + unit_of_measurement: 'Wh' + icon: mdi:flash + - tag_name: 'PAPP' + sensor: + name: 'papp' + unit_of_measurement: 'VA' + icon: mdi:flash + update_interval: 60s + historical_mode: true + - platform: mcp9808 + name: 'MCP9808 Temperature' + update_interval: 15s + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: 'pH' + - platform: mcp3008 + update_interval: 5s + mcp3008_id: 'mcp3008_hub' + id: freezer_temp_source + reference_voltage: 3.19 + number: 0 esp32_touch: setup_mode: False @@ -768,9 +823,27 @@ esp32_touch: voltage_attenuation: 1.5V binary_sensor: + - platform: gpio + name: 'MCP23S08 Pin #1' + pin: + mcp23s08: mcp23s08_hub + # Use pin number 1 + number: 1 + # One of INPUT or INPUT_PULLUP + mode: INPUT_PULLUP + inverted: False + - platform: gpio + name: 'MCP23S17 Pin #1' + pin: + mcp23s17: mcp23s17_hub + # Use pin number 1 + number: 1 + # One of INPUT or INPUT_PULLUP + mode: INPUT_PULLUP + inverted: False - platform: gpio pin: GPIO9 - name: "Living Room Window" + name: 'Living Room Window' device_class: window filters: - invert: @@ -797,55 +870,55 @@ binary_sensor: - min_length: 50ms max_length: 350ms then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); + - lambda: >- + ESP_LOGD("main", "Double Clicked"); - then: - - lambda: >- - ESP_LOGD("main", "Double Clicked"); + - lambda: >- + ESP_LOGD("main", "Double Clicked"); on_multi_click: - - timing: - - ON for at most 1s - - OFF for at most 1s - - ON for at most 1s - - OFF for at least 0.2s - then: - - logger.log: - format: "Multi Clicked TWO" - level: warn - - timing: - - OFF for 1s to 2s - - ON for 1s to 2s - - OFF for at least 0.5s - then: - - logger.log: - format: "Multi Clicked LONG SINGLE" - level: warn - - timing: - - ON for at most 1s - - OFF for at least 0.5s - then: - - logger.log: - format: "Multi Clicked SINGLE" - level: warn + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.2s + then: + - logger.log: + format: 'Multi Clicked TWO' + level: warn + - timing: + - OFF for 1s to 2s + - ON for 1s to 2s + - OFF for at least 0.5s + then: + - logger.log: + format: 'Multi Clicked LONG SINGLE' + level: warn + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - logger.log: + format: 'Multi Clicked SINGLE' + level: warn id: binary_sensor1 - platform: gpio pin: number: GPIO9 mode: INPUT_PULLUP - name: "Living Room Window 2" + name: 'Living Room Window 2' - platform: status - name: "Living Room Status" + name: 'Living Room Status' - platform: esp32_touch - name: "ESP32 Touch Pad GPIO27" + name: 'ESP32 Touch Pad GPIO27' pin: GPIO27 threshold: 1000 id: btn_left - platform: nextion page_id: 0 component_id: 2 - name: "Nextion Component 2 Touch" + name: 'Nextion Component 2 Touch' - platform: template - name: "Garage Door Open" + name: 'Garage Door Open' id: garage_door lambda: |- if (isnan(id(my_sensor).state)) { @@ -872,33 +945,33 @@ binary_sensor: frequency: !lambda 'return 500.0;' - platform: pn532 uid: 74-10-37-94 - name: "PN532 NFC Tag" + name: 'PN532 NFC Tag' - platform: rdm6300 uid: 7616525 - name: "RDM6300 NFC Tag" + name: 'RDM6300 NFC Tag' - platform: gpio - name: "PCF binary sensor" + name: 'PCF binary sensor' pin: pcf8574: pcf8574_hub number: 1 mode: INPUT inverted: True - platform: gpio - name: "MCP21 binary sensor" + name: 'MCP21 binary sensor' pin: mcp23017: mcp23017_hub number: 1 mode: INPUT inverted: True - platform: gpio - name: "MCP22 binary sensor" + name: 'MCP22 binary sensor' pin: mcp23008: mcp23008_hub number: 7 mode: INPUT_PULLUP inverted: False - platform: gpio - name: "MCP23 binary sensor" + name: 'MCP23 binary sensor' pin: mcp23016: mcp23016_hub number: 7 @@ -906,14 +979,48 @@ binary_sensor: inverted: False - platform: remote_receiver - name: "Raw Remote Receiver Test" + name: 'Raw Remote Receiver Test' raw: - code: [5685, -4252, 1711, -2265, 1712, -2265, 1711, -2264, 1712, -2266, - 3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267, - 1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265, - 1712, -2262, 1714, -2267, 1709] + code: + [ + 5685, + -4252, + 1711, + -2265, + 1712, + -2265, + 1711, + -2264, + 1712, + -2266, + 3700, + -2263, + 1712, + -4254, + 1711, + -4249, + 1715, + -2266, + 1710, + -2267, + 1709, + -2265, + 3704, + -4250, + 1712, + -4254, + 3700, + -2260, + 1714, + -2265, + 1712, + -2262, + 1714, + -2267, + 1709, + ] - platform: as3935 - name: "Storm Alert" + name: 'Storm Alert' pca9685: frequency: 500 @@ -1068,12 +1175,12 @@ e131: light: - platform: binary - name: "Desk Lamp" + name: 'Desk Lamp' output: gpio_26 effects: - strobe: - strobe: - name: "My Strobe" + name: 'My Strobe' colors: - state: True duration: 250ms @@ -1088,7 +1195,7 @@ light: id: livingroom_lights state: yes - platform: monochromatic - name: "Kitchen Lights" + name: 'Kitchen Lights' id: kitchen output: gpio_19 gamma_correct: 2.8 @@ -1097,7 +1204,7 @@ light: - strobe: - flicker: - flicker: - name: "My Flicker" + name: 'My Flicker' alpha: 98% intensity: 1.5% - lambda: @@ -1109,20 +1216,20 @@ light: if (state == 4) state = 0; - platform: rgb - name: "Living Room Lights" + name: 'Living Room Lights' id: living_room_lights red: pca_0 green: pca_1 blue: pca_2 - platform: rgbw - name: "Living Room Lights 2" + name: 'Living Room Lights 2' red: pca_3 green: pca_4 blue: pca_5 white: pca_6 color_interlock: true - platform: rgbww - name: "Living Room Lights 2" + name: 'Living Room Lights 2' red: pca_3 green: pca_4 blue: pca_5 @@ -1132,7 +1239,7 @@ light: warm_white_color_temperature: 500 mireds color_interlock: true - platform: cwww - name: "Living Room Lights 2" + name: 'Living Room Lights 2' cold_white: pca_6 warm_white: pca_6 cold_white_color_temperature: 153 mireds @@ -1147,104 +1254,105 @@ light: max_refresh_rate: 20ms power_supply: atx_power_supply color_correct: [75%, 100%, 50%] - name: "FastLED WS2811 Light" + name: 'FastLED WS2811 Light' effects: - - addressable_color_wipe: - - addressable_color_wipe: - name: Color Wipe Effect With Custom Values - colors: - - red: 100% - green: 100% - blue: 100% - num_leds: 1 - - red: 0% - green: 0% - blue: 0% - num_leds: 1 - add_led_interval: 100ms - reverse: False - - addressable_scan: - - addressable_scan: - name: Scan Effect With Custom Values - move_interval: 100ms - - addressable_twinkle: - - addressable_twinkle: - name: Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 4ms - - addressable_random_twinkle: - - addressable_random_twinkle: - name: Random Twinkle Effect With Custom Values - twinkle_probability: 5% - progress_interval: 32ms - - addressable_fireworks: - - addressable_fireworks: - name: Fireworks Effect With Custom Values - update_interval: 32ms - spark_probability: 10% - use_random_color: false - fade_out_rate: 120 - - addressable_flicker: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% - - addressable_lambda: - name: "Test For Custom Lambda Effect" + - addressable_color_wipe: + - addressable_color_wipe: + name: Color Wipe Effect With Custom Values + colors: + - red: 100% + green: 100% + blue: 100% + num_leds: 1 + - red: 0% + green: 0% + blue: 0% + num_leds: 1 + add_led_interval: 100ms + reverse: False + - addressable_scan: + - addressable_scan: + name: Scan Effect With Custom Values + move_interval: 100ms + - addressable_twinkle: + - addressable_twinkle: + name: Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 4ms + - addressable_random_twinkle: + - addressable_random_twinkle: + name: Random Twinkle Effect With Custom Values + twinkle_probability: 5% + progress_interval: 32ms + - addressable_fireworks: + - addressable_fireworks: + name: Fireworks Effect With Custom Values + update_interval: 32ms + spark_probability: 10% + use_random_color: false + fade_out_rate: 120 + - addressable_flicker: + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% + - addressable_lambda: + name: 'Test For Custom Lambda Effect' lambda: |- if (initial_run) { it[0] = current_color; } - - wled: - port: 11111 + - wled: + port: 11111 - - adalight: - uart_id: adalight_uart + - adalight: + uart_id: adalight_uart - - automation: - name: Custom Effect - sequence: - - light.addressable_set: - id: addr1 - red: 100% - green: 100% - blue: 0% - - delay: 100ms - - light.addressable_set: - id: addr1 - red: 0% - green: 100% - blue: 0% - - e131: - universe: 1 + - automation: + name: Custom Effect + sequence: + - light.addressable_set: + id: addr1 + red: 100% + green: 100% + blue: 0% + - delay: 100ms + - light.addressable_set: + id: addr1 + red: 0% + green: 100% + blue: 0% + - e131: + universe: 1 - platform: fastled_spi id: addr2 chipset: WS2801 data_pin: GPIO23 clock_pin: GPIO22 + data_rate: 2MHz num_leds: 60 rgb_order: BRG - name: "FastLED SPI Light" + name: 'FastLED SPI Light' - platform: neopixelbus id: addr3 - name: "Neopixelbus Light" + name: 'Neopixelbus Light' gamma_correct: 2.8 color_correct: [0.0, 0.0, 0.0, 0.0] default_transition_length: 10s power_supply: atx_power_supply effects: - - addressable_flicker: - name: Flicker Effect With Custom Values - update_interval: 16ms - intensity: 5% + - addressable_flicker: + name: Flicker Effect With Custom Values + update_interval: 16ms + intensity: 5% type: GRBW variant: SK6812 method: ESP32_I2S_0 num_leds: 60 pin: GPIO23 - platform: partition - name: "Partition Light" + name: 'Partition Light' segments: - id: addr1 from: 0 @@ -1289,12 +1397,30 @@ climate: name: LG Climate - platform: toshiba name: Toshiba Climate + - platform: hitachi_ac344 + name: Hitachi Climate switch: + - platform: gpio + name: 'MCP23S08 Pin #0' + pin: + mcp23s08: mcp23s08_hub + # Use pin number 0 + number: 0 + mode: OUTPUT + inverted: False + - platform: gpio + name: 'MCP23S17 Pin #0' + pin: + mcp23s17: mcp23s17_hub + # Use pin number 0 + number: 1 + mode: OUTPUT + inverted: False - platform: gpio pin: GPIO25 - name: "Living Room Dehumidifier" - icon: "mdi:restart" + name: 'Living Room Dehumidifier' + icon: 'mdi:restart' inverted: True command_topic: custom_command_topic restore_mode: ALWAYS_OFF @@ -1400,35 +1526,35 @@ switch: optimistic: True assumed_state: yes turn_on_action: - - switch.turn_on: living_room_lights_on - - output.set_level: - id: gpio_19 - level: 50% - - output.set_level: - id: gpio_19 - level: !lambda 'return 0.5;' - - output.set_level: - id: dac_output - level: 50% - - output.set_level: - id: dac_output - level: !lambda 'return 0.5;' + - switch.turn_on: living_room_lights_on + - output.set_level: + id: gpio_19 + level: 50% + - output.set_level: + id: gpio_19 + level: !lambda 'return 0.5;' + - output.set_level: + id: dac_output + level: 50% + - output.set_level: + id: dac_output + level: !lambda 'return 0.5;' turn_off_action: - - switch.turn_on: living_room_lights_off + - switch.turn_on: living_room_lights_off restore_state: False on_turn_on: - switch.template.publish: id: livingroom_lights state: yes - platform: restart - name: "Living Room Restart" + name: 'Living Room Restart' - platform: shutdown - name: "Living Room Shutdown" + name: 'Living Room Shutdown' - platform: output - name: "Generic Output" + name: 'Generic Output' output: pca_6 - platform: template - name: "Template Switch" + name: 'Template Switch' id: my_switch lambda: |- if (id(binary_sensor1).state) { @@ -1453,30 +1579,30 @@ switch: id: my_switch state: !lambda 'return false;' - platform: uart - name: "UART String Output" + name: 'UART String Output' data: 'DataToSend' - platform: uart - name: "UART Bytes Output" + name: 'UART Bytes Output' data: [0xDE, 0xAD, 0xBE, 0xEF] - platform: template assumed_state: yes name: Stepper Switch turn_on_action: - - stepper.set_target: - id: my_stepper - target: !lambda |- - static int32_t i = 0; - i += 1000; - if (i > 5000) { - i = -5000; - } - return i; - - stepper.report_position: - id: my_stepper - position: 0 + - stepper.set_target: + id: my_stepper + target: !lambda |- + static int32_t i = 0; + i += 1000; + if (i > 5000) { + i = -5000; + } + return i; + - stepper.report_position: + id: my_stepper + position: 0 - platform: gpio - name: "SN74HC595 Pin #0" + name: 'SN74HC595 Pin #0' pin: sn74hc595: sn74hc595_hub # Use pin number 0 @@ -1486,12 +1612,12 @@ switch: fan: - platform: binary output: gpio_26 - name: "Living Room Fan 1" + name: 'Living Room Fan 1' oscillation_output: gpio_19 direction_output: gpio_26 - platform: speed output: pca_6 - name: "Living Room Fan 2" + name: 'Living Room Fan 2' oscillation_output: gpio_19 direction_output: gpio_26 speed: @@ -1506,20 +1632,20 @@ fan: interval: - interval: 10s then: - - display.page.show: !lambda |- - if (true) return id(page1); else return id(page2); - - display.page.show_next: display1 - - display.page.show_previous: display1 + - display.page.show: !lambda |- + if (true) return id(page1); else return id(page2); + - display.page.show_next: display1 + - display.page.show_previous: display1 - interval: 2s then: - lambda: |- - static uint16_t btn_left_state = id(btn_left)->get_value(); + static uint16_t btn_left_state = id(btn_left)->get_value(); - ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); + ESP_LOGD("adaptive touch", "___ Touch Pad '%s' (T%u): val: %u state: %u tres:%u", id(btn_left)->get_name().c_str(), id(btn_left)->get_touch_pad(), id(btn_left)->get_value(), btn_left_state, id(btn_left)->get_threshold()); - btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; + btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; - id(btn_left)->set_threshold(btn_left_state * 0.9); + id(btn_left)->set_threshold(btn_left_state * 0.9); color: - id: kbx_red @@ -1532,104 +1658,149 @@ color: blue: 100% display: -- platform: lcd_gpio - dimensions: 18x4 - data_pins: - - GPIO19 - - GPIO21 - - GPIO22 - - GPIO23 - enable_pin: GPIO23 - rs_pin: GPIO25 - lambda: |- - it.print("Hello World!"); -- platform: lcd_pcf8574 - dimensions: 18x4 - address: 0x3F - lambda: |- - it.print("Hello World!"); -- platform: max7219 - cs_pin: GPIO23 - num_chips: 1 - lambda: |- - it.print("01234567"); -- platform: tm1637 - clk_pin: GPIO23 - dio_pin: GPIO25 - intensity: 3 - lambda: |- + - platform: lcd_gpio + dimensions: 18x4 + data_pins: + - GPIO19 + - GPIO21 + - GPIO22 + - GPIO23 + enable_pin: GPIO23 + rs_pin: GPIO25 + lambda: |- + it.print("Hello World!"); + - platform: lcd_pcf8574 + dimensions: 18x4 + address: 0x3F + lambda: |- + it.print("Hello World!"); + - platform: max7219 + cs_pin: GPIO23 + num_chips: 1 + lambda: |- + it.print("01234567"); + - platform: tm1637 + clk_pin: GPIO23 + dio_pin: GPIO25 + intensity: 3 + lambda: |- it.print("1234"); -- platform: tm1637 - clk_pin: - mcp23017: mcp23017_hub - number: 1 - dio_pin: - mcp23017: mcp23017_hub - number: 2 - intensity: 3 - lambda: |- + - platform: tm1637 + clk_pin: + mcp23017: mcp23017_hub + number: 1 + dio_pin: + mcp23017: mcp23017_hub + number: 2 + intensity: 3 + lambda: |- it.print("1234"); -- platform: nextion - lambda: |- - it.set_component_value("gauge", 50); - it.set_component_text("textview", "Hello World!"); -- platform: pcd8544 - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: ssd1306_i2c - model: "SSD1306_128X64" - reset_pin: GPIO23 - address: 0x3C - id: display1 - brightness: 60% - pages: - - id: page1 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - - id: page2 - lambda: |- - // Nothing -- platform: ssd1306_spi - model: "SSD1306 128x64" - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: ssd1325_spi - model: "SSD1325 128x64" - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: ssd1351_spi - model: "SSD1351 128x128" - cs_pin: GPIO23 - dc_pin: GPIO23 - reset_pin: GPIO23 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: waveshare_epaper - cs_pin: GPIO23 - dc_pin: GPIO23 - busy_pin: GPIO23 - reset_pin: GPIO23 - model: 2.90in - full_update_every: 30 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); -- platform: st7789v - cs_pin: GPIO5 - dc_pin: GPIO16 - reset_pin: GPIO23 - backlight_pin: GPIO4 - lambda: |- - it.rectangle(0, 0, it.get_width(), it.get_height()); - + - platform: nextion + lambda: |- + it.set_component_value("gauge", 50); + it.set_component_text("textview", "Hello World!"); + - platform: pcd8544 + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + contrast: 60 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1306_i2c + model: 'SSD1306_128X64' + reset_pin: GPIO23 + address: 0x3C + id: display1 + brightness: 60% + pages: + - id: page1 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page2 + lambda: |- + // Nothing + - platform: ssd1306_spi + model: 'SSD1306 128x64' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1322_spi + model: "SSD1322 256x64" + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1325_spi + model: 'SSD1325 128x64' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1327_i2c + model: 'SSD1327 128X128' + reset_pin: GPIO23 + address: 0x3D + id: display1327 + brightness: 60% + pages: + - id: page13271 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - id: page13272 + lambda: |- + // Nothing + - platform: ssd1327_spi + model: 'SSD1327 128x128' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1331_spi + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: ssd1351_spi + model: 'SSD1351 128x128' + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: waveshare_epaper + cs_pin: GPIO23 + dc_pin: GPIO23 + busy_pin: GPIO23 + reset_pin: GPIO23 + model: 2.90in + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7789v + cs_pin: GPIO5 + dc_pin: GPIO16 + reset_pin: GPIO23 + backlight_pin: GPIO4 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); + - platform: st7735 + model: 'INITR_BLACKTAB' + cs_pin: GPIO5 + dc_pin: GPIO16 + reset_pin: GPIO23 + rotation: 0 + devicewidth: 128 + deviceheight: 160 + colstart: 0 + rowstart: 0 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); tm1651: id: tm1651_battery clk_pin: GPIO23 @@ -1642,7 +1813,7 @@ remote_receiver: status_led: pin: GPIO2 -pn532: +pn532_spi: cs_pin: GPIO23 update_interval: 1s on_tag: @@ -1652,26 +1823,52 @@ pn532: topic: the/topic payload: !lambda 'return x;' +pn532_i2c: + rdm6300: +rc522_spi: + cs_pin: GPIO23 + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + +rc522_i2c: + update_interval: 1s + on_tag: + - lambda: |- + ESP_LOGD("main", "Found tag %s", x.c_str()); + gps: time: -- platform: sntp - id: sntp_time - servers: - - 0.pool.ntp.org - - 1.pool.ntp.org - - 192.168.178.1 - on_time: - cron: '/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI' - then: - - lambda: 'ESP_LOGD("main", "time");' -- platform: gps + - platform: sntp + id: sntp_time + servers: + - 0.pool.ntp.org + - 1.pool.ntp.org + - 192.168.178.1 + on_time: + cron: '/30 0-30,30/5 * ? JAN-DEC MON,SAT-SUN,TUE-FRI' + then: + - lambda: 'ESP_LOGD("main", "time");' + - platform: gps + on_time_sync: + then: + ds1307.write_time: + id: ds1307_time + - platform: ds1307 + id: ds1307_time + update_interval: never + on_time: + seconds: 0 + then: + ds1307.read_time cover: - platform: template - name: "Template Cover" + name: 'Template Cover' id: template_cover lambda: |- if (id(binary_sensor1).state) { @@ -1705,65 +1902,67 @@ mcp23016: address: 0x23 stepper: -- platform: a4988 - id: my_stepper - step_pin: GPIO23 - dir_pin: GPIO25 - sleep_pin: GPIO25 - max_speed: 250 steps/s - acceleration: 100 steps/s^2 - deceleration: 200 steps/s^2 - + - platform: a4988 + id: my_stepper + step_pin: GPIO23 + dir_pin: GPIO25 + sleep_pin: GPIO25 + max_speed: 250 steps/s + acceleration: 100 steps/s^2 + deceleration: 200 steps/s^2 globals: -- id: glob_int - type: int - restore_value: yes - initial_value: '0' -- id: glob_float - type: float - restore_value: yes - initial_value: '0.0f' -- id: glob_bool - type: bool - restore_value: no - initial_value: 'true' -- id: glob_string - type: std::string - restore_value: no - # initial_value: "" + - id: glob_int + type: int + restore_value: yes + initial_value: '0' + - id: glob_float + type: float + restore_value: yes + initial_value: '0.0f' + - id: glob_bool + type: bool + restore_value: no + initial_value: 'true' + - id: glob_string + type: std::string + restore_value: no + # initial_value: "" text_sensor: -- platform: mqtt_subscribe - name: "MQTT Subscribe Text" - topic: "the/topic" - qos: 2 - on_value: - - text_sensor.template.publish: - id: template_text - state: Hello World - - text_sensor.template.publish: - id: template_text - state: |- - return "Hello World2"; - - globals.set: - id: glob_int - value: '0' -- platform: template - name: Template Text Sensor - id: template_text -- platform: wifi_info - ip_address: - name: "IP Address" - ssid: - name: "SSID" - bssid: - name: "BSSID" - mac_address: - name: "Mac Address" -- platform: version - name: "ESPHome Version No Timestamp" - hide_timestamp: True + - platform: mqtt_subscribe + name: 'MQTT Subscribe Text' + topic: 'the/topic' + qos: 2 + on_value: + - text_sensor.template.publish: + id: template_text + state: Hello World + - text_sensor.template.publish: + id: template_text + state: |- + return "Hello World2"; + - globals.set: + id: glob_int + value: '0' + - canbus.send: + can_id: 23 + data: [0x10, 0x20, 0x30] + - platform: template + name: Template Text Sensor + id: template_text + - platform: wifi_info + ip_address: + name: 'IP Address' + ssid: + name: 'SSID' + bssid: + name: 'BSSID' + mac_address: + name: 'Mac Address' + - platform: version + name: 'ESPHome Version No Timestamp' + hide_timestamp: True sn74hc595: - id: 'sn74hc595_hub' @@ -1775,3 +1974,22 @@ sn74hc595: rtttl: output: gpio_19 + +canbus: + - platform: mcp2515 + cs_pin: GPIO17 + can_id: 4 + bit_rate: 50kbps + on_frame: + - can_id: 500 + then: + - lambda: |- + std::string b(x.begin(), x.end()); + ESP_LOGD("canid 500", "%s", &b[0] ); + - can_id: 23 + then: + - if: + condition: + lambda: 'return x[0] == 0x11;' + then: + light.toggle: living_room_lights diff --git a/tests/test2.yaml b/tests/test2.yaml index d19c8ade49..b975090531 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -40,6 +40,7 @@ uart: ota: safe_mode: True port: 3286 + num_attempts: 15 logger: level: DEBUG @@ -47,136 +48,146 @@ logger: as3935_i2c: irq_pin: GPIO12 - sensor: - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world - platform: ble_rssi mac_address: AC:37:43:77:5F:4C - name: "BLE Google Home Mini RSSI value" + name: 'BLE Google Home Mini RSSI value' - platform: ble_rssi service_uuid: '11aa' - name: "BLE Test Service 16" + name: 'BLE Test Service 16' - platform: ble_rssi service_uuid: '11223344' - name: "BLE Test Service 32" + name: 'BLE Test Service 32' - platform: ble_rssi service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: "BLE Test Service 128" + name: 'BLE Test Service 128' - platform: ruuvitag mac_address: FF:56:D3:2F:7D:E8 humidity: - name: "RuuviTag Humidity" + name: 'RuuviTag Humidity' temperature: - name: "RuuviTag Temperature" + name: 'RuuviTag Temperature' pressure: - name: "RuuviTag Pressure" + name: 'RuuviTag Pressure' acceleration_x: - name: "RuuviTag Acceleration X" + name: 'RuuviTag Acceleration X' acceleration_y: - name: "RuuviTag Acceleration Y" + name: 'RuuviTag Acceleration Y' acceleration_z: - name: "RuuviTag Acceleration Z" + name: 'RuuviTag Acceleration Z' battery_voltage: - name: "RuuviTag Battery Voltage" + name: 'RuuviTag Battery Voltage' tx_power: - name: "RuuviTag TX Power" + name: 'RuuviTag TX Power' movement_counter: - name: "RuuviTag Movement Counter" + name: 'RuuviTag Movement Counter' measurement_sequence_number: - name: "RuuviTag Measurement Sequence Number" + name: 'RuuviTag Measurement Sequence Number' - platform: as3935 lightning_energy: - name: "Lightning Energy" + name: 'Lightning Energy' distance: - name: "Distance Storm" + name: 'Distance Storm' - platform: xiaomi_hhccjcy01 mac_address: 94:2B:FF:5C:91:61 temperature: - name: "Xiaomi HHCCJCY01 Temperature" + name: 'Xiaomi HHCCJCY01 Temperature' moisture: - name: "Xiaomi HHCCJCY01 Moisture" + name: 'Xiaomi HHCCJCY01 Moisture' illuminance: - name: "Xiaomi HHCCJCY01 Illuminance" + name: 'Xiaomi HHCCJCY01 Illuminance' conductivity: - name: "Xiaomi HHCCJCY01 Soil Conductivity" + name: 'Xiaomi HHCCJCY01 Soil Conductivity' battery_level: - name: "Xiaomi HHCCJCY01 Battery Level" + name: 'Xiaomi HHCCJCY01 Battery Level' - platform: xiaomi_lywsdcgq mac_address: 7A:80:8E:19:36:BA temperature: - name: "Xiaomi LYWSDCGQ Temperature" + name: 'Xiaomi LYWSDCGQ Temperature' humidity: - name: "Xiaomi LYWSDCGQ Humidity" + name: 'Xiaomi LYWSDCGQ Humidity' battery_level: - name: "Xiaomi LYWSDCGQ Battery Level" + name: 'Xiaomi LYWSDCGQ Battery Level' - platform: xiaomi_lywsd02 mac_address: 3F:5B:7D:82:58:4E temperature: - name: "Xiaomi LYWSD02 Temperature" + name: 'Xiaomi LYWSD02 Temperature' humidity: - name: "Xiaomi LYWSD02 Humidity" + name: 'Xiaomi LYWSD02 Humidity' + battery_level: + name: 'Xiaomi LYWSD02 Battery Level' - platform: xiaomi_cgg1 mac_address: 7A:80:8E:19:36:BA temperature: - name: "Xiaomi CGG1 Temperature" + name: 'Xiaomi CGG1 Temperature' humidity: - name: "Xiaomi CGG1 Humidity" + name: 'Xiaomi CGG1 Humidity' battery_level: - name: "Xiaomi CGG1 Battery Level" + name: 'Xiaomi CGG1 Battery Level' - platform: xiaomi_gcls002 - mac_address: "94:2B:FF:5C:91:61" + mac_address: '94:2B:FF:5C:91:61' temperature: - name: "GCLS02 Temperature" + name: 'GCLS02 Temperature' moisture: - name: "GCLS02 Moisture" + name: 'GCLS02 Moisture' conductivity: - name: "GCLS02 Soil Conductivity" + name: 'GCLS02 Soil Conductivity' illuminance: - name: "GCLS02 Illuminance" + name: 'GCLS02 Illuminance' - platform: xiaomi_hhccpot002 - mac_address: "94:2B:FF:5C:91:61" + mac_address: '94:2B:FF:5C:91:61' moisture: - name: "HHCCPOT002 Moisture" + name: 'HHCCPOT002 Moisture' conductivity: - name: "HHCCPOT002 Soil Conductivity" + name: 'HHCCPOT002 Soil Conductivity' - platform: xiaomi_lywsd03mmc - mac_address: "A4:C1:38:4E:16:78" - bindkey: "e9efaa6873f9f9c87a5e75a5f814801c" + mac_address: 'A4:C1:38:4E:16:78' + bindkey: 'e9efaa6873f9f9c87a5e75a5f814801c' temperature: - name: "Xiaomi LYWSD03MMC Temperature" + name: 'Xiaomi LYWSD03MMC Temperature' humidity: - name: "Xiaomi LYWSD03MMC Humidity" + name: 'Xiaomi LYWSD03MMC Humidity' battery_level: - name: "Xiaomi LYWSD03MMC Battery Level" + name: 'Xiaomi LYWSD03MMC Battery Level' - platform: xiaomi_cgd1 - mac_address: "A4:C1:38:D1:61:7D" - bindkey: "c99d2313182473b38001086febf781bd" + mac_address: 'A4:C1:38:D1:61:7D' + bindkey: 'c99d2313182473b38001086febf781bd' temperature: - name: "Xiaomi CGD1 Temperature" + name: 'Xiaomi CGD1 Temperature' humidity: - name: "Xiaomi CGD1 Humidity" + name: 'Xiaomi CGD1 Humidity' battery_level: - name: "Xiaomi CGD1 Battery Level" + name: 'Xiaomi CGD1 Battery Level' - platform: xiaomi_jqjcy01ym - mac_address: "7A:80:8E:19:36:BA" + mac_address: '7A:80:8E:19:36:BA' temperature: - name: "JQJCY01YM Temperature" + name: 'JQJCY01YM Temperature' humidity: - name: "JQJCY01YM Humidity" + name: 'JQJCY01YM Humidity' formaldehyde: - name: "JQJCY01YM Formaldehyde" + name: 'JQJCY01YM Formaldehyde' battery_level: - name: "JQJCY01YM Battery Level" - + name: 'JQJCY01YM Battery Level' + - platform: atc_mithermometer + mac_address: 'A4:C1:38:4E:16:78' + temperature: + name: 'ATC Temperature' + humidity: + name: 'ATC Humidity' + battery_level: + name: 'ATC Battery-Level' + battery_voltage: + name: 'ATC Battery-Voltage' time: -- platform: homeassistant - on_time: - - at: '16:00:00' - then: - - logger.log: It's 16:00 + - platform: homeassistant + on_time: + - at: '16:00:00' + then: + - logger.log: It's 16:00 esp32_touch: setup_mode: True @@ -187,44 +198,43 @@ binary_sensor: id: ha_hello_world_binary - platform: ble_presence mac_address: AC:37:43:77:5F:4C - name: "ESP32 BLE Tracker Google Home Mini" + name: 'ESP32 BLE Tracker Google Home Mini' - platform: ble_presence service_uuid: '11aa' - name: "BLE Test Service 16 Presence" + name: 'BLE Test Service 16 Presence' - platform: ble_presence service_uuid: '11223344' - name: "BLE Test Service 32 Presence" + name: 'BLE Test Service 32 Presence' - platform: ble_presence service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' - name: "BLE Test Service 128 Presence" + name: 'BLE Test Service 128 Presence' - platform: esp32_touch - name: "ESP32 Touch Pad GPIO27" + name: 'ESP32 Touch Pad GPIO27' pin: GPIO27 threshold: 1000 - platform: as3935 - name: "Storm Alert" + name: 'Storm Alert' - platform: xiaomi_mue4094rt - name: "MUE4094RT Motion" - mac_address: "7A:80:8E:19:36:BA" - timeout: "5s" + name: 'MUE4094RT Motion' + mac_address: '7A:80:8E:19:36:BA' + timeout: '5s' - platform: xiaomi_mjyd02yla - name: "MJYD02YL-A Motion" - mac_address: "50:EC:50:CD:32:02" - bindkey: "48403ebe2d385db8d0c187f81e62cb64" + name: 'MJYD02YL-A Motion' + mac_address: '50:EC:50:CD:32:02' + bindkey: '48403ebe2d385db8d0c187f81e62cb64' idle_time: - name: "MJYD02YL-A Idle Time" + name: 'MJYD02YL-A Idle Time' light: - name: "MJYD02YL-A Light Status" + name: 'MJYD02YL-A Light Status' battery_level: - name: "MJYD02YL-A Battery Level" + name: 'MJYD02YL-A Battery Level' - platform: xiaomi_wx08zm - name: "WX08ZM Activation State" - mac_address: "74:a3:4a:b5:07:34" + name: 'WX08ZM Activation State' + mac_address: '74:a3:4a:b5:07:34' tablet: - name: "WX08ZM Tablet Resource" + name: 'WX08ZM Tablet Resource' battery_level: - name: "WX08ZM Battery Level" - + name: 'WX08ZM Battery Level' esp32_ble_tracker: on_ble_advertise: @@ -246,7 +256,6 @@ esp32_ble_tracker: - lambda: !lambda |- ESP_LOGD("main", "Length of manufacturer data is %i", x.size()); - #esp32_ble_beacon: # type: iBeacon # uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' @@ -256,7 +265,7 @@ status_led: text_sensor: - platform: version - name: "ESPHome Version" + name: 'ESPHome Version' icon: mdi:icon id: version_sensor on_value: @@ -264,8 +273,8 @@ text_sensor: condition: - api.connected: then: - - lambda: !lambda |- - ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); + - lambda: !lambda |- + ESP_LOGD("main", "The state is %s=%s", x.c_str(), id(version_sensor).state.c_str()); - script.execute: my_script - homeassistant.service: service: notify.html5 @@ -286,7 +295,7 @@ text_sensor: tag: 1234-abcd - homeassistant.tag_scanned: 1234-abcd - platform: template - name: "Template Text Sensor" + name: 'Template Text Sensor' lambda: |- return {"Hello World"}; - platform: homeassistant @@ -333,6 +342,6 @@ stepper: interval: interval: 5s then: - - logger.log: "Interval Run" + - logger.log: 'Interval Run' display: diff --git a/tests/test3.yaml b/tests/test3.yaml index 14cb4a4e47..64d5dbfc9b 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -8,14 +8,15 @@ esphome: - wait_until: - api.connected - wifi.connected + - time.has_time includes: - custom.h substitutions: devicename: test3 devicecomment: test3 device - min_sub: "0.03" - max_sub: "12.0%" + min_sub: '0.03' + max_sub: '12.0%' api: port: 8000 @@ -203,6 +204,7 @@ uart: ota: safe_mode: True port: 3286 + reboot_timeout: 15min logger: hardware_uart: UART1 @@ -224,7 +226,7 @@ sensor: type: proximity name: APDS9960 Proximity - platform: vl53l0x - name: "VL53L0x Distance" + name: 'VL53L0x Distance' address: 0x29 update_interval: 60s - platform: apds9960 @@ -244,14 +246,14 @@ sensor: id: ha_hello_world - platform: aht10 temperature: - name: "Temperature" + name: 'Temperature' humidity: - name: "Humidity" + name: 'Humidity' - platform: am2320 temperature: - name: "Temperature" + name: 'Temperature' humidity: - name: "Humidity" + name: 'Humidity' - platform: adc pin: VCC id: my_sensor @@ -337,6 +339,7 @@ sensor: - binary_sensor: bin3 value: 100.0 - platform: ade7953 + irq_pin: GPIO16 voltage: name: ADE7953 Voltage current_a: @@ -349,77 +352,83 @@ sensor: name: ADE7953 Active Power B - platform: pzem004t voltage: - name: "PZEM00T Voltage" + name: 'PZEM00T Voltage' current: - name: "PZEM004T Current" + name: 'PZEM004T Current' power: - name: "PZEM004T Power" + name: 'PZEM004T Power' - platform: pzemac voltage: - name: "PZEMAC Voltage" + name: 'PZEMAC Voltage' current: - name: "PZEMAC Current" + name: 'PZEMAC Current' power: - name: "PZEMAC Power" + name: 'PZEMAC Power' energy: - name: "PZEMAC Energy" + name: 'PZEMAC Energy' frequency: - name: "PZEMAC Frequency" + name: 'PZEMAC Frequency' power_factor: - name: "PZEMAC Power Factor" + name: 'PZEMAC Power Factor' - platform: pzemdc voltage: - name: "PZEMDC Voltage" + name: 'PZEMDC Voltage' current: - name: "PZEMDC Current" + name: 'PZEMDC Current' power: - name: "PZEMDC Power" + name: 'PZEMDC Power' + - platform: tmp102 + name: 'TMP102 Temperature' - platform: hm3301 pm_1_0: - name: "PM1.0" + name: 'PM1.0' pm_2_5: - name: "PM2.5" + name: 'PM2.5' pm_10_0: - name: "PM10.0" + name: 'PM10.0' aqi: - name: "AQI" - calculation_type: "AQI" + name: 'AQI' + calculation_type: 'AQI' - platform: pmsx003 type: PMSX003 pm_1_0: - name: "PM 1.0 Concentration" + name: 'PM 1.0 Concentration' pm_2_5: - name: "PM 2.5 Concentration" + name: 'PM 2.5 Concentration' pm_10_0: - name: "PM 10.0 Concentration" + name: 'PM 10.0 Concentration' - platform: pmsx003 type: PMS5003T pm_2_5: - name: "PM 2.5 Concentration" + name: 'PM 2.5 Concentration' temperature: - name: "PMS Temperature" + name: 'PMS Temperature' humidity: - name: "PMS Humidity" + name: 'PMS Humidity' - platform: pmsx003 type: PMS5003ST pm_2_5: - name: "PM 2.5 Concentration" + name: 'PM 2.5 Concentration' temperature: - name: "PMS Temperature" + name: 'PMS Temperature' humidity: - name: "PMS Humidity" + name: 'PMS Humidity' formaldehyde: - name: "PMS Formaldehyde Concentration" + name: 'PMS Formaldehyde Concentration' - platform: cse7766 voltage: - name: "CSE7766 Voltage" + name: 'CSE7766 Voltage' current: - name: "CSE7766 Current" + name: 'CSE7766 Current' power: - name: "CSE776 Power" + name: 'CSE776 Power' + - platform: ezo + id: ph_ezo + address: 99 + unit_of_measurement: 'pH' time: -- platform: homeassistant + - platform: homeassistant apds9960: address: 0x20 @@ -457,18 +466,18 @@ binary_sensor: - platform: mpr121 id: touchkey0 channel: 0 - name: "touchkey0" + name: 'touchkey0' - platform: mpr121 channel: 1 - name: "touchkey1" + name: 'touchkey1' id: bin1 - platform: mpr121 channel: 2 - name: "touchkey2" + name: 'touchkey2' id: bin2 - platform: mpr121 channel: 3 - name: "touchkey3" + name: 'touchkey3' id: bin3 on_press: then: @@ -502,7 +511,7 @@ status_led: text_sensor: - platform: version - name: "ESPHome Version" + name: 'ESPHome Version' icon: mdi:icon id: version_sensor on_value: @@ -521,7 +530,7 @@ text_sensor: my_variable: |- return id(version_sensor).state; - platform: template - name: "Template Text Sensor" + name: 'Template Text Sensor' lambda: |- return {"Hello World"}; - platform: homeassistant @@ -543,7 +552,7 @@ script: switch: - platform: template - name: "mpr121_toggle" + name: 'mpr121_toggle' id: mpr121_toggle optimistic: True - platform: gpio @@ -573,10 +582,10 @@ switch: name: Custom Switch custom_component: - lambda: |- - auto s = new CustomComponent(); - s->set_update_interval(15000); - return {s}; + lambda: |- + auto s = new CustomComponent(); + s->set_update_interval(15000); + return {s}; stepper: - platform: uln2003 @@ -601,7 +610,7 @@ stepper: interval: interval: 5s then: - - logger.log: "Interval Run" + - logger.log: 'Interval Run' - stepper.set_target: id: my_stepper2 target: 500 @@ -689,7 +698,7 @@ climate: default_target_temperature_high: 20°C - platform: pid id: pid_climate - name: "PID Climate Controller" + name: 'PID Climate Controller' sensor: ha_hello_world default_target_temperature: 21°C heat_output: my_slow_pwm @@ -697,7 +706,6 @@ climate: kp: 0.0 ki: 0.0 kd: 0.0 - cover: - platform: endstop @@ -742,22 +750,24 @@ cover: close_duration: 4.5min - platform: template name: Template Cover with Tilt - tilt_lambda: "return 0.5;" + tilt_lambda: 'return 0.5;' tilt_action: - output.set_level: id: out - level: !lambda "return tilt;" + level: !lambda 'return tilt;' position_action: - output.set_level: id: out - level: !lambda "return pos;" - + level: !lambda 'return pos;' output: - platform: esp8266_pwm id: out pin: D3 frequency: 50Hz + - platform: esp8266_pwm + id: out2 + pin: D4 - platform: custom type: binary lambda: |- @@ -783,7 +793,6 @@ output: id: my_slow_pwm period: 15s - mcp23017: id: mcp23017_hub @@ -806,6 +815,10 @@ light: uart_id: adalight_uart - e131: universe: 1 + - platform: hbridge + name: Icicle Lights + pin_a: out + pin_b: out2 servo: id: my_servo @@ -835,8 +848,7 @@ dfplayer: then: if: condition: - not: - dfplayer.is_playing + not: dfplayer.is_playing then: logger.log: 'Playback finished event' tm1651: @@ -859,6 +871,22 @@ rf_bridge: code: 0x123456 - rf_bridge.learn + on_advanced_code_received: + - lambda: |- + uint32_t test; + std::string test_code; + test = data.length; + test = data.protocol; + test_code = data.code; + - rf_bridge.start_advanced_sniffing + - rf_bridge.stop_advanced_sniffing + - rf_bridge.send_advanced_code: + length: 0x04 + protocol: 0x01 + code: 'ABC123' + - rf_bridge.send_raw: + raw: 'AAA5070008001000ABC12355' + display: - platform: max7219digit cs_pin: GPIO15 diff --git a/tests/test4.yaml b/tests/test4.yaml index 74c898a410..bfeff01e93 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -5,7 +5,7 @@ esphome: build_path: build/test4 substitutions: - devicename: test4 + devicename: test-4 ethernet: type: LAN8720 @@ -49,7 +49,12 @@ web_server: username: admin password: admin +time: + - platform: sntp + id: sntp_time + tuya: + time_id: sntp_time sensor: - platform: homeassistant @@ -87,7 +92,8 @@ climate: id: tuya_climate switch_datapoint: 1 target_temperature_datapoint: 3 - temperature_multiplier: 0.5 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 switch: - platform: tuya diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index ced75af105..846df71a94 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -27,7 +27,7 @@ def test_alphanumeric__invalid(value): actual = config_validation.alphanumeric(value) -@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_")) +@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_-")) def test_valid_name__valid(value): actual = config_validation.valid_name(value)