Skip to content

Commit

Permalink
Add basic cardMedia support
Browse files Browse the repository at this point in the history
  • Loading branch information
olicooper committed Jul 16, 2024
1 parent cc6e047 commit e1e6d60
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 22 deletions.
30 changes: 22 additions & 8 deletions components/nspanel_lovelace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def AUTO_LOAD():
ENTITY_TYPES = [
'sensor','binary_sensor','light','switch','scene','timer','weather','navigate',
'alarm_control_panel','input_boolean','input_button','cover','fan','automation',
'script', 'climate'
'script', 'climate', 'media_player'
]

CONF_INCOMING_MSG = "on_incoming_msg"
Expand Down Expand Up @@ -110,12 +110,14 @@ def AUTO_LOAD():
CARD_QR="cardQR"
CARD_ALARM="cardAlarm"
CARD_THERMO="cardThermo"
CARD_TYPE_OPTIONS = [CARD_ENTITIES, CARD_GRID, CARD_GRID2, CARD_QR, CARD_ALARM, CARD_THERMO]
CARD_MEDIA="cardMedia"
CARD_TYPE_OPTIONS = [CARD_ENTITIES, CARD_GRID, CARD_GRID2, CARD_QR, CARD_ALARM, CARD_THERMO, CARD_MEDIA]

CONF_CARD_QR_TEXT = "qr_text"
CONF_CARD_ALARM_ENTITY_ID = "alarm_entity_id"
CONF_CARD_ALARM_SUPPORTED_MODES = "supported_modes"
CONF_CARD_THERMO_ENTITY_ID = "thermo_entity_id"
CONF_CARD_MEDIA_ENTITY_ID = "media_entity_id"

def load_icons():
global iconJson
Expand Down Expand Up @@ -311,6 +313,8 @@ def get_card_entities_length_limits(card_type: str, model: str = 'eu') -> list[i
return [1,8]
if (card_type == CARD_QR):
return [1,2]
if (card_type == CARD_MEDIA):
return [0,8]
return [0,0]

def validate_config(config):
Expand Down Expand Up @@ -351,6 +355,8 @@ def validate_config(config):
add_entity_id(card_config.get(CONF_CARD_ALARM_ENTITY_ID))
if CONF_CARD_THERMO_ENTITY_ID in card_config:
add_entity_id(card_config.get(CONF_CARD_THERMO_ENTITY_ID))
if CONF_CARD_MEDIA_ENTITY_ID in card_config:
add_entity_id(card_config.get(CONF_CARD_MEDIA_ENTITY_ID))

if CONF_SCREENSAVER in config:
screensaver_config = config.get(CONF_SCREENSAVER)
Expand Down Expand Up @@ -403,6 +409,10 @@ def validate_config(config):
CARD_THERMO: SCHEMA_CARD_BASE.extend({
cv.Required(CONF_CARD_THERMO_ENTITY_ID): valid_entity_id(['climate'])
}),
CARD_MEDIA: SCHEMA_CARD_BASE.extend({
cv.Optional(CONF_CARD_ENTITIES): cv.ensure_list(SCHEMA_CARD_ENTITY),
cv.Required(CONF_CARD_MEDIA_ENTITY_ID): valid_entity_id(['media_player'])
}),
},
default_type=CARD_GRID))
),
Expand All @@ -423,6 +433,7 @@ def validate_config(config):
QRCard = nspanel_lovelace_ns.class_("QRCard")
AlarmCard = nspanel_lovelace_ns.class_("AlarmCard")
ThermoCard = nspanel_lovelace_ns.class_("ThermoCard")
MediaCard = nspanel_lovelace_ns.class_("MediaCard")

DeleteItem = nspanel_lovelace_ns.class_("DeleteItem")
NavigationItem = nspanel_lovelace_ns.class_("NavigationItem")
Expand All @@ -443,6 +454,7 @@ def validate_config(config):
CARD_QR: ["nspanel_card_", QRCard, PageType.cardQR, EntitiesCardEntityItem],
CARD_ALARM: ["nspanel_card_", AlarmCard, PageType.cardAlarm, AlarmButtonItem],
CARD_THERMO: ["nspanel_card_", ThermoCard, PageType.cardThermo, None],
CARD_MEDIA: ["nspanel_card_", MediaCard, PageType.cardMedia, GridCardEntityItem],
}

def get_new_uuid(prefix: str = ""):
Expand Down Expand Up @@ -707,14 +719,16 @@ async def to_code(config):
# cg.add(card_class.set_sleep_timeout(sleep_timeout))
# cg.add(cg.RawExpression(f"{card_variable}->set_sleep_timeout({sleep_timeout})"))

if card_config[CONF_CARD_TYPE] == CARD_ALARM:
cg.add(cg.RawExpression(
f"auto {card_variable} = "
f"{nspanel.create_page.template(page_info[1]).__call__(card_uuids[i], get_entity_id(card_config[CONF_CARD_ALARM_ENTITY_ID]), title, sleep_timeout)}"))
elif card_config[CONF_CARD_TYPE] == CARD_THERMO:
if card_config[CONF_CARD_TYPE] in [CARD_ALARM, CARD_THERMO, CARD_MEDIA]:
if (card_config[CONF_CARD_TYPE] == CARD_ALARM):
entity_id_key = CONF_CARD_ALARM_ENTITY_ID
elif (card_config[CONF_CARD_TYPE] == CARD_THERMO):
entity_id_key = CONF_CARD_THERMO_ENTITY_ID
else:
entity_id_key = CONF_CARD_MEDIA_ENTITY_ID
cg.add(cg.RawExpression(
f"auto {card_variable} = "
f"{nspanel.create_page.template(page_info[1]).__call__(card_uuids[i], get_entity_id(card_config[CONF_CARD_THERMO_ENTITY_ID]), title, sleep_timeout)}"))
f"{nspanel.create_page.template(page_info[1]).__call__(card_uuids[i], get_entity_id(card_config[entity_id_key]), title, sleep_timeout)}"))
else:
cg.add(cg.RawExpression(
f"auto {card_variable} = "
Expand Down
3 changes: 3 additions & 0 deletions components/nspanel_lovelace/card_items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ void EntitiesCardEntityItem::on_entity_attribute_change(
attr != ha_attr_type::current_temperature) {
return;
}
} else if (!this->is_type(entity_type::media_player)) {
// Any entity type not mentioned above doesn't need re-rendering
return;
}

this->on_state_callback_(this);
Expand Down
105 changes: 105 additions & 0 deletions components/nspanel_lovelace/cards.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,5 +383,110 @@ std::string &ThermoCard::render(std::string &buffer) {
return buffer;
}

/*
* =============== MediaCard ===============
*/

MediaCard::MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity) :
Card(page_type::cardMedia, uuid),
media_entity_(media_entity) {
media_entity->add_subscriber(this);
}

MediaCard::MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity,
const std::string &title) :
Card(page_type::cardMedia, uuid, title),
media_entity_(media_entity) {
media_entity->add_subscriber(this);
}

MediaCard::MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity,
const std::string &title, const uint16_t sleep_timeout) :
Card(page_type::cardMedia, uuid, title, sleep_timeout),
media_entity_(media_entity) {
media_entity->add_subscriber(this);
}

MediaCard::~MediaCard() {
media_entity_->remove_subscriber(this);
}

void MediaCard::accept(PageVisitor& visitor) { visitor.visit(*this); }

// entityUpd~{heading}~{navigation}~{entityId}~{title}~~{author}~~{volume}~{iconplaypause}~{onoffbutton}~{shuffleBtn}{media_icon}{item_str}
std::string &MediaCard::render(std::string &buffer) {
buffer.assign(this->get_render_instruction())
.append(1, SEPARATOR)
.append(this->get_title())
.append(1, SEPARATOR);

this->render_nav(buffer).append(1, SEPARATOR);

buffer.append(this->media_entity_->get_entity_id());
buffer.append(1, SEPARATOR);

buffer.append(this->media_entity_->get_attribute(
ha_attr_type::media_title));
buffer.append(2, SEPARATOR);

buffer.append(this->media_entity_->get_attribute(
ha_attr_type::media_artist));
buffer.append(2, SEPARATOR);

buffer.append(std::to_string(
std::stoi(this->media_entity_->get_attribute(
ha_attr_type::volume_level, "0")) * 100));
buffer.append(1, SEPARATOR);

auto icon = this->media_entity_->is_state("playing")
? icon_t::pause : icon_t::play;
buffer.append(icon).append(1, SEPARATOR);

uint32_t supported_features = value_or_default(this->media_entity_->
get_attribute(ha_attr_type::supported_features), 0UL);

// on/off button colour
if (supported_features & 0b10000000) {
if (this->media_entity_->is_state(generic_type::off))
buffer.append(std::to_string(1374)); // light blue
else
buffer.append(std::to_string(64704)); // orange
} else {
buffer.append(generic_type::disable);
}
buffer.append(1, SEPARATOR);

// shuffle button icon
if (supported_features & 0b100000000000000) {
if (this->media_entity_->get_attribute(ha_attr_type::shuffle) == generic_type::on)
buffer.append(icon_t::shuffle);
else
buffer.append(icon_t::shuffle_disable);
} else {
buffer.append(generic_type::disable);
}
buffer.append(1, SEPARATOR);

// todo: create PageItem for this?
// media icon/button: type~internalName~icon~iconColor~displayName~
// on btn press: event,buttonPress2,{entity_id},button
buffer.append(entity_render_type::media_pl).append(1, SEPARATOR);
buffer.append(this->media_entity_->get_entity_id()).append(1, SEPARATOR);
auto media_icon = get_icon_by_name(MEDIA_TYPE_MAP,
this->media_entity_->get_attribute(ha_attr_type::media_content_type));
buffer.append(media_icon).append(1, SEPARATOR);
buffer.append(std::to_string(17299)).append(2, SEPARATOR);

for (auto& item : this->items_) {
buffer.append(1, SEPARATOR).append(item->render());
}
if (this->items_.size() > 0) buffer.append(1, SEPARATOR);

return buffer;
}

} // namespace nspanel_lovelace
} // namespace esphome
25 changes: 25 additions & 0 deletions components/nspanel_lovelace/cards.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,30 @@ class ThermoCard : public Card, public IEntitySubscriber {
const char* temperature_unit_icon_;
};

/*
* =============== MediaCard ===============
*/

class MediaCard : public Card, public IEntitySubscriber {
public:
MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity);
MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity,
const std::string &title);
MediaCard(const std::string &uuid,
const std::shared_ptr<Entity> &media_entity,
const std::string &title,
const uint16_t sleep_timeout);
virtual ~MediaCard();

void accept(PageVisitor& visitor) override;

std::string &render(std::string &buffer) override;

protected:
std::shared_ptr<Entity> media_entity_;
};

} // namespace nspanel_lovelace
} // namespace esphome
5 changes: 4 additions & 1 deletion components/nspanel_lovelace/entity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@ void Entity::set_attribute(ha_attr_type attr, const std::string &value) {
attr == ha_attr_type::preset_modes ||
attr == ha_attr_type::swing_modes ||
attr == ha_attr_type::fan_modes ||
attr == ha_attr_type::hvac_modes) {
attr == ha_attr_type::hvac_modes ||
// todo: this list can contain any value (including ones with commas),
// convert_python_arr_str does not support this scenario!
attr == ha_attr_type::source_list) {
this->attributes_[attr] = convert_python_arr_str(value);
} else {
this->attributes_[attr] = value;
Expand Down
Loading

0 comments on commit e1e6d60

Please sign in to comment.