Skip to content

Commit 721b2e1

Browse files
committed
Interactive map device
0 parents  commit 721b2e1

File tree

3 files changed

+188
-0
lines changed

3 files changed

+188
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Gitignore settings for ESPHome
2+
# This is an example and may include too much for your use-case.
3+
# You can modify this file to suit your needs.
4+
.esphome
5+
secrets.yaml

Readme.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ESPHome configurations
2+
3+
Create `secrets.yaml` file `wifi_ssid` and `wifi_password` variables.
4+
5+
## Dashboard
6+
7+
Run `esphome dashboard .` from within this repository.
8+
9+
## Install
10+
11+
Install code from ESPHome dashboard.

interactive-map.yaml

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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

Comments
 (0)