<?xml version="1.0" encoding="utf-8"?>
<Templates format="2" protocolVersion="83">
  <Template id="5b7428f3-e567-4f77-8edc-532a6946f1fb" revision="1.1">
    <RevisionHistory>
      <Revision id="1.0">Initial version</Revision>
      <Revision id="1.1">doplneni textu ohledne prirpavy dat</Revision>
    </RevisionHistory>
    <Name>SpotLogic Energy Optimizer</Name>
    <CcuModel>PacketParserCCU</CcuModel>
    <Producer>SpotLogic</Producer>
    <Model>SpotLogic API (optimize + dispatch)</Model>
    <Description>Functional and working, but getting the data in is painful until TapHome ships one feature. The forecast and price series (~500 values) have to be collected from your other scripts and pushed in through hundreds of helper devices, because TapHome cannot yet share one JSON between scripts (https://taphome.com/cs/ideas/script-data-http/). Until those series are filled, SpotLogic answers HTTP 422 and no plan is computed.</Description>
    <Category>2</Category>
    <ImportParameters>
      <Parameter>
        <Id>PlcId</Id>
        <Name>SpotLogic PLC ID (from the activation e-mail)</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>Token</Id>
        <Name>SpotLogic access token (from the activation e-mail, secret)</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>TimeZone</Id>
        <Name>Time zone (IANA name)</Name>
        <Value>Europe/Prague</Value>
      </Parameter>
      <Parameter>
        <Id>BatCapacity</Id>
        <Name>Battery capacity [kWh]</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>BatPmax</Id>
        <Name>Battery max charge and discharge power [kW]</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>GridImportPmax</Id>
        <Name>Max grid import power [kW] (0 = never import)</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>GridExportPmax</Id>
        <Name>Max grid export power [kW] (0 = never export)</Name>
        <Value/>
      </Parameter>
      <Parameter>
        <Id>SocMax</Id>
        <Name>Max state of charge [%] (recorded only)</Name>
        <Value>100</Value>
      </Parameter>
      <Parameter>
        <Id>SocHardMin</Id>
        <Name>Hard battery floor from the inverter manufacturer [%]</Name>
        <Value>10</Value>
      </Parameter>
    </ImportParameters>
    <Module>
      <Name>SpotLogic</Name>
      <Id>-1</Id>
      <Model>PacketParserModuleHttp</Model>
      <DeviceProperties>
        <IpAddress>spotlogic.cz</IpAddress>
        <Port>443</Port>
        <UseHttps>True</UseHttps>
        <HttpAuthentification>0</HttpAuthentification>
        <InternalPollInterval>20000</InternalPollInterval>
        <CustomVariables>[{&quot;Name&quot;: &quot;PlcId&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[PlcId]&quot;}, {&quot;Name&quot;: &quot;Token&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[Token]&quot;}, {&quot;Name&quot;: &quot;TimeZone&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[TimeZone]&quot;}, {&quot;Name&quot;: &quot;BatCapacity&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[BatCapacity]&quot;}, {&quot;Name&quot;: &quot;BatPmax&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[BatPmax]&quot;}, {&quot;Name&quot;: &quot;GridImportPmax&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[GridImportPmax]&quot;}, {&quot;Name&quot;: &quot;GridExportPmax&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[GridExportPmax]&quot;}, {&quot;Name&quot;: &quot;SocMax&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[SocMax]&quot;}, {&quot;Name&quot;: &quot;SocHardMin&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;$[SocHardMin]&quot;}, {&quot;Name&quot;: &quot;SocNow&quot;, &quot;ValueTypeId&quot;: 1, &quot;NumericValue&quot;: -1}, {&quot;Name&quot;: &quot;SocMin&quot;, &quot;ValueTypeId&quot;: 1, &quot;NumericValue&quot;: 10}, {&quot;Name&quot;: &quot;PvForecastTod&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;PvForecastTom&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;LoadForecastTod&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;LoadForecastTom&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;PriceImp&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;PriceExp&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;[]&quot;}, {&quot;Name&quot;: &quot;DispatchJson&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;&quot;}, {&quot;Name&quot;: &quot;DispatchCode&quot;, &quot;ValueTypeId&quot;: 1, &quot;NumericValue&quot;: 0}, {&quot;Name&quot;: &quot;OptimizeJson&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;&quot;}, {&quot;Name&quot;: &quot;OptimizeCode&quot;, &quot;ValueTypeId&quot;: 1, &quot;NumericValue&quot;: 0}, {&quot;Name&quot;: &quot;OptimizeAt&quot;, &quot;ValueTypeId&quot;: 2, &quot;NumericValue&quot;: 0, &quot;StringValue&quot;: &quot;&quot;}, {&quot;Name&quot;: &quot;LastOptimizeUnix&quot;, &quot;ValueTypeId&quot;: 1, &quot;NumericValue&quot;: 0}]</CustomVariables>
        <ReadScriptPacketParser># =============================================================================
#  STATUS: WORKING, BUT THE DATA MUST BE BROUGHT IN BY HAND
# =============================================================================
#  The integration itself works - dispatch reading, action_id -&gt; inverter mode
#  mapping, next command, plan diagnostics and error reporting are finished.
#  What it needs is the data: six series (PV forecast, load forecast, import and
#  export spot price; ~500 values, hourly or 15-minute, horizon now -&gt; end of
#  tomorrow) as JSON text [[unix_ts, value], ...] in the module variables
#  PvForecastTod ... PriceExp (see below).
#  Until TapHome ships a shared variable between scripts, those series must be
#  collected from other scripts and handed over through hundreds of helper
#  devices - that is why the integration is only really practical once that
#  feature lands.
#  Tracking: https://taphome.com/cs/ideas/script-data-http/  (TapHome idea FR-003)
#  In the meantime: fill the six variables below and the template works as is.
# =============================================================================

# 1) GET /api/v1/dispatch - the command for the slot in progress (module poll ~20 s).
#    plc_id and token travel ONLY in the query string (SpotLogic contract).
VAR r := SENDHTTPREQUEST(&quot;/api/v1/dispatch?plc_id=&quot; + PlcId + &quot;&amp;token=&quot; + Token);
DispatchCode := r.StatusCode;
DispatchJson := r.Content;
IF r.StatusCode = 401
    ADDERROR(1, &quot;SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token; the subscription may have expired. Not retried by SpotLogic rules, fix the credentials.&quot;);
ELSEIF r.StatusCode != 200 AND r.StatusCode != 404
    VAR dmsg := PARSEJSON(r.Content, &quot;$.message&quot;, true);
    ADDERROR(2, &quot;SpotLogic dispatch failed: HTTP &quot; + r.StatusCode + IF(ISNULL(dmsg), &quot;&quot;, &quot; &quot; + dmsg));
END

# 2) POST /api/v1/optimize - telemetry, at most every ~2 min (SpotLogic asks for no more often).
datetime now := NOW();
VAR nowUnix := now.UNIXTIME;
IF nowUnix - LastOptimizeUnix &gt;= 115
    LastOptimizeUnix := nowUnix;
    VAR cap := TODOUBLE(REPLACE(BatCapacity, &quot;,&quot;, &quot;.&quot;));
    VAR bp := TODOUBLE(REPLACE(BatPmax, &quot;,&quot;, &quot;.&quot;));
    VAR gi := TODOUBLE(REPLACE(GridImportPmax, &quot;,&quot;, &quot;.&quot;));
    VAR ge := TODOUBLE(REPLACE(GridExportPmax, &quot;,&quot;, &quot;.&quot;));
    VAR smax := TODOUBLE(SocMax);
    VAR shard := TODOUBLE(SocHardMin);
    IF SocNow &lt; 0 OR SocNow &gt; 100
        OptimizeCode := -1;
    ELSEIF ISNAN(cap) OR ISNAN(bp) OR ISNAN(gi) OR ISNAN(ge) OR ISNAN(smax) OR ISNAN(shard) OR LENGTH(PlcId) = 0 OR LENGTH(Token) = 0
        OptimizeCode := -2;
    ELSE
        # Forecast and price series: module variables PvForecastTod, PvForecastTom,
        # LoadForecastTod, LoadForecastTom, PriceImp, PriceExp hold JSON text
        # [[unix_ts, value], ...]. The template does NOT fill them - see the banner
        # at the top of this script. Until they are filled, SpotLogic answers 422.
        VAR body := &quot;{\&quot;PLCtype\&quot;:\&quot;taphome\&quot;&quot;;
        body := body + &quot;,\&quot;TimeZone\&quot;:\&quot;&quot; + TimeZone + &quot;\&quot;&quot;;
        body := body + &quot;,\&quot;TimeStampUnix\&quot;:&quot; + TOSTRING(nowUnix, &quot;F0&quot;);
        body := body + &quot;,\&quot;BatCapacity\&quot;:&quot; + TOSTRING(cap, &quot;F2&quot;);
        body := body + &quot;,\&quot;BatPmax\&quot;:&quot; + TOSTRING(bp, &quot;F2&quot;);
        body := body + &quot;,\&quot;GridImportPmax\&quot;:&quot; + TOSTRING(gi, &quot;F2&quot;);
        body := body + &quot;,\&quot;GridExportPmax\&quot;:&quot; + TOSTRING(ge, &quot;F2&quot;);
        body := body + &quot;,\&quot;SOCnow\&quot;:&quot; + TOSTRING(SocNow, &quot;F0&quot;);
        body := body + &quot;,\&quot;SOCmin\&quot;:&quot; + TOSTRING(SocMin, &quot;F0&quot;);
        body := body + &quot;,\&quot;SOCmax\&quot;:&quot; + TOSTRING(smax, &quot;F0&quot;);
        body := body + &quot;,\&quot;SOChardmin\&quot;:&quot; + TOSTRING(shard, &quot;F0&quot;);
        body := body + &quot;,\&quot;PvForecastTod\&quot;:&quot; + PvForecastTod;
        body := body + &quot;,\&quot;PvForecastTom\&quot;:&quot; + PvForecastTom;
        body := body + &quot;,\&quot;LoadForecastTod\&quot;:&quot; + LoadForecastTod;
        body := body + &quot;,\&quot;LoadForecastTom\&quot;:&quot; + LoadForecastTom;
        body := body + &quot;,\&quot;PriceImp\&quot;:&quot; + PriceImp;
        body := body + &quot;,\&quot;PriceExp\&quot;:&quot; + PriceExp + &quot;}&quot;;
        VAR o := SENDHTTPREQUEST(&quot;/api/v1/optimize?plc_id=&quot; + PlcId + &quot;&amp;token=&quot; + Token, &quot;POST&quot;, body, &quot;Content-Type: application/json&quot;);
        OptimizeCode := o.StatusCode;
        OptimizeJson := o.Content;
        OptimizeAt := TOSTRING(now, &quot;yyyy-MM-dd HH:mm:ss&quot;);
    END
END

# The outcome of the last telemetry upload is reported on every run, not only on the upload run.
VAR omsg := &quot;&quot;;
IF LENGTH(OptimizeJson) &gt; 0
    omsg := PARSEJSON(OptimizeJson, &quot;$.message&quot;, true);
    IF ISNULL(omsg)
        omsg := &quot;&quot;;
    END
END
IF OptimizeCode = -1
    ADDWARNING(3, &quot;Telemetry not sent: the device Battery state of charge (input) has no value 0-100 yet - write it from the inverter with a smart rule.&quot;);
ELSEIF OptimizeCode = -2
    ADDWARNING(7, &quot;Telemetry not sent: fill the module variables PlcId, Token, BatCapacity, BatPmax, GridImportPmax, GridExportPmax, SocMax and SocHardMin (numbers with a decimal point).&quot;);
ELSEIF OptimizeCode = 401
    ADDERROR(4, &quot;SpotLogic rejected the telemetry: PLC ID or token invalid (HTTP 401).&quot;);
ELSEIF OptimizeCode = 422
    ADDERROR(5, &quot;SpotLogic rejected the telemetry (HTTP 422): &quot; + omsg + &quot; - the forecast and price series (module variables PvForecastTod ... PriceExp) must be filled, see the template description.&quot;);
ELSEIF OptimizeCode != 0 AND OptimizeCode != 202
    ADDERROR(6, &quot;SpotLogic telemetry upload failed: HTTP &quot; + OptimizeCode + &quot; &quot; + omsg);
END</ReadScriptPacketParser>
        <ServiceAttributesScriptsPacketParser>[{&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Last telemetry upload&quot;}, &quot;ReadFormula&quot;: &quot;IF(OptimizeCode = 0, \&quot;not sent yet\&quot;, IF(OptimizeCode &lt; 0, \&quot;waiting for inputs\&quot;, \&quot;HTTP \&quot; + OptimizeCode + \&quot; at \&quot; + OptimizeAt))&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Plan recompute interval&quot;}, &quot;ReadFormula&quot;: &quot;VAR v := PARSEJSON(OptimizeJson, \&quot;$.data.recompute_interval_sec\&quot;, true);\nIF ISNULL(v)\n    RETURN(\&quot;-\&quot;);\nEND\nRETURN(TOSTRING(v, \&quot;F0\&quot;) + \&quot; s\&quot;);&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Plan computed at&quot;}, &quot;ReadFormula&quot;: &quot;VAR s := PARSEJSON(DispatchJson, \&quot;$.meta.plan_created_at\&quot;, true);\nIF ISNULL(s)\n    RETURN(\&quot;-\&quot;);\nEND\ndatetime t := DATETIME(s);\nVAR l := t.LOCALDATETIME;\nRETURN(TOSTRING(l, \&quot;yyyy-MM-dd HH:mm\&quot;));&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Plan run&quot;}, &quot;ReadFormula&quot;: &quot;VAR v := PARSEJSON(DispatchJson, \&quot;$.meta.run_id\&quot;, true);\nRETURN(IF(ISNULL(v), \&quot;-\&quot;, v));&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;API version&quot;}, &quot;ReadFormula&quot;: &quot;VAR v := PARSEJSON(DispatchJson, \&quot;$.meta.version.api\&quot;, true);\nRETURN(IF(ISNULL(v), \&quot;-\&quot;, v));&quot;}]</ServiceAttributesScriptsPacketParser>
      </DeviceProperties>
      <Devices>
        <Device>
          <Name>SpotLogic command</Name>
          <Id>-2</Id>
          <Model>PacketParserMultiValueSwitch</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>True</IsReadOnly>
            <ReadSwitchStateScriptPacketParser>IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, &quot;SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.&quot;);
    RETURN(NaN);
END
IF DispatchCode = 404
    ADDINFO(2, &quot;No usable plan yet (no_plan) - keep the inverter in its own safe mode.&quot;);
    RETURN(0);
END
IF DispatchCode != 200
    ADDERROR(3, &quot;Command not available: HTTP &quot; + DispatchCode);
    RETURN(NaN);
END
VAR id := PARSEJSON(DispatchJson, &quot;$.data.command.action_id&quot;, true);
IF ISNULL(id)
    ADDERROR(4, &quot;Unexpected SpotLogic answer: data.command.action_id is missing.&quot;);
    RETURN(NaN);
END
RETURN(id);</ReadSwitchStateScriptPacketParser>
            <ServiceAttributesScriptsPacketParser>[{&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Command name&quot;}, &quot;ReadFormula&quot;: &quot;VAR v := PARSEJSON(DispatchJson, \&quot;$.data.command.action_name\&quot;, true);\nIF ISNULL(v)\n    v := PARSEJSON(DispatchJson, \&quot;$.data.action_name\&quot;, true);\nEND\nRETURN(IF(ISNULL(v), \&quot;-\&quot;, v));&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Command valid until&quot;}, &quot;ReadFormula&quot;: &quot;VAR s := PARSEJSON(DispatchJson, \&quot;$.data.command.valid_until\&quot;, true);\nIF ISNULL(s)\n    RETURN(\&quot;-\&quot;);\nEND\ndatetime t := DATETIME(s);\nVAR l := t.LOCALDATETIME;\nRETURN(TOSTRING(l, \&quot;yyyy-MM-dd HH:mm\&quot;));&quot;}]</ServiceAttributesScriptsPacketParser>
            <Value0>0</Value0>
            <Value0Index>0</Value0Index>
            <Value0Name>No plan</Value0Name>
            <Value0IconId>229</Value0IconId>
            <Value1>1</Value1>
            <Value1Index>1</Value1Index>
            <Value1Name>Self-use</Value1Name>
            <Value1IconId>230</Value1IconId>
            <Value2>2</Value2>
            <Value2Index>2</Value2Index>
            <Value2Name>Force charge</Value2Name>
            <Value2IconId>231</Value2IconId>
            <Value3>3</Value3>
            <Value3Index>3</Value3Index>
            <Value3Name>Force discharge to grid</Value3Name>
            <Value3IconId>253</Value3IconId>
            <Value4>4</Value4>
            <Value4Index>4</Value4Index>
            <Value4Name>Self-use, no discharging</Value4Name>
            <Value4IconId>252</Value4IconId>
            <Value5>5</Value5>
            <Value5Index>5</Value5Index>
            <Value5Name>Self-use, no charging</Value5Name>
            <Value5IconId>367</Value5IconId>
            <Value6>6</Value6>
            <Value6Index>6</Value6Index>
            <Value6Name>Stop PV (reserved)</Value6Name>
            <Value6IconId>2022</Value6IconId>
            <Value7>7</Value7>
            <Value7Index>7</Value7Index>
            <Value7Name>Grid charge without PV (reserved)</Value7Name>
            <Value7IconId>368</Value7IconId>
            <Value8Index>72</Value8Index>
            <Value9Index>73</Value9Index>
          </DeviceProperties>
        </Device>
        <Device>
          <Name>SpotLogic target state of charge</Name>
          <Id>-3</Id>
          <Model>PacketParserVariable</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>True</IsReadOnly>
            <ShouldDoPeriodicWrite>False</ShouldDoPeriodicWrite>
            <ReadStateScriptPacketParser>IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, &quot;SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.&quot;);
    RETURN(NaN);
END
IF DispatchCode != 200
    RETURN(NaN);
END
VAR v := PARSEJSON(DispatchJson, &quot;$.data.command.target_soc_percent&quot;, true);
IF ISNULL(v)
    RETURN(NaN);
END
RETURN(v);</ReadStateScriptPacketParser>
          </DeviceProperties>
        </Device>
        <Device>
          <Name>SpotLogic next command</Name>
          <Id>-4</Id>
          <Model>PacketParserMultiValueSwitch</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>True</IsReadOnly>
            <ReadSwitchStateScriptPacketParser>IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, &quot;SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.&quot;);
    RETURN(NaN);
END
IF DispatchCode = 404
    RETURN(0);
END
IF DispatchCode != 200
    ADDERROR(3, &quot;Next command not available: HTTP &quot; + DispatchCode);
    RETURN(NaN);
END
VAR id := PARSEJSON(DispatchJson, &quot;$.data.context.next_action_id&quot;, true);
IF ISNULL(id)
    RETURN(NaN);
END
RETURN(id);</ReadSwitchStateScriptPacketParser>
            <ServiceAttributesScriptsPacketParser>[{&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Next command name&quot;}, &quot;ReadFormula&quot;: &quot;VAR v := PARSEJSON(DispatchJson, \&quot;$.data.context.next_action_name\&quot;, true);\nRETURN(IF(ISNULL(v), \&quot;-\&quot;, v));&quot;}, {&quot;AttributeDefinition&quot;: {&quot;Name&quot;: &quot;Next command starts&quot;}, &quot;ReadFormula&quot;: &quot;VAR s := PARSEJSON(DispatchJson, \&quot;$.data.context.next_action_time\&quot;, true);\nIF ISNULL(s)\n    RETURN(\&quot;-\&quot;);\nEND\ndatetime t := DATETIME(s);\nVAR l := t.LOCALDATETIME;\nRETURN(TOSTRING(l, \&quot;yyyy-MM-dd HH:mm\&quot;));&quot;}]</ServiceAttributesScriptsPacketParser>
            <Value0>0</Value0>
            <Value0Index>0</Value0Index>
            <Value0Name>No plan</Value0Name>
            <Value0IconId>229</Value0IconId>
            <Value1>1</Value1>
            <Value1Index>1</Value1Index>
            <Value1Name>Self-use</Value1Name>
            <Value1IconId>230</Value1IconId>
            <Value2>2</Value2>
            <Value2Index>2</Value2Index>
            <Value2Name>Force charge</Value2Name>
            <Value2IconId>231</Value2IconId>
            <Value3>3</Value3>
            <Value3Index>3</Value3Index>
            <Value3Name>Force discharge to grid</Value3Name>
            <Value3IconId>253</Value3IconId>
            <Value4>4</Value4>
            <Value4Index>4</Value4Index>
            <Value4Name>Self-use, no discharging</Value4Name>
            <Value4IconId>252</Value4IconId>
            <Value5>5</Value5>
            <Value5Index>5</Value5Index>
            <Value5Name>Self-use, no charging</Value5Name>
            <Value5IconId>367</Value5IconId>
            <Value6>6</Value6>
            <Value6Index>6</Value6Index>
            <Value6Name>Stop PV (reserved)</Value6Name>
            <Value6IconId>2022</Value6IconId>
            <Value7>7</Value7>
            <Value7Index>7</Value7Index>
            <Value7Name>Grid charge without PV (reserved)</Value7Name>
            <Value7IconId>368</Value7IconId>
            <Value8Index>72</Value8Index>
            <Value9Index>73</Value9Index>
          </DeviceProperties>
        </Device>
        <Device>
          <Name>SpotLogic plan age</Name>
          <Id>-5</Id>
          <Model>PacketParserVariable</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>True</IsReadOnly>
            <ShouldDoPeriodicWrite>False</ShouldDoPeriodicWrite>
            <ReadStateScriptPacketParser>IF DispatchCode = 0
    RETURN(NaN);
END
IF DispatchCode = 401
    ADDERROR(1, &quot;SpotLogic rejected the PLC ID or the token (HTTP 401) - check the module variables PlcId and Token.&quot;);
    RETURN(NaN);
END
IF DispatchCode != 200
    RETURN(NaN);
END
VAR v := PARSEJSON(DispatchJson, &quot;$.meta.plan_age_sec&quot;, true);
IF ISNULL(v)
    RETURN(NaN);
END
RETURN(v);</ReadStateScriptPacketParser>
            <IconId>2027</IconId>
          </DeviceProperties>
        </Device>
        <Device>
          <Name>Battery state of charge (input)</Name>
          <Id>-6</Id>
          <Model>PacketParserVariable</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>False</IsReadOnly>
            <ShouldDoPeriodicWrite>False</ShouldDoPeriodicWrite>
            <ReadStateScriptPacketParser>IF(SocNow &lt; 0, NaN, SocNow)</ReadStateScriptPacketParser>
            <WriteStateScriptPacketParser>SocNow := Va;</WriteStateScriptPacketParser>
          </DeviceProperties>
        </Device>
        <Device>
          <Name>Battery reserve for SpotLogic</Name>
          <Id>-7</Id>
          <Model>PacketParserVariable</Model>
          <DeviceProperties>
            <DeviceType>0</DeviceType>
            <InternalPollInterval>20000</InternalPollInterval>
            <IsReadOnly>False</IsReadOnly>
            <ShouldDoPeriodicWrite>False</ShouldDoPeriodicWrite>
            <ReadStateScriptPacketParser>RETURN(SocMin);</ReadStateScriptPacketParser>
            <WriteStateScriptPacketParser>SocMin := Va;</WriteStateScriptPacketParser>
          </DeviceProperties>
        </Device>
      </Devices>
    </Module>
  </Template>
</Templates>
