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_z1", "name": "LED Strip 1" },
|
||||||
{ "type": "range", "entity_id": "light.rgbic_strip_lights_z2", "name": "LED Strip 2" },
|
{ "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": "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": "vacuum", "entity_id": "vacuum.living_room_roborock_s7_pro_ultra", "name": "Roborock" },
|
||||||
{ "type": "media", "entity_id": "media_player.music_assistant", "name": "Music" }
|
{ "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 }),
|
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),
|
MediaNext(String),
|
||||||
MediaPrev(String),
|
MediaPrev(String),
|
||||||
SetVolume(String, f32),
|
SetVolume(String, f32),
|
||||||
|
SetCoverPosition(String, u8),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const KEYS: [&str; 40] = [
|
pub const KEYS: [&str; 40] = [
|
||||||
@@ -181,6 +182,8 @@ pub enum HaCardType {
|
|||||||
Action,
|
Action,
|
||||||
Vacuum,
|
Vacuum,
|
||||||
Media,
|
Media,
|
||||||
|
Curtain,
|
||||||
|
Blinds,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -290,6 +293,8 @@ impl HaTabConfig {
|
|||||||
Some("action") => HaCardType::Action,
|
Some("action") => HaCardType::Action,
|
||||||
Some("vacuum") => HaCardType::Vacuum,
|
Some("vacuum") => HaCardType::Vacuum,
|
||||||
Some("media") => HaCardType::Media,
|
Some("media") => HaCardType::Media,
|
||||||
|
Some("curtain") => HaCardType::Curtain,
|
||||||
|
Some("blinds") => HaCardType::Blinds,
|
||||||
_ => continue,
|
_ => continue,
|
||||||
};
|
};
|
||||||
let eid = card.get("entity_id").and_then(|v| v.as_str()).unwrap_or("").to_owned();
|
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
|
.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> {
|
pub fn scroll_ha(&mut self, delta: i32) -> Vec<Effect> {
|
||||||
if let Target::HaCard(room, index) = &self.focus {
|
if let Target::HaCard(room, index) = &self.focus {
|
||||||
let card = &self.ha.config.rooms[*room].cards[*index];
|
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.);
|
let new = (vol + delta as f32 * 0.05).clamp(0., 1.);
|
||||||
return vec![Effect::Ha(HaEffect::SetVolume(card.entity_id.clone(), new))];
|
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 => {
|
HaCardType::Media => {
|
||||||
return vec![Effect::Ha(HaEffect::MediaPlayPause(card.entity_id.clone()))];
|
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 => {
|
HaCardType::Media => {
|
||||||
ha_media_card(out, state, assets, rect, card, entity, focused);
|
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);
|
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::Action => "scene",
|
||||||
HaCardType::Vacuum => "vacuum",
|
HaCardType::Vacuum => "vacuum",
|
||||||
HaCardType::Media => "spotify",
|
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::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 => {
|
HaCardType::Vacuum => {
|
||||||
match entity.map(|e| e.state.as_str()) {
|
match entity.map(|e| e.state.as_str()) {
|
||||||
Some("cleaning") => c(0x7EB8DA, 0.25),
|
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 ────────────────────────────────────────────
|
// ── Weather hero card ────────────────────────────────────────────
|
||||||
|
|
||||||
fn ha_weather_card(
|
fn ha_weather_card(
|
||||||
|
|||||||
Reference in new issue
Block a user