Compare commits

...
Sign in to create a new pull request.

16 commits

Author SHA1 Message Date
c54ff63f89 feat(effect): add attributes, getters, and setters for effect-speed and -width 2023-06-10 18:50:46 +02:00
47f31301dd chore: update gitignore file 2023-06-10 14:36:06 +02:00
587a968451 feat(esp32): move to esp32 platform and add configuration inputs. 2023-06-10 14:33:07 +02:00
4962f344e3 fix: brightness attribute now correctly controls the brightness, even with active effect. 2023-06-10 14:02:39 +02:00
e4bdcf71dc feat: add variable to toggle the rainbow effect on or off 2023-06-10 14:01:29 +02:00
03a012650f chore: minor refactoring 2023-06-10 13:59:29 +02:00
8f90d6b1ca feat(case): initial commit of the case 2023-06-10 13:54:17 +02:00
4e9041aec5 feat(esp8266): random changes to now abandonned esp8266 platform. 2023-06-10 13:53:48 +02:00
3e31b32853 chore(flake): switch to nixpkgs unstable 2023-06-10 13:52:13 +02:00
519b36bfed chore: cleanup and mild refactoring 2023-05-12 17:37:21 +02:00
70458d4ed9 chore: add gitignore rule for ignored folder 2023-05-12 17:18:15 +02:00
b712b90be1 feat: add rainbow effect for segments 2023-05-12 17:16:24 +02:00
dd1408c5cb feat: add transitions and clean up code. 2023-05-12 05:07:21 +02:00
7f091409cf feat: wordclock works! 2023-05-09 20:01:36 +02:00
5b0930d616 feat: add external component and some test configurations 2023-05-03 20:55:22 +02:00
4c612fc462 feat: 🎉 add first try at an esphome component for this wordclock firmware
The code is loosely based on https://github.com/leinich/ha-wordclock-esphome
2022-05-25 02:06:52 +02:00
19 changed files with 1905 additions and 0 deletions

13
.gitignore vendored Normal file
View file

@ -0,0 +1,13 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml
/secrets.*.yaml
!/secrets.example.yaml
/.pio
/.vscode
__pycache__
/_ignored
esp32_test.code-workspace
case/d1_mini_esp32.FCStd1

BIN
case/d1_mini_esp32-Body.stl Normal file

Binary file not shown.

BIN
case/d1_mini_esp32.FCStd Normal file

Binary file not shown.

View file

@ -0,0 +1,153 @@
import logging
from esphome import core
from esphome.components import display, font, time, light, color
import esphome.config_validation as cv
import esphome.codegen as cg
import esphome.cpp_generator as cpp
from esphome.const import (
CONF_ID,
CONF_NAME,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_TIME_ID,
CONF_SEGMENTS,
CONF_ADDRESSABLE_LIGHT_ID,
CONF_HOUR,
CONF_HOURS,
CONF_MINUTE,
CONF_MINUTES,
CONF_BRIGHTNESS,
CONF_UPDATE_INTERVAL,
)
from esphome.core import CORE, HexInt
_LOGGER = logging.getLogger(__name__)
CONF_DISPLAY_ID = "display_id"
CONF_LINE_START_X = "x1"
CONF_LINE_START_Y = "y1"
CONF_LINE_END_X = "x2"
CONF_LINE_END_Y = "y2"
CONF_LINE = "line"
CONF_COLOR_ON = "color_on"
CONF_COLOR_OFF = "color_off"
DATA_VECTOR_SEGMENTS_HOUR = "data_vector_segments_hour"
DATA_VECTOR_SEGMENTS_MINUTE = "data_vector_segments_minute"
DATA_VECTOR_SEGMENTS = "data_vector_segments_ptr"
CONF_WORDCLOCK_STATIC_SEGMENTS = "static_segments"
CONF_HOUR_OFFSET = "hour_offset"
CONF_FADE_LENGTH = "fade_length"
DEPENDENCIES = ["display", "time"]
MULTI_CONF = False
int8 = cg.global_ns.namespace("int8_t")
wordclock_ns = cg.esphome_ns.namespace("wordclock")
SegmentCoords = wordclock_ns.struct("SegmentCoords")
Wordclock = wordclock_ns.class_(
"Wordclock", cg.PollingComponent
)
WORDCLOCK_SEGMENT_SCHEMA = {
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_LINE): {
cv.Required(CONF_LINE_START_X): cv.int_,
cv.Required(CONF_LINE_START_Y): cv.int_,
cv.Required(CONF_LINE_END_X): cv.int_,
cv.Optional(CONF_LINE_END_Y): cv.int_,
},
}
WORDCLOCK_HOUR_SCHEMA = {
cv.Required(CONF_HOUR): cv.uint16_t,
cv.Required(CONF_SEGMENTS): cv.ensure_list(cv.string_strict),
}
WORDCLOCK_MINUTE_SCHEMA = {
cv.Required(CONF_MINUTE): cv.uint16_t,
cv.Required(CONF_HOUR_OFFSET): cv.int_range(-1,1),
cv.Required(CONF_SEGMENTS): cv.ensure_list(cv.string_strict),
}
WORDCLOCK_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(Wordclock),
cv.Required(CONF_DISPLAY_ID): cv.use_id(
display.DisplayBuffer
),
cv.Required(CONF_TIME_ID): cv.use_id(
time.RealTimeClock
),
cv.Required(CONF_ADDRESSABLE_LIGHT_ID): cv.use_id(
light.AddressableLightState
),
cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage,
cv.Optional(CONF_COLOR_ON): cv.use_id(color.ColorStruct),
cv.Optional(CONF_COLOR_OFF): cv.use_id(color.ColorStruct),
cv.Optional(CONF_WORDCLOCK_STATIC_SEGMENTS): cv.ensure_list(cv.string_strict),
cv.Required(CONF_SEGMENTS): cv.ensure_list(WORDCLOCK_SEGMENT_SCHEMA),
cv.Required(CONF_MINUTES): cv.ensure_list(WORDCLOCK_MINUTE_SCHEMA),
cv.Required(CONF_HOURS): cv.ensure_list(WORDCLOCK_HOUR_SCHEMA),
cv.Optional(
CONF_FADE_LENGTH, default="700ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_UPDATE_INTERVAL, default="16ms"
): cv.positive_time_period_milliseconds,
cv.GenerateID(DATA_VECTOR_SEGMENTS): cv.declare_id(cg.std_vector.template(cg.uint16)),
}
)
CONFIG_SCHEMA = WORDCLOCK_SCHEMA
async def to_code(config):
wrapped_display = await cg.get_variable(config[CONF_DISPLAY_ID])
wrapped_time = await cg.get_variable(config[CONF_TIME_ID])
wrapped_light_state = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
var = cg.new_Pvariable(config[CONF_ID], wrapped_time, wrapped_display, wrapped_light_state)
if CONF_COLOR_ON in config:
wrapped_color_on = await cg.get_variable(config[CONF_COLOR_ON])
cg.add(var.set_on_color(wrapped_color_on))
if CONF_COLOR_OFF in config:
wrapped_color_off = await cg.get_variable(config[CONF_COLOR_OFF])
cg.add(var.set_off_color(wrapped_color_off))
cg.add(var.setup_transitions(config[CONF_FADE_LENGTH]))
cg.add(var.set_brightness(int(config[CONF_BRIGHTNESS] * 255)))
SEGMENT_MAP = dict()
for idx, segment in enumerate(config[CONF_SEGMENTS]):
SEGMENT_MAP[segment[CONF_NAME]] = idx
line = segment[CONF_LINE]
x1 = line[CONF_LINE_START_X]
y1 = line[CONF_LINE_START_Y]
x2 = line[CONF_LINE_END_X]
y2 = line.get(CONF_LINE_END_Y, y1)
exp = cg.StructInitializer(SegmentCoords, ("x1", x1), ("y1", y1), ("x2", x2), ("y2", y2),)
cg.add(var.add_segment(exp))
if CONF_WORDCLOCK_STATIC_SEGMENTS in config:
for segment_name in config[CONF_WORDCLOCK_STATIC_SEGMENTS]:
cg.add(var.add_static(SEGMENT_MAP[segment_name]))
for idx, hour in enumerate(config[CONF_HOURS]):
segment_ids = [SEGMENT_MAP[a] for a in hour[CONF_SEGMENTS]]
cg.add(var.add_hour(hour[CONF_HOUR], cg.std_vector.template(cg.uint16).new(segment_ids)))
for idx, minute in enumerate(config[CONF_MINUTES]):
segment_ids = [SEGMENT_MAP[a] for a in minute[CONF_SEGMENTS]]
cg.add(var.add_minute(minute[CONF_MINUTE], minute[CONF_HOUR_OFFSET], cg.std_vector.template(cg.uint16).new(segment_ids)))
await cg.register_component(var, config)

View file

@ -0,0 +1,184 @@
#include "wordclock.h"
namespace esphome {
namespace wordclock {
void Wordclock::setup() {
this->valid_time = this->time->now().is_valid();
if (!this->valid_time) {
this->start_idle_animation();
}
}
void Wordclock::update() {
esphome::time::ESPTime time = this->time->now();
if (!time.is_valid()) {
if (this->valid_time) {
ESP_LOGD("wordclock.cpp", "time is not valid [%02i:%02i:%02i]", time.hour, time.minute, time.second);
this->start_idle_animation();
this->valid_time = false;
return;
}
}
else {
if (!this->valid_time) {
ESP_LOGD("wordclock.cpp", "time is valid [%02i:%02i:%02i]", time.hour, time.minute, time.second);
this->end_idle_animation();
this->valid_time = true;
return;
}
bool dirty = false;
int8_t minute = this->find_minute(time.minute);
int8_t hour = this->find_hour((time.hour + this->minutes->at(minute).hour_offset) % 24);
if (hour != this->current_hour) {
this->current_hour = hour;
dirty = true;
}
if (minute != this->current_minute) {
this->current_minute = minute;
dirty = true;
}
if (dirty) {
this->previous_segments->clear();
for (uint16_t segment_idx : *this->current_segments) {
this->previous_segments->push_back(segment_idx);
}
this->current_segments->clear();
for (uint16_t segment_idx : *this->static_segments) {
this->current_segments->push_back(segment_idx);
}
for (uint16_t segment_idx : *(this->minutes->at(minute).segments)) {
this->current_segments->push_back(segment_idx);
}
for (uint16_t segment_idx : *(this->hours->at(hour).segments)) {
this->current_segments->push_back(segment_idx);
}
std::sort(this->current_segments->begin(), this->current_segments->end());
this->find_difference(this->previous_segments, this->current_segments);
this->on_transformer->reset();
this->off_transformer->reset();
}
this->current_position = 0;
uint8_t transition_progress = this->on_transformer->apply().value_or(255);
Color removed_color = this->on_color.gradient(this->off_color, transition_progress);
for (uint16_t segment_idx : *this->removed_segments) {
this->disable_segment_effect(segment_idx, this->off_color, transition_progress);
}
for (uint16_t segment_idx : *this->added_segments) {
this->enable_segment_effect(segment_idx, this->off_color, transition_progress);
}
for (uint16_t segment_idx : *this->staying_segments) {
this->apply_segment_effect(segment_idx);
}
}
}
void Wordclock::find_difference(std::vector<uint16_t> *a_vec, std::vector<uint16_t> *b_vec) {
this->added_segments->clear();
this->removed_segments->clear();
this->staying_segments->clear();
auto a = a_vec->begin();
auto b = b_vec->begin();
while (!(a == a_vec->end() && b == b_vec->end())) {
if (a == a_vec->end()) { this->added_segments-> push_back(*b); b++; }
else if (b == b_vec->end()) { this->removed_segments->push_back(*a); a++; }
else if (*a > *b) { this->added_segments-> push_back(*b); b++; }
else if (*a < *b) { this->removed_segments->push_back(*a); a++; }
else if (*a == *b) { this->staying_segments->push_back(*a); a++; b++; }
}
}
Color Wordclock::get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color &current_color) {
esphome::light::ESPHSVColor hsv;
hsv.value = 255;
hsv.saturation = 240;
hsv.hue = ((millis() * speed + (position * (0xFFFF / width))) % 0xFFFF) >> 8;
return hsv.to_rgb();
}
Color Wordclock::get_next_color(uint32_t position, const Color &current_color) {
if (this->effect_active) {
uint32_t speed_ = 3;
uint16_t width_ = 100; // Original width_ = 50
return get_next_color_base_(position, speed_, width_, current_color);
}
else {
return Color(this->on_color);
}
}
int8_t Wordclock::find_hour(uint8_t target_value) {
std::vector<Hour> *elements = this->hours;
for (int i = 0; i < elements->size(); i++) {
if (elements->at(i).hour == target_value) {
return i;
} else if (elements->at(i).hour > target_value) {
return i - 1;
}
}
return elements->size() - 1;
}
int8_t Wordclock::find_minute(uint8_t target_value) {
std::vector<Minute> *elements = this->minutes;
for (int i = 0; i < elements->size(); i++) {
if (elements->at(i).minute == target_value) {
return i;
} else if (elements->at(i).minute > target_value) {
return i - 1;
}
}
return elements->size() - 1;
}
void Wordclock::setup_transitions(uint32_t milliseconds) {
this->on_transformer->setup(0, 255, milliseconds);
this->off_transformer->setup(255, 0, milliseconds);
}
Wordclock::Wordclock(
esphome::time::RealTimeClock *time,
esphome::addressable_light::AddressableLightDisplay *display,
esphome::light::AddressableLightState *light_state) {
this->time = time;
this->display = display;
this->light_state = light_state;
this->light_state->set_default_transition_length(1);
this->minutes = new std::vector<Minute>();
this->hours = new std::vector<Hour>();
this->segments = new std::vector<SegmentCoords>();
this->static_segments = new std::vector<uint16_t>();
this->previous_segments = new std::vector<uint16_t>();
this->current_segments = new std::vector<uint16_t>();
this->added_segments = new std::vector<uint16_t>();
this->removed_segments = new std::vector<uint16_t>();
this->staying_segments = new std::vector<uint16_t>();
this->on_transformer = new BrightnessTransitionTransformer();
this->off_transformer = new BrightnessTransitionTransformer();
}
} // namespace wordclock
} // namespace esphome

View file

@ -0,0 +1,223 @@
#pragma once
#include "esphome.h"
// #include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace wordclock {
struct Minute {
uint8_t minute;
uint8_t hour_offset;
std::vector<uint16_t> *segments;
};
struct Hour {
uint8_t hour;
std::vector<uint16_t> *segments;
};
struct SegmentCoords {
uint16_t x1;
uint16_t y1;
uint16_t x2;
uint16_t y2;
};
class BrightnessTransitionTransformer {
public:
virtual ~BrightnessTransitionTransformer() = default;
void setup(const uint8_t start_values, const uint8_t target_values, uint32_t length) {
this->start_time_ = millis();
this->length_ = length;
this->start_values_ = start_values;
this->target_values_ = target_values;
this->start();
}
virtual bool is_finished() { return this->get_progress_() >= 1.0f; }
virtual void start() {}
// virtual optional<uint8_t> apply() = 0;
virtual optional<uint8_t> apply() {
float v = BrightnessTransitionTransformer::smoothed_progress(this->get_progress_());
return esphome::lerp(v, this->start_values_, this->target_values_);
}
virtual void stop() {}
void reset() {
this->start_time_ = millis();
}
const uint8_t &get_start_values() const { return this->start_values_; }
const uint8_t &get_target_values() const { return this->target_values_; }
protected:
// The progress of this transition, on a scale of 0 to 1.
float get_progress_() {
uint32_t now = esphome::millis();
if (now < this->start_time_)
return 0.0f;
if (now >= this->start_time_ + this->length_)
return 1.0f;
return clamp((now - this->start_time_) / float(this->length_), 0.0f, 1.0f);
}
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
uint32_t start_time_;
uint32_t length_;
uint8_t start_values_;
uint8_t target_values_;
};
class Wordclock : public esphome::PollingComponent {
public:
Wordclock(
esphome::time::RealTimeClock *time,
esphome::addressable_light::AddressableLightDisplay *display,
esphome::light::AddressableLightState *light_state
);
void setup();
void update();
void set_brightness(uint8_t brightness) {
this->brightness = brightness;
}
bool get_effect_active() { return this->effect_active; }
void set_effect_active(bool x) { this->effect_active = x; }
bool get_effect_width() { return this->effect_width; }
void set_effect_width(bool x) { this->effect_width = x; }
bool get_effect_speed() { return this->effect_speed; }
void set_effect_speed(bool x) { this->effect_speed = x; }
void set_on_color(Color on_color) {
this->on_color = on_color;
}
void set_off_color(Color off_color) {
this->off_color = off_color;
}
void setup_transitions(uint32_t milliseconds);
void add_segment(SegmentCoords segment) {
this->segments->push_back(segment);
}
void add_static(uint16_t segment_id) {
this->static_segments->push_back(segment_id);
}
void add_hour(uint8_t hour, std::vector<uint16_t> *segments_ptr) {
this->hours->push_back(Hour{
.hour = hour,
.segments = segments_ptr,
});
}
void add_minute(uint8_t minute, uint8_t hour_offset, std::vector<uint16_t> *segments_ptr) {
this->minutes->push_back(Minute{
.minute = minute,
.hour_offset = hour_offset,
.segments = segments_ptr,
});
}
Color on_color {Color(0xFFFFFF)};
Color off_color {Color(0x000000)};
uint8_t brightness{255};
protected:
// ESPHome Components
esphome::time::RealTimeClock *time;
esphome::light::AddressableLightState *light_state;
esphome::addressable_light::AddressableLightDisplay *display;
// Time related state
bool valid_time{false};
int8_t current_hour{-1};
int8_t current_minute{-1};
// Segments Configuration
std::vector<SegmentCoords> *segments;
std::vector<uint16_t> *static_segments;
std::vector<Minute> *minutes;
std::vector<Hour> *hours;
BrightnessTransitionTransformer *off_transformer;
BrightnessTransitionTransformer *on_transformer;
std::vector<uint16_t> *added_segments;
std::vector<uint16_t> *removed_segments;
std::vector<uint16_t> *staying_segments;
std::vector<uint16_t> *current_segments;
std::vector<uint16_t> *previous_segments;
uint32_t current_position{0};
bool effect_active{true};
uint32_t effect_speed{12};
uint16_t effect_width{50};
Color get_next_color_base_(uint32_t position, uint32_t speed, uint16_t width, const Color &current_color);
Color get_next_color(uint32_t position, const Color &current_color);
// Utils
int8_t find_hour(uint8_t target_value);
int8_t find_minute(uint8_t target_value);
void find_difference(std::vector<uint16_t> *a_vec, std::vector<uint16_t> *b_vec);
void segment_effect_base(uint16_t segment_id, bool to_effect, Color base_color, uint8_t transition_progress) {
SegmentCoords s = this->segments->at(segment_id);
int x1 = s.x1; int y1 = s.y1; int x2 = s.x2; int y2 = s.y2;
Color color_to_draw;
Color effect_color;
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
int32_t err = dx + dy;
while (true) {
effect_color = this->get_next_color(x1 + y1, Color(0)) * this->brightness;
if (to_effect) {
color_to_draw = base_color.gradient(effect_color, transition_progress);
} else {
color_to_draw = effect_color.gradient(base_color, transition_progress);
}
this->display->draw_pixel_at(x1, y1, color_to_draw);
if (x1 == x2 && y1 == y2) break;
int32_t e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x1 += sx;
}
if (e2 <= dx) {
err += dx;
y1 += sy;
}
}
}
void enable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) {
segment_effect_base(segment_id, true, off_color, transition_progress);
}
void disable_segment_effect(uint16_t segment_id, Color off_color, uint8_t transition_progress) {
segment_effect_base(segment_id, false, off_color, transition_progress);
}
void apply_segment_effect(uint16_t segment_id) {
segment_effect_base(segment_id, true, Color(0), 255);
}
void start_idle_animation() {
this->display->fill(this->off_color);
this->display->set_enabled(false);
this->light_state->turn_on().set_effect("random_twinkle").perform();
}
void end_idle_animation() {
this->light_state->turn_off().perform();
this->display->set_enabled(true);
this->display->fill(this->off_color);
this->previous_segments->clear();
this->current_segments->clear();
}
};
} // namespace wordclock
} // namespace esphome

9
devlog.md Normal file
View file

@ -0,0 +1,9 @@
# Wordclock Development Log
## Feature "make speed and width of rainbow effect user-configurable" [2023-06-10 17:13]
[17:16] Step - add attributes, getters, and setters for effect-speed and -width.
- [x] add attr/funcs to header file.
- [x] add inputs to yaml.
-

98
display_test.yaml Normal file
View file

@ -0,0 +1,98 @@
esphome:
name: "display-thing"
esp8266:
board: d1_mini
framework:
version: recommended
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "${devicename}"
password: !secret ap_password
manual_ip:
static_ip: !secret manualip_static_ip
gateway: !secret manualip_gateway
subnet: !secret manualip_subnet
dns1: 1.1.1.1
dns2: 1.0.0.1
api:
ota:
password: "${devicename}"
logger:
# esp8266_store_log_strings_in_flash: false
# web_server:
# port: 80
light:
- name: NeoPixel Strip 1
id: neopixel_strip_1
platform: neopixelbus
type: GRB
variant: WS2812
pin: GPIO3
num_leds: 199
restore_mode: ALWAYS_ON
default_transition_length: 0s
color_correct: [100%, 100%, 100%]
method:
type: esp8266_dma
display:
- platform: addressable_light
id: led_matrix_display
addressable_light_id: neopixel_strip_1
width: 18
height: 11
rotation: 270°
update_interval: 16ms
pixel_mapper: |-
int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16};
int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16};
if (x > 9) return -1;
if (y % 2 == 0) {
return (y * 18 + mapping_even[x]); // mapping_even[y];
} else {
return (y * 18 + mapping_odd[x]); // mapping_odd[y];
//return (y * 18 + 17 - x); // mapping_odd[y];
}
//if (x % 2 == 0) {
// return (x * 8) + y;
//}
//return (x * 8) + (7 - y);
lambda: |-
Color red = Color(0xFF0000);
Color green = Color(0x00FF00);
Color blue = Color(0x0000FF);
Color turquoise = Color(0x00FFFF);
it.draw_pixel_at(0,0, green);
it.draw_pixel_at(10,0, red);
it.draw_pixel_at(0,9, turquoise);
it.draw_pixel_at(10,9, blue);
//it.line(0,0, 11,0, red);
//it.line(0,1, 11,1, green);
//it.line(0,2, 11,2, blue);
//it.line(0,3, 11,3, red);
//it.line(0,4, 11,4, green);
//it.line(0,5, 11,5, blue);
//it.line(0,6, 11,6, red);
//it.line(0,7, 11,7, green);
//it.line(0,8, 11,8, blue);
//it.line(0,9, 11,9, red);
//it.line(0,10, 9,10, green);
//it.line(0,11, 9,11, blue);
// it.line(5,14, 10,14, green);
//it.rectangle(0, 0, 17, 10, red);
//it.rectangle(1, 1, 16, 9, green);
//it.rectangle(2, 2, 15, 8, blue);
//it.rectangle(3, 3, 14, 7, red);
time:
- platform: sntp
id: current_time
timezone: !secret timezone

64
flake.lock generated Normal file
View file

@ -0,0 +1,64 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1683777345,
"narHash": "sha256-V2p/A4RpEGqEZussOnHYMU6XglxBJGCODdzoyvcwig8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "635a306fc8ede2e34cb3dd0d6d0a5d49362150ed",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"systems": "systems",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1680978846,
"narHash": "sha256-Gtqg8b/v49BFDpDetjclCYXm8mAnTrUzR0JnE2nv5aw=",
"owner": "nix-systems",
"repo": "x86_64-linux",
"rev": "2ecfcac5e15790ba6ce360ceccddb15ad16d08a8",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "x86_64-linux",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": [
"systems"
]
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

67
flake.nix Normal file
View file

@ -0,0 +1,67 @@
{
description = "EspHome Word Clock";
inputs = {
systems.url = "github:nix-systems/x86_64-linux";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
# nixpkgs.url = "github:NixOS/nixpkgs/22.11";
utils.url = "github:numtide/flake-utils";
utils.inputs.systems.follows = "systems";
};
outputs = { self, nixpkgs, utils, ... }: utils.lib.eachDefaultSystem (
system:
let pkgs = import nixpkgs { inherit system; };
in {
# This block here is used when running `nix develop`
devShells.default = pkgs.mkShell rec {
# Update the name to something that suites your project.
name = "esphome-wordclock";
# build environment dependencies
packages = with pkgs; [
esphome
];
# Setting up the environment variables you need during development.
# Todo figure out why I can't use clang on Asahi but can on Darwin
# Use "clang++" for most systems but OSX Asahi requires g++ for some reason or a runtime error occurs
shellHook = let
# This is for an icon that is used below for the command line input below
icon = "f121";
in ''
export PS1="$(echo -e '\u${icon}') {\[$(tput sgr0)\]\[\033[38;5;228m\]\w\[$(tput sgr0)\]\[\033[38;5;15m\]} (${name}) \\$ \[$(tput sgr0)\]"
# export COMPILER="clang++"
#export COMPILER="g++"
echo "test";
'';
};
# This is used when running `nix build`
# packages.default = pkgs.llvmPackages_14.stdenv.mkDerivation rec {
packages.default = pkgs.esphome.stdenv.mkDerivation rec {
name = "esphome-wordclock";
version = "2.0.0";
src = self;
# buildInputs = [ pkgs.esphome ];
# buildPhase = "COMPILER='clang++' make";
# installPhase = ''
# mkdir -p $out/bin;
# install -t $out/bin worm
# '';
# meta = with inputs.utils.lib; {
# homepage = "https://github.com/icecreammatt/ssu-cs315-worm";
# description = ''
# Terminal CLI Worm Game
# '';
# };
};
# packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
# packages.x86_64-linux.default = self.packages.x86_64-linux.hello;
});
}

154
image_test.yaml Normal file
View file

@ -0,0 +1,154 @@
esphome:
name: "display-thing"
esp8266:
board: d1_mini
framework:
version: recommended
external_components:
- source:
type: local
path: components
components: [ wordclock, addressable_light ]
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "${devicename}"
password: !secret ap_password
manual_ip:
static_ip: !secret manualip_static_ip
gateway: !secret manualip_gateway
subnet: !secret manualip_subnet
dns1: 1.1.1.1
dns2: 1.0.0.1
api:
# ota:
# password: "${devicename}"
logger:
# esp8266_store_log_strings_in_flash: false
web_server:
port: 80
light:
- name: NeoPixel Strip 1
id: neopixel_strip_1
platform: neopixelbus
type: GRB
variant: WS2812
pin: GPIO3
num_leds: 199
restore_mode: ALWAYS_ON
default_transition_length: 0s
color_correct: [100%, 100%, 100%]
method:
type: esp8266_dma
display:
- platform: addressable_light
id: led_matrix_display
addressable_light_id: neopixel_strip_1
width: 18
height: 11
rotation: 270°
update_interval: 16ms
auto_clear_enabled: false
pixel_mapper: |-
int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16};
int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16};
if (x > 9) return -1;
if (y % 2 == 0) {
return (y * 18 + mapping_even[x]); // mapping_even[y];
} else {
return (y * 18 + mapping_odd[x]); // mapping_odd[y];
}
# lambda: |-
# it.fill(Color(0));
# it.image(0, 0, id(foo));
# //it.line(0,0,5,5, Color(0x00FF00));
# image:
# - id: foo
# file: ./test.png
# type: RGB24
# resize: 18x11
time:
- platform: sntp
id: current_time
timezone: !secret timezone
wordclock:
time_id: current_time
display_id: led_matrix_display
static_segments: ["IT", "IS"]
segments:
- {name: "IT", line: {x1: 0, x2: 1, y1: 0}}
- {name: "IS", line: {x1: 3, x2: 4, y1: 0}}
- {name: "AM", line: {x1: 7, x2: 8, y1: 0}}
- {name: "PM", line: {x1: 9, x2: 10, y1: 0}}
- {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}}
- {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}}
- {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}}
- {name: "HALF", line: {x1: 0, x2: 3, y1: 3}}
- {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}}
- {name: "TO", line: {x1: 9, x2: 10, y1: 3}}
- {name: "PAST", line: {x1: 0, x2: 3, y1: 4}}
- {name: "NINE", line: {x1: 7, x2: 10, y1: 4}}
- {name: "ONE", line: {x1: 0, x2: 2, y1: 5}}
- {name: "SIX", line: {x1: 3, x2: 5, y1: 5}}
- {name: "THREE", line: {x1: 6, x2: 10, y1: 5}}
- {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}}
- {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}}
- {name: "TWO", line: {x1: 8, x2: 10, y1: 6}}
- {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}}
- {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}}
- {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}}
- {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}}
- {name: "TEN", line: {x1: 0, x2: 2, y1: 9}}
- {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}}
minutes:
- {minute: 0, segments: ["OCLOCK"]}
- {minute: 5, segments: ["FIVE_MINUTES", "PAST"]}
- {minute: 10, segments: ["TEN_MINUTES", "PAST"]}
- {minute: 15, segments: ["QUARTER", "PAST"]}
- {minute: 20, segments: ["TWENTY", "PAST"]}
- {minute: 25, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]}
- {minute: 30, segments: ["HALF", "PAST"]}
- {minute: 35, segments: ["TWENTY", "FIVE_MINUTES", "TO"]}
- {minute: 40, segments: ["TWENTY", "TO"]}
- {minute: 45, segments: ["QUARTER", "TO"]}
- {minute: 50, segments: ["TEN_MINUTES", "TO"]}
- {minute: 55, segments: ["FIVE_MINUTES", "TO"]}
hours:
- {hour: 0, segments: ["TWELVE", "AM"]}
- {hour: 1, segments: ["ONE", "AM"]}
- {hour: 2, segments: ["TWO", "AM"]}
- {hour: 3, segments: ["THREE", "AM"]}
- {hour: 4, segments: ["FOUR", "AM"]}
- {hour: 5, segments: ["FIVE", "AM"]}
- {hour: 6, segments: ["SIX", "AM"]}
- {hour: 7, segments: ["SEVEN", "AM"]}
- {hour: 8, segments: ["EIGHT", "AM"]}
- {hour: 9, segments: ["NINE", "AM"]}
- {hour: 10, segments: ["TEN", "AM"]}
- {hour: 11, segments: ["ELEVEN", "AM"]}
- {hour: 12, segments: ["TWELVE", "PM"]}
- {hour: 13, segments: ["ONE", "PM"]}
- {hour: 14, segments: ["TWO", "PM"]}
- {hour: 15, segments: ["THREE", "PM"]}
- {hour: 16, segments: ["FOUR", "PM"]}
- {hour: 17, segments: ["FIVE", "PM"]}
- {hour: 18, segments: ["SIX", "PM"]}
- {hour: 19, segments: ["SEVEN", "PM"]}
- {hour: 20, segments: ["EIGHT", "PM"]}
- {hour: 21, segments: ["NINE", "PM"]}
- {hour: 22, segments: ["TEN", "PM"]}
- {hour: 23, segments: ["ELEVEN", "PM"]}

7
secrets.example.yaml Normal file
View file

@ -0,0 +1,7 @@
wifi_password: secret
wifi_ssid: example
manualip_static_ip: 192.168.1.177
manualip_gateway: 192.168.1.1
manualip_subnet: 255.255.255.0
timezone: Europe/Zurich
ap_password: "password for internal fallback accesspoint"

186
susannes_wordclock.yaml Normal file
View file

@ -0,0 +1,186 @@
esphome:
name: "wordclock"
esp8266:
board: d1_mini
framework:
version: recommended
# esp32:
# board: ttgo-t7-v13-mini32
# framework:
# type: arduino
external_components:
- source:
type: local
path: components
components: [ wordclock ]
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "WordClock Configuration"
password: !secret ap_password
manual_ip:
static_ip: !secret manualip_static_ip
gateway: !secret manualip_gateway
subnet: !secret manualip_subnet
dns1: 1.1.1.1
dns2: 1.0.0.1
captive_portal:
api:
ota:
password: "wordclock"
logger:
select:
- platform: template
name: "Template select"
restore_value: true
set_action:
- lambda: |-
return id(wordclock_1)->set_effect_active(false);
optimistic: true
options:
- effect
- color
initial_option: effect
web_server:
port: 80
version: 1
time:
- platform: sntp
id: current_time
timezone: !secret timezone
light:
- name: NeoPixel Strip 1
id: neopixel_strip_1
platform: neopixelbus
type: GRB
variant: WS2812
pin: GPIO3
# pin: GPIO22
num_leds: 198
restore_mode: ALWAYS_OFF
method:
type: esp8266_dma
# type: esp32_i2s
effects:
- addressable_random_twinkle:
name: "random_twinkle"
display:
- platform: addressable_light
id: led_matrix_display
addressable_light_id: neopixel_strip_1
width: 18
height: 11
rotation: 270°
update_interval: 16ms
auto_clear_enabled: false
pixel_mapper: |-
int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16};
int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16};
if (x > 9) return -1;
if (y % 2 == 0) {
return (y * 18 + mapping_even[x]);
} else {
return (y * 18 + mapping_odd[x]);
}
color:
- id: col_on
red: 90%
green: 50%
blue: 0%
- id: col_off
red: 0%
green: 0%
blue: 0%
wordclock:
id: wordclock_1
time_id: current_time
display_id: led_matrix_display
addressable_light_id: neopixel_strip_1
brightness: 100%
color_on: col_on
color_off: col_off
update_interval: 16ms
static_segments: ["IT", "IS"]
segments:
- {name: "IT", line: {x1: 0, x2: 1, y1: 0}}
- {name: "IS", line: {x1: 3, x2: 4, y1: 0}}
- {name: "AM", line: {x1: 7, x2: 8, y1: 0}}
- {name: "PM", line: {x1: 9, x2: 10, y1: 0}}
- {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}}
- {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}}
- {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}}
- {name: "HALF", line: {x1: 0, x2: 3, y1: 3}}
- {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}}
- {name: "TO", line: {x1: 9, x2: 10, y1: 3}}
- {name: "PAST", line: {x1: 0, x2: 3, y1: 4}}
- {name: "NINE", line: {x1: 7, x2: 10, y1: 4}}
- {name: "ONE", line: {x1: 0, x2: 2, y1: 5}}
- {name: "SIX", line: {x1: 3, x2: 5, y1: 5}}
- {name: "THREE", line: {x1: 6, x2: 10, y1: 5}}
- {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}}
- {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}}
- {name: "TWO", line: {x1: 8, x2: 10, y1: 6}}
- {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}}
- {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}}
- {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}}
- {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}}
- {name: "TEN", line: {x1: 0, x2: 2, y1: 9}}
- {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}}
minutes:
- {minute: 0, hour_offset: 0, segments: ["OCLOCK"]}
- {minute: 5, hour_offset: 0, segments: ["FIVE_MINUTES", "PAST"]}
- {minute: 10, hour_offset: 0, segments: ["TEN_MINUTES", "PAST"]}
- {minute: 15, hour_offset: 0, segments: ["QUARTER", "PAST"]}
- {minute: 20, hour_offset: 0, segments: ["TWENTY", "PAST"]}
- {minute: 25, hour_offset: 0, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]}
- {minute: 30, hour_offset: 0, segments: ["HALF", "PAST"]}
- {minute: 35, hour_offset: 1, segments: ["TWENTY", "FIVE_MINUTES", "TO"]}
- {minute: 40, hour_offset: 1, segments: ["TWENTY", "TO"]}
- {minute: 45, hour_offset: 1, segments: ["QUARTER", "TO"]}
- {minute: 50, hour_offset: 1, segments: ["TEN_MINUTES", "TO"]}
- {minute: 55, hour_offset: 1, segments: ["FIVE_MINUTES", "TO"]}
hours:
- {hour: 0, segments: "TWELVE"}
- {hour: 1, segments: "ONE"}
- {hour: 2, segments: "TWO"}
- {hour: 3, segments: "THREE"}
- {hour: 4, segments: "FOUR"}
- {hour: 5, segments: "FIVE"}
- {hour: 6, segments: "SIX"}
- {hour: 7, segments: "SEVEN"}
- {hour: 8, segments: "EIGHT"}
- {hour: 9, segments: "NINE"}
- {hour: 10, segments: "TEN"}
- {hour: 11, segments: "ELEVEN"}
- {hour: 12, segments: "TWELVE"}
- {hour: 13, segments: "ONE"}
- {hour: 14, segments: "TWO"}
- {hour: 15, segments: "THREE"}
- {hour: 16, segments: "FOUR"}
- {hour: 17, segments: "FIVE"}
- {hour: 18, segments: "SIX"}
- {hour: 19, segments: "SEVEN"}
- {hour: 20, segments: "EIGHT"}
- {hour: 21, segments: "NINE"}
- {hour: 22, segments: "TEN"}
- {hour: 23, segments: "ELEVEN"}

View file

@ -0,0 +1,327 @@
esphome:
name: "wordclock"
esp32:
# board: ttgo-t7-v13-mini32
board: wemos_d1_mini32
framework:
type: arduino
external_components:
- source:
type: local
path: components
components: [ wordclock ]
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "WordClock Configuration"
password: !secret ap_password
manual_ip:
static_ip: !secret manualip_static_ip
gateway: !secret manualip_gateway
subnet: !secret manualip_subnet
dns1: 1.1.1.1
dns2: 1.0.0.1
captive_portal:
# api:
ota:
password: "wordclock"
logger:
switch:
- platform: template
name: "5 Rainbow ON/OFF"
# lambda: |-
# return id(wordclock_1)->get_effect_active();
restore_state: true
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
turn_on_action:
- lambda: |-
id(wordclock_1)->set_effect_active(true);
turn_off_action:
- lambda: |-
id(wordclock_1)->set_effect_active(false);
# on_...:
# - globals.set:
# id: my_global_var
# value: '10'
# output:
# - platform: template
# id: dummy_out
# type: float
# write_action:
# - lambda: |-
# return id(wordclock_1)->set_effect_active(true);
# Example configuration entry
globals:
- id: my_global_int
type: int
restore_value: no
initial_value: '0'
# Example for global string variable
- id: my_global_string
type: std::string
restore_value: no # Strings cannot be saved/restored
initial_value: '"Global value is"'
number:
- platform: template
name: "1 Red"
optimistic: true
restore_value: true
initial_value: 255
# lambda: |-
# return id(wordclock_1)->on_color.red;
on_value:
- lambda: |-
id(wordclock_1)->on_color.red = x;
set_action:
- lambda: |-
id(wordclock_1)->on_color.red = x;
min_value: 0
max_value: 255
step: 1
- platform: template
name: "2 Green"
optimistic: true
restore_value: true
initial_value: 255
# lambda: |-
# return id(wordclock_1)->on_color.green;
on_value:
- lambda: |-
id(wordclock_1)->on_color.green = x;
set_action:
- lambda: |-
id(wordclock_1)->on_color.green = x;
min_value: 0
max_value: 255
step: 1
- platform: template
name: "3 Blue"
optimistic: true
restore_value: true
initial_value: 255
# lambda: |-
# return id(wordclock_1)->on_color.blue;
on_value:
- lambda: |-
id(wordclock_1)->on_color.blue = x;
set_action:
- lambda: |-
id(wordclock_1)->on_color.blue = x;
min_value: 0
max_value: 255
step: 1
- platform: template
name: "4 Brightness"
optimistic: true
restore_value: true
initial_value: 255
# lambda: |-
# return id(wordclock_1)->brightness;
on_value:
- lambda: |-
id(wordclock_1)->brightness = x;
set_action:
- lambda: |-
id(wordclock_1)->brightness = x;
min_value: 0
max_value: 255
step: 1
- platform: template
name: "6 Rainbow Speed"
optimistic: true
restore_value: true
initial_value: 12
on_value:
- lambda: |-
id(wordclock_1)->set_effect_speed(x);
set_action:
- lambda: |-
id(wordclock_1)->set_effect_speed(x);
min_value: 1
max_value: 4096
step: 1
- platform: template
name: "7 Rainbow Width"
optimistic: true
restore_value: true
initial_value: 50
on_value:
- lambda: |-
id(wordclock_1)->set_effect_width(x);
set_action:
- lambda: |-
id(wordclock_1)->set_effect_width(x);
min_value: 1
max_value: 65535
step: 1
# select:
# - platform: template
# name: "Rainbow ON/OFF"
# restore_value: true
# set_action:
# - lambda: |-
# return id(wordclock_1)->set_color_value(x);
# optimistic: true
# options:
# - effect
# - color
# initial_option: effect
# - platform: template
# name: "Template select"
# restore_value: true
# set_action:
# - lambda: |-
# return id(wordclock_1)->set_color_value(x);
# optimistic: true
# options:
# - effect
# - color
# initial_option: effect
web_server:
port: 80
version: 2
local: true
time:
- platform: sntp
id: current_time
timezone: !secret timezone
light:
- name: NeoPixel Strip 1
id: neopixel_strip_1
platform: neopixelbus
type: GRB
variant: WS2812
pin: GPIO22
num_leds: 198
restore_mode: ALWAYS_OFF
internal: true
method:
type: esp32_i2s
effects:
- addressable_random_twinkle:
name: "random_twinkle"
display:
- platform: addressable_light
id: led_matrix_display
addressable_light_id: neopixel_strip_1
width: 18
height: 11
rotation: 270°
update_interval: 16ms
auto_clear_enabled: false
pixel_mapper: |-
int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17, 16,16,16,16, 16,16,16,16};
int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0, 16,16,16,16, 16,16,16,16};
if (x > 9) return -1;
if (y % 2 == 0) {
return (y * 18 + mapping_even[x]);
} else {
return (y * 18 + mapping_odd[x]);
}
color:
- id: col_on
red: 90%
green: 50%
blue: 0%
- id: col_off
red: 0%
green: 0%
blue: 0%
wordclock:
id: wordclock_1
time_id: current_time
display_id: led_matrix_display
addressable_light_id: neopixel_strip_1
brightness: 100%
color_on: col_on
color_off: col_off
update_interval: 16ms
static_segments: ["IT", "IS"]
segments:
- {name: "IT", line: {x1: 0, x2: 1, y1: 0}}
- {name: "IS", line: {x1: 3, x2: 4, y1: 0}}
- {name: "AM", line: {x1: 7, x2: 8, y1: 0}}
- {name: "PM", line: {x1: 9, x2: 10, y1: 0}}
- {name: "QUARTER", line: {x1: 2, x2: 8, y1: 1}}
- {name: "TWENTY", line: {x1: 0, x2: 5, y1: 2}}
- {name: "FIVE_MINUTES", line: {x1: 6, x2: 9, y1: 2}}
- {name: "HALF", line: {x1: 0, x2: 3, y1: 3}}
- {name: "TEN_MINUTES", line: {x1: 5, x2: 7, y1: 3}}
- {name: "TO", line: {x1: 9, x2: 10, y1: 3}}
- {name: "PAST", line: {x1: 0, x2: 3, y1: 4}}
- {name: "NINE", line: {x1: 7, x2: 10, y1: 4}}
- {name: "ONE", line: {x1: 0, x2: 2, y1: 5}}
- {name: "SIX", line: {x1: 3, x2: 5, y1: 5}}
- {name: "THREE", line: {x1: 6, x2: 10, y1: 5}}
- {name: "FOUR", line: {x1: 0, x2: 3, y1: 6}}
- {name: "FIVE", line: {x1: 4, x2: 7, y1: 6}}
- {name: "TWO", line: {x1: 8, x2: 10, y1: 6}}
- {name: "EIGHT", line: {x1: 0, x2: 4, y1: 7}}
- {name: "ELEVEN", line: {x1: 5, x2: 10, y1: 7}}
- {name: "SEVEN", line: {x1: 0, x2: 4, y1: 8}}
- {name: "TWELVE", line: {x1: 5, x2: 10, y1: 8}}
- {name: "TEN", line: {x1: 0, x2: 2, y1: 9}}
- {name: "OCLOCK", line: {x1: 5, x2: 10, y1: 9}}
minutes:
- {minute: 0, hour_offset: 0, segments: ["OCLOCK"]}
- {minute: 5, hour_offset: 0, segments: ["FIVE_MINUTES", "PAST"]}
- {minute: 10, hour_offset: 0, segments: ["TEN_MINUTES", "PAST"]}
- {minute: 15, hour_offset: 0, segments: ["QUARTER", "PAST"]}
- {minute: 20, hour_offset: 0, segments: ["TWENTY", "PAST"]}
- {minute: 25, hour_offset: 0, segments: ["TWENTY", "FIVE_MINUTES", "PAST"]}
- {minute: 30, hour_offset: 0, segments: ["HALF", "PAST"]}
- {minute: 35, hour_offset: 1, segments: ["TWENTY", "FIVE_MINUTES", "TO"]}
- {minute: 40, hour_offset: 1, segments: ["TWENTY", "TO"]}
- {minute: 45, hour_offset: 1, segments: ["QUARTER", "TO"]}
- {minute: 50, hour_offset: 1, segments: ["TEN_MINUTES", "TO"]}
- {minute: 55, hour_offset: 1, segments: ["FIVE_MINUTES", "TO"]}
hours:
- {hour: 0, segments: "TWELVE"}
- {hour: 1, segments: "ONE"}
- {hour: 2, segments: "TWO"}
- {hour: 3, segments: "THREE"}
- {hour: 4, segments: "FOUR"}
- {hour: 5, segments: "FIVE"}
- {hour: 6, segments: "SIX"}
- {hour: 7, segments: "SEVEN"}
- {hour: 8, segments: "EIGHT"}
- {hour: 9, segments: "NINE"}
- {hour: 10, segments: "TEN"}
- {hour: 11, segments: "ELEVEN"}
- {hour: 12, segments: "TWELVE"}
- {hour: 13, segments: "ONE"}
- {hour: 14, segments: "TWO"}
- {hour: 15, segments: "THREE"}
- {hour: 16, segments: "FOUR"}
- {hour: 17, segments: "FIVE"}
- {hour: 18, segments: "SIX"}
- {hour: 19, segments: "SEVEN"}
- {hour: 20, segments: "EIGHT"}
- {hour: 21, segments: "NINE"}
- {hour: 22, segments: "TEN"}
- {hour: 23, segments: "ELEVEN"}

BIN
test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

228
wordclock.h Normal file
View file

@ -0,0 +1,228 @@
#include "esphome.h"
#include <FastLED.h>
// By now only loosely based on https://github.com/leinich/ha-wordclock-esphome
// esphome dependencies:
// needs: esphome time --> id: current_time
// needs: esphome fastled --> id: fastledlight
///// Random Stuff /////
#ifndef WORDCLOCK_NUM_LEDS
#define WORDCLOCK_NUM_LEDS 198
#endif
#ifndef WORDCLOCK_DATA_PIN
#define WORDCLOCK_DATA_PIN 22
#endif
/* NOTE:
This section is about mapping LED indices to
positions in the grid.
On EVEN rows, the LED strip is running along the
matrix indices, but the physical positions of the
LEDs on the strip did not align with the letter
cutouts.
On ODD rows, the string is running backwards, because
it is snaking its way back and forth. */
int mapping_even[] = {0, 2, 4, 6,8,10,11,13,15,17};
int mapping_odd[] = {17,15,13,11,9, 7, 6, 4, 2, 0};
int map_coords_to_strip(int row, int column) {
if (column % 2) {
return (10 - column) * 18 + mapping_odd[row];
} else {
return (10 - column) * 18 + mapping_even [row];
}
}
///// Word Table /////
int WORD_IT_IS[5][2] = {{4,0}, {0,0}, {0,1}, {0,3}, {0,4}};
int WORD_QUARTER[8][2] = {{7,0}, {1,2}, {1,3}, {1,4}, {1,5}, {1,6}, {1,7}, {1,8}};
int WORD_TWENTY[7][2] = {{6,0}, {2,0}, {2,1}, {2,2}, {2,3}, {2,4}, {2,5}};
int WORD_FIVE_MINUTES[5][2] = {{4,0}, {2,6}, {2,7}, {2,8}, {2,9}};
int WORD_HALF[5][2] = {{4,0}, {3,0}, {3,1}, {3,2}, {3,3}};
int WORD_TEN_MINUTES[4][2] = {{3,0}, {3,5}, {3,6}, {3,7}};
int WORD_TO[3][2] = {{2,0}, {3,9}, {3,10}};
int WORD_PAST[5][2] = {{4,0}, {4,0}, {4,1}, {4,2}, {4,3}};
int WORD_NINE[5][2] = {{4,0}, {4,7}, {4,8}, {4,9}, {4,10}};
int WORD_ONE[4][2] = {{3,0}, {5,0}, {5,1}, {5,2}};
int WORD_SIX[4][2] = {{3,0}, {5,3}, {5,4}, {5,5}};
int WORD_THREE[6][2] = {{5,0}, {5,6}, {5,7}, {5,8}, {5,9}, {5,10}};
int WORD_FOUR[5][2] = {{4,0}, {6,0}, {6,1}, {6,2}, {6,3}};
int WORD_FIVE[5][2] = {{4,0}, {6,4}, {6,5}, {6,6}, {6,7}};
int WORD_TWO[4][2] = {{3,0}, {6,8}, {6,9}, {6,10}};
int WORD_EIGHT[6][2] = {{5,0}, {7,0}, {7,1}, {7,2}, {7,3}, {7,4}};
int WORD_ELEVEN[7][2] = {{6,0}, {7,5}, {7,6}, {7,7}, {7,8}, {7,9}, {7,10}};
int WORD_SEVEN[6][2] = {{5,0}, {8,0}, {8,1}, {8,2}, {8,3}, {8,4}};
int WORD_TWELFE[7][2] = {{6,0}, {8,5}, {8,6}, {8,7}, {8,8}, {8,9}, {8,10}};
int WORD_TEN[4][2] = {{3,0}, {9,0}, {9,1}, {9,2}};
int WORD_OCLOCK[7][2] = {{6,0}, {9,5}, {9,6}, {9,7}, {9,8}, {9,9}, {9,10}};
// TODO: It's probably unnecessary to have the "API" in CustomAPIDevice, since this
// component doesn't actually register any services anymore.
class Wordclock : public Component, public CustomAPIDevice {
public:
CRGB leds[WORDCLOCK_NUM_LEDS];
int hour = -1;
int minute = -1;
int second = -1;
int red = 124;
int green = 124;
int blue = 124;
int brightness = 50;
void setup() override {
FastLED.addLeds<NEOPIXEL, WORDCLOCK_DATA_PIN>(leds, WORDCLOCK_NUM_LEDS);
FastLED.setBrightness(brightness);
clear_all_leds();
FastLED.show();
// TODO: Set up some kind of initialization sequence. But it should be based on an effect or similarly supporting the
// cooperative multithreading. delay() calls are uncalled for.
}
void clear_all_leds() {
for(int i = 0; i < WORDCLOCK_NUM_LEDS; i++) {
leds[i].setRGB(0, 0, 0);
}
}
void display_word(const int word[][2], const CRGB& c) {
for (int i=1; i < word[0][0] + 1; i++) {
leds[map_coords_to_strip(word[i][0], word[i][1])].setRGB(c.r, c.g, c.b);
}
}
void display_minutes(int minutes, const CRGB& color) {
int five_minute_chunk = minutes / 5;
switch (five_minute_chunk)
{
case 0: // sharp
display_word(WORD_OCLOCK, color); ESP_LOGD("minute", "oclock "); break;
case 1: // five past
display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five past "); break;
case 2: // ten past
display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten past "); break;
case 3: // quarter past
display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter past "); break;
case 4: // twenty past
display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty past "); break;
case 5: // twenty five past
display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color);
ESP_LOGD("minute", "twenty five past "); break;
case 6: // half past
display_word(WORD_HALF, color); ESP_LOGD("minute", "half past "); break;
case 7: // twenty five to
display_word(WORD_TWENTY, color); display_word(WORD_FIVE_MINUTES, color);
ESP_LOGD("minute", "twenty five to "); break;
case 8: // twenty to
display_word(WORD_TWENTY, color); ESP_LOGD("minute", "twenty to "); break;
case 9: // quarter to
display_word(WORD_QUARTER, color); ESP_LOGD("minute", "quarter to "); break;
case 10: // ten to
display_word(WORD_TEN_MINUTES, color); ESP_LOGD("minute", "ten to "); break;
case 11: // five to
display_word(WORD_FIVE_MINUTES, color); ESP_LOGD("minute", "five to "); break;
default:
break;
}
if (five_minute_chunk > 6) {
display_word(WORD_TO, color);
} else if (five_minute_chunk > 0) {
display_word(WORD_PAST, color);
}
}
void display_hour(int hour, int minutes, const CRGB& color) {
int five_minute_chunk = minutes / 5;
if (five_minute_chunk > 6) {
hour += 1;
}
switch (hour % 12)
{
case 0: // twelve
display_word(WORD_TWELFE, color); ESP_LOGD("hour", "twelve "); break;
case 1: // one
display_word(WORD_ONE, color); ESP_LOGD("hour", "one "); break;
case 2: // two
display_word(WORD_TWO, color); ESP_LOGD("hour", "two "); break;
case 3: // three
display_word(WORD_THREE, color); ESP_LOGD("hour", "three "); break;
case 4: // four
display_word(WORD_FOUR, color); ESP_LOGD("hour", "four "); break;
case 5: // five
display_word(WORD_FIVE, color); ESP_LOGD("hour", "five "); break;
case 6: // six
display_word(WORD_SIX, color); ESP_LOGD("hour", "six "); break;
case 7: // seven
display_word(WORD_SEVEN, color); ESP_LOGD("hour", "seven "); break;
case 8: // eight
display_word(WORD_EIGHT, color); ESP_LOGD("hour", "eight "); break;
case 9: // nine
display_word(WORD_NINE, color); ESP_LOGD("hour", "nine "); break;
case 10: // ten
display_word(WORD_TEN, color); ESP_LOGD("hour", "ten "); break;
case 11: // eleven
display_word(WORD_ELEVEN, color); ESP_LOGD("hour", "eleven "); break;
default:
break;
}
}
void display_time(int hour, int minutes, const CRGB& color) {
display_word(WORD_IT_IS, color);
display_hour(hour, minutes, color);
display_minutes(minutes, color);
}
void loop() override {
auto time = id(current_time).now();
// https://www.esphome.io/api/classesphome_1_1light_1_1_light_color_values.html LightColorValues Class
auto fastledlight2 = id(fastledlight).current_values;
//convert float 0.0 till 1.0 into int 0 till 255
red = (int) (fastledlight2.get_red() * 125);
green = (int) (fastledlight2.get_green() * 125);
blue = (int) (fastledlight2.get_blue() * 125);
brightness = 0;
//check if light is on and set brightness
if (fastledlight2.get_state() > 0 ) {
brightness = (int) (fastledlight2.get_brightness()*125);
} else {
// ESP_LOGD("loop", "fastledlight state off - b: %i rgb %i %i %i", brightness, red, green, blue); delay(100);
}
FastLED.setBrightness(brightness);
FastLED.show();
//check if valid time. Blink red,green,blue until valid time is present
if (time.is_valid() == false) {
ESP_LOGD("loop", "time is not valid");
// do something to show that the clock isn't dead. Maybe I can instantiate and effect and use that for this.
}
else {
if (time.second != second) {
second = time.second;
ESP_LOGD("loop", "time is now [%02i:%02i:%02i]", time.hour, time.minute, time.second);
}
if(time.hour != hour || time.minute != minute) {
hour = time.hour;
minute = time.minute;
if (hour >= 0 && time.is_valid() == true){
clear_all_leds();
display_time(time.hour, time.minute, CRGB(red, green, blue));
FastLED.show();
ESP_LOGE("loop", "Update Time: %i:%i Brightness: %i RGB: %i-%i-%i", time.hour, time.minute, brightness, red, green, blue);
}
}
}
}
};

74
wordclock.yaml Normal file
View file

@ -0,0 +1,74 @@
esphome:
name: "${devicename}"
platformio_options:
build_flags:
- -DWORDCLOCK_DATA_PIN=22
- -DWORDCLOCK_NUM_LEDS=198
includes:
- wordclock.h
external_components:
- source:
type: local
path: components
components: [ wordcl ]
esp8266:
board: d1_mini
framework:
version: recommended
# esp32:
# board: ttgo-t7-v13-mini32
# framework:
# type: arduino
substitutions:
devicename: wordclock
friendly_name: "Wordclock"
light_friendly_name: "Wordclock Light"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "${devicename}"
password: !secret ap_password
manual_ip:
static_ip: !secret manualip_static_ip
gateway: !secret manualip_gateway
subnet: !secret manualip_subnet
dns1: 1.1.1.1
dns2: 1.0.0.1
api:
ota:
password: "${devicename}"
logger:
# esp8266_store_log_strings_in_flash: false
web_server:
port: 80
light:
- platform: fastled_clockless
id: fastledlight
chipset: WS2812
pin: 23
num_leds: 198
rgb_order: GRB
name: ${light_friendly_name}
restore_mode: ALWAYS_ON
time:
- platform: sntp
id: current_time
timezone: !secret timezone
custom_component:
- lambda: |-
auto wordclock = new Wordclock();
return {wordclock};
components:
- id: wordclock

89
wordclock8266.yaml Normal file
View file

@ -0,0 +1,89 @@
esphome:
name: "wordclock"
esp8266:
board: d1_mini
framework:
version: recommended
external_components:
- source:
type: local
path: components
components: [ wordcl ]
# substitutions:
# devicename: wordclock
# friendly_name: "Wordclock"
# light_friendly_name: "Wordclock Light"
# wifi:
# ssid: !secret wifi_ssid
# password: !secret wifi_password
# ap:
# ssid: "${devicename}"
# password: !secret ap_password
# manual_ip:
# static_ip: !secret manualip_static_ip
# gateway: !secret manualip_gateway
# subnet: !secret manualip_subnet
# dns1: 1.1.1.1
# dns2: 1.0.0.1
# api:
# ota:
# password: "${devicename}"
# logger:
# # esp8266_store_log_strings_in_flash: false
# web_server:
# port: 80
wordclock:
light:
- name: neopixel
id: neopixel_1
platform: neopixelbus
type: GRB
variant: WS2812
pin: GPIO3
num_leds: 30
default_transition_length: 0.2s
# restore_mode: RESTORE_DEFAULT_ON
method:
type: esp8266_dma
on_turn_on:
then:
- light.turn_on:
id: neopixel_1
brightness: 35%
effect: rainbow
effects:
# - random:
# - pulse:
# - strobe:
# - flicker:
- addressable_rainbow:
name: rainbow
# - addressable_color_wipe:
# - addressable_scan:
# - addressable_twinkle:
# - addressable_random_twinkle:
# - addressable_fireworks:
# - addressable_flicker:
# - wled:
# time:
# - platform: sntp
# id: current_time
# timezone: !secret timezone
# - light.turn_on:
# id: light_1
# brightness: 100%
# effect: addressable_rainbow

29
wordclock_new.yaml Normal file
View file

@ -0,0 +1,29 @@
esphome:
name: wordclock-livingroom
esp32:
board: ttgo-t7-v13-mini32
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
password: ""
ota:
password: ""
wifi:
ssid: "wifithing"
password: "lostandfound1"
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Wordclock-Livingroom"
password: "wGfGBPnJcbzE"
captive_portal: