Added Raspiaudio Muse Luxe again as a sendspin player.

This commit is contained in:
2026-09-27 18:59:01 +02:00
parent e5d4d6abf2
commit e949ebacc7
3 changed files with 643 additions and 0 deletions
@@ -0,0 +1,50 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor, i2c, sensor
from esphome.const import CONF_ID, ENTITY_CATEGORY_DIAGNOSTIC
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "binary_sensor"]
ns = cg.esphome_ns.namespace("luxe_ip5306")
LuxeIP5306 = ns.class_("LuxeIP5306", cg.PollingComponent, i2c.I2CDevice)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LuxeIP5306),
cv.Required("available"): binary_sensor.binary_sensor_schema(
device_class="connectivity", entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Required("charging"): binary_sensor.binary_sensor_schema(
device_class="battery_charging"
),
cv.Required("full"): binary_sensor.binary_sensor_schema(),
cv.Required("charge_allowed"): binary_sensor.binary_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Required("level"): sensor.sensor_schema(
unit_of_measurement="%", accuracy_decimals=0, device_class="battery",
state_class="measurement", entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
cv.Required("current_limit"): sensor.sensor_schema(
unit_of_measurement="mA", accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC
),
}
)
.extend(cv.polling_component_schema("10s"))
.extend(i2c.i2c_device_schema(0x75))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
for key in ("available", "charging", "full", "charge_allowed"):
entity = await binary_sensor.new_binary_sensor(config[key])
cg.add(getattr(var, f"set_{key}")(entity))
for key in ("level", "current_limit"):
entity = await sensor.new_sensor(config[key])
cg.add(getattr(var, f"set_{key}")(entity))
@@ -0,0 +1,108 @@
#pragma once
#include <cmath>
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome::luxe_ip5306 {
// Optional IP5306-I2C on recent Luxe boards. Never changes power settings at boot.
class LuxeIP5306 : public PollingComponent, public i2c::I2CDevice {
public:
void set_available(binary_sensor::BinarySensor *s) { available_ = s; }
void set_charging(binary_sensor::BinarySensor *s) { charging_ = s; }
void set_full(binary_sensor::BinarySensor *s) { full_ = s; }
void set_charge_allowed(binary_sensor::BinarySensor *s) { charge_allowed_ = s; }
void set_level(sensor::Sensor *s) { level_ = s; }
void set_current_limit(sensor::Sensor *s) { current_limit_ = s; }
float get_setup_priority() const override { return setup_priority::DATA; }
void setup() override { update(); }
void update() override {
uint8_t control, charge, full, limit, gauge;
// Address-only ACK probe, then individual register reads. No assumption
// that the chip supports register auto-increment.
if (write(nullptr, 0) != i2c::ERROR_OK ||
!read_byte(0x00, &control) || !read_byte(0x70, &charge) ||
!read_byte(0x71, &full) || !read_byte(0x24, &limit)) {
unavailable_();
return;
}
valid_ = true;
charge_enabled_ = (control & 0x10) != 0;
available_->publish_state(true);
charge_allowed_->publish_state(charge_enabled_);
const bool full_flag = (full & 0x08) != 0;
full_->publish_state(full_flag);
charging_->publish_state(charge_enabled_ && (charge & 0x08) && !full_flag);
// Configured current, NOT measured current. Keep it separate from the
// charging state: 0x70 alone cannot establish USB presence in every mode.
current_limit_->publish_state(50 + 100 * (limit & 0x1F));
// Gauge is optional: undocumented on some revisions. Failure/unknown
// codes must never be presented as an empty battery.
float percent = NAN;
if (read_byte(0x78, &gauge)) {
switch (gauge >> 4) {
case 0x0: percent = 100; break;
case 0x8: percent = 75; break;
case 0xC: percent = 50; break;
case 0xE: percent = 25; break;
case 0xF: percent = 0; break;
}
}
level_->publish_state(percent);
}
optional<bool> charge_enabled() const {
if (!valid_) return {};
return charge_enabled_;
}
void set_charge_enabled(bool enabled) {
uint8_t before;
// Read afresh even if the last poll succeeded. Modify only bit 4, leaving
// boost, key behaviour and all reserved bits untouched.
if (write(nullptr, 0) != i2c::ERROR_OK || !read_byte(0x00, &before)) {
unavailable_();
ESP_LOGW("luxe_ip5306", "Recharge command rejected: circuit unavailable");
return;
}
const uint8_t after = enabled ? (before | 0x10) : (before & ~0x10);
if (after != before && !write_byte(0x00, after)) {
unavailable_();
ESP_LOGW("luxe_ip5306", "Recharge command failed");
return;
}
update(); // Read-back is authoritative; the switch is not optimistic.
if (valid_ && charge_enabled_ != enabled)
ESP_LOGW("luxe_ip5306", "Recharge command not confirmed by circuit");
}
protected:
void unavailable_() {
valid_ = false;
available_->publish_state(false);
charging_->invalidate_state();
full_->invalidate_state();
charge_allowed_->invalidate_state();
level_->publish_state(NAN);
current_limit_->publish_state(NAN);
// No mark_failed(): an older board must still play audio, and a device
// temporarily asleep or disconnected must be detected on the next poll.
}
binary_sensor::BinarySensor *available_{nullptr};
binary_sensor::BinarySensor *charging_{nullptr};
binary_sensor::BinarySensor *full_{nullptr};
binary_sensor::BinarySensor *charge_allowed_{nullptr};
sensor::Sensor *level_{nullptr};
sensor::Sensor *current_limit_{nullptr};
bool valid_{false};
bool charge_enabled_{false};
};
} // namespace esphome::luxe_ip5306