node-red-contrib-eskomsepush 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Victron Energy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ ### EskomSePush API
2
+
3
+ ![EskomsePush API](img/eskomsepush-flow.png)
4
+
5
+ A node for retrieving info from the EskomSePush API.
6
+
7
+ The EskomSePush-API node gives the tools to make working with the load shedding in South Africa as easy as possible.
8
+
9
+ First you need to configure the node by entering the license key and entering the correct area.
10
+
11
+ Once deployed, the node will fetch the data from EskomSePush every hour. As every fetch from the API takes 2 calls, the 50 free queries per day on a free account should suffice. Every ten minutes the API status is checked to see how many queries you have left.
12
+
13
+ Internally the node checks every minute if a schedule is currently active or not. It will also output a message on the first deployment.
14
+
15
+ ### Configuration
16
+
17
+ First you will need a _licence key_. You can get one from [here](https://eskomsepush.gumroad.com/l/api), by subsribing to the Free model. Note that this is for personal use only.
18
+
19
+ Next you need to insert the correct area. Once you entered 5 characters (and a valid license is used), the API will be used for searching the correct area. Do note that this will cost some of the daily queries. If you don't want that and you already know the id of the area, fill out the area first and then the license key.
20
+
21
+ Then you need to fill out which status to follow. This can be either _National_ (eskom) or _Capetown_.
22
+
23
+ If the _test_ checkbox has been selected, test data for the area will be fetched instead of the actual schedule. This is useful when debugging.
24
+
25
+ ### Outputs
26
+
27
+ The first output of the node outputs a boolean value. When load shedding is active, the `msg.payload` will be _true_, otherwise it will be _false_. It also outputs some extra values:
28
+ ```
29
+ {
30
+ "payload":false,
31
+ "LoadShedding":{
32
+ "schedule":{
33
+ "next":{
34
+ "start":1683561600000,
35
+ "end":1683570600000,
36
+ "type":"schedule"
37
+ },
38
+ "active":false
39
+ },
40
+ "event":{
41
+ "next":{
42
+ "start":1683561600000,
43
+ "end":1683570600000
44
+ },
45
+ "active":false
46
+ },
47
+ "checked":"13:35",
48
+ "next":{
49
+ "start":1683561600000,
50
+ "end":1683570600000,
51
+ "type":"schedule"
52
+ },
53
+ "active":false
54
+ },
55
+ "stage":"5",
56
+ "statusselect":"capetown",
57
+ "api":{
58
+ "count":12,
59
+ "limit":50,
60
+ "lastStatusUpdate":"Mon May 08 2023 13:34:30 GMT+0200 (Central European Summer Time)",
61
+ "lastScheduleUpdate":"Mon May 08 2023 13:34:30 GMT+0200 (Central European Summer Time)"
62
+ },
63
+ "\_msgid":"6e77b593b7f9eda4"
64
+ }
65
+ ```
66
+
67
+ The start and end objects contain the time as unix timestamp in the Javascript format (milliseconds after the epoch).
68
+
69
+ The second output shows `msg.stage` and `msg.schedule`, containing the latest information as retrieved from the API. This output is mainly useful when writing your own functions and logic.
70
+
71
+ ### Status
72
+
73
+ The status will show the situation regarding the API calls and when the next
74
+ shedding wil start or end. It also shows the count of API calls that have been
75
+ done and how many are left. This updates every 10 minutes.
76
+
77
+
78
+ ### Documentation
79
+
80
+ Documentation for the API can be found [here](https://documenter.getpostman.com/view/1296288/UzQuNk3E)
81
+
82
+ When quota has been exceeded:
83
+ ```
84
+ {"error":"Quota Exceeded - Reminder: you can use the 'test' query param for development. Check the docs! \ud83d\ude05"}
85
+ ```
@@ -0,0 +1,56 @@
1
+ [
2
+ {
3
+ "id": "d083105e39f8ba68",
4
+ "type": "debug",
5
+ "z": "614c1c00b0cf1810",
6
+ "name": "Load shedding",
7
+ "active": true,
8
+ "tosidebar": true,
9
+ "console": false,
10
+ "tostatus": false,
11
+ "complete": "true",
12
+ "targetType": "full",
13
+ "statusVal": "",
14
+ "statusType": "auto",
15
+ "x": 340,
16
+ "y": 60,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "1d3e18f144e4911a",
21
+ "type": "debug",
22
+ "z": "614c1c00b0cf1810",
23
+ "name": "Schedule and events",
24
+ "active": true,
25
+ "tosidebar": true,
26
+ "console": false,
27
+ "tostatus": false,
28
+ "complete": "true",
29
+ "targetType": "full",
30
+ "statusVal": "",
31
+ "statusType": "auto",
32
+ "x": 360,
33
+ "y": 100,
34
+ "wires": []
35
+ },
36
+ {
37
+ "id": "854cb9223eb1d95f",
38
+ "type": "eskomsepush",
39
+ "z": "614c1c00b0cf1810",
40
+ "name": "",
41
+ "licensekey": "",
42
+ "area": "capetown-3-helderbergvillage",
43
+ "statusselect": "capetown",
44
+ "test": false,
45
+ "x": 130,
46
+ "y": 80,
47
+ "wires": [
48
+ [
49
+ "d083105e39f8ba68"
50
+ ],
51
+ [
52
+ "1d3e18f144e4911a"
53
+ ]
54
+ ]
55
+ }
56
+ ]
Binary file
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "node-red-contrib-eskomsepush",
3
+ "version": "0.0.1",
4
+ "description": "Node-RED interface for the Eskomsepush API",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "standard --fix"
8
+ },
9
+ "keywords": [
10
+ "node-red",
11
+ "eskomsepush",
12
+ "loadshedding",
13
+ "south africa",
14
+ "scheduled charging",
15
+ "victron",
16
+ "ess"
17
+ ],
18
+ "author": "Dirk-Jan Faber <dfaber@victronenergy.com>",
19
+ "license": "MIT",
20
+ "node-red": {
21
+ "nodes": {
22
+ "eskomepush": "./src/nodes/eskomsepush.js"
23
+ },
24
+ "version": ">=3.0.2"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/dirkjanfaber/node-red-contrib-eskomepush"
29
+ },
30
+ "dependencies": {
31
+ "axios": "^1.3.5"
32
+ },
33
+ "devDependencies": {
34
+ "standard": "^17.0.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=14.17.4"
38
+ }
39
+ }
@@ -0,0 +1,185 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('eskomsepush',{
3
+ category: 'network',
4
+ color: '#ffffff',
5
+ defaults: {
6
+ name: {value:""},
7
+ licensekey: {value:"", validate: RED.validators.regex(/^[0-9A-F\-]{35}$/)},
8
+ area: {value:""},
9
+ statusselect: {value:"", validate: RED.validators.regex(/^(eskom|capetown)$/)},
10
+ test: { value: false }
11
+ },
12
+ inputs:0,
13
+ outputs:2,
14
+ icon: "eskomsepush.svg",
15
+ label: function() {
16
+ return this.name||"EskomSePush API";
17
+ },
18
+ oneditprepare: function oneditprepare() {
19
+
20
+ }
21
+ });
22
+
23
+ function searchArea() {
24
+ var input = document.getElementById("node-input-area");
25
+ var dropdown = document.getElementById("areaDropdown");
26
+ var licensekey = document.getElementById("node-input-licensekey");
27
+
28
+ if (!RED.validators.regex(licensekey.value, /^[0-9A-F\-]{35}$/)) {
29
+ return
30
+ }
31
+
32
+ if (input.value.length >= 5) {
33
+ dropdown.innerHTML = "";
34
+
35
+ $.ajax({
36
+ url: '/eskomsepush/search',
37
+ method: 'GET',
38
+ data: {
39
+ token: licensekey.value,
40
+ search: input.value
41
+ },
42
+ success: function(response) {
43
+ for (var i = 0; i < response.areas.length; i++) {
44
+ var option = document.createElement("option");
45
+ option.text = response.areas[i].name;
46
+ option.value = response.areas[i].id;
47
+ dropdown.add(option);
48
+ }
49
+ },
50
+ error: function(error) {
51
+ node.warn(error)
52
+ }
53
+ })
54
+ dropdown.style.display = "block";
55
+ dropdown.addEventListener("change", function() {
56
+ input.value = dropdown.value;
57
+ dropdown.style.display = "none";
58
+ });
59
+ input.addEventListener("focusout", function() {
60
+ dropdown.style.display = "none"
61
+ })
62
+ } else {
63
+ dropdown.style.display = "none";
64
+ }
65
+ }
66
+ </script>
67
+
68
+ <script type="text/html" data-template-name="eskomsepush">
69
+ <div class="form-row">
70
+ <label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
71
+ <input type="text" id="node-input-name" placeholder="Name">
72
+ </div>
73
+ <div class="form-row">
74
+ <label for="node-input-licensekey"><i class="fa fa-user-secret"></i> License key</label>
75
+ <input type="password" id="node-input-licensekey" placeholder="License key" required>
76
+ </div>
77
+ <div class="form-row">
78
+ <label for="node-input-area"><i class="fa fa-tag"></i> Area</label>
79
+ <input type="text" id="node-input-area" placeholder="Area" onkeyup="searchArea()">
80
+ <select id="areaDropdown" style="display: none;"></select>
81
+ </div>
82
+ <div class="form-row">
83
+ <label style="min-width:190px" for="node-input-test"><i class="fa fa-volume-up"></i> Use test data</label>
84
+ <input type="checkbox" checked id="node-input-test" style="max-width:30px">
85
+ </div>
86
+ <div class="form-row">
87
+ <label for="node-input-statusselect"><i class="fa fa-location-arrow"></i> Status select</label>
88
+ <select id="node-input-statusselect" required>
89
+ <option value="eskom">National (eskom)</option>
90
+ <option value="capetown">Cape Town</option>
91
+ </select>
92
+ </div>
93
+ </script>
94
+
95
+ <script type="text/html" data-help-name="eskomsepush">
96
+ <h3 id=""eskomsepushapi>EskomSePush API</h3>
97
+
98
+ <p>A node for retrieving info from the EskomSePush API.</p>
99
+
100
+ <p>The EskomSePush-API node gives the tools to make working with the load shedding
101
+ in South Africa as easy as possible.</p>
102
+ <p>First you need to configure the node by entering the license key and entering
103
+ the correct area.</p>
104
+ <p>Once deployed, the node will fetch the data from EskomSePush every hour. As
105
+ every fetch from the API takes 2 calls, the 50 free queries per day on a
106
+ free account should suffice. Every ten minutes the API status is checked to
107
+ see how many queries you have left.</p>
108
+ <p>Internally the node checks every minute if a schedule is currently active or not. It
109
+ will also output a message on the first deployment.</p>
110
+
111
+ <h3 id="configuration">Configuration</h3>
112
+
113
+ <p>First you will need a <em>licence key</em>. You can get one from <a href="https://eskomsepush.gumroad.com/l/api">here</a>, by subsribing to the Free model. Note that this is for personal use only.</p>
114
+ <p>Next you need to insert the correct area. Once you entered 5 characters (and a valid license is used), the API will be used for searching the correct area. Do note that this will cost some of the daily queries. If you don&#39;t want that and you already know the id of the area, fill out the area first and then the license
115
+ key.</p>
116
+ <p>Then you need to fill out which status to follow. This can be either <em>National</em> (eskom) or <em>Capetown</em>.</p>
117
+ <p>If the <em>test</em> checkbox has been selected, test data for the area will be fetched instead of the actual schedule. This is useful when debugging.</p>
118
+
119
+ <h3 id="outputs">Outputs</h3>
120
+
121
+ <p>The first output of the node outputs a boolean value. When load shedding is active, the <code>msg.payload</code> will be <em>true</em>, otherwise it will be <em>false</em>. It also outputs some extra values:</p>
122
+
123
+ <pre>
124
+ {
125
+ "payload":false,
126
+ "LoadShedding":{
127
+ "schedule":{
128
+ "next":{
129
+ "start":1683561600000,
130
+ "end":1683570600000,
131
+ "type":"schedule"
132
+ },
133
+ "active":false
134
+ },
135
+ "event":{
136
+ "next":{
137
+ "start":1683561600000,
138
+ "end":1683570600000
139
+ },
140
+ "active":false
141
+ },
142
+ "checked":"13:35",
143
+ "next":{
144
+ "start":1683561600000,
145
+ "end":1683570600000,
146
+ "type":"schedule"
147
+ },
148
+ "active":false
149
+ },
150
+ "stage":"5",
151
+ "statusselect":"capetown",
152
+ "api":{
153
+ "count":12,
154
+ "limit":50,
155
+ "lastStatusUpdate":"Mon May 08 2023 13:34:30 GMT+0200 (Central European Summer Time)",
156
+ "lastScheduleUpdate":"Mon May 08 2023 13:34:30 GMT+0200 (Central European Summer Time)"
157
+ },
158
+ "_msgid":"6e77b593b7f9eda4"
159
+ }
160
+ </pre>
161
+
162
+ <p>The <tt>start</tt> and <tt>end</tt> objects contain the time as unix timestamp in the Javascript format (milliseconds after the epoch).</p>
163
+
164
+ <p>The second output shows <code>msg.stage</code> and <code>msg.schedule</code>, containing the latest information as retrieved from the API.
165
+ This output is mainly useful when writing your own functions and logic.</p>
166
+
167
+ <h3 id="status">Status</h3>
168
+
169
+ <p>The status will show the situation regarding the API calls. It will show the count of API calls that have been done and
170
+ how many are left. This updates every 10 minutes.</p>
171
+
172
+ <h1 id="documentation">Documentation</h1>
173
+ <p>Documentation for the API can be found <a href="https://documenter.getpostman.com/view/1296288/UzQuNk3E">here</a></p>
174
+
175
+ </script>
176
+
177
+ <style>
178
+ input#node-input-area {
179
+ margin-bottom: 5px;
180
+ }
181
+ select#areaDropdown {
182
+ position: absolute;
183
+ left: 120px;
184
+ }
185
+ </style>
@@ -0,0 +1,218 @@
1
+ module.exports = function (RED) {
2
+ 'use strict'
3
+
4
+ const axios = require('axios')
5
+ let EskomSePushAPI = null
6
+ let Stage = null
7
+ let Schedule = null
8
+ let lastStatusUpdate = new Date()
9
+ let lastStageUpdate = new Date()
10
+ let lastScheduleUpdate = new Date()
11
+ let LoadShedding = false
12
+ let fill = 'green'
13
+ let shape = 'ring'
14
+
15
+ function checkAllowance (node) {
16
+ const options = {}
17
+ const headers = { token: node.config.licensekey }
18
+ axios.get('https://developer.sepush.co.za/business/2.0/api_allowance',
19
+ { params: options, headers }).then(function (response) {
20
+ EskomSePushAPI = response.data
21
+ })
22
+ .catch(error => {
23
+ node.warn({ error: error.message })
24
+ })
25
+ }
26
+
27
+ function checkStage (node) {
28
+ const options = {}
29
+ const headers = { token: node.config.licensekey }
30
+ axios.get('https://developer.sepush.co.za/business/2.0/status',
31
+ { params: options, headers }).then(function (response) {
32
+ Stage = response.data
33
+ })
34
+ .catch(error => {
35
+ node.warn({ error: error.message })
36
+ })
37
+ }
38
+
39
+ function checkSchedule (node) {
40
+ const options = { id: node.config.area }
41
+ const headers = { token: node.config.licensekey }
42
+ let url = 'https://developer.sepush.co.za/business/2.0/area'
43
+ if (node.config.test) {
44
+ options.test = 'current'
45
+ }
46
+ axios.get(url,
47
+ { params: options, headers }).then(function (response) {
48
+ Schedule = response.data
49
+ Schedule.info.area = node.config.area
50
+ })
51
+ .catch(error => {
52
+ node.warn({ error: error.message })
53
+ })
54
+ }
55
+
56
+ function updateStatus(node) {
57
+ const now = new Date();
58
+ let statusText = ''
59
+
60
+ if (EskomSePushAPI === null || (now.getTime() - lastStatusUpdate.getTime()) > 600000) {
61
+ checkAllowance(node)
62
+ lastStatusUpdate = now
63
+ }
64
+
65
+ if (Schedule && Schedule.info.area !== node.config.area) {
66
+ Schedule = null
67
+ }
68
+
69
+ if (EskomSePushAPI && EskomSePushAPI.allowance.count >= EskomSePushAPI.allowance.limit) {
70
+ statusText += 'API quota reached'
71
+ fill = 'red'
72
+ shape = 'dot'
73
+
74
+ } else {
75
+ if (Stage === null || (now.getTime() - lastStatusUpdate.getTime()) > 3600000) {
76
+ node.status({fill: 'yellow', shape, text: 'Fetching status'})
77
+ checkStage(node)
78
+ lastStageUpdate = now
79
+ }
80
+
81
+ if (Schedule === null || (now.getTime() - lastScheduleUpdate.getTime()) > 3600000) {
82
+ node.status({fill: 'yellow', shape, text: 'Fetching schedule'})
83
+ checkSchedule(node)
84
+ lastScheduleUpdate = now
85
+ }
86
+ }
87
+
88
+ if (Stage && Schedule && EskomSePushAPI) {
89
+ let stage = Stage.status[node.config.statusselect].stage
90
+ let nowtime = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit'})
91
+ let LoadShedding = {
92
+ schedule: {
93
+ next: {},
94
+ active: false
95
+ },
96
+ event: {
97
+ next: {},
98
+ active: false
99
+ },
100
+ checked: nowtime
101
+ }
102
+ fill = 'green'
103
+ for (let schedule of Schedule.schedule.days[0].stages[stage-1]) {
104
+ if ( nowtime >= schedule.split('-')[0] && nowtime <= schedule.split('-')[1] ) {
105
+ LoadShedding.schedule = {
106
+ active: true,
107
+ start: Date.parse(Schedule.schedule.days[0].date + ' ' + schedule.split('-')[0]),
108
+ end: Date.parse(Schedule.schedule.days[0].date + ' ' + schedule.split('-')[1])
109
+ }
110
+ }
111
+ if ( Object.keys(LoadShedding.schedule.next).length === 0 && nowtime < schedule.split('-')[0] ) {
112
+ LoadShedding.schedule.next = {
113
+ start: Date.parse(Schedule.schedule.days[0].date + ' ' + schedule.split('-')[0]),
114
+ end: Date.parse(Schedule.schedule.days[0].date + ' ' + schedule.split('-')[1])
115
+ }
116
+ }
117
+ }
118
+ if (Object.keys(LoadShedding.schedule.next).length === 0) {
119
+ let s = Schedule.schedule.days[1].stages[stage-1][0]
120
+ LoadShedding.schedule.next = {
121
+ start: Date.parse(Schedule.sschedule.days[1].date + s.split('-')[0]),
122
+ end: Date.parse(Schedule.sschedule.days[1].date + s.split('-')[1])
123
+ }
124
+ }
125
+ LoadShedding.event.next = {
126
+ start: Date.parse(Schedule.events[0].start),
127
+ end: Date.parse(Schedule.events[0].end)
128
+ }
129
+ if ( nowtime >= LoadShedding.event.start && nowtime < LoadShedding.event.end ) {
130
+ LoadShedding.event.active = true
131
+ LoadShedding.start = LoadShedding.event.start
132
+ LoadShedding.end = LoadShedding.event.end
133
+ }
134
+
135
+ if (!LoadShedding.schedule.next.start || !LoadShedding.event.next.start) {
136
+ node.warn("Unable to find next scheduled event and/or schedule")
137
+ return
138
+ }
139
+ if (LoadShedding.schedule.next.start <= LoadShedding.event.next.start) {
140
+ LoadShedding.next = LoadShedding.schedule.next
141
+ LoadShedding.next.type = 'schedule'
142
+ } else {
143
+ LoadShedding.next = LoadShedding.event.next
144
+ LoadShedding.next.type = 'event'
145
+ }
146
+
147
+ LoadShedding.active = (LoadShedding.schedule.active || LoadShedding.event.active)
148
+ if (LoadShedding.active) {
149
+ statusText += 'Until: '+ new Date(LoadShedding.end).toLocaleTimeString()
150
+ fill = 'red'
151
+ } else {
152
+ statusText += 'Next: ' + new Date(LoadShedding.next.start).toLocaleTimeString()
153
+ }
154
+ node.send([{
155
+ payload: LoadShedding.active,
156
+ LoadShedding,
157
+ stage,
158
+ statusselect: node.config.statusselect,
159
+ api: {
160
+ count: EskomSePushAPI.allowance.count,
161
+ limit: EskomSePushAPI.allowance.limit,
162
+ lastStatusUpdate: lastStatusUpdate.toString(),
163
+ lastScheduleUpdate: lastScheduleUpdate.toString()
164
+ }
165
+ }, {
166
+ stage: Stage,
167
+ schedule: Schedule }])
168
+ }
169
+
170
+ if (EskomSePushAPI) {
171
+ statusText += ` (API: ${EskomSePushAPI.allowance.count}/${EskomSePushAPI.allowance.limit})`
172
+ }
173
+
174
+ node.status({
175
+ fill, shape, text: ( statusText || 'Ok' )
176
+ })
177
+ }
178
+
179
+ function EskomSePush (config) {
180
+ RED.nodes.createNode(this, config)
181
+
182
+ const node = this
183
+ node.config = config
184
+
185
+ updateStatus(node)
186
+ const intervalId = setInterval(function () {
187
+ updateStatus(node)
188
+ }, 60000)
189
+
190
+ node.on('close', function () {
191
+ clearInterval(intervalId)
192
+ })
193
+ }
194
+
195
+ RED.nodes.registerType('eskomsepush', EskomSePush)
196
+
197
+ RED.httpNode.get('/eskomsepush/search', (req, res) => {
198
+ if (!req.query || !req.query.token || !req.query.search) {
199
+ res.setHeader('Content-Type', 'application/json')
200
+ return res.send('invalid')
201
+ }
202
+ const headers = {
203
+ token: req.query.token
204
+ }
205
+ const options = {
206
+ text: req.query.search
207
+ }
208
+
209
+ res.setHeader('Content-Type', 'application/json')
210
+ axios.get('https://developer.sepush.co.za/business/2.0/areas_search',
211
+ { params: options, headers }).then(function (response) {
212
+ return res.send(response.data)
213
+ })
214
+ .catch(error => {
215
+ return res.send({ error: error.message })
216
+ })
217
+ })
218
+ }
Binary file
@@ -0,0 +1,4 @@
1
+ <svg width="1024" height="1024" viewBox="0 0 1024 1024" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <circle cx="512" cy="512" r="512" fill="white"/>
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M418.116 126C396.51 208.249 354.298 358.268 354.298 358.268L469.894 412.544L412.096 568.21L496.587 594.526L430.541 807.246L609.333 568.21L527.504 541.414L655.131 358.268L548.085 318.998L653.067 140.876C803.266 197.97 910.002 343.27 910.002 513.499C910.002 733.585 731.587 912 511.501 912C291.415 912 113 733.585 113 513.499C113 325.573 243.082 168.03 418.116 126Z" fill="#FF4738"/>
4
+ </svg>