Add curtain/blinds cover cards with daylight glow and navy darkening
New HaCardType::Curtain and HaCardType::Blinds with: - Scroll to adjust position (10% steps via cover.set_cover_position) - OK to toggle open/close - Dark navy overlay when closed, warm daylight glow when open - Curtain glow radiates from center, blinds from top Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8e6fcb659
commit
e29050dc32
4 files changed
+139
-1
No files matched your search
+1
-1
@@ -39,7 +39,7 @@
|
||||
{ "type": "range", "entity_id": "light.rgbic_strip_lights_z1", "name": "LED Strip 1" },
|
||||
{ "type": "range", "entity_id": "light.rgbic_strip_lights_z2", "name": "LED Strip 2" },
|
||||
{ "type": "toggle", "entity_id": "light.picture_frame_lamp", "name": "Picture Lamp" },
|
||||
{ "type": "toggle", "entity_id": "cover.living_room_curtain", "name": "Curtain" },
|
||||
{ "type": "curtain", "entity_id": "cover.living_room_curtain", "name": "Curtain" },
|
||||
{ "type": "vacuum", "entity_id": "vacuum.living_room_roborock_s7_pro_ultra", "name": "Roborock" },
|
||||
{ "type": "media", "entity_id": "media_player.music_assistant", "name": "Music" }
|
||||
]
|
||||
|
||||
@@ -363,6 +363,15 @@ fn ha_dispatch(service: &ha_client::HaService, effect: state::HaEffect) {
|
||||
data: serde_json::json!({ "volume_level": volume }),
|
||||
});
|
||||
}
|
||||
HaEffect::SetCoverPosition(entity_id, position) => {
|
||||
service.send(ha_client::HaCommand::CallService {
|
||||
id: next_id(),
|
||||
domain: "cover".into(),
|
||||
service: "set_cover_position".into(),
|
||||
target: serde_json::json!({ "entity_id": entity_id }),
|
||||
data: serde_json::json!({ "position": position }),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,7 @@ pub enum HaEffect {
|
||||
MediaNext(String),
|
||||
MediaPrev(String),
|
||||
SetVolume(String, f32),
|
||||
SetCoverPosition(String, u8),
|
||||
}
|
||||
|
||||
pub const KEYS: [&str; 40] = [
|
||||
@@ -181,6 +182,8 @@ pub enum HaCardType {
|
||||
Action,
|
||||
Vacuum,
|
||||
Media,
|
||||
Curtain,
|
||||
Blinds,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -290,6 +293,8 @@ impl HaTabConfig {
|
||||
Some("action") => HaCardType::Action,
|
||||
Some("vacuum") => HaCardType::Vacuum,
|
||||
Some("media") => HaCardType::Media,
|
||||
Some("curtain") => HaCardType::Curtain,
|
||||
Some("blinds") => HaCardType::Blinds,
|
||||
_ => continue,
|
||||
};
|
||||
let eid = card.get("entity_id").and_then(|v| v.as_str()).unwrap_or("").to_owned();
|
||||
@@ -1236,6 +1241,13 @@ impl State {
|
||||
.unwrap_or(0.) as f32
|
||||
}
|
||||
|
||||
fn ha_entity_position(&self, entity_id: &str) -> f32 {
|
||||
self.ha.entities.get(entity_id)
|
||||
.and_then(|e| e.attributes.get("current_position"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.) as f32
|
||||
}
|
||||
|
||||
pub fn scroll_ha(&mut self, delta: i32) -> Vec<Effect> {
|
||||
if let Target::HaCard(room, index) = &self.focus {
|
||||
let card = &self.ha.config.rooms[*room].cards[*index];
|
||||
@@ -1255,6 +1267,11 @@ impl State {
|
||||
let new = (vol + delta as f32 * 0.05).clamp(0., 1.);
|
||||
return vec![Effect::Ha(HaEffect::SetVolume(card.entity_id.clone(), new))];
|
||||
}
|
||||
HaCardType::Curtain | HaCardType::Blinds => {
|
||||
let pos = self.ha_entity_position(&card.entity_id);
|
||||
let new = (pos + delta as f32 * 10.).clamp(0., 100.) as u8;
|
||||
return vec![Effect::Ha(HaEffect::SetCoverPosition(card.entity_id.clone(), new))];
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1692,6 +1709,9 @@ impl State {
|
||||
HaCardType::Media => {
|
||||
return vec![Effect::Ha(HaEffect::MediaPlayPause(card.entity_id.clone()))];
|
||||
}
|
||||
HaCardType::Curtain | HaCardType::Blinds => {
|
||||
return vec![Effect::Ha(HaEffect::Toggle(card.entity_id.clone()))];
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
+109
@@ -1710,6 +1710,12 @@ fn ha(out: &mut Scene, state: &State, assets: &Assets) {
|
||||
HaCardType::Media => {
|
||||
ha_media_card(out, state, assets, rect, card, entity, focused);
|
||||
}
|
||||
HaCardType::Curtain => {
|
||||
ha_cover_card(out, state, assets, rect, card, entity, focused, true);
|
||||
}
|
||||
HaCardType::Blinds => {
|
||||
ha_cover_card(out, state, assets, rect, card, entity, focused, false);
|
||||
}
|
||||
}
|
||||
out.control(Target::HaCard(state.ha.room, index), rect, true);
|
||||
}
|
||||
@@ -1756,6 +1762,8 @@ fn ha_entity_icon(card_type: &crate::state::HaCardType, entity_id: &str) -> &'st
|
||||
HaCardType::Action => "scene",
|
||||
HaCardType::Vacuum => "vacuum",
|
||||
HaCardType::Media => "spotify",
|
||||
HaCardType::Curtain => "curtain",
|
||||
HaCardType::Blinds => "curtain",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1794,6 +1802,17 @@ fn ha_fill_color(card_type: &crate::state::HaCardType, entity: Option<&crate::st
|
||||
}
|
||||
}
|
||||
HaCardType::Media => c(0x8060c0, 0.25),
|
||||
HaCardType::Curtain | HaCardType::Blinds => {
|
||||
let pos = entity
|
||||
.and_then(|e| e.attributes.get("current_position"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.);
|
||||
if pos > 5. {
|
||||
c(0xdcd2b4, (0.15 + pos as f32 / 100. * 0.25).min(0.40))
|
||||
} else {
|
||||
c(0x0c1024, 0.50)
|
||||
}
|
||||
}
|
||||
HaCardType::Vacuum => {
|
||||
match entity.map(|e| e.state.as_str()) {
|
||||
Some("cleaning") => c(0x7EB8DA, 0.25),
|
||||
@@ -2333,6 +2352,96 @@ fn ha_media_card(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cover card (curtain / blinds) ────────────────────────────────
|
||||
|
||||
fn ha_cover_card(
|
||||
out: &mut Scene,
|
||||
_state: &State,
|
||||
assets: &Assets,
|
||||
rect: Rect,
|
||||
card: &crate::state::HaCardConfig,
|
||||
entity: Option<&crate::state::HaEntity>,
|
||||
focused: bool,
|
||||
is_curtain: bool,
|
||||
) {
|
||||
ha_glass(out, rect, focused);
|
||||
let position = entity
|
||||
.and_then(|e| e.attributes.get("current_position"))
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.) as f32;
|
||||
let is_open = position > 5.;
|
||||
|
||||
// Dark navy overlay when closed (proportional to how closed it is)
|
||||
let closed_pct = 1. - (position / 100.).clamp(0., 1.);
|
||||
if closed_pct > 0.05 {
|
||||
out.shape(rect, 22., c(0x0c1024, 0.5 * closed_pct));
|
||||
}
|
||||
|
||||
// Daylight glow when open
|
||||
if is_open {
|
||||
let open_pct = (position / 100.).clamp(0., 1.);
|
||||
let fill_color = ha_fill_color(&card.card_type, entity);
|
||||
if is_curtain {
|
||||
// Curtain: glow from center
|
||||
ha_card_fill(out, rect, open_pct, fill_color);
|
||||
} else {
|
||||
// Blinds: glow from top
|
||||
ha_card_fill(out, rect, open_pct, fill_color);
|
||||
}
|
||||
}
|
||||
|
||||
let accent = if is_open { c(0xdcd2b4, 1.) } else { c(0xbbbbbb, 0.5) };
|
||||
|
||||
// Icon
|
||||
let icon_bg = if is_open { [accent[0], accent[1], accent[2], 0.2] } else { c(0xffffff, 0.08) };
|
||||
out.shape(Rect::new(rect.x + 16., rect.y + 16., 48., 48.), 24., icon_bg);
|
||||
out.icon(
|
||||
assets,
|
||||
ha_entity_icon(&card.card_type, &card.entity_id),
|
||||
Rect::new(rect.x + 24., rect.y + 24., 32., 32.),
|
||||
accent,
|
||||
);
|
||||
|
||||
// Type label
|
||||
let type_label = if is_curtain { "Curtain" } else { "Blinds" };
|
||||
out.text(
|
||||
type_label,
|
||||
Rect::new(rect.x + 76., rect.y + 18., rect.w - 92., 14.),
|
||||
text(12., 400, 0.04),
|
||||
if is_open { accent } else { c(0xe8e8e8, 0.45) },
|
||||
false,
|
||||
);
|
||||
// Entity name
|
||||
out.text(
|
||||
&card.name,
|
||||
Rect::new(rect.x + 76., rect.y + 36., rect.w - 92., 24.),
|
||||
text(18., 600, -0.02),
|
||||
WHITE,
|
||||
false,
|
||||
);
|
||||
|
||||
// Position value at bottom
|
||||
let label = if position > 0. { format!("{}%", position.round() as u8) } else { "Closed".into() };
|
||||
out.text(
|
||||
&label,
|
||||
Rect::new(rect.x + 16., rect.y + rect.h - 38., rect.w - 32., 26.),
|
||||
text(22., 700, -0.02),
|
||||
if is_open { WHITE } else { c(0xbbbbbb, 0.5) },
|
||||
false,
|
||||
);
|
||||
|
||||
// Scroll hint
|
||||
if focused {
|
||||
out.text(
|
||||
"scroll",
|
||||
Rect::new(rect.x + rect.w - 68., rect.y + rect.h - 28., 52., 14.),
|
||||
text(11., 400, 0.03).right(),
|
||||
c(0xeeeeee, 0.35),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Weather hero card ────────────────────────────────────────────
|
||||
|
||||
fn ha_weather_card(
|
||||
|
||||
Reference in new issue
Block a user