OMIE - obter médias num ciclo hoje e amanha

Boas pessoas estou a tentar obter a media de valores do mercado OMIE no ciclo de faturação para depois usar em outros sensore para no fim calcular custo de energia no mercado indexado para depois tomar decições de manter ou mudar.

Antes de mais eu sei que existe a integração OMIE mas tem um pequeno snão recentemente tive aguns reboots a maquina e os sensores ficaram sem dados necessarios para obter o desejavel. Estava a usar query sql para obter a media de valores entre datas mas a ausencia de alguns valores gera dados errados.

Estou a utilizar AI para me facilitar a vida :sweat_smile:

Neste momento estou a usar os ficheiros da OMIE para gerar a informação mais correta mas estou a obter um valor sobre o preço de ‘amanha’ errado, desconfio que seja pelo factor das 23h.

image

site: tiagofelicia.pt
21/02/2026 até 07/03/2026

image

site: tiagofelicia.pt
21/02/2026 até 08/03/2026
image

do mesmo flow se verificar o dia isolado para ‘amanha’ o valor esta certo:

image

flow:

[
    {
        "id": "inj_range_001",
        "type": "inject",
        "z": "fd173bdc0d418401",
        "name": "run range",
        "props": [
            {
                "p": "startDate",
                "v": "2026-02-21",
                "vt": "str"
            },
            {
                "p": "endDate",
                "v": "2026-03-07",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 160,
        "y": 1900,
        "wires": [
            [
                "fn_build_range_001"
            ]
        ]
    },
    {
        "id": "fn_build_range_001",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "build date range",
        "func": "function buildUrl(d){\n  const [y,m,dd] = d.split('-');\n  const s = `${dd}_${m}_${y}`;\n  return `https://www.omie.es/sites/default/files/dados/AGNO_${y}/MES_${m}/TXT/INT_PBC_EV_H_1_${s}_${s}.TXT`;\n}\n\nfunction formatDateLisbon(date) {\n  return new Intl.DateTimeFormat('en-CA', {\n    timeZone: 'Europe/Lisbon',\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit'\n  }).format(date);\n}\n\nfunction addDays(date, days) {\n  const d = new Date(date);\n  d.setDate(d.getDate() + days);\n  return d;\n}\n\nconst startDateStr = msg.startDate;\nconst endDateStr   = msg.endDate;\n\nif (!startDateStr || !endDateStr) {\n  node.error(\"Faltam startDate e/ou endDate no msg.payload\");\n  return null;\n}\n\nconst startDate = new Date(`${startDateStr}T00:00:00`);\nconst endDate   = new Date(`${endDateStr}T00:00:00`);\n\nif (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {\n  node.error(\"Datas inválidas. Usa o formato YYYY-MM-DD\");\n  return null;\n}\n\nif (startDate > endDate) {\n  node.error(\"startDate não pode ser maior que endDate\");\n  return null;\n}\n\nconst items = [];\nlet current = new Date(startDate);\n\nwhile (current <= endDate) {\n  const today = formatDateLisbon(current);\n  const tomorrow = formatDateLisbon(addDays(current, 1));\n\n  items.push({\n    today_date: today,\n    tomorrow_date: tomorrow,\n    today_url: buildUrl(today),\n    tomorrow_url: buildUrl(tomorrow)\n  });\n\n  current.setDate(current.getDate() + 1);\n}\n\nmsg.payload = items;\nmsg.parts = undefined;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 360,
        "y": 1900,
        "wires": [
            [
                "split_range_001"
            ]
        ]
    },
    {
        "id": "split_range_001",
        "type": "split",
        "z": "fd173bdc0d418401",
        "name": "split dates",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "property": "payload",
        "x": 340,
        "y": 1940,
        "wires": [
            [
                "fn_prepare_today_001"
            ]
        ]
    },
    {
        "id": "fn_prepare_today_001",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "prepare today request",
        "func": "msg.today_date = msg.payload.today_date;\nmsg.tomorrow_date = msg.payload.tomorrow_date;\nmsg.today_url = msg.payload.today_url;\nmsg.tomorrow_url = msg.payload.tomorrow_url;\nmsg.url = msg.today_url;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 380,
        "y": 1980,
        "wires": [
            [
                "http_today_001"
            ]
        ]
    },
    {
        "id": "http_today_001",
        "type": "http request",
        "z": "fd173bdc0d418401",
        "name": "fetch today",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 350,
        "y": 2020,
        "wires": [
            [
                "fn_store_today_001"
            ]
        ]
    },
    {
        "id": "fn_store_today_001",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "store today raw",
        "func": "msg.today_raw = msg.payload;\nmsg.url = msg.tomorrow_url;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 360,
        "y": 2060,
        "wires": [
            [
                "http_tomorrow_001"
            ]
        ]
    },
    {
        "id": "http_tomorrow_001",
        "type": "http request",
        "z": "fd173bdc0d418401",
        "name": "fetch tomorrow",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 360,
        "y": 2100,
        "wires": [
            [
                "fn_parse_pair_001"
            ]
        ]
    },
    {
        "id": "fn_parse_pair_001",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "parse today + tomorrow",
        "func": "function parseOMIE(text) {\n    if (!text || typeof text !== 'string') return [];\n\n    const lines = text.split('\\n').map(l => l.trim()).filter(l => l);\n    const dataLines = lines.slice(2);\n    const series = {};\n\n    for (const line of dataLines) {\n        const cols = line.split(';').map(c => c.trim());\n        const name = cols[0];\n        if (!name) continue;\n\n        const values = [];\n        for (let i = 1; i < cols.length; i++) {\n            if (!cols[i]) continue;\n            const val = parseFloat(cols[i].replace(',', '.'));\n            if (!isNaN(val)) values.push(val);\n        }\n\n        if (values.length > 0) series[name] = values;\n    }\n\n    const keys = Object.keys(series);\n    const ptKey = keys.find(k => k.toLowerCase().includes('portugu'));\n    return ptKey ? series[ptKey] : [];\n}\n\nfunction mean(arr) {\n    if (!arr || arr.length === 0) return null;\n    return Math.round((arr.reduce((a, b) => a + b, 0) / arr.length) * 100) / 100;\n}\n\nconst today_pt = parseOMIE(msg.today_raw);\nconst tomorrow_pt = parseOMIE(msg.payload);\n\nconst today_lisbon_pt = [...today_pt.slice(4), ...tomorrow_pt.slice(0, 4)];\nconst tomorrow_lisbon_pt = tomorrow_pt.slice(4);\n\nmsg.payload = {\n    today_date: msg.today_date,\n    tomorrow_date: msg.tomorrow_date,\n    today_url: msg.today_url,\n    tomorrow_url: msg.tomorrow_url,\n    today_pt: today_pt,\n    tomorrow_pt: tomorrow_pt,\n    OMIE_today_avg_pt: mean(today_pt),\n    OMIE_tomorrow_avg_pt: mean(tomorrow_pt),\n    today_avg_pt: mean(today_lisbon_pt),\n    tomorrow_avg_pt: mean(tomorrow_lisbon_pt)\n};\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 390,
        "y": 2140,
        "wires": [
            [
                "join_results_001"
            ]
        ]
    },
    {
        "id": "join_results_001",
        "type": "join",
        "z": "fd173bdc0d418401",
        "name": "join results",
        "mode": "auto",
        "build": "array",
        "property": "payload",
        "propertyType": "msg",
        "key": "topic",
        "joiner": "\\n",
        "joinerType": "str",
        "useparts": true,
        "accumulate": false,
        "timeout": "",
        "count": "",
        "reduceRight": false,
        "reduceExp": "",
        "reduceInit": "",
        "reduceInitType": "",
        "reduceFixup": "",
        "x": 350,
        "y": 2180,
        "wires": [
            [
                "fn_summary_001"
            ]
        ]
    },
    {
        "id": "fn_summary_001",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "summary",
        "func": "const rows = msg.payload || [];\n\nfunction mean(values) {\n  const valid = values.filter(v => typeof v === 'number' && !isNaN(v));\n  if (valid.length === 0) return null;\n  return Math.round((valid.reduce((a, b) => a + b, 0) / valid.length) * 100) / 100;\n}\n\nmsg.payload = {\n  total_days: rows.length,\n  start_date: rows.length ? rows[0].today_date : null,\n  end_date: rows.length ? rows[rows.length - 1].today_date : null,\n\n  period_avg: {\n    OMIE_today_avg_pt: mean(rows.map(r => r.OMIE_today_avg_pt)),\n    OMIE_tomorrow_avg_pt: mean(rows.map(r => r.OMIE_tomorrow_avg_pt)),\n    today_avg_pt: mean(rows.map(r => r.today_avg_pt)),\n    tomorrow_avg_pt: mean(rows.map(r => r.tomorrow_avg_pt))\n  },\n\n  results: rows\n};\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 340,
        "y": 2220,
        "wires": [
            [
                "debug_results_001"
            ]
        ]
    },
    {
        "id": "debug_results_001",
        "type": "debug",
        "z": "fd173bdc0d418401",
        "name": "results",
        "active": true,
        "tosidebar": true,
        "complete": "payload",
        "targetType": "msg",
        "x": 510,
        "y": 2220,
        "wires": []
    }
]

@luuuis uma ajudinha :smiley:

penso ja ter encontrado a solução e refiz o flow. estava a faltar valores do dia anterior…

image

image
image

[
    {
        "id": "540a581db411bf9b",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "create_OMIE_url",
        "func": "const session = msg.session || 1;\nconst ss = String(session);\nconst baseUrl = 'https://www.omie.es/sites/default/files/dados/';\n\nconst startParts = msg.startDate.split('-');\nconst endParts   = msg.endDate.split('-');\n\nconst startDate = new Date(parseInt(startParts[0]), parseInt(startParts[1]) - 1, parseInt(startParts[2]));\nconst endDate   = new Date(parseInt(endParts[0]),   parseInt(endParts[1]) - 1,   parseInt(endParts[2]));\n\nconst urls = [];\n\nfor (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {\n    const yyyy = String(d.getFullYear());\n    const mm   = String(d.getMonth() + 1).padStart(2, '0');\n    const dd   = String(d.getDate()).padStart(2, '0');\n    const url = `${baseUrl}AGNO_${yyyy}/MES_${mm}/TXT/INT_PBC_EV_H_${ss}_${dd}_${mm}_${yyyy}_${dd}_${mm}_${yyyy}.TXT`;\n    urls.push({ date: `${yyyy}-${mm}-${dd}`, url: url });\n}\n\nmsg.payload = urls;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 470,
        "y": 2700,
        "wires": [
            [
                "d0b347c80d584a57"
            ]
        ]
    },
    {
        "id": "d0b347c80d584a57",
        "type": "split",
        "z": "fd173bdc0d418401",
        "name": "split_url",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "property": "payload",
        "x": 640,
        "y": 2700,
        "wires": [
            [
                "74b2d7866c326778"
            ]
        ]
    },
    {
        "id": "74b2d7866c326778",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "set_url",
        "func": "msg.url  = msg.payload.url;\nmsg.date = msg.payload.date;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 770,
        "y": 2700,
        "wires": [
            [
                "61cd09df0c15230e"
            ]
        ]
    },
    {
        "id": "61cd09df0c15230e",
        "type": "http request",
        "z": "fd173bdc0d418401",
        "name": "OMIE_data",
        "method": "GET",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [],
        "x": 910,
        "y": 2700,
        "wires": [
            [
                "a4b51ce07f4ef862"
            ]
        ]
    },
    {
        "id": "a4b51ce07f4ef862",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "pack_response",
        "func": "if (msg.statusCode !== 200) {\n    node.warn(`Erro ${msg.date}: HTTP ${msg.statusCode}`);\n    return null;\n}\nmsg.payload = {\n    date: msg.date,\n    url: msg.url,\n    content: msg.payload\n};\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 200,
        "y": 2740,
        "wires": [
            [
                "38722438e50994d7"
            ]
        ]
    },
    {
        "id": "38722438e50994d7",
        "type": "join",
        "z": "fd173bdc0d418401",
        "name": "join",
        "mode": "auto",
        "build": "array",
        "property": "payload",
        "propertyType": "msg",
        "key": "topic",
        "joiner": "\\n",
        "joinerType": "str",
        "useparts": true,
        "accumulate": false,
        "timeout": "",
        "count": "",
        "reduceRight": false,
        "x": 350,
        "y": 2740,
        "wires": [
            [
                "2e3d2a689b55a36d"
            ]
        ]
    },
    {
        "id": "2e3d2a689b55a36d",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "group_date",
        "func": "msg.payload = msg.payload\n    .sort((a, b) => a.date.localeCompare(b.date))\n    .reduce((acc, item) => {\n        acc[item.date] = {\n            url: item.url,\n            content: item.content\n        };\n        return acc;\n    }, {});\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 490,
        "y": 2740,
        "wires": [
            [
                "710a999b9df4d2e2"
            ]
        ]
    },
    {
        "id": "4224ed2231f8756c",
        "type": "debug",
        "z": "fd173bdc0d418401",
        "name": "resultado",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 1060,
        "y": 2740,
        "wires": []
    },
    {
        "id": "b8b4ce6107398906",
        "type": "inject",
        "z": "fd173bdc0d418401",
        "name": "range_date",
        "props": [
            {
                "p": "startDate",
                "v": "2026-02-21",
                "vt": "str"
            },
            {
                "p": "endDate",
                "v": "2026-03-07",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "x": 130,
        "y": 2700,
        "wires": [
            [
                "cb1d87e032b5afaa"
            ]
        ]
    },
    {
        "id": "cb1d87e032b5afaa",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "dates_offset",
        "func": "const parseDate = (str) => {\n    const [y, m, d] = str.split('-').map(Number);\n    return new Date(y, m - 1, d);\n};\n\nconst formatDate = (date) => {\n    const yyyy = date.getFullYear();\n    const mm = String(date.getMonth() + 1).padStart(2, '0');\n    const dd = String(date.getDate()).padStart(2, '0');\n    return `${yyyy}-${mm}-${dd}`;\n};\n\nconst start = parseDate(msg.startDate);\nconst end   = parseDate(msg.endDate);\n\nstart.setDate(start.getDate() - 1);\nend.setDate(end.getDate() + 1);\n\nmsg.startDate = formatDate(start);\nmsg.endDate   = formatDate(end);\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 290,
        "y": 2700,
        "wires": [
            [
                "540a581db411bf9b"
            ]
        ]
    },
    {
        "id": "295b67541d06b748",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "aggregate_today_tomorrow",
        "func": "const data = msg.payload;\nconst dates = Object.keys(data).sort();\n\n// today: ignora primeiro e último\nconst todayDates = dates.slice(1, -1);\nconst todayCount = todayDates.reduce((acc, d) => acc + data[d].count, 0);\nconst todaySum = todayDates.reduce((acc, d) => acc + data[d].sum, 0);\n\n// tomorrow: ignora apenas o primeiro\nconst tomorrowDates = dates.slice(1);\nconst tomorrowCount = tomorrowDates.reduce((acc, d) => acc + data[d].count, 0);\nconst tomorrowSum = tomorrowDates.reduce((acc, d) => acc + data[d].sum, 0);\n\nmsg.payload = {\n    today: {\n        start_date: dates[1] || null,\n        end_date: dates[dates.length - 2] || null,\n        count: todayCount,\n        sum: parseFloat(todaySum.toFixed(2)),\n        avg: parseFloat((todaySum / todayCount).toFixed(2))\n    },\n    tomorrow: {\n        start_date: dates[1] || null,\n        end_date: dates[dates.length - 1] || null,\n        count: tomorrowCount,\n        sum: parseFloat(tomorrowSum.toFixed(2)),\n        avg: parseFloat((tomorrowSum / tomorrowCount).toFixed(2))\n    }\n};\n\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 860,
        "y": 2740,
        "wires": [
            [
                "4224ed2231f8756c"
            ]
        ]
    },
    {
        "id": "710a999b9df4d2e2",
        "type": "function",
        "z": "fd173bdc0d418401",
        "name": "parse_prices",
        "func": "const data = msg.payload;\nconst dates = Object.keys(data).sort();\nconst result = {};\n\nfunction extractValues(content) {\n    const lines = content.split('\\n');\n    const priceLine = lines.find(l => l.includes('Precio marginal en el sistema portugu'));\n    if (!priceLine) return null;\n\n    return priceLine.split(';').slice(1)\n        .map(v => v.trim().replace(',', '.'))\n        .filter(v => v !== '' && !isNaN(v))\n        .map(v => parseFloat(v));\n}\n\n// Deteta formato: <=24 valores = hora a hora (step 60min), caso contrário = quarto de hora (step 15min)\nfunction detectStep(values) {\n    return values.length <= 24 ? 60 : 15;\n}\n\nfor (let i = 0; i < dates.length; i++) {\n    const date = dates[i];\n    const nextDate = dates[i + 1];\n\n    const values = extractValues(data[date].content);\n    if (!values) {\n        result[date] = { error: 'linha de preços não encontrada' };\n        continue;\n    }\n\n    const step = detectStep(values);\n    const periodsPerHour = 60 / step; // 1 para hourly, 4 para quarterly\n    const startOffset = periodsPerHour; // salta 1h (hora 00:xx) do dia atual\n    const tailCount   = periodsPerHour; // busca 1h do dia seguinte\n\n    const ptValues = values.slice(startOffset);\n\n    if (nextDate) {\n        const nextValues = extractValues(data[nextDate].content);\n        if (nextValues) {\n            ptValues.push(...nextValues.slice(0, tailCount));\n        }\n    }\n\n    const sum = ptValues.reduce((a, b) => a + b, 0);\n    const avg = sum / ptValues.length;\n\n    // Labels dinâmicos: começa em 01:00 com step correto\n    const quarters = {};\n    ptValues.forEach((v, idx) => {\n        const totalMinutes = (periodsPerHour * 60) + (idx * step);\n        const hh = String(Math.floor(totalMinutes / 60) % 24).padStart(2, '0');\n        const mm = String(totalMinutes % 60).padStart(2, '0');\n        quarters[`${hh}:${mm}`] = v;\n    });\n\n    result[date] = {\n        count: ptValues.length,\n        step,                          // 60 ou 15 — útil para debug\n        sum:   parseFloat(sum.toFixed(2)),\n        avg:   parseFloat(avg.toFixed(2)),\n        quarters\n    };\n}\n\nmsg.payload = result;\nreturn msg;",
        "outputs": 1,
        "timeout": "",
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 650,
        "y": 2740,
        "wires": [
            [
                "295b67541d06b748"
            ]
        ]
    }
]

Para calcular a média de vários dias penso até podes ignorar a diferença horária, não?

Se quiseres acelerar a coisa podes ir buscar as médias directamente aos ficheiros mensais, assim só tens 1 ou 2 ficheiros para processar.

https://www.omie.es/sites/default/files/dados/AGNO_2026/MES_03/TXT/INT_MERCADO_DIARIO_MIN_MAX_9_01_03_2026_31_03_2026.TXT

1 Curtiu

A minha ideia é calcular a média de ciclo de faturação. No meu caso começa a 21.
E como quero acompanhar diariamente o valor tenho de saber até ao dia de hoje e saber o de amanhã e recomeçar novamente no próximo dia 21.

não vi como posso fazer com esse txt

Com o anterior post ele vai buscar o txt de cada dia até ao dia de hoje e assim consigo obter os dados.

Mas se tiver margem de melhorias estou aberto às sugestões claro.


Copyright © 2017-. Todos os direitos reservados
CPHA.pt - info@cpha.pt


FAQ | Termos de Serviço/Regras | Política de Privacidade