33const Homey = require ( 'homey' ) ;
44const axios = require ( 'axios' ) ;
55const PriceCalculator = require ( '../../helpers/PriceCalculator' ) ;
6+ const Logger = require ( '../../helpers/Logger' ) ;
67
78class SpotPriceAPI {
8- constructor ( homey ) {
9- this . homey = homey ;
10- this . baseUrl = 'https://spotovaelektrina.cz/api/v1/price' ;
11- this . backupUrl = 'https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh/@@chart-data' ;
12- this . exchangeRateUrl = 'https://data.kurzy.cz/json/meny/b[6].json' ;
13- this . exchangeRate = 25.25 ;
14- this . homeyTimezone = this . homey . clock . getTimezone ( ) ;
15- this . priceCalculator = new PriceCalculator ( this . homey ) ;
16- this . logger = null ;
17- }
9+ constructor ( homey , deviceContext = 'SpotPriceAPI' ) { // zde byl problém
10+ this . homey = homey ;
11+ // Vytvoříme vlastní instanci loggeru pro API
12+ this . logger = new Logger ( this . homey , deviceContext ) ; // používáme deviceContext, ne context
13+ // Defaultně zapneme logging pro API
14+ this . logger . setEnabled ( true ) ;
15+
16+ this . baseUrl = 'https://spotovaelektrina.cz/api/v1/price' ;
17+ const today = new Date ( ) . toISOString ( ) . slice ( 0 , 10 ) ; // získá datum ve formátu RRRR-MM-DD
18+ this . backupUrl = `https://www.ote-cr.cz/cs/kratkodobe-trhy/elektrina/denni-trh/@@chart-data?date=${ today } ` ;
19+ this . exchangeRateUrl = 'https://data.kurzy.cz/json/meny/b[6].json' ;
20+ this . exchangeRate = 25.25 ;
21+ this . homeyTimezone = this . homey . clock . getTimezone ( ) ;
22+ this . priceCalculator = new PriceCalculator ( this . homey , 'PriceCalculator' ) ;
23+
24+ this . logger . debug ( 'SpotPriceAPI inicializován' ) ;
25+ }
1826
1927 setLogger ( logger ) {
2028 this . logger = logger ;
@@ -133,8 +141,24 @@ class SpotPriceAPI {
133141 // Pokus o získání dat z primárního API
134142 const data = await this . _fetchFromPrimaryAPI ( timeoutMs ) ;
135143
144+ // Logování vrácených dat z `_fetchFromPrimaryAPI`
145+ if ( this . logger ) {
146+ this . logger . debug ( 'Výsledná data vrácená z _fetchFromPrimaryAPI' , { data } ) ;
147+ }
148+
149+ // Ověříme, že `data` je správně definována, a logujeme
150+ if ( ! data ) {
151+ this . logger . error ( 'Chyba: data jsou undefined po volání _fetchFromPrimaryAPI' ) ;
152+ throw new Error ( 'Data z primárního API jsou undefined' ) ;
153+ }
154+
155+ // Logování dat před validací
156+ if ( this . logger ) {
157+ this . logger . debug ( 'Data předaná do validatePriceData' , { hoursToday : data } ) ;
158+ }
159+
136160 // Validace dat z primárního API
137- if ( ! this . priceCalculator . validatePriceData ( data . hoursToday ) ) {
161+ if ( ! this . priceCalculator . validatePriceData ( data ) ) {
138162 throw new Error ( 'Neplatný formát dat z primárního API' ) ;
139163 }
140164
@@ -145,7 +169,7 @@ class SpotPriceAPI {
145169 this . logger . log ( 'Data úspěšně získána z primárního API' , { source : 'Primary API' } ) ;
146170 }
147171
148- return data . hoursToday ;
172+ return data ;
149173
150174 } catch ( error ) {
151175 spotElektrinaError = error ;
@@ -212,119 +236,214 @@ class SpotPriceAPI {
212236
213237// Pomocná metoda pro volání primárního API
214238async _fetchFromPrimaryAPI ( timeoutMs ) {
215- const url = `${ this . baseUrl } /get-prices-json` ;
216- const controller = new AbortController ( ) ;
217- const timeout = setTimeout ( ( ) => controller . abort ( ) , timeoutMs ) ;
239+ const url = `${ this . baseUrl } /get-prices-json` ;
240+ let timeout ;
218241
219- if ( this . logger ) {
220- this . logger . debug ( 'Volání primárního API pro získání cen' , { url, timeoutMs } ) ;
221- }
242+ if ( this . logger ) {
243+ this . logger . debug ( 'Volání primárního API pro získání cen' , { url, timeoutMs } ) ;
244+ }
222245
223- try {
224- const response = await fetch ( url , { signal : controller . signal } ) ;
225- clearTimeout ( timeout ) ;
226-
227- if ( ! response . ok ) {
228- const errorMessage = `HTTP error! status: ${ response . status } ` ;
229- if ( this . logger ) {
230- this . logger . error ( 'Chyba při volání primárního API' , new Error ( errorMessage ) , { url, status : response . status } ) ;
231- }
232- throw new Error ( errorMessage ) ;
233- }
234-
235- const data = await response . json ( ) ;
236-
237- if ( ! Array . isArray ( data . hoursToday ) || data . hoursToday . length !== 24 ) {
238- const invalidDataError = new Error ( 'Neplatná struktura dat z API' ) ;
239- if ( this . logger ) {
240- this . logger . error ( 'Neplatná struktura dat z primárního API' , invalidDataError , { url } ) ;
241- }
242- throw invalidDataError ;
243- }
244-
245- if ( this . logger ) {
246- this . logger . debug ( 'Data úspěšně načtena z primárního API' , { url, dataLength : data . hoursToday . length } ) ;
247- }
248-
249- return data ;
250-
251- } catch ( error ) {
252- if ( error . name === 'AbortError' ) {
253- const timeoutError = new Error ( 'Timeout při volání API' ) ;
254- if ( this . logger ) {
255- this . logger . error ( 'Timeout při volání primárního API' , timeoutError , { url, timeoutMs } ) ;
256- }
257- throw timeoutError ;
258- }
259-
260- if ( this . logger ) {
261- this . logger . error ( 'Neočekávaná chyba při volání primárního API' , error , { url } ) ;
262- }
263- throw error ;
264-
265- } finally {
266- clearTimeout ( timeout ) ;
267- }
268- }
246+ try {
247+ const source = axios . CancelToken . source ( ) ;
248+ timeout = setTimeout ( ( ) => {
249+ source . cancel ( `Timeout při volání primárního API po ${ timeoutMs } ms` ) ;
250+ } , timeoutMs ) ;
251+
252+ const response = await axios . get ( url , { cancelToken : source . token } ) ;
253+ clearTimeout ( timeout ) ;
269254
255+ if ( response . status !== 200 ) {
256+ const errorMessage = `HTTP error! status: ${ response . status } ` ;
257+ if ( this . logger ) {
258+ this . logger . error ( 'Chyba při volání primárního API' , new Error ( errorMessage ) , { url, status : response . status } ) ;
259+ }
260+ throw new Error ( errorMessage ) ;
261+ }
262+
263+ const data = response . data ;
264+
265+ // Logování celé vstupní hodnoty z API bez ořezání
266+ if ( this . logger ) {
267+ this . logger . debug ( 'Vstupní hodnota z primárního API' , {
268+ url,
269+ receivedData : data // Log celého objektu bez ořezání
270+ } ) ;
271+ }
272+
273+ // Ověření struktury dat
274+ if ( ! data . hoursToday || ! Array . isArray ( data . hoursToday ) || data . hoursToday . length !== 24 ) {
275+ const invalidDataError = new Error ( 'Neplatná struktura dat z API' ) ;
276+ if ( this . logger ) {
277+ this . logger . error ( 'Neplatná struktura dat z primárního API' , invalidDataError , {
278+ url,
279+ receivedData : data // Log celého objektu bez ořezání při chybě
280+ } ) ;
281+ }
282+ throw invalidDataError ;
283+ }
284+
285+ // Logování celé výstupní hodnoty z funkce bez ořezání
286+ if ( this . logger ) {
287+ this . logger . debug ( 'Výstupní hodnota z _fetchFromPrimaryAPI' , {
288+ hoursToday : data . hoursToday // Log celého pole `hoursToday` bez ořezání
289+ } ) ;
290+ }
291+
292+ // Vrátíme pouze pole `hoursToday`
293+ return data . hoursToday ;
294+
295+ } catch ( error ) {
296+ if ( axios . isCancel ( error ) ) {
297+ const timeoutError = new Error ( 'Timeout při volání API' ) ;
298+ if ( this . logger ) {
299+ this . logger . error ( 'Timeout při volání primárního API' , timeoutError , { url, timeoutMs } ) ;
300+ }
301+ throw timeoutError ;
302+ }
303+
304+ if ( this . logger ) {
305+ this . logger . error ( 'Neočekávaná chyba při volání primárního API' , error , { url } ) ;
306+ }
307+ throw error ;
308+
309+ } finally {
310+ if ( timeout ) clearTimeout ( timeout ) ;
311+ }
312+ }
270313
271314async getBackupDailyPrices ( device ) {
272- try {
273- if ( this . logger ) {
274- this . logger . debug ( 'Začátek získávání cen ze záložního API' , { url : this . backupUrl } ) ;
275- }
276-
277- await this . updateExchangeRate ( ) ;
278- const timeInfo = this . getCurrentTimeInfo ( ) ;
279-
280- if ( this . logger ) {
281- this . logger . debug ( 'Načítání dat z backup API s parametry' , { date : timeInfo . date } ) ;
282- }
283-
284- const response = await axios . get ( this . backupUrl , {
285- params : { report_date : timeInfo . date }
286- } ) ;
287-
288- const data = response . data ;
289- const dataLine = data ?. data ?. dataLine . find ( line => line . title === "Cena (EUR/MWh)" ) ;
290-
291- if ( ! dataLine || ! Array . isArray ( dataLine . point ) ) {
292- const errorMessage = 'Invalid data structure from backup API' ;
293- if ( this . logger ) {
294- this . logger . error ( errorMessage , new Error ( errorMessage ) , { url : this . backupUrl } ) ;
295- }
296- throw new Error ( errorMessage ) ;
297- }
298-
299- const hoursToday = dataLine . point . slice ( 0 , 24 ) . map ( ( point , index ) => {
300- let priceCZK = point . y * this . exchangeRate ;
301- return { hour : index , priceCZK : parseFloat ( priceCZK . toFixed ( 2 ) ) } ;
302- } ) ;
303-
304- // Validace dat pomocí PriceCalculatoru
305- if ( ! this . priceCalculator . validatePriceData ( hoursToday ) ) {
306- const validationError = new Error ( 'Invalid backup price data format' ) ;
307- if ( this . logger ) {
308- this . logger . error ( 'Chyba validace dat záložního API' , validationError , { url : this . backupUrl } ) ;
309- }
310- throw validationError ;
311- }
312-
313- if ( this . logger ) {
314- this . logger . debug ( 'Úspěšné načtení cen ze záložního API' , { url : this . backupUrl , dataLength : hoursToday . length } ) ;
315- }
316-
317- return hoursToday ;
315+ try {
316+ if ( this . logger ) {
317+ this . logger . debug ( 'Začátek získávání cen ze záložního API' , { url : this . backupUrl } ) ;
318+ }
319+
320+ await this . updateExchangeRate ( ) ;
321+ const timeInfo = this . getCurrentTimeInfo ( ) ;
322+
323+ if ( this . logger ) {
324+ this . logger . debug ( 'Načítání dat z backup API s parametry' , { date : timeInfo . date } ) ;
325+ }
326+
327+ const response = await axios . get ( this . backupUrl , {
328+ params : { report_date : timeInfo . date }
329+ } ) ;
330+
331+ const data = response . data ;
332+
333+ // Najdeme data pro ceny v EUR/MWh
334+ const dataLine = data ?. data ?. dataLine . find ( line => line . title === "Cena (EUR/MWh)" ) ;
335+
336+ if ( ! dataLine || ! Array . isArray ( dataLine . point ) ) {
337+ const errorMessage = 'Invalid data structure from backup API' ;
338+ if ( this . logger ) {
339+ this . logger . error ( errorMessage , new Error ( errorMessage ) , { url : this . backupUrl } ) ;
340+ }
341+ throw new Error ( errorMessage ) ;
342+ }
343+
344+ // Kontrola vstupních dat
345+ if ( dataLine . point . length < 24 ) {
346+ const error = new Error ( `Nedostatečný počet hodin ve vstupních datech: ${ dataLine . point . length } ` ) ;
347+ if ( this . logger ) {
348+ this . logger . error ( 'Chyba vstupních dat' , error ) ;
349+ }
350+ throw error ;
351+ }
352+
353+ // Map pro konverzi hodin 1-24 na 0-23
354+ const hourMap = new Map ( [ ...Array ( 24 ) ] . map ( ( _ , i ) => [ i + 1 , i === 24 ? 0 : i ] ) ) ;
355+
356+ // Převod hodin a výpočet ceny v CZK
357+ const hoursToday = dataLine . point . slice ( 0 , 24 ) . map ( point => {
358+ try {
359+ // Převod x (1-24) na hour (0-23)
360+ const inputHour = parseInt ( point . x , 10 ) ;
361+ if ( ! hourMap . has ( inputHour ) ) {
362+ throw new Error ( `Neplatná vstupní hodina: ${ inputHour } ` ) ;
363+ }
364+ const hour = hourMap . get ( inputHour ) ;
365+
366+ // Zpracování ceny
367+ const priceEUR = parseFloat ( point . y ) ;
368+ if ( isNaN ( priceEUR ) ) {
369+ throw new Error ( `Neplatná cena pro hodinu ${ inputHour } : ${ point . y } ` ) ;
370+ }
371+
372+ const priceCZK = priceEUR * this . exchangeRate ;
373+
374+ if ( this . logger ) {
375+ this . logger . debug ( 'Mapování hodiny' , {
376+ vstupníHodina : inputHour ,
377+ výstupníHodina : hour ,
378+ vstupníCenaEUR : priceEUR ,
379+ výstupníCenaCZK : priceCZK
380+ } ) ;
381+ }
382+
383+ return {
384+ hour,
385+ priceCZK : parseFloat ( priceCZK . toFixed ( 2 ) ) ,
386+ priceEur : priceEUR // pro debugging
387+ } ;
388+ } catch ( error ) {
389+ if ( this . logger ) {
390+ this . logger . error ( 'Chyba při zpracování hodinových dat' , error , {
391+ point,
392+ exchangeRate : this . exchangeRate
393+ } ) ;
394+ }
395+ throw error ;
396+ }
397+ } ) ;
398+
399+ // Seřazení podle hodin (0-23)
400+ hoursToday . sort ( ( a , b ) => a . hour - b . hour ) ;
318401
402+ // Kontrola výstupních dat
403+ const hoursCheck = new Set ( hoursToday . map ( h => h . hour ) ) ;
404+ if ( hoursCheck . size !== 24 || ! [ ...hoursCheck ] . every ( h => h >= 0 && h <= 23 ) ) {
405+ const error = new Error ( 'Neplatná transformace hodin' ) ;
406+ if ( this . logger ) {
407+ this . logger . error ( 'Chyba výstupních dat' , error , {
408+ uniqueHours : [ ...hoursCheck ] . sort ( ( a , b ) => a - b )
409+ } ) ;
410+ }
411+ throw error ;
412+ }
413+
414+ // Validace dat pomocí PriceCalculatoru
415+ if ( ! this . priceCalculator . validatePriceData ( hoursToday ) ) {
416+ const validationError = new Error ( 'Invalid backup price data format' ) ;
417+ if ( this . logger ) {
418+ this . logger . error ( 'Chyba validace dat záložního API' , validationError , {
419+ url : this . backupUrl ,
420+ sampleData : hoursToday [ 0 ]
421+ } ) ;
422+ }
423+ throw validationError ;
424+ }
425+
426+ if ( this . logger ) {
427+ this . logger . debug ( 'Úspěšné načtení cen ze záložního API' , {
428+ url : this . backupUrl ,
429+ dataLength : hoursToday . length ,
430+ firstHour : hoursToday [ 0 ] ,
431+ lastHour : hoursToday [ 23 ]
432+ } ) ;
433+ }
434+
435+ return hoursToday ;
436+
319437 } catch ( error ) {
320- if ( this . logger ) {
321- this . logger . error ( 'Chyba při získávání cen ze záložního API' , error , { url : this . backupUrl } ) ;
322- } else {
323- this . homey . error ( 'Error fetching backup daily prices:' , error ) ;
324- }
325- throw error ;
438+ if ( this . logger ) {
439+ this . logger . error ( 'Chyba při získávání cen ze záložního API' , error , {
440+ url : this . backupUrl ,
441+ exchangeRate : this . exchangeRate
442+ } ) ;
443+ }
444+ throw error ;
326445 }
327- }
446+ }
328447
329448 async updateCurrentValues ( device ) {
330449 try {
0 commit comments