mirror of
https://github.com/esphome/esphome.git
synced 2025-01-31 10:10:56 +00:00
Add ESP32 Camera (#475)
* Add ESP32 Camera * Fixes * Updates * Fix substitutions not working for non-ASCII * Update docker base image to 1.3.0
This commit is contained in:
parent
c4ada8c9f0
commit
f3ec83fe31
@ -41,11 +41,11 @@ stages:
|
|||||||
|
|
||||||
- |
|
- |
|
||||||
if [[ "${IS_HASSIO}" == "YES" ]]; then
|
if [[ "${IS_HASSIO}" == "YES" ]]; then
|
||||||
BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.2.1
|
BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.3.0
|
||||||
BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH}
|
BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH}
|
||||||
DOCKERFILE=docker/Dockerfile.hassio
|
DOCKERFILE=docker/Dockerfile.hassio
|
||||||
else
|
else
|
||||||
BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.2.1
|
BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.3.0
|
||||||
if [[ "${BUILD_ARCH}" == "amd64" ]]; then
|
if [[ "${BUILD_ARCH}" == "amd64" ]]; then
|
||||||
BUILD_TO=esphome/esphome
|
BUILD_TO=esphome/esphome
|
||||||
else
|
else
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ARG BUILD_FROM=esphome/esphome-base-amd64:1.2.1
|
ARG BUILD_FROM=esphome/esphome-base-amd64:1.3.0
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
ARG BUILD_FROM=esphome/esphome-hassio-base-amd64:1.2.1
|
ARG BUILD_FROM=esphome/esphome-hassio-base-amd64:1.3.0
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
# Copy root filesystem
|
# Copy root filesystem
|
||||||
|
@ -16,11 +16,11 @@ echo "PWD: $PWD"
|
|||||||
|
|
||||||
if [[ ${IS_HASSIO} = "YES" ]]; then
|
if [[ ${IS_HASSIO} = "YES" ]]; then
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg "BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.2.1" \
|
--build-arg "BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.3.0" \
|
||||||
--build-arg "BUILD_VERSION=${CACHE_TAG}" \
|
--build-arg "BUILD_VERSION=${CACHE_TAG}" \
|
||||||
-t "${IMAGE_NAME}" -f ../docker/Dockerfile.hassio ..
|
-t "${IMAGE_NAME}" -f ../docker/Dockerfile.hassio ..
|
||||||
else
|
else
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg "BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.2.1" \
|
--build-arg "BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.3.0" \
|
||||||
-t "${IMAGE_NAME}" -f ../docker/Dockerfile ..
|
-t "${IMAGE_NAME}" -f ../docker/Dockerfile ..
|
||||||
fi
|
fi
|
||||||
|
130
esphome/components/esp32_camera.py
Normal file
130
esphome/components/esp32_camera.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from esphome import config_validation as cv, pins
|
||||||
|
from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \
|
||||||
|
ESP_PLATFORM_ESP32
|
||||||
|
from esphome.cpp_generator import Pvariable, add
|
||||||
|
from esphome.cpp_types import App, Nameable, PollingComponent, esphome_ns
|
||||||
|
|
||||||
|
ESP_PLATFORMS = [ESP_PLATFORM_ESP32]
|
||||||
|
|
||||||
|
ESP32Camera = esphome_ns.class_('ESP32Camera', PollingComponent, Nameable)
|
||||||
|
ESP32CameraFrameSize = esphome_ns.enum('ESP32CameraFrameSize')
|
||||||
|
FRAME_SIZES = {
|
||||||
|
'160X120': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
|
||||||
|
'QQVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120,
|
||||||
|
'128x160': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160,
|
||||||
|
'QQVGA2': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_128X160,
|
||||||
|
'176X144': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
|
||||||
|
'QCIF': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144,
|
||||||
|
'240X176': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176,
|
||||||
|
'HQVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176,
|
||||||
|
'320X240': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240,
|
||||||
|
'QVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240,
|
||||||
|
'400X296': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296,
|
||||||
|
'CIF': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296,
|
||||||
|
'640X480': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480,
|
||||||
|
'VGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480,
|
||||||
|
'800X600': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600,
|
||||||
|
'SVGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600,
|
||||||
|
'1024X768': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768,
|
||||||
|
'XGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768,
|
||||||
|
'1280x1024': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024,
|
||||||
|
'SXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024,
|
||||||
|
'1600X1200': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200,
|
||||||
|
'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200,
|
||||||
|
}
|
||||||
|
|
||||||
|
CONF_DATA_PINS = 'data_pins'
|
||||||
|
CONF_VSYNC_PIN = 'vsync_pin'
|
||||||
|
CONF_HREF_PIN = 'href_pin'
|
||||||
|
CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin'
|
||||||
|
CONF_EXTERNAL_CLOCK = 'external_clock'
|
||||||
|
CONF_I2C_PINS = 'i2c_pins'
|
||||||
|
CONF_RESET_PIN = 'reset_pin'
|
||||||
|
CONF_POWER_DOWN_PIN = 'power_down_pin'
|
||||||
|
|
||||||
|
CONF_MAX_FRAMERATE = 'max_framerate'
|
||||||
|
CONF_IDLE_FRAMERATE = 'idle_framerate'
|
||||||
|
CONF_RESOLUTION = 'resolution'
|
||||||
|
CONF_JPEG_QUALITY = 'jpeg_quality'
|
||||||
|
CONF_VERTICAL_FLIP = 'vertical_flip'
|
||||||
|
CONF_HORIZONTAL_MIRROR = 'horizontal_mirror'
|
||||||
|
CONF_CONTRAST = 'contrast'
|
||||||
|
CONF_BRIGHTNESS = 'brightness'
|
||||||
|
CONF_SATURATION = 'saturation'
|
||||||
|
CONF_TEST_PATTERN = 'test_pattern'
|
||||||
|
|
||||||
|
camera_range_param = vol.All(cv.int_, vol.Range(min=-2, max=2))
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema({
|
||||||
|
cv.GenerateID(): cv.declare_variable_id(ESP32Camera),
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_DATA_PINS): vol.All([pins.input_pin], vol.Length(min=8, max=8)),
|
||||||
|
vol.Required(CONF_VSYNC_PIN): pins.input_pin,
|
||||||
|
vol.Required(CONF_HREF_PIN): pins.input_pin,
|
||||||
|
vol.Required(CONF_PIXEL_CLOCK_PIN): pins.input_pin,
|
||||||
|
vol.Required(CONF_EXTERNAL_CLOCK): vol.Schema({
|
||||||
|
vol.Required(CONF_PIN): pins.output_pin,
|
||||||
|
vol.Optional(CONF_FREQUENCY, default='20MHz'): vol.All(cv.frequency, vol.In([20e6, 10e6])),
|
||||||
|
}),
|
||||||
|
vol.Required(CONF_I2C_PINS): vol.Schema({
|
||||||
|
vol.Required(CONF_SDA): pins.output_pin,
|
||||||
|
vol.Required(CONF_SCL): pins.output_pin,
|
||||||
|
}),
|
||||||
|
vol.Optional(CONF_RESET_PIN): pins.output_pin,
|
||||||
|
vol.Optional(CONF_POWER_DOWN_PIN): pins.output_pin,
|
||||||
|
|
||||||
|
vol.Optional(CONF_MAX_FRAMERATE, default='10 fps'): vol.All(cv.framerate,
|
||||||
|
vol.Range(min=0, min_included=False,
|
||||||
|
max=60)),
|
||||||
|
vol.Optional(CONF_IDLE_FRAMERATE, default='0.1 fps'): vol.All(cv.framerate,
|
||||||
|
vol.Range(min=0, max=1)),
|
||||||
|
vol.Optional(CONF_RESOLUTION, default='640X480'): cv.one_of(*FRAME_SIZES, upper=True),
|
||||||
|
vol.Optional(CONF_JPEG_QUALITY, default=10): vol.All(cv.int_, vol.Range(min=10, max=63)),
|
||||||
|
vol.Optional(CONF_CONTRAST, default=0): camera_range_param,
|
||||||
|
vol.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
|
||||||
|
vol.Optional(CONF_SATURATION, default=0): camera_range_param,
|
||||||
|
vol.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean,
|
||||||
|
vol.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean,
|
||||||
|
vol.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
|
||||||
|
}).extend(cv.COMPONENT_SCHEMA.schema)
|
||||||
|
|
||||||
|
SETTERS = {
|
||||||
|
CONF_DATA_PINS: 'set_data_pins',
|
||||||
|
CONF_VSYNC_PIN: 'set_vsync_pin',
|
||||||
|
CONF_HREF_PIN: 'set_href_pin',
|
||||||
|
CONF_PIXEL_CLOCK_PIN: 'set_pixel_clock_pin',
|
||||||
|
CONF_RESET_PIN: 'set_reset_pin',
|
||||||
|
CONF_POWER_DOWN_PIN: 'set_power_down_pin',
|
||||||
|
CONF_JPEG_QUALITY: 'set_jpeg_quality',
|
||||||
|
CONF_VERTICAL_FLIP: 'set_vertical_flip',
|
||||||
|
CONF_HORIZONTAL_MIRROR: 'set_horizontal_mirror',
|
||||||
|
CONF_CONTRAST: 'set_contrast',
|
||||||
|
CONF_BRIGHTNESS: 'set_brightness',
|
||||||
|
CONF_SATURATION: 'set_saturation',
|
||||||
|
CONF_TEST_PATTERN: 'set_test_pattern',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
rhs = App.register_component(ESP32Camera.new(config[CONF_NAME]))
|
||||||
|
cam = Pvariable(config[CONF_ID], rhs)
|
||||||
|
|
||||||
|
for key, setter in SETTERS.items():
|
||||||
|
if key in config:
|
||||||
|
add(getattr(cam, setter)(config[key]))
|
||||||
|
|
||||||
|
extclk = config[CONF_EXTERNAL_CLOCK]
|
||||||
|
add(cam.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
|
||||||
|
i2c_pins = config[CONF_I2C_PINS]
|
||||||
|
add(cam.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL]))
|
||||||
|
add(cam.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))
|
||||||
|
if config[CONF_IDLE_FRAMERATE] == 0:
|
||||||
|
add(cam.set_idle_update_interval(0))
|
||||||
|
else:
|
||||||
|
add(cam.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE]))
|
||||||
|
add(cam.set_frame_size(FRAME_SIZES[config[CONF_RESOLUTION]]))
|
||||||
|
|
||||||
|
|
||||||
|
BUILD_FLAGS = '-DUSE_ESP32_CAMERA'
|
59
esphome/components/servo.py
Normal file
59
esphome/components/servo.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from esphome.automation import ACTION_REGISTRY
|
||||||
|
from esphome.components.output import FloatOutput
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_IDLE_LEVEL, CONF_MAX_LEVEL, CONF_MIN_LEVEL, CONF_OUTPUT, \
|
||||||
|
CONF_LEVEL
|
||||||
|
from esphome.cpp_generator import Pvariable, add, get_variable, templatable
|
||||||
|
from esphome.cpp_helpers import setup_component
|
||||||
|
from esphome.cpp_types import App, Component, esphome_ns, Action, float_
|
||||||
|
|
||||||
|
Servo = esphome_ns.class_('Servo', Component)
|
||||||
|
ServoWriteAction = esphome_ns.class_('ServoWriteAction', Action)
|
||||||
|
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema({
|
||||||
|
vol.Required(CONF_ID): cv.declare_variable_id(Servo),
|
||||||
|
vol.Required(CONF_OUTPUT): cv.use_variable_id(FloatOutput),
|
||||||
|
vol.Optional(CONF_MIN_LEVEL, default='3%'): cv.percentage,
|
||||||
|
vol.Optional(CONF_IDLE_LEVEL, default='7.5%'): cv.percentage,
|
||||||
|
vol.Optional(CONF_MAX_LEVEL, default='12%'): cv.percentage,
|
||||||
|
}).extend(cv.COMPONENT_SCHEMA.schema)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
for out in get_variable(config[CONF_OUTPUT]):
|
||||||
|
yield
|
||||||
|
|
||||||
|
rhs = App.register_component(Servo.new(out))
|
||||||
|
servo = Pvariable(config[CONF_ID], rhs)
|
||||||
|
|
||||||
|
add(servo.set_min_level(config[CONF_MIN_LEVEL]))
|
||||||
|
add(servo.set_idle_level(config[CONF_IDLE_LEVEL]))
|
||||||
|
add(servo.set_max_level(config[CONF_MAX_LEVEL]))
|
||||||
|
|
||||||
|
setup_component(servo, config)
|
||||||
|
|
||||||
|
|
||||||
|
BUILD_FLAGS = '-DUSE_SERVO'
|
||||||
|
|
||||||
|
CONF_SERVO_WRITE = 'servo.write'
|
||||||
|
SERVO_WRITE_ACTION_SCHEMA = cv.Schema({
|
||||||
|
vol.Required(CONF_ID): cv.use_variable_id(Servo),
|
||||||
|
vol.Required(CONF_LEVEL): cv.templatable(cv.possibly_negative_percentage),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ACTION_REGISTRY.register(CONF_SERVO_WRITE, SERVO_WRITE_ACTION_SCHEMA)
|
||||||
|
def servo_write_to_code(config, action_id, template_arg, args):
|
||||||
|
for var in get_variable(config[CONF_ID]):
|
||||||
|
yield None
|
||||||
|
rhs = ServoWriteAction.new(template_arg, var)
|
||||||
|
type = ServoWriteAction.template(template_arg)
|
||||||
|
action = Pvariable(action_id, rhs, type=type)
|
||||||
|
for template_ in templatable(config[CONF_LEVEL], args, float_):
|
||||||
|
yield None
|
||||||
|
add(action.set_value(template_))
|
||||||
|
yield action
|
@ -6,6 +6,7 @@ import voluptuous as vol
|
|||||||
from esphome import core
|
from esphome import core
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.core import EsphomeError
|
from esphome.core import EsphomeError
|
||||||
|
from esphome.py_compat import string_types
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ def _substitute_item(substitutions, item, path):
|
|||||||
for old, new in replace_keys:
|
for old, new in replace_keys:
|
||||||
item[new] = item[old]
|
item[new] = item[old]
|
||||||
del item[old]
|
del item[old]
|
||||||
elif isinstance(item, str):
|
elif isinstance(item, string_types):
|
||||||
sub = _expand_substitutions(substitutions, item, path)
|
sub = _expand_substitutions(substitutions, item, path)
|
||||||
if sub != item:
|
if sub != item:
|
||||||
return sub
|
return sub
|
||||||
|
@ -28,6 +28,7 @@ port = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
|||||||
float_ = vol.Coerce(float)
|
float_ = vol.Coerce(float)
|
||||||
positive_float = vol.All(float_, vol.Range(min=0))
|
positive_float = vol.All(float_, vol.Range(min=0))
|
||||||
zero_to_one_float = vol.All(float_, vol.Range(min=0, max=1))
|
zero_to_one_float = vol.All(float_, vol.Range(min=0, max=1))
|
||||||
|
negative_one_to_one_float = vol.All(float_, vol.Range(min=-1, max=1))
|
||||||
positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
|
positive_int = vol.All(vol.Coerce(int), vol.Range(min=0))
|
||||||
positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False))
|
positive_not_null_int = vol.All(vol.Coerce(int), vol.Range(min=0, min_included=False))
|
||||||
|
|
||||||
@ -442,6 +443,7 @@ resistance = float_with_unit("resistance", r"(Ω|Ω|ohm|Ohm|OHM)?")
|
|||||||
current = float_with_unit("current", r"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")
|
current = float_with_unit("current", r"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?")
|
||||||
voltage = float_with_unit("voltage", r"(v|V|volt|Volts)?")
|
voltage = float_with_unit("voltage", r"(v|V|volt|Volts)?")
|
||||||
distance = float_with_unit("distance", r"(m)")
|
distance = float_with_unit("distance", r"(m)")
|
||||||
|
framerate = float_with_unit("framerate", r"(FPS|fps|Fps|FpS|Hz)")
|
||||||
|
|
||||||
|
|
||||||
def validate_bytes(value):
|
def validate_bytes(value):
|
||||||
@ -606,6 +608,11 @@ i2c_address = hex_uint8_t
|
|||||||
|
|
||||||
|
|
||||||
def percentage(value):
|
def percentage(value):
|
||||||
|
value = possibly_negative_percentage(value)
|
||||||
|
return zero_to_one_float(value)
|
||||||
|
|
||||||
|
|
||||||
|
def possibly_negative_percentage(value):
|
||||||
has_percent_sign = isinstance(value, string_types) and value.endswith('%')
|
has_percent_sign = isinstance(value, string_types) and value.endswith('%')
|
||||||
if has_percent_sign:
|
if has_percent_sign:
|
||||||
value = float(value[:-1].rstrip()) / 100.0
|
value = float(value[:-1].rstrip()) / 100.0
|
||||||
@ -614,7 +621,12 @@ def percentage(value):
|
|||||||
if not has_percent_sign:
|
if not has_percent_sign:
|
||||||
msg += " Please put a percent sign after the number!"
|
msg += " Please put a percent sign after the number!"
|
||||||
raise vol.Invalid(msg)
|
raise vol.Invalid(msg)
|
||||||
return zero_to_one_float(value)
|
if value < -1:
|
||||||
|
msg = "Percentage must not be smaller than -100%."
|
||||||
|
if not has_percent_sign:
|
||||||
|
msg += " Please put a percent sign after the number!"
|
||||||
|
raise vol.Invalid(msg)
|
||||||
|
return negative_one_to_one_float(value)
|
||||||
|
|
||||||
|
|
||||||
def percentage_int(value):
|
def percentage_int(value):
|
||||||
|
@ -40,6 +40,9 @@ CONF_OTA = 'ota'
|
|||||||
CONF_MQTT = 'mqtt'
|
CONF_MQTT = 'mqtt'
|
||||||
CONF_BROKER = 'broker'
|
CONF_BROKER = 'broker'
|
||||||
CONF_USERNAME = 'username'
|
CONF_USERNAME = 'username'
|
||||||
|
CONF_MIN_LEVEL = 'min_level'
|
||||||
|
CONF_IDLE_LEVEL = 'idle_level'
|
||||||
|
CONF_MAX_LEVEL = 'max_level'
|
||||||
CONF_POWER_SUPPLY = 'power_supply'
|
CONF_POWER_SUPPLY = 'power_supply'
|
||||||
CONF_ID = 'id'
|
CONF_ID = 'id'
|
||||||
CONF_MQTT_ID = 'mqtt_id'
|
CONF_MQTT_ID = 'mqtt_id'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user