/** * Pstryk Energy Card v4.1.0 * * Przykładowe konfiguracje: * * FULL MODE: * type: custom:pstryk-card * buy_entity: sensor.pstryk_current_buy_price * sell_entity: sensor.pstryk_current_sell_price * card_mode: full # full | compact | super_compact * title: Energy Prices * show_title: true # true | false * show_legend: true # true | false * attribute_config: next_hour # next_hour | average_remaining | average_24 | custom_attribute_name | null * hover_effect: lift # none | lift | glow | shake | pulse * show_widget: sparkline # none | bars | sparkline * widget_hours: 24 # liczba godzin do pokazania (1-48) * widget_effect: pulse # none | pulse (dla sparkline) | fill (dla bars) * alert_buy_above: 1.15 # próg alertu lub null * alert_sell_below: 0.25 # próg alertu lub null * click_action: none # none | more-info * debug: false # true | false * * COMPACT MODE: * type: custom:pstryk-card * buy_entity: sensor.pstryk_current_buy_price * sell_entity: sensor.pstryk_current_sell_price * card_mode: compact * title: Energy Prices * show_title: true # true | false * show_legend: false # domyślnie false dla compact * attribute_config: average_24 # next_hour | average_remaining | average_24 | custom_attribute_name | null * hover_effect: glow # none | lift | glow | shake | pulse * show_widget: bars # none | bars | sparkline * widget_hours: 12 # liczba godzin do pokazania (1-48) * widget_effect: fill # none | pulse (dla sparkline) | fill (dla bars) * alert_buy_above: null * alert_sell_below: null * click_action: more-info # none | more-info * debug: false * * SUPER_COMPACT MODE: * type: custom:pstryk-card * buy_entity: sensor.pstryk_current_buy_price * sell_entity: sensor.pstryk_current_sell_price * card_mode: super_compact * title: Energy Prices # ignorowane w super_compact * show_title: false # zawsze false w super_compact * show_legend: false # zawsze false w super_compact * attribute_config: null # zawsze null w super_compact * hover_effect: none # none | lift | glow | shake | pulse * show_widget: none # zawsze none w super_compact * widget_hours: 12 # ignorowane w super_compact * widget_effect: none # ignorowane w super_compact * alert_buy_above: 1.0 * alert_sell_below: 0.1 * click_action: none # none | more-info * debug: false * * Changelog v4.1.0: * - DODANO opcję widget_effect: fill dla trybu bars * - Efekt wypełniania aktualnego słupka w trybie bars jest teraz opcjonalny * - widget_effect: none - wyłącza efekt wypełniania (wszystkie słupki jednolite) * - widget_effect: fill - włącza efekt wypełniania aktualnego słupka * * Changelog v4.0.12: * - USUNIĘTO linię zero z widgetu bars (była niewidoczna na dole) * - Zachowano linię zero w sparkline (bez zmian) * - Zachowano prawidłową obsługę wartości ujemnych w bars * - Wartości dodatnie: słupki w górę, ujemne: słupki w dół (od niewidocznej linii zero) */ class PstrykCard extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this._config = {}; this._hass = null; this._remainingHours = null; this._translations = null; this._avgPriceRegex = null; this._refreshInterval = null; } connectedCallback() { // Uruchom timer odświeżania co godzinę this._startRefreshTimer(); } disconnectedCallback() { // Zatrzymaj timer przy usuwaniu karty this._stopRefreshTimer(); } _startRefreshTimer() { this._stopRefreshTimer(); // Odświeżaj co godzinę jeśli widget jest włączony if (this._config.show_widget !== 'none') { // Oblicz czas do następnej pełnej godziny const now = new Date(); const msToNextHour = (60 - now.getMinutes()) * 60 * 1000 - now.getSeconds() * 1000 - now.getMilliseconds(); // Pierwsze odświeżenie na początku następnej godziny setTimeout(() => { this.render(); // Potem odświeżaj co godzinę this._refreshInterval = setInterval(() => { this.render(); }, 3600000); // 60 minut }, msToNextHour); } } _stopRefreshTimer() { if (this._refreshInterval) { clearInterval(this._refreshInterval); this._refreshInterval = null; } } setConfig(config) { if (!config.buy_entity || !config.sell_entity) { throw new Error('You must define buy_entity and sell_entity'); } // Walidacja card_mode const validModes = ['full', 'compact', 'super_compact']; if (config.card_mode && !validModes.includes(config.card_mode)) { throw new Error(`Invalid card_mode: ${config.card_mode}. Use: ${validModes.join(', ')}`); } // Walidacja show_widget const validWidgets = ['none', 'bars', 'sparkline']; if (config.show_widget && !validWidgets.includes(config.show_widget)) { throw new Error(`Invalid show_widget: ${config.show_widget}. Use: ${validWidgets.join(', ')}`); } // Walidacja widget_effect const validEffects = ['none', 'pulse', 'fill']; if (config.widget_effect && !validEffects.includes(config.widget_effect)) { throw new Error(`Invalid widget_effect: ${config.widget_effect}. Use: ${validEffects.join(', ')}`); } // Ustaw domyślny tryb jeśli nie podano const cardMode = config.card_mode || 'full'; // Zastosuj domyślne wartości w zależności od trybu this._config = this._applyModeDefaults({ ...config, card_mode: cardMode }); } _applyModeDefaults(config) { const mode = config.card_mode || 'full'; // Domyślne wartości dla wszystkich trybów const defaults = { title: 'Energy Prices', show_title: true, show_legend: true, attribute_config: 'next_hour', hover_effect: 'lift', show_widget: 'none', widget_hours: 24, widget_effect: 'none', alert_buy_above: null, alert_sell_below: null, click_action: 'none', debug: false }; // Modyfikacje dla trybu compact if (mode === 'compact') { defaults.show_legend = false; defaults.widget_hours = 12; } // Modyfikacje dla trybu super_compact if (mode === 'super_compact') { defaults.show_title = false; defaults.show_legend = false; defaults.attribute_config = null; defaults.show_widget = 'none'; } // Połącz z konfiguracją użytkownika (config ma priorytet) return { ...defaults, ...config }; } set hass(hass) { const oldHass = this._hass; this._hass = hass; // Optymalizacja: renderuj tylko gdy zmienią się wartości encji if (oldHass && this._config.buy_entity && this._config.sell_entity) { const oldBuy = oldHass.states[this._config.buy_entity]; const oldSell = oldHass.states[this._config.sell_entity]; const newBuy = hass.states[this._config.buy_entity]; const newSell = hass.states[this._config.sell_entity]; if (oldBuy === newBuy && oldSell === newSell) { return; } } // Czyść cache tłumaczeń przy zmianie języka if (oldHass && oldHass.language !== hass.language) { this._translations = null; } this.render(); } getCardSize() { switch(this._config.card_mode) { case 'super_compact': return 1; case 'compact': return this._config.show_widget !== 'none' ? 2 : 1; case 'full': default: return this._config.show_widget !== 'none' ? 3 : 2; } } static getStubConfig() { return { buy_entity: 'sensor.pstryk_current_buy_price', sell_entity: 'sensor.pstryk_current_sell_price', card_mode: 'full', show_widget: 'sparkline', widget_effect: 'pulse' }; } _getTranslations() { if (!this._translations) { const isPolish = this._hass?.language === 'pl' || (this._hass?.locale?.language === 'pl') || (navigator.language?.startsWith('pl')); this._translations = { 'buy_price': isPolish ? 'Cena Zakupu' : 'Buy Price', 'sell_price': isPolish ? 'Cena Sprzedaży' : 'Sell Price', 'best_prices': isPolish ? 'Najlepsze ceny' : 'Best prices', 'normal_prices': isPolish ? 'Normalne ceny' : 'Normal prices', 'worst_prices': isPolish ? 'Najgorsze ceny' : 'Worst prices', 'next_hour': isPolish ? 'Następna godzina' : 'Next hour', 'average_24': isPolish ? 'Średnia 24h' : 'Average 24h', 'alert': isPolish ? 'Uwaga!' : 'Alert!', 'high_price': isPolish ? 'Wysoka cena' : 'High price', 'low_price': isPolish ? 'Niska cena' : 'Low price', 'no_data': isPolish ? 'Brak danych' : 'No data', 'entity_not_found': isPolish ? 'Nie znaleziono encji' : 'Entity not found' }; } return this._translations; } translate(key) { return this._getTranslations()[key] || key; } checkAlert(type, value) { const numValue = parseFloat(value); if (isNaN(numValue)) return false; if (type === 'buy' && this._config.alert_buy_above) { return numValue > this._config.alert_buy_above; } if (type === 'sell' && this._config.alert_sell_below) { return numValue < this._config.alert_sell_below; } return false; } getPriceColor(entity, price, type) { if (!entity?.attributes) return 'var(--primary-text-color)'; const currentPrice = parseFloat(price); if (isNaN(currentPrice)) return 'var(--primary-text-color)'; // Pobierz tablice najlepszych i najgorszych cen const bestPrices = entity.attributes['Najlepsze ceny'] || entity.attributes['Best prices'] || entity.attributes['best_prices'] || []; const worstPrices = entity.attributes['Najgorsze ceny'] || entity.attributes['Worst prices'] || entity.attributes['worst_prices'] || []; const epsilon = 0.001; // Sprawdź w najlepszych for (const priceData of bestPrices) { const checkPrice = typeof priceData === 'object' ? priceData.price : priceData; if (Math.abs(checkPrice - currentPrice) < epsilon) { return '#4ade80'; // Zielony } } // Sprawdź w najgorszych for (const priceData of worstPrices) { const checkPrice = typeof priceData === 'object' ? priceData.price : priceData; if (Math.abs(checkPrice - currentPrice) < epsilon) { return '#f87171'; // Czerwony } } return 'var(--primary-text-color)'; } formatPrice(price) { if (price === null || price === undefined || price === '') return '--'; const numPrice = parseFloat(price); if (isNaN(numPrice)) return '--'; return `${numPrice.toFixed(2)} PLN/kWh`; } getAttributeValue(entity, config) { if (!entity?.attributes || !config) return null; switch(config) { case 'next_hour': return entity.attributes['Next hour']; case 'average_remaining': if (!this._avgPriceRegex) { this._avgPriceRegex = /Average price today \/(\d+)/; } for (const [key, value] of Object.entries(entity.attributes)) { const match = key.match(this._avgPriceRegex); if (match && match[1] !== '24') { this._remainingHours = match[1]; return value; } } return null; case 'average_24': return entity.attributes['Average price today /24']; default: return entity.attributes[config]; } } getAttributeLabel(config) { switch(config) { case 'next_hour': return this.translate('next_hour'); case 'average_remaining': const hours = this._remainingHours || 'X'; const translations = this._getTranslations(); const isPolish = translations['buy_price'] === 'Cena Zakupu'; return isPolish ? `Średnia ${hours}h` : `Average ${hours}h`; case 'average_24': return this.translate('average_24'); default: return config; } } generateWidgetData(entity, entityType) { if (!entity?.attributes) return { data: [], currentIndex: 0 }; // Debug if (this._config.debug) { console.log(`[PstrykCard] ${entityType} generateWidgetData - attributes:`, entity.attributes); } // Szukaj historii cen w różnych możliwych atrybutach let allPrices = entity.attributes['All prices'] || entity.attributes['all_prices'] || entity.attributes['prices'] || entity.attributes['hourly_prices'] || []; if (this._config.debug) { console.log(`[PstrykCard] ${entityType} raw prices data:`, allPrices); console.log(`[PstrykCard] ${entityType} raw data sample:`, allPrices.slice(0, 10).map((item, idx) => `${idx}: ${JSON.stringify(item)}`)); // Pokaż problematyczne elementy dla sell if (entityType === 'sell') { console.log(`[PstrykCard] ${entityType} items 9-16:`, allPrices.slice(9, 17).map((item, idx) => `${idx+9}: ${JSON.stringify(item)}`)); } } if (!Array.isArray(allPrices) || allPrices.length === 0) { if (this._config.debug) { console.warn(`[PstrykCard] No price data found for ${entityType}`); } return { data: [], currentIndex: 0 }; } const now = new Date(); const currentHour = now.getHours(); const currentMinutes = now.getMinutes(); const currentPrice = parseFloat(entity.state); if (this._config.debug) { console.log(`[PstrykCard] ${entityType} current: hour=${currentHour}, minutes=${currentMinutes}, price=${currentPrice}`); } // Konwertuj do tablicy z informacją o czasie - POPRAWIONE FILTROWANIE const pricesWithTime = allPrices.map((item, index) => { let price, hour, startTime, rawValue; if (typeof item === 'object') { rawValue = item.price || item.value || item.val || item; // Sprawdź czy to jest zagnieżdżony obiekt z wartościami if (typeof rawValue === 'object' && rawValue !== null) { rawValue = rawValue.price || rawValue.value || rawValue.val || 0; } price = parseFloat(rawValue); // Próbuj pobrać informację o czasie z różnych możliwych pól if (item.start_time) { startTime = new Date(item.start_time); hour = startTime.getHours(); } else if (item.time) { startTime = new Date(item.time); hour = startTime.getHours(); } else if (item.hour !== undefined) { hour = parseInt(item.hour); } else { // Jeśli nie ma informacji o czasie, zakładamy sekwencyjne godziny hour = index % 24; } } else { rawValue = item; price = parseFloat(item); hour = index % 24; } return { price, hour, index, originalIndex: index, rawValue }; }).filter(item => { // LEPSZE FILTROWANIE: sprawdź czy wartość da się przekonwertować na liczbę const isValid = !isNaN(item.price) && isFinite(item.price) && item.rawValue !== null && item.rawValue !== undefined && item.rawValue !== ''; if (this._config.debug && !isValid) { console.log(`[PstrykCard] ${entityType} filtered out:`, JSON.stringify(item.rawValue), 'at index', item.originalIndex, 'parsed as:', item.price); } return isValid; }); if (this._config.debug) { console.log(`[PstrykCard] ${entityType} processed prices:`, pricesWithTime.map(p => `${p.hour}:00 = ${p.price}`)); } // Sprawdź kolejność godzin w danych if (this._config.debug && pricesWithTime.length > 1) { const hourSequence = pricesWithTime.slice(0, 5).map(p => p.hour); console.log(`[PstrykCard] ${entityType} first 5 hours sequence:`, hourSequence); } // Znajdź indeks aktualnej godziny let currentIndex = -1; // Metoda 1: Szukaj dokładnego dopasowania godziny currentIndex = pricesWithTime.findIndex(item => item.hour === currentHour); if (currentIndex !== -1 && this._config.debug) { console.log(`[PstrykCard] ${entityType} found current hour ${currentHour} at index ${currentIndex}`); } // Metoda 2: Jeśli nie znaleziono, szukaj po cenie (z tolerancją) if (currentIndex === -1) { const epsilon = 0.01; currentIndex = pricesWithTime.findIndex(item => Math.abs(item.price - currentPrice) < epsilon); if (currentIndex !== -1 && this._config.debug) { console.log(`[PstrykCard] ${entityType} found current by price match at index ${currentIndex}, hour: ${pricesWithTime[currentIndex].hour}`); } } // Metoda 3: Jeśli nadal nie znaleziono i mamy pełne 24h danych if (currentIndex === -1 && pricesWithTime.length >= 24) { // Spróbuj znaleźć najbliższą godzinę let minDiff = 24; pricesWithTime.forEach((item, idx) => { const diff = Math.abs(item.hour - currentHour); if (diff < minDiff) { minDiff = diff; currentIndex = idx; } }); if (currentIndex !== -1 && this._config.debug) { console.log(`[PstrykCard] ${entityType} using closest hour at index ${currentIndex}, hour: ${pricesWithTime[currentIndex].hour}`); } } // Ostatnia deska ratunku - użyj środka if (currentIndex === -1) { currentIndex = Math.floor(pricesWithTime.length / 2); if (this._config.debug) { console.warn(`[PstrykCard] ${entityType} could not find current hour, using middle at index ${currentIndex}`); } } // Pobierz dane tylko dla przyszłych godzin (od aktualnej) const hoursToShow = Math.min(this._config.widget_hours || 24, pricesWithTime.length - currentIndex); const futureData = pricesWithTime.slice(currentIndex, currentIndex + hoursToShow); // Oblicz pozycję kropki (0-1) w aktualnej godzinie const dotPosition = currentMinutes / 60; if (this._config.debug) { console.log(`[PstrykCard] ${entityType} showing hours:`, futureData.map(d => `${d.hour}:00 = ${d.price}`).join(', ')); } return { data: futureData.map(d => d.price), hours: futureData.map(d => d.hour), currentIndex: 0, // Zawsze 0, bo zaczynamy od aktualnej dotPosition: dotPosition }; } createSparkline(data, hours, height, color, currentIndex, dotPosition, uniqueId) { if (!data || data.length < 2) return ''; const min = Math.min(...data); const max = Math.max(...data); // Zawsze uwzględnij 0 w zakresie const displayMin = Math.min(min, 0); const displayMax = Math.max(max, 0); const range = displayMax - displayMin || 1; const viewBoxWidth = 200; const viewBoxHeight = 50; const paddingX = 5; const paddingY = 5; const chartWidth = viewBoxWidth - (paddingX * 2); const chartHeight = viewBoxHeight - (paddingY * 2); // Oblicz pozycję linii zero - zawsze będzie widoczna const zeroY = paddingY + chartHeight - ((0 - displayMin) / range) * chartHeight; // Generuj punkty dla linii const points = data.map((value, index) => { const x = paddingX + (index / (data.length - 1)) * chartWidth; const y = paddingY + chartHeight - ((value - displayMin) / range) * chartHeight; return `${x},${y}`; }).join(' '); let currentDot = ''; let pulseEffect = ''; // Kropka na aktualnej pozycji if (currentIndex === 0 && data.length > 0) { // Pozycja X między pierwszym a drugim punktem const x1 = paddingX; const x2 = data.length > 1 ? paddingX + (1 / (data.length - 1)) * chartWidth : x1; const currentX = x1 + (x2 - x1) * dotPosition; // Pozycja Y - interpolacja między wartościami const y1 = paddingY + chartHeight - ((data[0] - displayMin) / range) * chartHeight; const y2 = data.length > 1 ? paddingY + chartHeight - ((data[1] - displayMin) / range) * chartHeight : y1; const currentY = y1 + (y2 - y1) * dotPosition; // Efekt pulsowania if (this._config.widget_effect === 'pulse') { pulseEffect = ` `; } currentDot = ` ${pulseEffect} `; } // Linia zero - zawsze widoczna z lepszym kolorem const zeroLine = ` `; // Debug info if (this._config.debug) { console.log(`[PstrykCard] Sparkline ${uniqueId}: data=[${data.join(', ')}]`); console.log(`[PstrykCard] Sparkline ${uniqueId}: min=${min}, max=${max}, displayMin=${displayMin}, displayMax=${displayMax}, zeroY=${zeroY}`); } return ` ${zeroLine} ${currentDot} `; } createBars(data, hours, height, color, currentIndex, dotPosition, type) { if (!data || data.length < 1) return ''; const viewBoxWidth = 200; const viewBoxHeight = 60; const paddingX = 5; const paddingY = 10; const chartWidth = viewBoxWidth - (paddingX * 2); const chartHeight = viewBoxHeight - (paddingY * 2); const prices = data; const min = Math.min(...prices, 0); const max = Math.max(...prices, 0); const range = max - min || 0.01; // Pozycja "linii zero" (środka) bez rysowania linii const zeroY = paddingY + chartHeight - ((0 - min) / range) * chartHeight; // Szerokość pojedynczego słupka const barWidth = Math.max(chartWidth / data.length * 0.8, 2); const barGap = chartWidth / data.length * 0.2; if (this._config.debug) { console.log(`[PstrykCard] Bars ${type}: data=[${data.join(', ')}], min=${min}, max=${max}, zeroY=${zeroY}, chartHeight=${chartHeight}`); } const bars = data.map((price, index) => { const x = paddingX + index * (barWidth + barGap); let barHeight, y; // POPRAWNA LOGIKA: Słupki rosną od linii zero (bez rysowania linii) if (price >= 0) { // Wartość dodatnia - słupek w górę od linii zero barHeight = ((price - 0) / range) * chartHeight; y = zeroY - barHeight; } else { // Wartość ujemna - słupek w dół od linii zero barHeight = ((0 - price) / range) * chartHeight; y = zeroY; } // Upewnij się, że słupek ma minimalną wysokość dla widoczności barHeight = Math.max(Math.abs(barHeight), 1); // Określ przeźroczystość let opacity = '0.3'; // Pierwszy słupek (aktualna godzina) - tylko jeśli widget_effect === 'fill' if (index === 0 && this._config.widget_effect === 'fill') { opacity = '1'; const filledHeight = barHeight * dotPosition; const unfilledHeight = barHeight - filledHeight; let filledY, filledBarHeight; if (price >= 0) { // Dodatnia wartość - wypełnienie od dołu słupka w górę filledY = y + unfilledHeight; filledBarHeight = filledHeight; } else { // Ujemna wartość - wypełnienie od góry słupka w dół filledY = y; filledBarHeight = filledHeight; } return ` ${hours[index]}:00 `; } // Jeśli to pierwszy słupek i efekt fill jest włączony, to dodaj etykietę z godziną if (index === 0 && this._config.widget_effect === 'fill') { return ` ${hours[index]}:00 `; } // Zwykły słupek return ` `; }).join(''); return ` ${bars} `; } handleClick(entityId) { if (!this._config.click_action || this._config.click_action === 'none') return; if (this._config.click_action === 'more-info') { const event = new CustomEvent('hass-more-info', { detail: { entityId }, bubbles: true, composed: true }); this.dispatchEvent(event); } } getHoverEffect() { if (!this._config.hover_effect || this._config.hover_effect === 'none') return ''; const effects = { 'lift': ` .price-box:hover { transform: translateY(-4px); box-shadow: 0 4px 8px rgba(0,0,0,0.2); } `, 'glow': ` .price-box:hover { box-shadow: 0 0 15px rgba(var(--rgb-primary-color), 0.5); } `, 'shake': ` @keyframes shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-2px); } 75% { transform: translateX(2px); } } .price-box:hover { animation: shake 0.3s ease-in-out; } `, 'pulse': ` @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .price-box:hover { animation: pulse 0.5s ease-in-out; } ` }; return effects[this._config.hover_effect] || ''; } _getModeStyles() { const mode = this._config.card_mode; const isCompact = mode === 'compact'; const isSuperCompact = mode === 'super_compact'; return { card: { padding: isSuperCompact ? '8px' : (isCompact ? '12px' : '16px') }, header: { fontSize: isCompact ? '18px' : '20px', marginBottom: isCompact ? '12px' : '16px' }, container: { gap: isCompact ? '16px' : '24px' }, priceBox: { padding: isCompact ? '12px' : '16px' }, priceLabel: { fontSize: isCompact ? '12px' : '14px', marginBottom: isCompact ? '4px' : '8px' }, priceValue: { fontSize: isCompact ? '20px' : '24px' }, priceIcon: { width: isCompact ? '20px' : '24px', height: isCompact ? '20px' : '24px', marginBottom: isCompact ? '4px' : '8px' }, attribute: { fontSize: isCompact ? '11px' : '12px', marginTop: isCompact ? '4px' : '8px', paddingTop: isCompact ? '4px' : '8px' }, legend: { gap: isCompact ? '16px' : '24px', marginTop: isCompact ? '12px' : '16px', paddingTop: isCompact ? '12px' : '16px', fontSize: isCompact ? '11px' : '12px' }, legendDot: { width: isCompact ? '10px' : '12px', height: isCompact ? '10px' : '12px' }, widget: { height: isCompact ? 40 : 50 } }; } render() { if (!this._hass || !this._config) return; const buyEntity = this._hass.states[this._config.buy_entity]; const sellEntity = this._hass.states[this._config.sell_entity]; if (!buyEntity || !sellEntity) { this.shadowRoot.innerHTML = `
${this.translate('entity_not_found')}: ${this._config.buy_entity} or ${this._config.sell_entity}
`; return; } const mode = this._config.card_mode; const isSuperCompact = mode === 'super_compact'; const styles = this._getModeStyles(); const buyPrice = buyEntity.state; const sellPrice = sellEntity.state; const buyAlert = this.checkAlert('buy', buyPrice); const sellAlert = this.checkAlert('sell', sellPrice); const buyColor = this.getPriceColor(buyEntity, buyPrice, 'buy'); const sellColor = this.getPriceColor(sellEntity, sellPrice, 'sell'); // Reset godzin przed pobraniem nowych wartości this._remainingHours = null; const buyAttribute = this._config.attribute_config ? this.getAttributeValue(buyEntity, this._config.attribute_config) : null; const sellAttribute = this._config.attribute_config ? this.getAttributeValue(sellEntity, this._config.attribute_config) : null; const attributeLabel = this.getAttributeLabel(this._config.attribute_config); // Generuj widgety let buyWidgetHtml = ''; let sellWidgetHtml = ''; if (this._config.show_widget !== 'none' && !isSuperCompact) { const buyData = this.generateWidgetData(buyEntity, 'buy'); const sellData = this.generateWidgetData(sellEntity, 'sell'); if (this._config.debug) { console.log('[PstrykCard] Buy widget data:', buyData); console.log('[PstrykCard] Sell widget data:', sellData); } if (this._config.show_widget === 'sparkline') { if (buyData.data.length > 1) { buyWidgetHtml = this.createSparkline( buyData.data, buyData.hours, styles.widget.height, buyColor, buyData.currentIndex, buyData.dotPosition, 'buy-sparkline' ); } if (sellData.data.length > 1) { sellWidgetHtml = this.createSparkline( sellData.data, sellData.hours, styles.widget.height, sellColor, sellData.currentIndex, sellData.dotPosition, 'sell-sparkline' ); } } else if (this._config.show_widget === 'bars') { if (buyData.data.length > 0) { buyWidgetHtml = this.createBars( buyData.data, buyData.hours, styles.widget.height, buyColor, buyData.currentIndex, buyData.dotPosition, 'buy' ); } else { buyWidgetHtml = `
${this.translate('no_data')} (Buy)
`; } if (sellData.data.length > 0) { sellWidgetHtml = this.createBars( sellData.data, sellData.hours, styles.widget.height, sellColor, sellData.currentIndex, sellData.dotPosition, 'sell' ); } else { sellWidgetHtml = `
${this.translate('no_data')} (Sell)
`; } } } // Restart timer jeśli potrzebny this._startRefreshTimer(); // Generowanie HTML elementów const titleHtml = this._config.show_title && !isSuperCompact ? `
${this._config.title}
` : ''; const attributeHtml = (value) => { if (isSuperCompact || !this._config.attribute_config || value === null) return ''; return `
${attributeLabel}: ${this.formatPrice(value)}
`; }; const legendHtml = this._config.show_legend && mode === 'full' ? `
${this.translate('best_prices')}
${this.translate('normal_prices')}
${this.translate('worst_prices')}
` : ''; // Style specyficzne dla trybu super_compact const superCompactStyles = isSuperCompact ? ` .prices-container { grid-template-columns: 1fr 1fr; gap: 8px !important; } .price-box { padding: 8px !important; display: flex; align-items: center; gap: 8px; } .price-content { display: flex; align-items: center; gap: 8px; width: 100%; } .price-icon { margin: 0 !important; width: 16px !important; height: 16px !important; } .price-label { display: none; } .price-value { font-size: 16px !important; margin: 0 !important; } ` : ''; this.shadowRoot.innerHTML = ` ${titleHtml}
${buyAlert && !isSuperCompact ? `
${this.translate('alert')}
` : ''} ${isSuperCompact ? `
${this.formatPrice(buyPrice)}
` : `
${this.translate('buy_price')}
${this.formatPrice(buyPrice)}
${attributeHtml(buyAttribute)} ${buyWidgetHtml} `}
${sellAlert && !isSuperCompact ? `
${this.translate('alert')}
` : ''} ${isSuperCompact ? `
${this.formatPrice(sellPrice)}
` : `
${this.translate('sell_price')}
${this.formatPrice(sellPrice)}
${attributeHtml(sellAttribute)} ${sellWidgetHtml} `}
${legendHtml}
`; // Dodaj event listenery po renderowaniu if (this._config.click_action !== 'none') { this.shadowRoot.querySelectorAll('.price-box').forEach((box, index) => { const entityId = index === 0 ? this._config.buy_entity : this._config.sell_entity; box.addEventListener('click', () => this.handleClick(entityId)); }); } } } // Rejestracja karty customElements.define('pstryk-card', PstrykCard); // Dodaj do okna dla łatwego debugowania window.customCards = window.customCards || []; window.customCards.push({ type: 'pstryk-card', name: 'Pstryk Energy Card', description: 'Display energy prices with color coding, widgets (sparkline/bars), and alerts', preview: true, version: '4.1.0' });