diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..844caea --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# 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 diff --git a/components/wordcl/__init__.py b/components/wordcl/__init__.py new file mode 100644 index 0000000..de1ab14 --- /dev/null +++ b/components/wordcl/__init__.py @@ -0,0 +1,53 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import light +from esphome.components import uart +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_HEIGHT, CONF_TIMEOUT, ICON_GAUGE + +DEPENDENCIES = ['time'] +AUTO_LOAD = ['light'] + +wordclock_ns = cg.esphome_ns.namespace('wordcl') + +Wordclock = wordclock_ns.class_('Wordclock', cg.Component, light.) +Desky = desky_ns.class_('Desky', cg.Component, uart.UARTDevice) + +CONF_UP = "up" +CONF_DOWN = "down" +CONF_REQUEST = "request" +CONF_STOPPING_DISTANCE = "stopping_distance" + + + +CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(Desky), + cv.Optional(CONF_UP): pins.gpio_output_pin_schema, + cv.Optional(CONF_DOWN): pins.gpio_output_pin_schema, + cv.Optional(CONF_REQUEST): pins.gpio_output_pin_schema, + cv.Optional(CONF_HEIGHT): sensor.sensor_schema(icon=ICON_GAUGE, accuracy_decimals=0), + cv.Optional(CONF_STOPPING_DISTANCE, default=15): cv.positive_int, + cv.Optional(CONF_TIMEOUT): cv.time_period, +}).extend(uart.UART_DEVICE_SCHEMA) + +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) + if CONF_UP in config: + pin = await cg.gpio_pin_expression(config[CONF_UP]) + cg.add(var.set_up_pin(pin)) + if CONF_DOWN in config: + pin = await cg.gpio_pin_expression(config[CONF_DOWN]) + cg.add(var.set_down_pin(pin)) + if CONF_REQUEST in config: + pin = await cg.gpio_pin_expression(config[CONF_REQUEST]) + cg.add(var.set_request_pin(pin)) + if CONF_HEIGHT in config: + sens = await sensor.new_sensor(config[CONF_HEIGHT]) + cg.add(var.set_height_sensor(sens)) + cg.add(var.set_stopping_distance(config[CONF_STOPPING_DISTANCE])) + if CONF_TIMEOUT in config: + cg.add(var.set_timeout(config[CONF_TIMEOUT].total_milliseconds)) + diff --git a/components/wordcl/desky.cpp b/components/wordcl/desky.cpp new file mode 100644 index 0000000..bcd6159 --- /dev/null +++ b/components/wordcl/desky.cpp @@ -0,0 +1,116 @@ +#include "desky.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace desky { + +static const char *TAG = "desky"; + +const char *desky_operation_to_str(DeskyOperation op) { + switch (op) { + case DESKY_OPERATION_IDLE: + return "IDLE"; + case DESKY_OPERATION_RAISING: + return "RAISING"; + case DESKY_OPERATION_LOWERING: + return "LOWERING"; + default: + return "UNKNOWN"; + } +} + +void Desky::setup() { + if (this->up_pin_ != nullptr) + this->up_pin_->digital_write(false); + if (this->down_pin_ != nullptr) + this->down_pin_->digital_write(false); + if (this->request_pin_ != nullptr) { + this->request_pin_->digital_write(true); + this->request_time_ = millis(); + } +} + +void Desky::loop() { + static int state = 0; + static uint8_t high_byte; + + while (this->available()) { + uint8_t c; + int value; + this->read_byte(&c); + switch (state) { + case 0: + if (c == 1) + state = 1; + break; + case 1: + if (c == 1) + state = 2; + else + state = 0; + break; + case 2: + high_byte = c; + state = 3; + break; + case 3: + value = (high_byte << 8) + c; + this->current_pos_ = value; + if (this->height_sensor_ != nullptr) + this->height_sensor_->publish_state(value); + state = 0; + break; + } + } + + if (this->target_pos_ >= 0) { + if (abs(this->target_pos_ - this->current_pos_) < this->stopping_distance_) + this->stop(); + if ((this->timeout_ >= 0) && (millis() - this->start_time_ >= this->timeout_)) + this->stop(); + } + + if ((this->request_time_ > 0) && (millis() - this->request_time_ >= 100)) { + this->request_pin_->digital_write(false); + this->request_time_ = 0; + } +} + +void Desky::dump_config() { + ESP_LOGCONFIG(TAG, "Desky desk:"); + LOG_SENSOR("", "Height", this->height_sensor_); + LOG_PIN("Up pin: ", this->up_pin_); + LOG_PIN("Down pin: ", this->down_pin_); + LOG_PIN("Request pin: ", this->request_pin_); +} + +void Desky::move_to(int target_pos) { + if (abs(target_pos - this->current_pos_) < this->stopping_distance_) + return; + if (target_pos > this->current_pos_) { + if (this->up_pin_ == nullptr) + return; + this->up_pin_->digital_write(true); + this->current_operation = DESKY_OPERATION_RAISING; + } else { + if (this->down_pin_ == nullptr) + return; + this->down_pin_->digital_write(true); + this->current_operation = DESKY_OPERATION_LOWERING; + } + this->target_pos_ = target_pos; + if (this->timeout_ >= 0) + this->start_time_ = millis(); +} + +void Desky::stop() { + this->target_pos_ = -1; + if (this->up_pin_ != nullptr) + this->up_pin_->digital_write(false); + if (this->down_pin_ != nullptr) + this->down_pin_->digital_write(false); + this->current_operation = DESKY_OPERATION_IDLE; +} + +} // namespace desky +} // namespace esphome diff --git a/components/wordcl/desky.h b/components/wordcl/desky.h new file mode 100644 index 0000000..fc9062c --- /dev/null +++ b/components/wordcl/desky.h @@ -0,0 +1,52 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/hal.h" + +namespace esphome { +namespace desky { + +enum DeskyOperation : uint8_t { + DESKY_OPERATION_IDLE = 0, + DESKY_OPERATION_RAISING, + DESKY_OPERATION_LOWERING, +}; + +const char *desky_operation_to_str(DeskyOperation op); + +class Desky : public Component, public sensor::Sensor, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::LATE; } + void setup() override; + void loop() override; + void dump_config() override; + + void set_height_sensor(sensor::Sensor *sensor) { this->height_sensor_ = sensor; } + void set_up_pin(GPIOPin *pin) { this->up_pin_ = pin; } + void set_down_pin(GPIOPin *pin) { this->down_pin_ = pin; } + void set_request_pin(GPIOPin *pin) { this->request_pin_ = pin; } + void set_stopping_distance(int distance) { this->stopping_distance_ = distance; } + void set_timeout(int timeout) { this->timeout_ = timeout; } + + void move_to(int height); + void stop(); + + DeskyOperation current_operation{DESKY_OPERATION_IDLE}; + + protected: + sensor::Sensor *height_sensor_{nullptr}; + GPIOPin *up_pin_{nullptr}; + GPIOPin *down_pin_{nullptr}; + GPIOPin *request_pin_{nullptr}; + int stopping_distance_; + int current_pos_{0}; + int target_pos_{-1}; + int timeout_{-1}; + uint64_t start_time_; + uint64_t request_time_{0}; +}; + +} // namespace desky +} // namespace esphome diff --git a/display_test.yaml b/display_test.yaml new file mode 100644 index 0000000..837eacf --- /dev/null +++ b/display_test.yaml @@ -0,0 +1,66 @@ +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: 200 + # default_transition_length: 0.2s + method: + type: esp8266_dma + +display: + - platform: addressable_light + id: led_matrix_display + addressable_light_id: neopixel_strip_1 + width: 18 + height: 11 + rotation: 0° + update_interval: 16ms + lambda: |- + // Draw a bulls-eye pattern + Color red = Color(0xFF0000); + Color green = Color(0x00FF00); + Color blue = Color(0x0000FF); + it.rectangle(0, 0, 18, 11, red); + it.rectangle(1, 1, 17, 10, green); + it.rectangle(2, 2, 16, 9, blue); + it.rectangle(3, 3, 15, 8, red); + + +time: + - platform: sntp + id: current_time + timezone: !secret timezone diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..d696ee7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,67 @@ +{ + description = "EspHome Word Clock"; + inputs = { + systems.url = "github:nix-systems/x86_64-linux"; + # nixpkgs.url = "github:NixOS/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; + + }); +} diff --git a/secrets.example.yaml b/secrets.example.yaml new file mode 100644 index 0000000..a6373f1 --- /dev/null +++ b/secrets.example.yaml @@ -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" diff --git a/wordclock.h b/wordclock.h index 758ef0b..95feeb5 100644 --- a/wordclock.h +++ b/wordclock.h @@ -72,6 +72,7 @@ class Wordclock : public Component, public CustomAPIDevice { CRGB leds[WORDCLOCK_NUM_LEDS]; int hour = -1; int minute = -1; + int second = -1; int red = 124; int green = 124; int blue = 124; @@ -83,7 +84,7 @@ class Wordclock : public Component, public CustomAPIDevice { 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. ^^ + // cooperative multithreading. delay() calls are uncalled for. } void clear_all_leds() { @@ -195,16 +196,21 @@ class Wordclock : public Component, public CustomAPIDevice { 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); + // 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) { + 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; diff --git a/wordclock.yaml b/wordclock.yaml index cd6027e..d3250e1 100644 --- a/wordclock.yaml +++ b/wordclock.yaml @@ -7,10 +7,21 @@ esphome: includes: - wordclock.h -esp32: - board: wemos_d1_mini32 +external_components: + - source: + type: local + path: components + components: [ wordcl ] + +esp8266: + board: d1_mini framework: - type: arduino + version: recommended + +# esp32: +# board: ttgo-t7-v13-mini32 +# framework: +# type: arduino substitutions: devicename: wordclock @@ -27,6 +38,8 @@ wifi: 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: diff --git a/wordclock8266.yaml b/wordclock8266.yaml new file mode 100644 index 0000000..5986c77 --- /dev/null +++ b/wordclock8266.yaml @@ -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 \ No newline at end of file diff --git a/wordclock_new.yaml b/wordclock_new.yaml new file mode 100644 index 0000000..cdaf34e --- /dev/null +++ b/wordclock_new.yaml @@ -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: + \ No newline at end of file