Skip to content

Commit 2243b7c

Browse files
committed
Add controls
1 parent 6df7746 commit 2243b7c

10 files changed

Lines changed: 257 additions & 7 deletions

File tree

components/votronic/votronic.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ std::string Votronic::solar_charger_status_bitmask_to_string_(const uint8_t mask
641641

642642
std::string Votronic::charger_status_bitmask_to_string_(const uint8_t mask) {
643643
bool first = true;
644-
std::string errors_list = "";
644+
std::string errors_list;
645645

646646
if (mask == 0x00) {
647647
return "Standby";

components/votronic_ble/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
"VotronicBle", ble_client.BLEClientNode, cg.PollingComponent
1616
)
1717

18+
VOTRONIC_BLE_COMPONENT_SCHEMA = cv.Schema(
19+
{
20+
cv.GenerateID(CONF_VOTRONIC_BLE_ID): cv.use_id(VotronicBle),
21+
}
22+
)
23+
1824
CONFIG_SCHEMA = cv.All(
1925
cv.require_esphome_version(2026, 1, 0),
2026
cv.Schema(
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import esphome.codegen as cg
2+
from esphome.components import button
3+
import esphome.config_validation as cv
4+
from esphome.const import CONF_ID
5+
6+
from .. import CONF_VOTRONIC_BLE_ID, VOTRONIC_BLE_COMPONENT_SCHEMA, votronic_ble_ns
7+
8+
DEPENDENCIES = ["votronic_ble"]
9+
10+
CODEOWNERS = ["@syssi"]
11+
12+
CONF_RETRIEVE_SETTINGS = "retrieve_settings"
13+
CONF_RETRIEVE_DEVICE_INFO = "retrieve_device_info"
14+
15+
CONF_RETRIEVE_DAILY_OPERATING_STATISTICS = "retrieve_daily_operating_statistics"
16+
CONF_RETRIEVE_WEEKLY_OPERATING_STATISTICS = "retrieve_weekly_operating_statistics"
17+
CONF_RETRIEVE_MONTHLY_OPERATING_STATISTICS = "retrieve_monthly_operating_statistics"
18+
CONF_RETRIEVE_OPERATING_STATISTICS = "retrieve_operating_statistics"
19+
CONF_RESET_ACCUMULATOR = "reset_accumulator"
20+
21+
ICON_RETRIEVE_DAILY_OPERATING_STATISTICS = "mdi:chart-box-outline"
22+
ICON_RETRIEVE_WEEKLY_OPERATING_STATISTICS = "mdi:chart-box-outline"
23+
ICON_RETRIEVE_MONTHLY_OPERATING_STATISTICS = "mdi:chart-box-outline"
24+
ICON_RETRIEVE_OPERATING_STATISTICS = "mdi:chart-box-outline"
25+
ICON_RESET_ACCUMULATOR = "mdi:history"
26+
27+
BUTTONS = {
28+
CONF_RETRIEVE_DAILY_OPERATING_STATISTICS: 0x01,
29+
CONF_RETRIEVE_WEEKLY_OPERATING_STATISTICS: 0x02,
30+
CONF_RETRIEVE_MONTHLY_OPERATING_STATISTICS: 0x03,
31+
CONF_RETRIEVE_OPERATING_STATISTICS: 0x04,
32+
CONF_RESET_ACCUMULATOR: 0x06,
33+
}
34+
35+
VotronicButton = votronic_ble_ns.class_("VotronicButton", button.Button, cg.Component)
36+
37+
CONFIG_SCHEMA = VOTRONIC_BLE_COMPONENT_SCHEMA.extend(
38+
{
39+
cv.Optional(CONF_RETRIEVE_DAILY_OPERATING_STATISTICS): button.button_schema(
40+
VotronicButton, icon=ICON_RETRIEVE_DAILY_OPERATING_STATISTICS
41+
).extend(cv.COMPONENT_SCHEMA),
42+
cv.Optional(CONF_RETRIEVE_WEEKLY_OPERATING_STATISTICS): button.button_schema(
43+
VotronicButton, icon=ICON_RETRIEVE_WEEKLY_OPERATING_STATISTICS
44+
).extend(cv.COMPONENT_SCHEMA),
45+
cv.Optional(CONF_RETRIEVE_MONTHLY_OPERATING_STATISTICS): button.button_schema(
46+
VotronicButton, icon=ICON_RETRIEVE_MONTHLY_OPERATING_STATISTICS
47+
).extend(cv.COMPONENT_SCHEMA),
48+
cv.Optional(CONF_RETRIEVE_OPERATING_STATISTICS): button.button_schema(
49+
VotronicButton, icon=ICON_RETRIEVE_OPERATING_STATISTICS
50+
).extend(cv.COMPONENT_SCHEMA),
51+
cv.Optional(CONF_RESET_ACCUMULATOR): button.button_schema(
52+
VotronicButton, icon=ICON_RESET_ACCUMULATOR
53+
).extend(cv.COMPONENT_SCHEMA),
54+
}
55+
)
56+
57+
58+
async def to_code(config):
59+
hub = await cg.get_variable(config[CONF_VOTRONIC_BLE_ID])
60+
for key, address in BUTTONS.items():
61+
if key in config:
62+
conf = config[key]
63+
var = cg.new_Pvariable(conf[CONF_ID])
64+
await cg.register_component(var, conf)
65+
await button.register_button(var, conf)
66+
cg.add(var.set_parent(hub))
67+
cg.add(var.set_holding_register(address))
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#include "votronic_button.h"
2+
#include "esphome/core/log.h"
3+
#include "esphome/core/application.h"
4+
5+
namespace esphome::votronic_ble {
6+
7+
static const char *const TAG = "votronic_ble.button";
8+
9+
void VotronicButton::dump_config() { LOG_BUTTON("", "VotronicBle Button", this); }
10+
void VotronicButton::press_action() { this->parent_->send_command(this->holding_register_); }
11+
12+
} // namespace esphome::votronic_ble
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#pragma once
2+
3+
#include "../votronic_ble.h"
4+
#include "esphome/core/component.h"
5+
#include "esphome/components/button/button.h"
6+
7+
namespace esphome::votronic_ble {
8+
9+
class VotronicBle;
10+
class VotronicButton : public button::Button, public Component {
11+
public:
12+
void set_parent(VotronicBle *parent) { this->parent_ = parent; };
13+
void set_holding_register(uint8_t holding_register) { this->holding_register_ = holding_register; };
14+
void dump_config() override;
15+
void loop() override {}
16+
float get_setup_priority() const override { return setup_priority::DATA; }
17+
18+
protected:
19+
void press_action() override;
20+
VotronicBle *parent_;
21+
uint8_t holding_register_;
22+
};
23+
24+
} // namespace esphome::votronic_ble

components/votronic_ble/votronic_ble.cpp

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,40 @@ void VotronicBle::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
5454
}
5555
this->char_solar_charger_handle_ = 0;
5656

57+
if (this->char_bulk_data_handle_ != 0) {
58+
auto status = esp_ble_gattc_unregister_for_notify(
59+
this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), this->char_bulk_data_handle_);
60+
if (status) {
61+
ESP_LOGW(TAG, "esp_ble_gattc_unregister_for_notify failed, status=%d", status);
62+
}
63+
}
64+
this->char_bulk_data_handle_ = 0;
65+
this->bulk_data_receiving_ = false;
66+
this->bulk_data_frames_.clear();
67+
5768
break;
5869
}
5970
case ESP_GATTC_SEARCH_CMPL_EVT: {
71+
// [60:A4:23:91:8F:55] ESP_GATTC_SEARCH_CMPL_EVT
72+
// [60:A4:23:91:8F:55] Service UUID: 0x1801
73+
// [60:A4:23:91:8F:55] start_handle: 0x1 end_handle: 0x4
74+
// [60:A4:23:91:8F:55] Service UUID: 0x1800
75+
// [60:A4:23:91:8F:55] start_handle: 0x5 end_handle: 0x9
76+
// [60:A4:23:91:8F:55] Service UUID: 0x180A
77+
// [60:A4:23:91:8F:55] start_handle: 0xa end_handle: 0x10
78+
// [60:A4:23:91:8F:55] Service UUID: 1D14D6EE-FD63-4FA1-BFA4-8F47B42119F0
79+
// [60:A4:23:91:8F:55] start_handle: 0x11 end_handle: 0x13
80+
// [60:A4:23:91:8F:55] Service UUID: D0CB6AA7-8548-46D0-99F8-2D02611E5270
81+
// [60:A4:23:91:8F:55] start_handle: 0x14 end_handle: 0x21
82+
// [60:A4:23:91:8F:55] Service UUID: AE64A924-1184-4554-8BBC-295DB9F2324A
83+
// [60:A4:23:91:8F:55] start_handle: 0x22 end_handle: 0xffff
84+
// [60:A4:23:91:8F:55] characteristic 9A082A4E-5BCC-4B1D-9958-A97CFCCFA5EC, handle 0x16, properties 0x12
85+
// [60:A4:23:91:8F:55] characteristic 971CCEC2-521D-42FD-B570-CF46FE5CEB65, handle 0x19, properties 0x12
86+
// [60:A4:23:91:8F:55] characteristic 9E298E7F-2594-49DE-BE51-39153A6250E4, handle 0x1c, properties 0xa
87+
// [60:A4:23:91:8F:55] characteristic CFA6E099-FA0F-43AA-88D2-4508B986A67F, handle 0x1e, properties 0xa
88+
// [60:A4:23:91:8F:55] characteristic D2296045-A715-4458-850F-0800C7E11CEC, handle 0x20, properties 0x1a
89+
// [60:A4:23:91:8F:55] gattc_event_handler: event=18 gattc_if=3
90+
// [60:A4:23:91:8F:55] cfg_mtu status 0, mtu 247
6091
auto *char_battery_computer =
6192
this->parent_->get_characteristic(this->service_monitoring_uuid_, this->char_battery_computer_uuid_);
6293
if (char_battery_computer == nullptr) {
@@ -87,6 +118,28 @@ void VotronicBle::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t
87118
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status2);
88119
}
89120

121+
auto *char_management =
122+
this->parent_->get_characteristic(this->service_monitoring_uuid_, this->char_management_uuid_);
123+
if (char_management == nullptr) {
124+
ESP_LOGW(TAG, "[%s] No management characteristic found at device.", this->parent_->address_str());
125+
break;
126+
}
127+
this->char_management_handle_ = char_management->handle;
128+
129+
auto *char_bulk_data =
130+
this->parent_->get_characteristic(this->service_log_data_uuid_, this->char_bulk_data_uuid_);
131+
if (char_bulk_data == nullptr) {
132+
ESP_LOGW(TAG, "[%s] No bulk data characteristic found at device.", this->parent_->address_str());
133+
break;
134+
}
135+
this->char_bulk_data_handle_ = char_bulk_data->handle;
136+
137+
auto status3 = esp_ble_gattc_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(),
138+
char_bulk_data->handle);
139+
if (status3) {
140+
ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed (bulk data), status=%d", status3);
141+
}
142+
90143
break;
91144
}
92145
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
@@ -114,6 +167,23 @@ void VotronicBle::update() {
114167
}
115168
}
116169

170+
bool VotronicBle::send_command(uint8_t command) {
171+
ESP_LOGD(TAG, "Send command: 0x%02X", command);
172+
173+
uint8_t frame[1];
174+
frame[0] = command;
175+
176+
auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
177+
this->char_management_handle_, sizeof(frame), frame, ESP_GATT_WRITE_TYPE_RSP,
178+
ESP_GATT_AUTH_REQ_NONE);
179+
180+
if (status) {
181+
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
182+
}
183+
184+
return (status == 0);
185+
}
186+
117187
void VotronicBle::on_votronic_ble_data(const uint8_t &handle, const std::vector<uint8_t> &data) {
118188
if (handle == this->char_solar_charger_handle_) {
119189
this->decode_solar_charger_data_(data);
@@ -125,12 +195,50 @@ void VotronicBle::on_votronic_ble_data(const uint8_t &handle, const std::vector<
125195
return;
126196
}
127197

198+
if (handle == this->char_bulk_data_handle_) {
199+
this->decode_bulk_data_(data);
200+
return;
201+
}
202+
128203
ESP_LOGW(TAG, "Your device is probably not supported. Please create an issue here: "
129204
"https://github.com/syssi/esphome-votronic/issues");
130205
ESP_LOGW(TAG, "Please provide the following unhandled message data: %s",
131206
format_hex_pretty(&data.front(), data.size()).c_str()); // NOLINT
132207
}
133208

209+
void VotronicBle::decode_bulk_data_(const std::vector<uint8_t> &data) {
210+
if (data.empty())
211+
return;
212+
213+
if (data[0] == 0xAA) {
214+
ESP_LOGD(TAG, "Bulk data: begin of transmission");
215+
this->bulk_data_frames_.clear();
216+
this->bulk_data_receiving_ = true;
217+
return;
218+
}
219+
220+
if (data[0] == 0xFF) {
221+
ESP_LOGD(TAG, "Bulk data: end of transmission (%zu frames)", this->bulk_data_frames_.size());
222+
this->bulk_data_receiving_ = false;
223+
for (size_t i = 0; i < this->bulk_data_frames_.size(); i++) {
224+
const auto &frame = this->bulk_data_frames_[i];
225+
ESP_LOGD(TAG, " Frame[%zu]: %s", i, format_hex_pretty(frame.data(), frame.size()).c_str()); // NOLINT
226+
}
227+
this->bulk_data_frames_.clear();
228+
return;
229+
}
230+
231+
if (!this->bulk_data_receiving_) {
232+
ESP_LOGW(TAG, "Bulk data frame outside transmission: %s",
233+
format_hex_pretty(data.data(), data.size()).c_str()); // NOLINT
234+
return;
235+
}
236+
237+
ESP_LOGD(TAG, "Bulk data frame [%zu]: %s", this->bulk_data_frames_.size(),
238+
format_hex_pretty(data.data(), data.size()).c_str()); // NOLINT
239+
this->bulk_data_frames_.push_back(data);
240+
}
241+
134242
void VotronicBle::decode_battery_computer_data_(const std::vector<uint8_t> &data) {
135243
if (data.size() != 20) {
136244
ESP_LOGW(TAG, "Invalid frame size: %zu", data.size());

components/votronic_ble/votronic_ble.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class VotronicBle : public esphome::ble_client::BLEClientNode, public PollingCom
8282

8383
void on_votronic_ble_data(const uint8_t &handle, const std::vector<uint8_t> &data);
8484
void set_throttle(uint32_t throttle) { this->throttle_ = throttle; }
85+
bool send_command(uint8_t command);
8586

8687
protected:
8788
binary_sensor::BinarySensor *charging_binary_sensor_{nullptr};
@@ -110,9 +111,13 @@ class VotronicBle : public esphome::ble_client::BLEClientNode, public PollingCom
110111

111112
uint16_t char_battery_computer_handle_{0x22};
112113
uint16_t char_solar_charger_handle_{0x25};
114+
uint16_t char_management_handle_{0x24};
115+
uint16_t char_bulk_data_handle_{0};
113116
uint32_t last_battery_computer_data_{0};
114117
uint32_t last_solar_charger_data_{0};
115118
uint32_t throttle_;
119+
bool bulk_data_receiving_{false};
120+
std::vector<std::vector<uint8_t>> bulk_data_frames_;
116121

117122
esp32_ble_tracker::ESPBTUUID service_bond_uuid_ =
118123
esp32_ble_tracker::ESPBTUUID::from_raw("70521e61-022d-f899-d046-4885a76acbd0");
@@ -122,17 +127,18 @@ class VotronicBle : public esphome::ble_client::BLEClientNode, public PollingCom
122127
esp32_ble_tracker::ESPBTUUID::from_raw("ae64a924-1184-4554-8bbc-295db9f2324a");
123128

124129
esp32_ble_tracker::ESPBTUUID char_battery_computer_uuid_ =
125-
esp32_ble_tracker::ESPBTUUID::from_raw("9a082a4e-5bcc-4b1d-9958-a97cfccfa5ec");
130+
esp32_ble_tracker::ESPBTUUID::from_raw("9a082a4e-5bcc-4b1d-9958-a97cfccfa5ec"); // Handle 0x16 (22)
126131
esp32_ble_tracker::ESPBTUUID char_solar_charger_uuid_ =
127-
esp32_ble_tracker::ESPBTUUID::from_raw("971ccec2-521d-42fd-b570-cf46fe5ceb65");
132+
esp32_ble_tracker::ESPBTUUID::from_raw("971ccec2-521d-42fd-b570-cf46fe5ceb65"); // Handle 0x19 (25)
128133

129134
esp32_ble_tracker::ESPBTUUID char_management_uuid_ =
130-
esp32_ble_tracker::ESPBTUUID::from_raw("ac12f485-cab7-4e0a-aac5-3585918852f6");
135+
esp32_ble_tracker::ESPBTUUID::from_raw("ac12f485-cab7-4e0a-aac5-3585918852f6"); // Handle 0x24 (36)
131136
esp32_ble_tracker::ESPBTUUID char_bulk_data_uuid_ =
132137
esp32_ble_tracker::ESPBTUUID::from_raw("b8a37ffe-c57b-4007-b3c1-ca05a6b7f0c6");
133138

134139
void decode_solar_charger_data_(const std::vector<uint8_t> &data);
135140
void decode_battery_computer_data_(const std::vector<uint8_t> &data);
141+
void decode_bulk_data_(const std::vector<uint8_t> &data);
136142
void publish_state_(binary_sensor::BinarySensor *binary_sensor, const bool &state);
137143
void publish_state_(sensor::Sensor *sensor, float value);
138144
void publish_state_(text_sensor::TextSensor *text_sensor, const std::string &state);

esp32-ble-example.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
substitutions:
22
name: votronic
33
device_description: "Monitor a votronic device via BLE"
4-
external_components_source: github://syssi/esphome-votronic@main
4+
external_components_source: github://syssi/esphome-votronic@add-controls
55
mac_address: 60:A4:23:91:8F:55
66

77
esphome:
@@ -80,6 +80,20 @@ binary_sensor:
8080
aes_active:
8181
name: "aes active"
8282

83+
button:
84+
- platform: votronic_ble
85+
votronic_ble_id: votronic0
86+
retrieve_daily_operating_statistics:
87+
name: "${name} retrieve daily operating statistics"
88+
retrieve_weekly_operating_statistics:
89+
name: "${name} retrieve weekly operating statistics"
90+
retrieve_monthly_operating_statistics:
91+
name: "${name} retrieve monthly operating statistics"
92+
retrieve_operating_statistics:
93+
name: "${name} retrieve operating statistics"
94+
reset_accumulator:
95+
name: "${name} reset accumulator"
96+
8397
sensor:
8498
- platform: votronic_ble
8599
votronic_ble_id: votronic0

test-esp32.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
#!/bin/bash
22

3-
esphome -s external_components_source components ${1:-run} ${2:-esp32-ble-example-faker.yaml}
3+
4+
if [[ $2 == tests/* ]]; then
5+
C="../components"
6+
else
7+
C="components"
8+
fi
9+
10+
esphome -s external_components_source $C ${1:-run} ${2:-esp32-ble-example-faker.yaml}

test-esp8266.sh

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
#!/bin/bash
22

3-
esphome -s external_components_source components ${1:-run} ${2:-esp8266-solar-charger-example-faker.yaml}
3+
if [[ $2 == tests/* ]]; then
4+
C="../components"
5+
else
6+
C="components"
7+
fi
8+
9+
esphome -s external_components_source $C ${1:-run} ${2:-esp8266-solar-charger-example-faker.yaml}

0 commit comments

Comments
 (0)