From 19959cf5079681df80fbeaa54d356b1dd1da40ca Mon Sep 17 00:00:00 2001
From: Otto Winter <otto@otto-winter.com>
Date: Sat, 18 Aug 2018 21:40:59 +0200
Subject: [PATCH] Displays

---
 esphomeyaml/components/display/__init__.py    |  56 ++++++++
 esphomeyaml/components/display/lcd_gpio.py    |  74 ++++++++++
 esphomeyaml/components/display/lcd_pcf8574.py |  37 +++++
 esphomeyaml/components/display/max7129.py     |  49 +++++++
 esphomeyaml/components/display/ssd1306_i2c.py |  39 ++++++
 esphomeyaml/components/display/ssd1306_spi.py |  57 ++++++++
 .../components/display/waveshare_epaper.py    |  84 ++++++++++++
 esphomeyaml/components/font.py                | 126 ++++++++++++++++++
 esphomeyaml/components/image.py               |  85 ++++++++++++
 esphomeyaml/components/pn532.py               |   6 +-
 esphomeyaml/components/sensor/max6675.py      |   7 +-
 esphomeyaml/components/spi.py                 |  33 ++---
 esphomeyaml/components/time/__init__.py       | 109 +++++++++++++++
 esphomeyaml/components/time/sntp.py           |  24 ++++
 esphomeyaml/config_validation.py              |  20 ++-
 esphomeyaml/const.py                          |  27 +++-
 esphomeyaml/core.py                           |   6 +-
 esphomeyaml/helpers.py                        |  15 ++-
 18 files changed, 823 insertions(+), 31 deletions(-)
 create mode 100644 esphomeyaml/components/display/__init__.py
 create mode 100644 esphomeyaml/components/display/lcd_gpio.py
 create mode 100644 esphomeyaml/components/display/lcd_pcf8574.py
 create mode 100644 esphomeyaml/components/display/max7129.py
 create mode 100644 esphomeyaml/components/display/ssd1306_i2c.py
 create mode 100644 esphomeyaml/components/display/ssd1306_spi.py
 create mode 100644 esphomeyaml/components/display/waveshare_epaper.py
 create mode 100644 esphomeyaml/components/font.py
 create mode 100644 esphomeyaml/components/image.py
 create mode 100644 esphomeyaml/components/time/__init__.py
 create mode 100644 esphomeyaml/components/time/sntp.py

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