import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import time as time_, uart from esphome.const import ( CONF_BRIGHTNESS, CONF_DATA, CONF_ID, CONF_INTERVAL, CONF_MODE, CONF_STATE, CONF_TEXT, CONF_TIME_ID, ) DEPENDENCIES = ["uart", "time"] CONF_COLS = "cols" CONF_COLUMN = "column" CONF_ROWS = "rows" CONF_LINE = "line" CONF_FORMAT = "format" CONF_CENTERED = "centered" CONF_SEND_NEWLINES = "send_newlines" CONF_CODE_TABLE = "code_table" CONF_INTERNATIONAL_CHARSET = "international_charset" CONF_USER_CHARACTERS = "user_characters" CONF_CODE = "code" CONF_PATTERN = "pattern" samsung_vfd_ns = cg.esphome_ns.namespace("samsung_vfd") SamsungVfd = samsung_vfd_ns.class_("SamsungVfd", cg.Component, uart.UARTDevice) CodeTable = samsung_vfd_ns.enum("CodeTable") CODE_TABLES = { "PC437": CodeTable.CODE_TABLE_PC437, "PC850": CodeTable.CODE_TABLE_PC850, "PC866": CodeTable.CODE_TABLE_PC866, "PC737": CodeTable.CODE_TABLE_PC737, "PC775": CodeTable.CODE_TABLE_PC775, "CP10": CodeTable.CODE_TABLE_CP10, "PC864": CodeTable.CODE_TABLE_PC864, "PC852": CodeTable.CODE_TABLE_PC852, } InternationalCharset = samsung_vfd_ns.enum("InternationalCharset") CHARSETS = { "USA": InternationalCharset.CHARSET_USA, "FRANCE": InternationalCharset.CHARSET_FRANCE, "GERMANY": InternationalCharset.CHARSET_GERMANY, "UK": InternationalCharset.CHARSET_UK, "DENMARK1": InternationalCharset.CHARSET_DENMARK1, "SWEDEN": InternationalCharset.CHARSET_SWEDEN, "ITALY": InternationalCharset.CHARSET_ITALY, "SPAIN1": InternationalCharset.CHARSET_SPAIN1, "JAPAN": InternationalCharset.CHARSET_JAPAN, "NORWAY": InternationalCharset.CHARSET_NORWAY, "DENMARK2": InternationalCharset.CHARSET_DENMARK2, } DisplayMode = samsung_vfd_ns.enum("DisplayMode") DISPLAY_MODES = { "OVERWRITE": DisplayMode.MODE_OVERWRITE, "VERTICAL_SCROLL": DisplayMode.MODE_VERTICAL_SCROLL, "HORIZONTAL_SCROLL": DisplayMode.MODE_HORIZONTAL_SCROLL, } PrintAction = samsung_vfd_ns.class_("PrintAction", automation.Action) PrintfAction = samsung_vfd_ns.class_("PrintfAction", automation.Action) ClearAction = samsung_vfd_ns.class_("ClearAction", automation.Action) HomeAction = samsung_vfd_ns.class_("HomeAction", automation.Action) SetBrightnessAction = samsung_vfd_ns.class_("SetBrightnessAction", automation.Action) SetBlinkAction = samsung_vfd_ns.class_("SetBlinkAction", automation.Action) TurnOnAction = samsung_vfd_ns.class_("TurnOnAction", automation.Action) TurnOffAction = samsung_vfd_ns.class_("TurnOffAction", automation.Action) SetAnnunciatorAction = samsung_vfd_ns.class_("SetAnnunciatorAction", automation.Action) SetCursorAction = samsung_vfd_ns.class_("SetCursorAction", automation.Action) SetDisplayModeAction = samsung_vfd_ns.class_("SetDisplayModeAction", automation.Action) WriteAction = samsung_vfd_ns.class_("WriteAction", automation.Action) ReinitAction = samsung_vfd_ns.class_("ReinitAction", automation.Action) SelfTestAction = samsung_vfd_ns.class_("SelfTestAction", automation.Action) PIXEL_ON = "#X1*" PIXEL_OFF = ".-0 _" def user_char_code(value): if isinstance(value, str): if len(value) != 1: raise cv.Invalid("code must be a single character or a number") value = ord(value) value = cv.int_(value) if not 0x20 <= value <= 0x7E: raise cv.Invalid("code must be between 0x20 and 0x7E (a printable ASCII character)") return value def user_char_pattern(value): """7 strings of 5 pixels, top row first, e.g. '.###.'.""" value = cv.ensure_list(cv.string_strict)(value) if len(value) != 7: raise cv.Invalid("pattern must have exactly 7 rows") for row in value: if len(row) != 5: raise cv.Invalid(f"pattern row '{row}' must be exactly 5 pixels wide") for ch in row: if ch not in PIXEL_ON + PIXEL_OFF: raise cv.Invalid( f"pattern row '{row}': use one of '{PIXEL_ON}' for on and '{PIXEL_OFF}' for off" ) return value def pattern_to_columns(pattern): # One byte per column; bit 6 = top row, bit 0 = bottom row (datasheet 5.4.14). cols = [] for c in range(5): byte = 0 for r, row in enumerate(pattern): if row[c] in PIXEL_ON: byte |= 1 << (6 - r) cols.append(byte) return cols def unique_codes(value): codes = [uc[CONF_CODE] for uc in value] for code in codes: if codes.count(code) > 1: raise cv.Invalid(f"user character code 0x{code:02X} defined twice") return value def validate_write(config): if (CONF_TEXT in config) == (CONF_DATA in config): raise cv.Invalid("specify exactly one of 'text' or 'data'") return config def raw_data(value): if isinstance(value, str): return list(value.encode("latin-1")) return cv.ensure_list(cv.hex_uint8_t)(value) USER_CHAR_SCHEMA = cv.Schema( { cv.Required(CONF_CODE): user_char_code, cv.Required(CONF_PATTERN): user_char_pattern, } ) CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(SamsungVfd), cv.Optional(CONF_COLS, default=20): cv.int_range(min=1, max=80), cv.Optional(CONF_ROWS, default=2): cv.int_range(min=1, max=8), # Off by default: lines are padded to exactly `cols` characters and the # display wraps to the next line by itself. cv.Optional(CONF_SEND_NEWLINES, default=False): cv.boolean, cv.Optional(CONF_BRIGHTNESS, default="100%"): cv.percentage, cv.Optional(CONF_CODE_TABLE, default="PC437"): cv.enum(CODE_TABLES, upper=True), cv.Optional(CONF_INTERNATIONAL_CHARSET, default="USA"): cv.enum(CHARSETS, upper=True), cv.Optional(CONF_USER_CHARACTERS, default=[]): cv.All( cv.ensure_list(USER_CHAR_SCHEMA), cv.Length(max=4), unique_codes ), } ) .extend(cv.COMPONENT_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA) ) # Baud rate and parity must match the display's jumpers, so they are left to # the uart: block. FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema("samsung_vfd", require_tx=True) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) cg.add(var.set_size(config[CONF_COLS], config[CONF_ROWS])) cg.add(var.set_send_newlines(config[CONF_SEND_NEWLINES])) cg.add(var.set_initial_brightness(config[CONF_BRIGHTNESS])) cg.add(var.set_initial_code_table(config[CONF_CODE_TABLE])) cg.add(var.set_initial_charset(config[CONF_INTERNATIONAL_CHARSET])) for uc in config[CONF_USER_CHARACTERS]: cg.add(var.add_user_char(uc[CONF_CODE], *pattern_to_columns(uc[CONF_PATTERN]))) # ---------- actions ---------- SIMPLE_ACTION_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(SamsungVfd)}) async def simple_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var for _name, _cls in ( ("clear", ClearAction), ("home", HomeAction), ("turn_on", TurnOnAction), ("turn_off", TurnOffAction), ("reinit", ReinitAction), ("self_test", SelfTestAction), ): automation.register_action( f"samsung_vfd.{_name}", _cls, SIMPLE_ACTION_SCHEMA, synchronous=True )(simple_action_to_code) @automation.register_action( "samsung_vfd.print", PrintAction, cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_TEXT): cv.templatable(cv.string), cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)), cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean), } ), synchronous=True, ) async def samsung_vfd_print_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_text(await cg.templatable(config[CONF_TEXT], args, cg.std_string))) if CONF_LINE in config: cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_))) if CONF_CENTERED in config: cg.add(var.set_centered(await cg.templatable(config[CONF_CENTERED], args, cg.bool_))) return var @automation.register_action( "samsung_vfd.printf", PrintfAction, cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_TIME_ID): cv.use_id(time_.RealTimeClock), cv.Required(CONF_FORMAT): cv.templatable(cv.string), cv.Optional(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)), cv.Optional(CONF_CENTERED): cv.templatable(cv.boolean), } ), synchronous=True, ) async def samsung_vfd_printf_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_time(await cg.get_variable(config[CONF_TIME_ID]))) cg.add(var.set_format(await cg.templatable(config[CONF_FORMAT], args, cg.std_string))) if CONF_LINE in config: cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_))) if CONF_CENTERED in config: cg.add(var.set_centered(await cg.templatable(config[CONF_CENTERED], args, cg.bool_))) return var @automation.register_action( "samsung_vfd.set_brightness", SetBrightnessAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_BRIGHTNESS): cv.templatable(cv.percentage), }, key=CONF_BRIGHTNESS, ), synchronous=True, ) async def samsung_vfd_set_brightness_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_brightness(await cg.templatable(config[CONF_BRIGHTNESS], args, cg.float_))) return var @automation.register_action( "samsung_vfd.set_blink", SetBlinkAction, cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_INTERVAL): cv.templatable(cv.positive_time_period_milliseconds), } ), synchronous=True, ) async def samsung_vfd_set_blink_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_interval(await cg.templatable(config[CONF_INTERVAL], args, cg.uint32))) return var @automation.register_action( "samsung_vfd.set_annunciator", SetAnnunciatorAction, cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_COLUMN): cv.templatable(cv.int_range(min=0, max=20)), cv.Required(CONF_STATE): cv.templatable(cv.boolean), } ), synchronous=True, ) async def samsung_vfd_set_annunciator_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_column(await cg.templatable(config[CONF_COLUMN], args, cg.int_))) cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.bool_))) return var @automation.register_action( "samsung_vfd.set_cursor", SetCursorAction, cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_COLUMN): cv.templatable(cv.int_range(min=0, max=79)), cv.Required(CONF_LINE): cv.templatable(cv.int_range(min=0, max=7)), } ), synchronous=True, ) async def samsung_vfd_set_cursor_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_column(await cg.templatable(config[CONF_COLUMN], args, cg.int_))) cg.add(var.set_line(await cg.templatable(config[CONF_LINE], args, cg.int_))) return var @automation.register_action( "samsung_vfd.set_display_mode", SetDisplayModeAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Required(CONF_MODE): cv.enum(DISPLAY_MODES, upper=True), }, key=CONF_MODE, ), synchronous=True, ) async def samsung_vfd_set_display_mode_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) cg.add(var.set_mode(config[CONF_MODE])) return var @automation.register_action( "samsung_vfd.write", WriteAction, cv.All( cv.Schema( { cv.GenerateID(): cv.use_id(SamsungVfd), cv.Optional(CONF_TEXT): cv.templatable(cv.string), cv.Optional(CONF_DATA): raw_data, } ), validate_write, ), synchronous=True, ) async def samsung_vfd_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) if CONF_TEXT in config: cg.add(var.set_text(await cg.templatable(config[CONF_TEXT], args, cg.std_string))) else: cg.add(var.set_data(config[CONF_DATA])) return var