forked from HomeAssistant/xiaoshi-pad-card
Add files via upload
This commit is contained in:
24
xiaoshi-pad-card.js
Normal file
24
xiaoshi-pad-card.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
console.info("%c 消逝卡-平板端 \n%c v 0.0.0 ", "color: red; font-weight: bold; background: black", "color: white; font-weight: bold; background: black");
|
||||||
|
|
||||||
|
const loadCards = async () => {
|
||||||
|
await import('./xiaoshi-pad-grid-card.js');
|
||||||
|
await import('./xiaoshi-pad-slider-card.js');
|
||||||
|
|
||||||
|
window.customCards = window.customCards || [];
|
||||||
|
window.customCards.push(...cardConfigs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardConfigs = [
|
||||||
|
{
|
||||||
|
type: 'xiaoshi-pad-grid-card',
|
||||||
|
name: '消逝卡(平板端)-分布卡',
|
||||||
|
description: '温度分布、湿度分布'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'xiaoshi-pad-slider-card',
|
||||||
|
name: '消逝卡(平板端)-进度条',
|
||||||
|
description: '进度条'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
loadCards();
|
||||||
142
xiaoshi-pad-grid-card.js
Normal file
142
xiaoshi-pad-grid-card.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
|
||||||
|
|
||||||
|
export class XiaoshiPadGridCard extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
config: Object,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
.container {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.grid-item {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
if (!config.entities) {
|
||||||
|
throw new Error('You need to define entities');
|
||||||
|
}
|
||||||
|
this.config = {
|
||||||
|
width: config.width || '400px',
|
||||||
|
height: config.height || '80px',
|
||||||
|
min: config.min || 0,
|
||||||
|
max: config.max || 100,
|
||||||
|
mode: config.mode || '温度',
|
||||||
|
display: config.display || false,
|
||||||
|
entities: config.entities.map(entity => ({
|
||||||
|
...entity,
|
||||||
|
state: entity.state !== false,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if(this._display()) return;
|
||||||
|
return html`
|
||||||
|
<div class="container"\n
|
||||||
|
style="width: ${this.config.width}; height: ${this.config.height};">
|
||||||
|
${this.config.entities.map((entityConfig) => {
|
||||||
|
const entity = this.hass.states[entityConfig.entity];
|
||||||
|
if (!entity) return html``;
|
||||||
|
const value = parseFloat(entity.state);
|
||||||
|
const grid = entityConfig.grid ? entityConfig.grid.split(',') : ['0%', '0%', '100%', '100%'];
|
||||||
|
const unit = entityConfig.unit || '';
|
||||||
|
let filter;
|
||||||
|
if (this.config.mode === '温度') {
|
||||||
|
filter = this._calculateTemperatureFilter(value);
|
||||||
|
} else if (this.config.mode === '湿度') {
|
||||||
|
filter = this._calculateHumidityFilter(value);
|
||||||
|
};
|
||||||
|
let size = Number(grid[2].slice(0, grid[2].length-1));
|
||||||
|
let fsize ="11px";
|
||||||
|
if (size<25 ) fsize ="10px";
|
||||||
|
if (size<20 ) fsize ="9px";
|
||||||
|
if (size<15 ) fsize ="8px";
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="grid-item"\n
|
||||||
|
style="left: ${grid[0]};top: ${grid[1]};width: ${grid[2]};height: ${grid[3]};background-color: rgba(0, 200, 0, 0.8);filter: ${filter};font-size: ${fsize};">
|
||||||
|
${entityConfig.state !== false ? html`${entity.state}${unit}` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_display() {
|
||||||
|
try {
|
||||||
|
if (this.config.display === undefined) return false;
|
||||||
|
if (typeof this.config.display === 'boolean') {
|
||||||
|
return this.config.display;
|
||||||
|
};
|
||||||
|
if (typeof this.config.display === 'function') {
|
||||||
|
const result = this.config.display();
|
||||||
|
return result === true || result === "true"; // 同时接受 true 和 "true"
|
||||||
|
};
|
||||||
|
if (typeof this.config.display === 'string') {
|
||||||
|
const displayStr = this.config.display.trim();
|
||||||
|
if (displayStr.startsWith('[[[') && displayStr.endsWith(']]]')) {
|
||||||
|
const funcBody = displayStr.slice(3, -3).trim();
|
||||||
|
const result = new Function('states', funcBody)(this.hass.states);
|
||||||
|
return result === true || result === "true"; // 同时接受 true 和 "true"
|
||||||
|
}
|
||||||
|
if (displayStr.includes('return') || displayStr.includes('=>')) {
|
||||||
|
const result = (new Function(`return ${displayStr}`))();
|
||||||
|
return result === true || result === "true";
|
||||||
|
}
|
||||||
|
const result = (new Function(`return ${displayStr}`))();
|
||||||
|
return result === true || result === "true";
|
||||||
|
};
|
||||||
|
return false;
|
||||||
|
} catch(e) {
|
||||||
|
console.error('显示出错:', e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateTemperatureFilter(temp) {
|
||||||
|
temp = parseFloat(temp);
|
||||||
|
const { min, max } = this.config;
|
||||||
|
let deg;
|
||||||
|
if (temp > 25) {
|
||||||
|
deg = (25 - temp) * 120 / (max - 25);
|
||||||
|
} else {
|
||||||
|
deg = (25 - temp) * 100 / (25 - min);
|
||||||
|
};
|
||||||
|
return `hue-rotate(${deg}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_calculateHumidityFilter(hum) {
|
||||||
|
hum = parseFloat(hum);
|
||||||
|
const { min, max } = this.config;
|
||||||
|
let deg;
|
||||||
|
if (hum > 50) {
|
||||||
|
deg = (50 - hum) * 100 / (50 - max);
|
||||||
|
} else {
|
||||||
|
deg = (50 - hum) * 120 / (min - 50);
|
||||||
|
};
|
||||||
|
return `hue-rotate(${deg}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardSize() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('xiaoshi-pad-grid-card', XiaoshiPadGridCard);
|
||||||
163
xiaoshi-pad-slider-card.js
Normal file
163
xiaoshi-pad-slider-card.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { LitElement, html, css } from "https://unpkg.com/lit-element@2.4.0/lit-element.js?module";
|
||||||
|
|
||||||
|
export class XiaoshiPadSliderCard extends LitElement {
|
||||||
|
static get properties() {
|
||||||
|
return {
|
||||||
|
hass: Object,
|
||||||
|
config: Object,
|
||||||
|
_value: Number,
|
||||||
|
_min: Number,
|
||||||
|
_max: Number,
|
||||||
|
_dragging: Boolean
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
.slider-root {
|
||||||
|
position: relative;
|
||||||
|
width: var(--slider-width, 100%);
|
||||||
|
height: var(--slider-height, 30px);
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.slider-track {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 100%;
|
||||||
|
height: var(--track-height, 5px);
|
||||||
|
background: var(--track-color, rgba(255,255,255,0.3));
|
||||||
|
border-radius: var(--track-radius, 2px);
|
||||||
|
}
|
||||||
|
.slider-fill {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--slider-color, #f00);
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
.slider-thumb {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: var(--thumb-size, 15px);
|
||||||
|
height: var(--thumb-size, 15px);
|
||||||
|
background: var(--thumb-color, #fff);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._value = 0;
|
||||||
|
this._min = 0;
|
||||||
|
this._max = 100;
|
||||||
|
this._dragging = false;
|
||||||
|
this._startX = 0;
|
||||||
|
this._startValue = 0;
|
||||||
|
this._moveHandler = (e) => this._handleDrag(e);
|
||||||
|
this._endHandler = () => this._endDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig(config) {
|
||||||
|
if (!config.entity) throw new Error('必须指定实体');
|
||||||
|
this.config = config;
|
||||||
|
if (config.style) {
|
||||||
|
Object.keys(config.style).forEach(key => {
|
||||||
|
this.style.setProperty(`--${key}`, config.style[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated(changedProperties) {
|
||||||
|
if (changedProperties.has('hass')) {
|
||||||
|
const state = this.hass.states[this.config.entity];
|
||||||
|
if (state) {
|
||||||
|
this._value = Number(state.state);
|
||||||
|
this._min = Number(state.attributes.min || 0);
|
||||||
|
this._max = Number(state.attributes.max || 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const percent = Math.max(0, Math.min(100, (this._value - this._min) / (this._max - this._min) * 100));
|
||||||
|
return html`
|
||||||
|
<div class="slider-root"\n
|
||||||
|
@mousedown=${this._startDrag}\n
|
||||||
|
@touchstart=${this._startDrag}>
|
||||||
|
<div class="slider-track">
|
||||||
|
<div class="slider-fill"\nstyle="width: ${percent}%"></div>
|
||||||
|
<div class="slider-thumb"\nstyle="left: ${percent}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startDrag(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._dragging = true;
|
||||||
|
const slider = this.shadowRoot.querySelector('.slider-track');
|
||||||
|
const rect = slider.getBoundingClientRect();
|
||||||
|
this._sliderLeft = rect.left;
|
||||||
|
this._sliderWidth = rect.width;
|
||||||
|
window.addEventListener('mousemove', this._moveHandler);
|
||||||
|
window.addEventListener('touchmove', this._moveHandler, { passive: false });
|
||||||
|
window.addEventListener('mouseup', this._endHandler);
|
||||||
|
window.addEventListener('touchend', this._endHandler);
|
||||||
|
const clientX = e.type === 'touchstart' ? e.touches[0].clientX : e.clientX;
|
||||||
|
this._updateValue((clientX - this._sliderLeft) / this._sliderWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
_endDrag() {
|
||||||
|
if (!this._dragging) return;
|
||||||
|
this._dragging = false;
|
||||||
|
this._removeEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDrag(e) {
|
||||||
|
if (!this._dragging) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const clientX = e.type === 'touchmove' ? e.touches[0].clientX : e.clientX;
|
||||||
|
this._updateValue((clientX - this._sliderLeft) / this._sliderWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
_removeEventListeners() {
|
||||||
|
window.removeEventListener('mousemove', this._moveHandler);
|
||||||
|
window.removeEventListener('touchmove', this._moveHandler);
|
||||||
|
window.removeEventListener('mouseup', this._endHandler);
|
||||||
|
window.removeEventListener('touchend', this._endHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._removeEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateValue(ratio) {
|
||||||
|
const safeRatio = Math.max(0, Math.min(1, ratio));
|
||||||
|
const newValue = this._min + safeRatio * (this._max - this._min);
|
||||||
|
const roundedValue = Math.round(newValue);
|
||||||
|
if (roundedValue !== this._value) {
|
||||||
|
this._value = roundedValue;
|
||||||
|
this._debouncedSetValue(roundedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_debouncedSetValue(value) {
|
||||||
|
clearTimeout(this._debounceTimer);
|
||||||
|
this._debounceTimer = setTimeout(() => {
|
||||||
|
this._callService(value);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
_callService(value) {
|
||||||
|
const service = this.config.entity.split('.')[0];
|
||||||
|
this.hass.callService(service, 'set_value', {
|
||||||
|
entity_id: this.config.entity,
|
||||||
|
value: value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
customElements.define('xiaoshi-pad-slider-card', XiaoshiPadSliderCard);
|
||||||
Reference in New Issue
Block a user