Skip to content

Commit

Permalink
Merge branch 'dev' into homeassistant-number
Browse files Browse the repository at this point in the history
  • Loading branch information
jesserockz authored Aug 14, 2024
2 parents 8b5cda2 + 1d25db4 commit 3351556
Show file tree
Hide file tree
Showing 50 changed files with 527 additions and 35 deletions.
4 changes: 2 additions & 2 deletions .github/actions/build-image/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.6.1
uses: docker/build-push-action@v6.7.0
with:
context: .
file: ./docker/Dockerfile
Expand All @@ -69,7 +69,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.6.1
uses: docker/build-push-action@v6.7.0
with:
context: .
file: ./docker/Dockerfile
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths:
- "**"
- "!.github/workflows/*.yml"
- "!.github/actions/build-image/*"
- ".github/workflows/ci.yml"
- "!.yamllint"
- "!.github/dependabot.yml"
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hm3301/* @freekode
esphome/components/homeassistant/* @OttoWinter @esphome/core
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
esphome/components/honeywell_hih_i2c/* @Benichou34
esphome/components/honeywellabp/* @RubyBailey
esphome/components/honeywellabp2_i2c/* @jpfaff
Expand Down
30 changes: 30 additions & 0 deletions esphome/components/homeassistant/switch/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_ID

from .. import (
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
homeassistant_ns,
setup_home_assistant_entity,
)

CODEOWNERS = ["@Links2004"]
DEPENDENCIES = ["api"]

HomeassistantSwitch = homeassistant_ns.class_(
"HomeassistantSwitch", switch.Switch, cg.Component
)

CONFIG_SCHEMA = (
switch.switch_schema(HomeassistantSwitch)
.extend(cv.COMPONENT_SCHEMA)
.extend(HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA)
)


async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await switch.register_switch(var, config)
setup_home_assistant_entity(var, config)
59 changes: 59 additions & 0 deletions esphome/components/homeassistant/switch/homeassistant_switch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "homeassistant_switch.h"
#include "esphome/components/api/api_server.h"
#include "esphome/core/log.h"

namespace esphome {
namespace homeassistant {

static const char *const TAG = "homeassistant.switch";

using namespace esphome::switch_;

void HomeassistantSwitch::setup() {
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) {
auto val = parse_on_off(state.c_str());
switch (val) {
case PARSE_NONE:
case PARSE_TOGGLE:
ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str());
break;
case PARSE_ON:
case PARSE_OFF:
bool new_state = val == PARSE_ON;
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
this->publish_state(new_state);
break;
}
});
}

void HomeassistantSwitch::dump_config() {
LOG_SWITCH("", "Homeassistant Switch", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
}

float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }

void HomeassistantSwitch::write_state(bool state) {
if (!api::global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients connected to API server");
return;
}

api::HomeassistantServiceResponse resp;
if (state) {
resp.service = "switch.turn_on";
} else {
resp.service = "switch.turn_off";
}

api::HomeassistantServiceMap entity_id_kv;
entity_id_kv.key = "entity_id";
entity_id_kv.value = this->entity_id_;
resp.data.push_back(entity_id_kv);

api::global_api_server->send_homeassistant_service_call(resp);
}

} // namespace homeassistant
} // namespace esphome
22 changes: 22 additions & 0 deletions esphome/components/homeassistant/switch/homeassistant_switch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once

#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"

namespace esphome {
namespace homeassistant {

class HomeassistantSwitch : public switch_::Switch, public Component {
public:
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;

protected:
void write_state(bool state) override;
std::string entity_id_;
};

} // namespace homeassistant
} // namespace esphome
17 changes: 16 additions & 1 deletion esphome/components/light/automation.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
namespace esphome {
namespace light {

enum class LimitMode { CLAMP, DO_NOTHING };

template<typename... Ts> class ToggleAction : public Action<Ts...> {
public:
explicit ToggleAction(LightState *state) : state_(state) {}
Expand Down Expand Up @@ -77,16 +79,29 @@ template<typename... Ts> class DimRelativeAction : public Action<Ts...> {
float rel = this->relative_brightness_.value(x...);
float cur;
this->parent_->remote_values.as_brightness(&cur);
float new_brightness = clamp(cur + rel, 0.0f, 1.0f);
if ((limit_mode_ == LimitMode::DO_NOTHING) && ((cur < min_brightness_) || (cur > max_brightness_))) {
return;
}
float new_brightness = clamp(cur + rel, min_brightness_, max_brightness_);
call.set_state(new_brightness != 0.0f);
call.set_brightness(new_brightness);

call.set_transition_length(this->transition_length_.optional_value(x...));
call.perform();
}

void set_min_max_brightness(float min, float max) {
this->min_brightness_ = min;
this->max_brightness_ = max;
}

void set_limit_mode(LimitMode limit_mode) { this->limit_mode_ = limit_mode; }

protected:
LightState *parent_;
float min_brightness_{0.0};
float max_brightness_{1.0};
LimitMode limit_mode_{LimitMode::CLAMP};
};

template<typename... Ts> class LightIsOnCondition : public Condition<Ts...> {
Expand Down
21 changes: 21 additions & 0 deletions esphome/components/light/automation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
CONF_WARM_WHITE,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_BRIGHTNESS_LIMITS,
CONF_LIMIT_MODE,
CONF_MIN_BRIGHTNESS,
CONF_MAX_BRIGHTNESS,
)
from .types import (
ColorMode,
COLOR_MODES,
LIMIT_MODES,
DimRelativeAction,
ToggleAction,
LightState,
Expand Down Expand Up @@ -167,6 +172,15 @@ async def light_control_to_code(config, action_id, template_arg, args):
cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(
cv.positive_time_period_milliseconds
),
cv.Optional(CONF_BRIGHTNESS_LIMITS): cv.Schema(
{
cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage,
cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage,
cv.Optional(CONF_LIMIT_MODE, default="CLAMP"): cv.enum(
LIMIT_MODES, upper=True, space="_"
),
}
),
}
)

Expand All @@ -182,6 +196,13 @@ async def light_dim_relative_to_code(config, action_id, template_arg, args):
if CONF_TRANSITION_LENGTH in config:
templ = await cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32)
cg.add(var.set_transition_length(templ))
if conf := config.get(CONF_BRIGHTNESS_LIMITS):
cg.add(
var.set_min_max_brightness(
conf[CONF_MIN_BRIGHTNESS], conf[CONF_MAX_BRIGHTNESS]
)
)
cg.add(var.set_limit_mode(conf[CONF_LIMIT_MODE]))
return var


Expand Down
10 changes: 5 additions & 5 deletions esphome/components/light/base_light_effects.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PulseLightEffect : public LightEffect {
return;
}
auto call = this->state_->turn_on();
float out = this->on_ ? this->max_brightness : this->min_brightness;
float out = this->on_ ? this->max_brightness_ : this->min_brightness_;
call.set_brightness_if_supported(out);
call.set_transition_length_if_supported(this->on_ ? this->transition_on_length_ : this->transition_off_length_);
this->on_ = !this->on_;
Expand All @@ -43,8 +43,8 @@ class PulseLightEffect : public LightEffect {
void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }

void set_min_max_brightness(float min, float max) {
this->min_brightness = min;
this->max_brightness = max;
this->min_brightness_ = min;
this->max_brightness_ = max;
}

protected:
Expand All @@ -53,8 +53,8 @@ class PulseLightEffect : public LightEffect {
uint32_t transition_on_length_{};
uint32_t transition_off_length_{};
uint32_t update_interval_{};
float min_brightness{0.0};
float max_brightness{1.0};
float min_brightness_{0.0};
float max_brightness_{1.0};
};

/// Random effect. Sets random colors every 10 seconds and slowly transitions between them.
Expand Down
7 changes: 7 additions & 0 deletions esphome/components/light/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
"RGB_COLD_WARM_WHITE": ColorMode.RGB_COLD_WARM_WHITE,
}

# Limit modes
LimitMode = light_ns.enum("LimitMode", is_class=True)
LIMIT_MODES = {
"CLAMP": LimitMode.CLAMP,
"DO_NOTHING": LimitMode.DO_NOTHING,
}

# Actions
ToggleAction = light_ns.class_("ToggleAction", automation.Action)
LightControlAction = light_ns.class_("LightControlAction", automation.Action)
Expand Down
3 changes: 2 additions & 1 deletion esphome/components/lvgl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, update_to_code
from .defines import CONF_SKIP
from .encoders import ENCODERS_CONFIG, encoders_to_code
from .encoders import ENCODERS_CONFIG, encoders_to_code, initial_focus_to_code
from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent
from .schemas import (
Expand Down Expand Up @@ -272,6 +272,7 @@ async def to_code(config):
templ = await cg.templatable(conf[CONF_TIMEOUT], [], cg.uint32)
idle_trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], lv_component, templ)
await build_automation(idle_trigger, [], conf)
await initial_focus_to_code(config)

for comp in helpers.lvgl_components_required:
CORE.add_define(f"USE_LVGL_{comp.upper()}")
Expand Down
1 change: 1 addition & 0 deletions esphome/components/lvgl/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ def extend(self, *choices):
CONF_GRID_ROWS = "grid_rows"
CONF_HEADER_MODE = "header_mode"
CONF_HOME = "home"
CONF_INITIAL_FOCUS = "initial_focus"
CONF_KEY_CODE = "key_code"
CONF_LAYOUT = "layout"
CONF_LEFT_BUTTON = "left_button"
Expand Down
8 changes: 8 additions & 0 deletions esphome/components/lvgl/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
CONF_DEFAULT_GROUP,
CONF_ENCODERS,
CONF_ENTER_BUTTON,
CONF_INITIAL_FOCUS,
CONF_LEFT_BUTTON,
CONF_LONG_PRESS_REPEAT_TIME,
CONF_LONG_PRESS_TIME,
Expand Down Expand Up @@ -67,3 +68,10 @@ async def encoders_to_code(var, config):
else:
group = default_group
lv.indev_set_group(lv_expr.indev_drv_register(listener.get_drv()), group)


async def initial_focus_to_code(config):
for enc_conf in config[CONF_ENCODERS]:
if default_focus := enc_conf.get(CONF_INITIAL_FOCUS):
obj = await cg.get_variable(default_focus)
lv.group_focus_obj(obj)
25 changes: 17 additions & 8 deletions esphome/components/lvgl/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,19 @@
from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT

from . import defines as df, lv_validation as lvalid, types as ty
from . import defines as df, lv_validation as lvalid
from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image
from .lvcode import LvglComponent
from .types import WidgetType, lv_group_t
from .types import (
LVEncoderListener,
LvType,
WidgetType,
lv_group_t,
lv_obj_t,
lv_pseudo_button_t,
lv_style_t,
)

# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
Expand Down Expand Up @@ -46,7 +54,7 @@
LIST_ACTION_SCHEMA = cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_pseudo_button_t),
cv.Required(CONF_ID): cv.use_id(lv_pseudo_button_t),
},
key=CONF_ID,
)
Expand All @@ -59,9 +67,10 @@
ENCODER_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.All(
cv.declare_id(ty.LVEncoderListener), requires_component("binary_sensor")
cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
),
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t),
cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
}
Expand Down Expand Up @@ -161,7 +170,7 @@
# Complete object style schema
STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend(
{
cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(ty.lv_style_t)),
cv.Optional(df.CONF_STYLES): cv.ensure_list(cv.use_id(lv_style_t)),
cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant(
"LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO"
).one_of,
Expand Down Expand Up @@ -193,12 +202,12 @@ def part_schema(widget_type: WidgetType):
)


def automation_schema(typ: ty.LvType):
def automation_schema(typ: LvType):
if typ.has_on_value:
events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,)
else:
events = df.LV_EVENT_TRIGGERS
if isinstance(typ, ty.LvType):
if isinstance(typ, LvType):
template = Trigger.template(typ.get_arg_type())
else:
template = Trigger.template()
Expand Down Expand Up @@ -261,7 +270,7 @@ def obj_schema(widget_type: WidgetType):
ALIGN_TO_SCHEMA = {
cv.Optional(df.CONF_ALIGN_TO): cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t),
cv.Required(CONF_ID): cv.use_id(lv_obj_t),
cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of,
cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent,
cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent,
Expand Down
Loading

0 comments on commit 3351556

Please sign in to comment.