diff --git a/esphomeyaml/components/display/__init__.py b/esphomeyaml/components/display/__init__.py new file mode 100644 index 0000000000..addf174561 --- /dev/null +++ b/esphomeyaml/components/display/__init__.py @@ -0,0 +1,56 @@ +# coding=utf-8 +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_LAMBDA, CONF_ROTATION, CONF_UPDATE_INTERVAL +from esphomeyaml.helpers import add, add_job, esphomelib_ns + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + +}) + +display_ns = esphomelib_ns.namespace('display') +DisplayBuffer = display_ns.DisplayBuffer +DisplayBufferRef = DisplayBuffer.operator('ref') + +DISPLAY_ROTATIONS = { + 0: display_ns.DISPLAY_ROTATION_0_DEGREES, + 90: display_ns.DISPLAY_ROTATION_90_DEGREES, + 180: display_ns.DISPLAY_ROTATION_180_DEGREES, + 270: display_ns.DISPLAY_ROTATION_270_DEGREES, +} + + +def validate_rotation(value): + value = cv.string(value) + if value.endswith(u"°"): + value = value[:-1] + try: + value = int(value) + except ValueError: + raise vol.Invalid(u"Expected integer for rotation") + return cv.one_of(*DISPLAY_ROTATIONS)(value) + + +BASIC_DISPLAY_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, +}) + +FULL_DISPLAY_PLATFORM_SCHEMA = BASIC_DISPLAY_PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_LAMBDA): cv.lambda_, + vol.Optional(CONF_ROTATION): validate_rotation, +}) + + +def setup_display_core_(display_var, config): + if CONF_UPDATE_INTERVAL in config: + add(display_var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + if CONF_ROTATION in config: + add(display_var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) + + +def setup_display(display_var, config): + add_job(setup_display_core_, display_var, config) + + +BUILD_FLAGS = '-DUSE_DISPLAY' diff --git a/esphomeyaml/components/display/lcd_gpio.py b/esphomeyaml/components/display/lcd_gpio.py new file mode 100644 index 0000000000..3b2fe630fb --- /dev/null +++ b/esphomeyaml/components/display/lcd_gpio.py @@ -0,0 +1,74 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import display +from esphomeyaml.const import CONF_DIMENSIONS, CONF_ENABLE_PIN, CONF_ID, CONF_LAMBDA, CONF_PINS, \ + CONF_RS_PIN, CONF_RW_PIN +from esphomeyaml.helpers import App, Pvariable, add, gpio_output_pin_expression, process_lambda + +GPIOLCDDisplay = display.display_ns.GPIOLCDDisplay +LCDDisplay = display.display_ns.LCDDisplay +LCDDisplayRef = LCDDisplay.operator('ref') + + +def validate_lcd_dimensions(value): + value = cv.dimensions(value) + if value[0] > 0x40: + raise vol.Invalid("LCD displays can't have more than 64 columns") + if value[1] > 4: + raise vol.Invalid("LCD displays can't have more than 4 rows") + return value + + +def validate_pin_length(value): + if len(value) != 4 and len(value) != 8: + raise vol.Invalid("LCD Displays can either operate in 4-pin or 8-pin mode," + "not {}-pin mode".format(len(value))) + return value + + +PLATFORM_SCHEMA = display.BASIC_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(GPIOLCDDisplay), + vol.Required(CONF_DIMENSIONS): validate_lcd_dimensions, + + vol.Required(CONF_PINS): vol.All([pins.gpio_output_pin_schema], validate_pin_length), + vol.Required(CONF_ENABLE_PIN): pins.gpio_output_pin_schema, + vol.Required(CONF_RS_PIN): pins.gpio_output_pin_schema, + vol.Optional(CONF_RW_PIN): pins.gpio_output_pin_schema, + + vol.Optional(CONF_LAMBDA): cv.lambda_, +}) + + +def to_code(config): + rhs = App.make_gpio_lcd_display(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) + lcd = Pvariable(config[CONF_ID], rhs) + pins_ = [] + for conf in config[CONF_PINS]: + for pin in gpio_output_pin_expression(conf): + yield + pins_.append(pin) + add(lcd.set_data_pins(*pins_)) + for enable in gpio_output_pin_expression(config[CONF_ENABLE_PIN]): + yield + add(lcd.set_enable_pin(enable)) + + for rs in gpio_output_pin_expression(config[CONF_RS_PIN]): + yield + add(lcd.set_rs_pin(rs)) + + if CONF_RW_PIN in config: + for rw in gpio_output_pin_expression(config[CONF_RW_PIN]): + yield + add(lcd.set_rw_pin(rw)) + + if CONF_LAMBDA in config: + for lambda_ in process_lambda(config[CONF_LAMBDA], [(LCDDisplayRef, 'it')]): + yield + add(lcd.set_writer(lambda_)) + + display.setup_display(lcd, config) + + +BUILD_FLAGS = '-DUSE_LCD_DISPLAY' diff --git a/esphomeyaml/components/display/lcd_pcf8574.py b/esphomeyaml/components/display/lcd_pcf8574.py new file mode 100644 index 0000000000..26ba9c8dff --- /dev/null +++ b/esphomeyaml/components/display/lcd_pcf8574.py @@ -0,0 +1,37 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import display +from esphomeyaml.components.display.lcd_gpio import LCDDisplayRef, validate_lcd_dimensions +from esphomeyaml.const import CONF_ADDRESS, CONF_DIMENSIONS, CONF_ID, CONF_LAMBDA +from esphomeyaml.helpers import App, Pvariable, add, process_lambda + +DEPENDENCIES = ['i2c'] + +PCF8574LCDDisplay = display.display_ns.PCF8574LCDDisplay + +PLATFORM_SCHEMA = display.BASIC_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(PCF8574LCDDisplay), + vol.Required(CONF_DIMENSIONS): validate_lcd_dimensions, + vol.Optional(CONF_ADDRESS): cv.i2c_address, + + vol.Optional(CONF_LAMBDA): cv.lambda_, +}) + + +def to_code(config): + rhs = App.make_pcf8574_lcd_display(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]) + lcd = Pvariable(config[CONF_ID], rhs) + + if CONF_ADDRESS in config: + add(lcd.set_address(config[CONF_ADDRESS])) + + if CONF_LAMBDA in config: + for lambda_ in process_lambda(config[CONF_LAMBDA], [(LCDDisplayRef, 'it')]): + yield + add(lcd.set_writer(lambda_)) + + display.setup_display(lcd, config) + + +BUILD_FLAGS = ['-DUSE_LCD_DISPLAY', '-DUSE_LCD_DISPLAY_PCF8574'] diff --git a/esphomeyaml/components/display/max7129.py b/esphomeyaml/components/display/max7129.py new file mode 100644 index 0000000000..353e09b935 --- /dev/null +++ b/esphomeyaml/components/display/max7129.py @@ -0,0 +1,49 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import display +from esphomeyaml.components.spi import SPIComponent +from esphomeyaml.const import CONF_CS_PIN, CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS, \ + CONF_SPI_ID +from esphomeyaml.helpers import App, Pvariable, add, get_variable, gpio_output_pin_expression, \ + process_lambda + +DEPENDENCIES = ['spi'] + +MAX7219Component = display.display_ns.MAX7219Component +MAX7219ComponentRef = MAX7219Component.operator('ref') + +PLATFORM_SCHEMA = display.BASIC_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(MAX7219Component), + cv.GenerateID(CONF_SPI_ID): cv.use_variable_id(SPIComponent), + vol.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + + vol.Optional(CONF_NUM_CHIPS): vol.All(cv.uint8_t, vol.Range(min=1)), + vol.Optional(CONF_INTENSITY): vol.All(cv.uint8_t, vol.Range(min=0, max=15)), + vol.Optional(CONF_LAMBDA): cv.lambda_, +}) + + +def to_code(config): + for spi in get_variable(config[CONF_SPI_ID]): + yield + for cs in gpio_output_pin_expression(config[CONF_CS_PIN]): + yield + rhs = App.make_max7219(spi, cs) + max7219 = Pvariable(config[CONF_ID], rhs) + + if CONF_NUM_CHIPS in config: + add(max7219.set_num_chips(config[CONF_NUM_CHIPS])) + if CONF_INTENSITY in config: + add(max7219.set_intensity(config[CONF_INTENSITY])) + + if CONF_LAMBDA in config: + for lambda_ in process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')]): + yield + add(max7219.set_writer(lambda_)) + + display.setup_display(max7219, config) + + +BUILD_FLAGS = '-DUSE_MAX7219' diff --git a/esphomeyaml/components/display/ssd1306_i2c.py b/esphomeyaml/components/display/ssd1306_i2c.py new file mode 100644 index 0000000000..925961ea71 --- /dev/null +++ b/esphomeyaml/components/display/ssd1306_i2c.py @@ -0,0 +1,39 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import display +from esphomeyaml.components.display import ssd1306_spi +from esphomeyaml.const import CONF_ADDRESS, CONF_EXTERNAL_VCC, CONF_ID, CONF_MODEL, CONF_RESET_PIN +from esphomeyaml.helpers import App, Pvariable, add, gpio_output_pin_expression + +DEPENDENCIES = ['i2c'] + +I2CSSD1306 = display.display_ns.I2CSSD1306 + +PLATFORM_SCHEMA = display.FULL_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(I2CSSD1306), + vol.Required(CONF_MODEL): cv.one_of(*ssd1306_spi.MODELS), + vol.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + vol.Optional(CONF_EXTERNAL_VCC): cv.boolean, + vol.Optional(CONF_ADDRESS): cv.i2c_address, +}) + + +def to_code(config): + ssd = Pvariable(config[CONF_ID], App.make_i2c_ssd1306()) + add(ssd.set_model(ssd1306_spi.MODELS[config[CONF_MODEL]])) + + if CONF_RESET_PIN in config: + for reset in gpio_output_pin_expression(config[CONF_RESET_PIN]): + yield + add(ssd.set_reset_pin(reset)) + if CONF_EXTERNAL_VCC in config: + add(ssd.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_ADDRESS in config: + add(ssd.set_address(config[CONF_ADDRESS])) + + display.setup_display(ssd, config) + + +BUILD_FLAGS = '-DUSE_SSD1306' diff --git a/esphomeyaml/components/display/ssd1306_spi.py b/esphomeyaml/components/display/ssd1306_spi.py new file mode 100644 index 0000000000..9aa2992934 --- /dev/null +++ b/esphomeyaml/components/display/ssd1306_spi.py @@ -0,0 +1,57 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import display +from esphomeyaml.components.spi import SPIComponent +from esphomeyaml.const import CONF_CS_PIN, CONF_DC_PIN, CONF_EXTERNAL_VCC, CONF_ID, CONF_MODEL, \ + CONF_RESET_PIN, CONF_SPI_ID +from esphomeyaml.helpers import App, Pvariable, add, get_variable, gpio_output_pin_expression + +DEPENDENCIES = ['spi'] + +SPISSD1306 = display.display_ns.SPISSD1306 + +MODELS = { + 'SSD1306_128X32': display.display_ns.SSD1306_MODEL_128_32, + 'SSD1306_128X64': display.display_ns.SSD1306_MODEL_128_64, + 'SSD1306_96X16': display.display_ns.SSD1306_MODEL_96_16, + 'SH1106_128X32': display.display_ns.SH1106_MODEL_128_32, + 'SH1106_128X64': display.display_ns.SH1106_MODEL_128_64, + 'SH1106_96X16': display.display_ns.SH1106_MODEL_96_16, +} + +PLATFORM_SCHEMA = display.FULL_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(SPISSD1306), + cv.GenerateID(CONF_SPI_ID): cv.use_variable_id(SPIComponent), + vol.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + vol.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + vol.Required(CONF_MODEL): cv.one_of(*MODELS), + vol.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + vol.Optional(CONF_EXTERNAL_VCC): cv.boolean, +}) + + +def to_code(config): + for spi in get_variable(config[CONF_SPI_ID]): + yield + for cs in gpio_output_pin_expression(config[CONF_CS_PIN]): + yield + for dc in gpio_output_pin_expression(config[CONF_DC_PIN]): + yield + + rhs = App.make_spi_ssd1306(spi, cs, dc) + ssd = Pvariable(config[CONF_ID], rhs) + add(ssd.set_model(MODELS[config[CONF_MODEL]])) + + if CONF_RESET_PIN in config: + for reset in gpio_output_pin_expression(config[CONF_RESET_PIN]): + yield + add(ssd.set_reset_pin(reset)) + if CONF_EXTERNAL_VCC in config: + add(ssd.set_external_vcc(config[CONF_EXTERNAL_VCC])) + + display.setup_display(ssd, config) + + +BUILD_FLAGS = '-DUSE_SSD1306' diff --git a/esphomeyaml/components/display/waveshare_epaper.py b/esphomeyaml/components/display/waveshare_epaper.py new file mode 100644 index 0000000000..fc0c266c02 --- /dev/null +++ b/esphomeyaml/components/display/waveshare_epaper.py @@ -0,0 +1,84 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import pins +from esphomeyaml.components import display +from esphomeyaml.components.spi import SPIComponent +from esphomeyaml.const import CONF_BUSY_PIN, CONF_CS_PIN, CONF_DC_PIN, CONF_FULL_UPDATE_EVERY, \ + CONF_ID, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, CONF_SPI_ID +from esphomeyaml.helpers import App, Pvariable, add, get_variable, gpio_input_pin_expression, \ + gpio_output_pin_expression, process_lambda + +DEPENDENCIES = ['spi'] + +WaveshareEPaperTypeA = display.display_ns.WaveshareEPaperTypeA +WaveshareEPaper = display.display_ns.WaveshareEPaper + +MODELS = { + '1.54in': ('a', display.display_ns.WAVESHARE_EPAPER_1_54_IN), + '2.13in': ('a', display.display_ns.WAVESHARE_EPAPER_2_13_IN), + '2.90in': ('a', display.display_ns.WAVESHARE_EPAPER_2_9_IN), + '2.70in': ('b', display.display_ns.WAVESHARE_EPAPER_2_7_IN), + '4.20in': ('b', display.display_ns.WAVESHARE_EPAPER_4_2_IN), + '7.50in': ('b', display.display_ns.WAVESHARE_EPAPER_7_5_IN), +} + + +def validate_full_update_every_only_type_a(value): + if CONF_FULL_UPDATE_EVERY not in value: + return value + if MODELS[value[CONF_MODEL]][0] != 'a': + raise vol.Invalid("The 'full_update_every' option is only available for models " + "'1.54in', '2.13in' and '2.90in'.") + return value + + +PLATFORM_SCHEMA = vol.All(display.FULL_DISPLAY_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(None), + cv.GenerateID(CONF_SPI_ID): cv.use_variable_id(SPIComponent), + vol.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + vol.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + vol.Required(CONF_MODEL): cv.one_of(*MODELS), + vol.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + vol.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + vol.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, +}), validate_full_update_every_only_type_a) + + +def to_code(config): + for spi in get_variable(config[CONF_SPI_ID]): + yield + for cs in gpio_output_pin_expression(config[CONF_CS_PIN]): + yield + for dc in gpio_output_pin_expression(config[CONF_DC_PIN]): + yield + + model_type, model = MODELS[config[CONF_MODEL]] + if model_type == 'a': + rhs = App.make_waveshare_epaper_type_a(spi, cs, dc, model) + epaper = Pvariable(config[CONF_ID], rhs, type=WaveshareEPaperTypeA) + elif model_type == 'b': + rhs = App.make_waveshare_epaper_type_b(spi, cs, dc, model) + epaper = Pvariable(config[CONF_ID], rhs, type=WaveshareEPaper) + else: + raise NotImplementedError() + + if CONF_LAMBDA in config: + for lambda_ in process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')]): + yield + add(epaper.set_writer(lambda_)) + if CONF_RESET_PIN in config: + for reset in gpio_output_pin_expression(config[CONF_RESET_PIN]): + yield + add(epaper.set_reset_pin(reset)) + if CONF_BUSY_PIN in config: + for reset in gpio_input_pin_expression(config[CONF_BUSY_PIN]): + yield + add(epaper.set_busy_pin(reset)) + if CONF_FULL_UPDATE_EVERY in config: + add(epaper.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + + display.setup_display(epaper, config) + + +BUILD_FLAGS = '-DUSE_WAVESHARE_EPAPER' diff --git a/esphomeyaml/components/font.py b/esphomeyaml/components/font.py new file mode 100644 index 0000000000..07530eded8 --- /dev/null +++ b/esphomeyaml/components/font.py @@ -0,0 +1,126 @@ +# coding=utf-8 +import os.path + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import core +from esphomeyaml.components import display +from esphomeyaml.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_SIZE +from esphomeyaml.core import HexInt +from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add + +DEPENDENCIES = ['display'] + +Font = display.display_ns.Font +Glyph = display.display_ns.Glyph + + +def validate_glyphs(value): + if isinstance(value, list): + value = vol.Schema([cv.string])(value) + value = vol.Schema([cv.string])(list(value)) + + def comparator(x, y): + x_ = x.encode('utf-8') + y_ = y.encode('utf-8') + + for c in range(min(len(x_), len(y_))): + if x_[c] < y_[c]: + return -1 + if x_[c] > y_[c]: + return 1 + + if len(x_) < len(y_): + return -1 + elif len(x_) > len(y_): + return 1 + else: + raise vol.Invalid(u"Found duplicate glyph {}".format(x)) + + value.sort(cmp=comparator) + return value + + +def validate_pillow_installed(value): + try: + import PIL + except ImportError: + raise vol.Invalid("Please install the pillow python package to use fonts. " + "(pip2 install pillow)") + + if PIL.__version__[0] < '4': + raise vol.Invalid("Please update your pillow installation to at least 4.0.x. " + "(pip2 install -U pillow)") + + return value + + +def validate_truetype_file(value): + value = cv.string(value) + path = os.path.join(core.CONFIG_PATH, value) + if not os.path.isfile(path): + raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format(path)) + if value.endswith('.zip'): # for Google Fonts downloads + raise vol.Invalid(u"Please unzip the font archive '{}' first and then use the .ttf files " + u"inside.".format(value)) + if not value.endswith('.ttf'): + raise vol.Invalid(u"Only truetype (.ttf) files are supported. Please make sure you're " + u"using the correct format or rename the extension to .ttf") + return value + + +DEFAULT_GLYPHS = u' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' +CONF_RAW_DATA_ID = 'raw_data_id' + +FONT_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): cv.declare_variable_id(Font), + vol.Required(CONF_FILE): validate_truetype_file, + vol.Optional(CONF_GLYPHS, default=DEFAULT_GLYPHS): validate_glyphs, + vol.Optional(CONF_SIZE, default=12): vol.All(cv.int_, vol.Range(min=1)), + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_variable_id(None), +}) + +CONFIG_SCHEMA = vol.All(validate_pillow_installed, cv.ensure_list, [FONT_SCHEMA]) + + +def to_code(config): + from PIL import ImageFont + + for conf in config: + path = os.path.join(core.CONFIG_PATH, conf[CONF_FILE]) + try: + font = ImageFont.truetype(path, conf[CONF_SIZE]) + except Exception as e: + raise core.ESPHomeYAMLError(u"Could not load truetype file {}: {}".format(path, e)) + + ascent, descent = font.getmetrics() + + glyph_args = {} + data = [] + for glyph in conf[CONF_GLYPHS]: + mask = font.getmask(glyph, mode='1') + _, (offset_x, offset_y) = font.font.getsize(glyph) + width, height = mask.size + width8 = ((width + 7) // 8) * 8 + glyph_data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if not mask.getpixel((x, y)): + continue + pos = x + y * width8 + glyph_data[pos // 8] |= 0x80 >> (pos % 8) + glyph_args[glyph] = (len(data), offset_x, offset_y, width, height) + data += glyph_data + + raw_data = MockObj(conf[CONF_RAW_DATA_ID]) + add(RawExpression('static const uint8_t {}[{}] PROGMEM = {}'.format( + raw_data, len(data), + ArrayInitializer(*[HexInt(x) for x in data], multiline=False)))) + + glyphs = [] + for glyph in conf[CONF_GLYPHS]: + glyphs.append(Glyph(glyph, raw_data, *glyph_args[glyph])) + + rhs = App.make_font(ArrayInitializer(*glyphs), ascent, ascent + descent) + Pvariable(conf[CONF_ID], rhs) diff --git a/esphomeyaml/components/image.py b/esphomeyaml/components/image.py new file mode 100644 index 0000000000..23ac742838 --- /dev/null +++ b/esphomeyaml/components/image.py @@ -0,0 +1,85 @@ +# coding=utf-8 +import logging +import os.path + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml import core +from esphomeyaml.components import display +from esphomeyaml.const import CONF_FILE, CONF_ID, CONF_RESIZE +from esphomeyaml.core import HexInt +from esphomeyaml.helpers import App, ArrayInitializer, MockObj, Pvariable, RawExpression, add + +_LOGGER = logging.getLogger(__name__) + +DEPENDENCIES = ['display'] + +Image_ = display.display_ns.Image + + +def validate_pillow_installed(value): + try: + # pylint: disable=unused-variable + import PIL + except ImportError: + raise vol.Invalid("Please install the pillow python package to use images. " + "(pip2 install pillow)") + + return value + + +def validate_image_file(value): + value = cv.string(value) + path = os.path.join(core.CONFIG_PATH, value) + if not os.path.isfile(path): + raise vol.Invalid(u"Could not find file '{}'. Please make sure it exists.".format(path)) + return value + + +CONF_RAW_DATA_ID = 'raw_data_id' + +FONT_SCHEMA = vol.Schema({ + vol.Required(CONF_ID): cv.declare_variable_id(Image_), + vol.Required(CONF_FILE): validate_image_file, + vol.Optional(CONF_RESIZE): cv.dimensions, + cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_variable_id(None), +}) + +CONFIG_SCHEMA = vol.All(validate_pillow_installed, cv.ensure_list, [FONT_SCHEMA]) + + +def to_code(config): + from PIL import Image + + for conf in config: + path = os.path.join(core.CONFIG_PATH, conf[CONF_FILE]) + try: + image = Image.open(path) + except Exception as e: + raise core.ESPHomeYAMLError(u"Could not load image file {}: {}".format(path, e)) + + if CONF_RESIZE in conf: + image.thumbnail(conf[CONF_RESIZE]) + + image = image.convert('1') + width, height = image.size + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using the resize " + "parameter") + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if image.getpixel((x, y)): + continue + pos = x + y * width8 + data[pos // 8] |= 0x80 >> (pos % 8) + + raw_data = MockObj(conf[CONF_RAW_DATA_ID]) + add(RawExpression('static const uint8_t {}[{}] PROGMEM = {}'.format( + raw_data, len(data), + ArrayInitializer(*[HexInt(x) for x in data], multiline=False)))) + + rhs = App.make_image(raw_data, width, height) + Pvariable(conf[CONF_ID], rhs) diff --git a/esphomeyaml/components/pn532.py b/esphomeyaml/components/pn532.py index e3e37b6b0f..08aa4d3f15 100644 --- a/esphomeyaml/components/pn532.py +++ b/esphomeyaml/components/pn532.py @@ -4,7 +4,7 @@ import esphomeyaml.config_validation as cv from esphomeyaml import pins from esphomeyaml.components import binary_sensor from esphomeyaml.components.spi import SPIComponent -from esphomeyaml.const import CONF_CS, CONF_ID, CONF_SPI_ID, CONF_UPDATE_INTERVAL +from esphomeyaml.const import CONF_CS_PIN, CONF_ID, CONF_SPI_ID, CONF_UPDATE_INTERVAL from esphomeyaml.helpers import App, Pvariable, get_variable, gpio_output_pin_expression DEPENDENCIES = ['spi'] @@ -14,7 +14,7 @@ PN532Component = binary_sensor.binary_sensor_ns.PN532Component CONFIG_SCHEMA = vol.All(cv.ensure_list, [vol.Schema({ cv.GenerateID(): cv.declare_variable_id(PN532Component), cv.GenerateID(CONF_SPI_ID): cv.use_variable_id(SPIComponent), - vol.Required(CONF_CS): pins.gpio_output_pin_schema, + vol.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, })]) @@ -25,7 +25,7 @@ def to_code(config): for spi in get_variable(conf[CONF_SPI_ID]): yield cs = None - for cs in gpio_output_pin_expression(conf[CONF_CS]): + for cs in gpio_output_pin_expression(conf[CONF_CS_PIN]): yield rhs = App.make_pn532_component(spi, cs, conf.get(CONF_UPDATE_INTERVAL)) Pvariable(conf[CONF_ID], rhs) diff --git a/esphomeyaml/components/sensor/max6675.py b/esphomeyaml/components/sensor/max6675.py index ce22f64720..db2484f23f 100644 --- a/esphomeyaml/components/sensor/max6675.py +++ b/esphomeyaml/components/sensor/max6675.py @@ -4,7 +4,8 @@ import esphomeyaml.config_validation as cv from esphomeyaml import pins from esphomeyaml.components import sensor from esphomeyaml.components.spi import SPIComponent -from esphomeyaml.const import CONF_CS, CONF_MAKE_ID, CONF_NAME, CONF_SPI_ID, CONF_UPDATE_INTERVAL +from esphomeyaml.const import CONF_CS_PIN, CONF_MAKE_ID, CONF_NAME, CONF_SPI_ID, \ + CONF_UPDATE_INTERVAL from esphomeyaml.helpers import App, Application, get_variable, gpio_output_pin_expression, variable MakeMAX6675Sensor = Application.MakeMAX6675Sensor @@ -12,7 +13,7 @@ MakeMAX6675Sensor = Application.MakeMAX6675Sensor PLATFORM_SCHEMA = cv.nameable(sensor.SENSOR_PLATFORM_SCHEMA.extend({ cv.GenerateID(CONF_MAKE_ID): cv.declare_variable_id(MakeMAX6675Sensor), cv.GenerateID(CONF_SPI_ID): cv.use_variable_id(SPIComponent), - vol.Required(CONF_CS): pins.gpio_output_pin_schema, + vol.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, vol.Optional(CONF_UPDATE_INTERVAL): cv.positive_time_period_milliseconds, })) @@ -22,7 +23,7 @@ def to_code(config): for spi in get_variable(config[CONF_SPI_ID]): yield cs = None - for cs in gpio_output_pin_expression(config[CONF_CS]): + for cs in gpio_output_pin_expression(config[CONF_CS_PIN]): yield rhs = App.make_max6675_sensor(config[CONF_NAME], spi, cs, config.get(CONF_UPDATE_INTERVAL)) diff --git a/esphomeyaml/components/spi.py b/esphomeyaml/components/spi.py index 40618a5793..116418e349 100644 --- a/esphomeyaml/components/spi.py +++ b/esphomeyaml/components/spi.py @@ -2,18 +2,18 @@ import voluptuous as vol import esphomeyaml.config_validation as cv from esphomeyaml import pins -from esphomeyaml.const import CONF_CLK, CONF_ID, CONF_MISO, CONF_MOSI +from esphomeyaml.const import CONF_CLK_PIN, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN from esphomeyaml.helpers import App, Pvariable, esphomelib_ns, gpio_input_pin_expression, \ - gpio_output_pin_expression + gpio_output_pin_expression, add SPIComponent = esphomelib_ns.SPIComponent -SPI_SCHEMA = vol.Schema({ +SPI_SCHEMA = vol.All(vol.Schema({ cv.GenerateID(): cv.declare_variable_id(SPIComponent), - vol.Required(CONF_CLK): pins.gpio_output_pin_schema, - vol.Required(CONF_MISO): pins.gpio_input_pin_schema, - vol.Optional(CONF_MOSI): pins.gpio_output_pin_schema, -}) + vol.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + vol.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, + vol.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, +}), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN)) CONFIG_SCHEMA = vol.All(cv.ensure_list, [SPI_SCHEMA]) @@ -21,17 +21,18 @@ CONFIG_SCHEMA = vol.All(cv.ensure_list, [SPI_SCHEMA]) def to_code(config): for conf in config: clk = None - for clk in gpio_output_pin_expression(conf[CONF_CLK]): + for clk in gpio_output_pin_expression(conf[CONF_CLK_PIN]): yield - miso = None - for miso in gpio_input_pin_expression(conf[CONF_MISO]): - yield - mosi = None - if CONF_MOSI in conf: - for mosi in gpio_output_pin_expression(conf[CONF_MOSI]): + rhs = App.init_spi(clk) + spi = Pvariable(conf[CONF_ID], rhs) + if CONF_MISO_PIN in conf: + for miso in gpio_input_pin_expression(conf[CONF_MISO_PIN]): yield - rhs = App.init_spi(clk, miso, mosi) - Pvariable(conf[CONF_ID], rhs) + add(spi.set_miso(miso)) + if CONF_MOSI_PIN in conf: + for mosi in gpio_input_pin_expression(conf[CONF_MOSI_PIN]): + yield + add(spi.set_mosi(mosi)) BUILD_FLAGS = '-DUSE_SPI' diff --git a/esphomeyaml/components/time/__init__.py b/esphomeyaml/components/time/__init__.py new file mode 100644 index 0000000000..090915247a --- /dev/null +++ b/esphomeyaml/components/time/__init__.py @@ -0,0 +1,109 @@ +import datetime +import logging +import math + +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.const import CONF_TIMEZONE +from esphomeyaml.helpers import add, add_job, esphomelib_ns + + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({ + +}) + +time_ns = esphomelib_ns.namespace('time') + + +def _tz_timedelta(td): + offset_hour = int(td.total_seconds() / (60 * 60)) + offset_minute = int(abs(td.total_seconds() / 60)) % 60 + offset_second = int(abs(td.total_seconds())) % 60 + if offset_hour == 0 and offset_minute == 0 and offset_second == 0: + return '0' + elif offset_minute == 0 and offset_second == 0: + return '{}'.format(offset_hour) + elif offset_second == 0: + return '{}:{}'.format(offset_hour, offset_minute) + return '{}:{}:{}'.format(offset_hour, offset_minute, offset_second) + + +# https://stackoverflow.com/a/16804556/8924614 +def _week_of_month(dt): + first_day = dt.replace(day=1) + dom = dt.day + adjusted_dom = dom + first_day.weekday() + return int(math.ceil(adjusted_dom / 7.0)) + + +def _tz_dst_str(dt): + td = datetime.timedelta(hours=dt.hour, minutes=dt.minute, seconds=dt.second) + return 'M{}.{}.{}/{}'.format(dt.month, _week_of_month(dt), dt.isoweekday() % 7, + _tz_timedelta(td)) + + +def detect_tz(): + try: + import tzlocal + except ImportError: + raise vol.Invalid("No timezone specified and 'tzlocal' not installed. To automatically " + "detect the timezone please install tzlocal (pip2 install tzlocal)") + tz = tzlocal.get_localzone() + dst_begins = None + dst_tzname = None + dst_utcoffset = None + dst_ends = None + norm_tzname = None + norm_utcoffset = None + + hour = datetime.timedelta(hours=1) + this_year = datetime.datetime.now().year + dt = datetime.datetime(year=this_year, month=1, day=1) + last_dst = None + while dt.year == this_year: + current_dst = tz.dst(dt, is_dst=not last_dst) + is_dst = bool(current_dst) + if is_dst != last_dst: + if is_dst: + dst_begins = dt + dst_tzname = tz.tzname(dt, is_dst=True) + dst_utcoffset = tz.utcoffset(dt, is_dst=True) + else: + dst_ends = dt + hour + norm_tzname = tz.tzname(dt, is_dst=False) + norm_utcoffset = tz.utcoffset(dt, is_dst=False) + last_dst = is_dst + dt += hour + + tzbase = '{}{}'.format(norm_tzname, _tz_timedelta(-1 * norm_utcoffset)) + if dst_begins is None: + # No DST in this timezone + _LOGGER.info("Auto-detected timezone '%s' with UTC offset %s", + norm_tzname, _tz_timedelta(norm_utcoffset)) + return tzbase + tzext = '{}{},{},{}'.format(dst_tzname, _tz_timedelta(-1 * dst_utcoffset), + _tz_dst_str(dst_begins), _tz_dst_str(dst_ends)) + _LOGGER.info("Auto-detected timezone '%s' with UTC offset %s and daylight savings time from " + "%s to %s", + norm_tzname, _tz_timedelta(norm_utcoffset), dst_begins.strftime("%x %X"), + dst_ends.strftime("%x %X")) + return tzbase + tzext + + +TIME_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_TIMEZONE, default=detect_tz): cv.string, +}) + + +def setup_time_core_(time_var, config): + add(time_var.set_timezone(config[CONF_TIMEZONE])) + + +def setup_time(time_var, config): + add_job(setup_time_core_, time_var, config) + + +BUILD_FLAGS = '-DUSE_TIME' diff --git a/esphomeyaml/components/time/sntp.py b/esphomeyaml/components/time/sntp.py new file mode 100644 index 0000000000..19ba250b10 --- /dev/null +++ b/esphomeyaml/components/time/sntp.py @@ -0,0 +1,24 @@ +import voluptuous as vol + +import esphomeyaml.config_validation as cv +from esphomeyaml.components import time as time_ +from esphomeyaml.const import CONF_ID, CONF_LAMBDA, CONF_SERVERS +from esphomeyaml.helpers import App, Pvariable + +SNTPComponent = time_.time_ns.SNTPComponent + +PLATFORM_SCHEMA = time_.TIME_PLATFORM_SCHEMA.extend({ + cv.GenerateID(): cv.declare_variable_id(SNTPComponent), + vol.Optional(CONF_SERVERS): vol.All(cv.ensure_list, [cv.string], vol.Length(max=3)), + vol.Optional(CONF_LAMBDA): cv.lambda_, +}) + + +def to_code(config): + rhs = App.make_sntp_component(*config.get(CONF_SERVERS, [])) + sntp = Pvariable(config[CONF_ID], rhs) + + time_.setup_time(sntp, config) + + +BUILD_FLAGS = '-DUSE_SNTP_COMPONENT' diff --git a/esphomeyaml/config_validation.py b/esphomeyaml/config_validation.py index 2e66efefcb..c4d0b4aa0a 100644 --- a/esphomeyaml/config_validation.py +++ b/esphomeyaml/config_validation.py @@ -102,7 +102,7 @@ def boolean(value): def ensure_list(value): """Wrap value in list if it is not one.""" - if value is None: + if value is None or (isinstance(value, dict) and not value): return [] if isinstance(value, list): return value @@ -566,6 +566,24 @@ def lambda_(value): return Lambda(string_strict(value)) +def dimensions(value): + if isinstance(value, list): + if len(value) != 2: + raise vol.Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + try: + width, height = int(value[0]), int(value[1]) + except ValueError: + raise vol.Invalid(u"Width and height dimensions must be integers") + if width <= 0 or height <= 0: + raise vol.Invalid(u"Width and height must at least be 1") + return [width, height] + value = string(value) + match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) + if not match: + raise vol.Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + return dimensions([match.group(1), match.group(2)]) + + REGISTERED_IDS = set() diff --git a/esphomeyaml/const.py b/esphomeyaml/const.py index 0e1ef49cf4..4e536e0541 100644 --- a/esphomeyaml/const.py +++ b/esphomeyaml/const.py @@ -220,10 +220,10 @@ CONF_ON_VALUE = 'on_value' CONF_ON_RAW_VALUE = 'on_raw_value' CONF_ON_VALUE_RANGE = 'on_value_range' CONF_ON_MESSAGE = 'on_message' -CONF_CS = 'cs' -CONF_CLK = 'clk' -CONF_MISO = 'miso' -CONF_MOSI = 'mosi' +CONF_CS_PIN = 'cs_pin' +CONF_CLK_PIN = 'clk_pin' +CONF_MISO_PIN = 'miso_pin' +CONF_MOSI_PIN = 'mosi_pin' CONF_TURN_ON_ACTION = 'turn_on_action' CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_OPEN_ACTION = 'open_action' @@ -289,6 +289,25 @@ CONF_ONE = 'one' CONF_GROUP = 'group' CONF_DEVICE = 'device' CONF_FAMILY = 'family' +CONF_FILE = 'file' +CONF_GLYPHS = 'glyphs' +CONF_SIZE = 'size' +CONF_RESIZE = 'resize' +CONF_ROTATION = 'rotation' +CONF_DC_PIN = 'dc_pin' +CONF_RESET_PIN = 'reset_pin' +CONF_BUSY_PIN = 'busy_pin' +CONF_FULL_UPDATE_EVERY = 'full_update_every' +CONF_PINS = 'pins' +CONF_ENABLE_PIN = 'enable_pin' +CONF_RS_PIN = 'rs_pin' +CONF_RW_PIN = 'rw_pin' +CONF_DIMENSIONS = 'dimensions' +CONF_NUM_CHIPS = 'num_chips' +CONF_INTENSITY = 'intensity' +CONF_EXTERNAL_VCC = 'external_vcc' +CONF_TIMEZONE = 'timezone' +CONF_SERVERS = 'servers' ESP32_BOARDS = [ 'featheresp32', 'node32s', 'espea32', 'firebeetle32', 'esp32doit-devkit-v1', diff --git a/esphomeyaml/core.py b/esphomeyaml/core.py index 2c51fbb8a1..edb96b2836 100644 --- a/esphomeyaml/core.py +++ b/esphomeyaml/core.py @@ -10,6 +10,8 @@ class ESPHomeYAMLError(Exception): class HexInt(long): def __str__(self): + if 0 <= self <= 255: + return "0x{:02X}".format(self) return "0x{:X}".format(self) @@ -181,8 +183,8 @@ class TimePeriodSeconds(TimePeriod): class Lambda(object): def __init__(self, value): self.value = value - self.parts = re.split(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)\.', value) - self.requires_ids = [ID(self.parts[i]) for i in range(1, len(self.parts), 2)] + self.parts = re.split(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)', value) + self.requires_ids = [ID(self.parts[i]) for i in range(1, len(self.parts), 3)] def __str__(self): return self.value diff --git a/esphomeyaml/helpers.py b/esphomeyaml/helpers.py index c45e1ec988..7f008507d4 100644 --- a/esphomeyaml/helpers.py +++ b/esphomeyaml/helpers.py @@ -226,7 +226,7 @@ class LambdaExpression(Expression): self.return_type = return_type if return_type is not None: self.requires.append(return_type) - for i in range(1, len(parts), 2): + for i in range(1, len(parts), 3): self.requires.append(parts[i]) def __str__(self): @@ -411,7 +411,11 @@ def process_lambda(value, parameters, capture='=', return_type=None): var = None for var in get_variable(id): yield - parts[i*2 + 1] = var._ + if parts[i * 3 + 2] == '.': + parts[i * 3 + 1] = var._ + else: + parts[i * 3 + 1] = var + parts[i * 3 + 2] = '' yield LambdaExpression(parts, parameters, capture, return_type) return @@ -522,6 +526,13 @@ class MockObj(Expression): obj.requires.append(self) return obj + def operator(self, name): + if name == 'ref': + obj = MockObj(u'{} &'.format(self.base), u'') + obj.requires.append(self) + return obj + raise NotImplementedError() + def has_side_effects(self): return self._has_side_effects