|
| 1 | +esp32: |
| 2 | + board: esp32dev |
| 3 | + framework: |
| 4 | + type: esp-idf |
| 5 | + |
| 6 | +esphome: |
| 7 | + name: iot-interactive-map |
| 8 | + friendly_name: Interactive Map |
| 9 | + min_version: 2025.9.0 |
| 10 | + name_add_mac_suffix: false |
| 11 | + on_boot: |
| 12 | + then: |
| 13 | + - light.turn_on: |
| 14 | + id: map |
| 15 | + brightness: 100% |
| 16 | + effect: "Border Chase" |
| 17 | + |
| 18 | +# Enable logging |
| 19 | +logger: |
| 20 | + level: DEBUG |
| 21 | + |
| 22 | +api: |
| 23 | + |
| 24 | +# Allow Over-The-Air updates |
| 25 | +ota: |
| 26 | +- platform: esphome |
| 27 | + |
| 28 | +wifi: |
| 29 | + ssid: !secret wifi_ssid |
| 30 | + password: !secret wifi_password |
| 31 | + on_connect: |
| 32 | + then: |
| 33 | + - delay: 2s |
| 34 | + - script.execute: fetch_data |
| 35 | + |
| 36 | +globals: |
| 37 | + - id: API_RESPONSE |
| 38 | + type: std::vector<std::array<int, 4>> |
| 39 | + restore_value: no |
| 40 | + |
| 41 | +interval: |
| 42 | + - interval: 10s |
| 43 | + then: |
| 44 | + - script.execute: fetch_data |
| 45 | + |
| 46 | +http_request: |
| 47 | + id: http_client |
| 48 | + |
| 49 | +light: |
| 50 | + - platform: esp32_rmt_led_strip |
| 51 | + rgb_order: GRB |
| 52 | + chipset: WS2812 |
| 53 | + pin: GPIO25 |
| 54 | + num_leds: 72 |
| 55 | + name: "Map" |
| 56 | + id: map |
| 57 | + default_transition_length: |
| 58 | + seconds: 0 |
| 59 | + effects: |
| 60 | + - addressable_lambda: |
| 61 | + name: "Cities from API" |
| 62 | + update_interval: 1s |
| 63 | + lambda: |- |
| 64 | + if (id(API_RESPONSE).empty()) { |
| 65 | + it.all() = Color::BLACK; |
| 66 | + return; |
| 67 | + } |
| 68 | +
|
| 69 | + it.all() = Color::BLACK; |
| 70 | + for (const auto &city : id(API_RESPONSE)) { |
| 71 | + int led_index = city[0]; |
| 72 | + int r = city[1]; |
| 73 | + int g = city[2]; |
| 74 | + int b = city[3]; |
| 75 | +
|
| 76 | + if (led_index < 0 || led_index >= it.size()) { |
| 77 | + continue; |
| 78 | + } |
| 79 | + it[led_index] = Color(r, g, b); |
| 80 | + } |
| 81 | + - addressable_lambda: |
| 82 | + name: Border Chase |
| 83 | + update_interval: 200ms |
| 84 | + lambda: |- |
| 85 | + // W=>E north 24, 19, 16, 10, 9, 6, 3, 0, 4, 1, 2, 5, 7, 12, 21, 29, 31, 17, 25, 30, 36, 35 |
| 86 | + // E=>W south 42, 56, 61, 65, 68, 71, 69, 64, 67, 70, 66, 60, 54, 50, 37 |
| 87 | + static const std::vector<int> north_path = { 19, 16, 10, 6, 0, 4, 1, 2, 5, 7, 12, 21, 29, 31, 17, 25, 30, 36 }; |
| 88 | + static const std::vector<int> south_path = { 35, 42, 44, 56, 61, 65, 68, 71, 69, 64, 67, 70, 66, 60, 54, 50, 37, 24 }; |
| 89 | + static size_t north_pos = 0; |
| 90 | + static size_t south_pos = 0; |
| 91 | +
|
| 92 | + it.all() = Color::BLACK; |
| 93 | +
|
| 94 | + auto set_dot = [&](const std::vector<int> &path, size_t pos, Color color) { |
| 95 | + if (path.empty()) return; |
| 96 | + int idx = path[pos % path.size()]; |
| 97 | + if (idx >= 0 && idx < it.size()) it[idx] = color; |
| 98 | + }; |
| 99 | +
|
| 100 | + set_dot(north_path, north_pos++, Color(0, 0, 255)); |
| 101 | + set_dot(south_path, south_pos++, Color(255, 0, 0)); |
| 102 | +
|
| 103 | +script: |
| 104 | + - id: fetch_data |
| 105 | + then: |
| 106 | + - logger.log: |
| 107 | + level: INFO |
| 108 | + format: "Fetching remote data" |
| 109 | + - http_request.get: |
| 110 | + url: "http://api.server.home/iot/interactive-map-feeder/v1/data-sources/radar/cities/iot" |
| 111 | + capture_response: true |
| 112 | + max_response_buffer_size: 15000 |
| 113 | + request_headers: |
| 114 | + Content-Type: application/json |
| 115 | + # Important, without this body is empty. Related to Traefik. |
| 116 | + Accept-Encoding: identity |
| 117 | + on_response: |
| 118 | + - if: |
| 119 | + condition: |
| 120 | + lambda: return response->status_code == 200; |
| 121 | + then: |
| 122 | + - lambda: |- |
| 123 | + json::parse_json(body, [&](JsonObject res) -> bool { |
| 124 | + if (!res["cities"]) { |
| 125 | + ESP_LOGI("log", "No 'cities' key in this json!"); |
| 126 | + return false; |
| 127 | + } |
| 128 | +
|
| 129 | + JsonArray cities = res["cities"].as<JsonArray>(); |
| 130 | + if (cities.isNull()) { |
| 131 | + ESP_LOGW("log", "'cities' key exists but is not an array"); |
| 132 | + return false; |
| 133 | + } |
| 134 | + ESP_LOGI("log", "Received %u cities", cities.size()); |
| 135 | +
|
| 136 | + id(API_RESPONSE).clear(); |
| 137 | + id(API_RESPONSE).reserve(cities.size()); |
| 138 | +
|
| 139 | + for (JsonObject city : cities) { |
| 140 | + if (!city["id"].is<int>() || !city["color"] || !city["color"].is<JsonObject>()) { |
| 141 | + ESP_LOGI("log", "City %u is invalid", city["id"].as<int>()); |
| 142 | + continue; |
| 143 | + } |
| 144 | +
|
| 145 | + JsonObject color = city["color"].as<JsonObject>(); |
| 146 | + std::array<int, 4> entry = { |
| 147 | + city["id"].as<int>(), |
| 148 | + color["r"].as<int>(), |
| 149 | + color["g"].as<int>(), |
| 150 | + color["b"].as<int>() |
| 151 | + }; |
| 152 | +
|
| 153 | + id(API_RESPONSE).push_back(entry); |
| 154 | + } |
| 155 | +
|
| 156 | + ESP_LOGI("log", "Stored %u cities in API_RESPONSE", id(API_RESPONSE).size()); |
| 157 | + return true; |
| 158 | + }); |
| 159 | + - light.turn_on: |
| 160 | + id: map |
| 161 | + brightness: 100% |
| 162 | + effect: "Cities from API" |
| 163 | + else: |
| 164 | + - logger.log: |
| 165 | + level: "WARN" |
| 166 | + format: "Error: Response status: %d, message %s" |
| 167 | + args: [ 'response->status_code', 'body.c_str()' ] |
| 168 | + on_error: |
| 169 | + then: |
| 170 | + - logger.log: |
| 171 | + level: "INFO" |
| 172 | + format: "Request failed!" |
0 commit comments