├── .github └── workflows │ └── check-for-dupes.yml ├── .gitignore ├── LICENSE ├── accuweather ├── requests.go └── structures.go ├── conditions.go ├── config-example.xml ├── config.go ├── countries.go ├── duplicates_test.go ├── go.mod ├── go.sum ├── header.go ├── laundry.go ├── locations.go ├── long_table.go ├── main.go ├── parser.go ├── pollen.go ├── short_bin.go ├── short_table.go ├── utils.go ├── uv.go └── weather.xml /.github/workflows/check-for-dupes.yml: -------------------------------------------------------------------------------- 1 | name: ForecastChannel 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | 11 | - name: Set up Go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: '1.20' 15 | 16 | - name: Install dependencies 17 | run: go get . 18 | 19 | - name: Build 20 | run: go build -v 21 | 22 | - name: Run 23 | run: cp config-example.xml config.xml; mkdir files files/0 files/1 files/2 files/3 files/4 files/5 files/6 files/7; ./ForecastChannel 24 | 25 | - name: Test for Duplicates 26 | run: go test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /accuweather/requests.go: -------------------------------------------------------------------------------- 1 | package accuweather 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "runtime" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | apiURL = "https://api.accuweather.com" 15 | ) 16 | 17 | var currentTime = time.Now().Unix() 18 | 19 | func GetWeather(longitude float64, latitude float64, _time int64, apiKey string) (w *Weather) { 20 | defer func() { 21 | if err := recover(); err != nil { 22 | buf := make([]byte, 2048) 23 | n := runtime.Stack(buf, false) 24 | buf = buf[:n] 25 | 26 | fmt.Printf("Recovering from error %v\n %s\n", err, buf) 27 | w = BlankData() 28 | } 29 | }() 30 | 31 | currentTime = _time 32 | weather := Weather{} 33 | weather.apiKey = apiKey 34 | 35 | // First retrieve the location code. 36 | queryParams := fmt.Sprintf("q=%f,%f&apikey=%s", latitude, longitude, apiKey) 37 | 38 | response, err := http.Get(fmt.Sprintf("%s/locations/v1/cities/geoposition/search.json?%s", apiURL, queryParams)) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | defer response.Body.Close() 44 | respBytes, _ := io.ReadAll(response.Body) 45 | 46 | jsonData := map[string]any{} 47 | err = json.Unmarshal(respBytes, &jsonData) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | if _, ok := jsonData["Key"]; !ok { 53 | return BlankData() 54 | } 55 | 56 | locationKey := jsonData["Key"].(string) 57 | weather.GetCurrentWeather(locationKey) 58 | weather.Get5DayWeather(locationKey) 59 | weather.Get10DayWeather(locationKey) 60 | 61 | return &weather 62 | } 63 | 64 | func (w *Weather) GetCurrentWeather(locationKey string) { 65 | response, err := http.Get(fmt.Sprintf("%s/currentconditions/v1/%s?apikey=%s&details=true", apiURL, locationKey, w.apiKey)) 66 | defer response.Body.Close() 67 | respBytes, _ := io.ReadAll(response.Body) 68 | 69 | jsonData := []any{map[string]any{}} 70 | err = json.Unmarshal(respBytes, &jsonData) 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | weather := jsonData[0].(map[string]any) 76 | w.LocalTime = weather["LocalObservationDateTime"].(string) 77 | w.Current.TempFahrenheit = weather["Temperature"].(map[string]any)["Imperial"].(map[string]any)["Value"].(float64) 78 | w.Current.TempCelsius = weather["Temperature"].(map[string]any)["Metric"].(map[string]any)["Value"].(float64) 79 | w.Current.WeatherIcon = int(weather["WeatherIcon"].(float64)) 80 | w.Current.WindDirection = weather["Wind"].(map[string]any)["Direction"].(map[string]any)["English"].(string) 81 | w.Current.WindImperial = weather["Wind"].(map[string]any)["Speed"].(map[string]any)["Imperial"].(map[string]any)["Value"].(float64) 82 | w.Current.WindMetric = weather["Wind"].(map[string]any)["Speed"].(map[string]any)["Metric"].(map[string]any)["Value"].(float64) 83 | 84 | one, err := strconv.ParseFloat(w.LocalTime[20:22], 32) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | two, err := strconv.ParseInt(w.LocalTime[23:25], 10, 32) 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | w.Globe.Offset = int(one + float64(two/60)) 95 | if string(w.LocalTime[19]) == "-" { 96 | w.Globe.Offset *= -1 97 | } 98 | 99 | w.Globe.Time = int(currentTime) + w.Globe.Offset*3600 100 | } 101 | 102 | func (w *Weather) Get5DayWeather(locationKey string) { 103 | response, err := http.Get(fmt.Sprintf("%s/forecasts/v1/daily/5day/quarters/%s?apikey=%s&details=true", apiURL, locationKey, w.apiKey)) 104 | defer response.Body.Close() 105 | respBytes, _ := io.ReadAll(response.Body) 106 | 107 | jsonData := []any{map[string]any{}} 108 | err = json.Unmarshal(respBytes, &jsonData) 109 | if err != nil { 110 | panic(err) 111 | } 112 | 113 | index := 0 114 | hourlyStart := 0 115 | isRightDay := false 116 | for !isRightDay { 117 | temp, err := strconv.ParseInt(jsonData[index].(map[string]any)["EffectiveDate"].(string)[11:13], 10, 32) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | hourlyStart = int(temp) / 6 123 | if jsonData[index].(map[string]any)["EffectiveDate"].(string)[:10] == w.LocalTime[:10] { 124 | isRightDay = true 125 | } else { 126 | index++ 127 | } 128 | } 129 | 130 | // Make precipitation and icons together to save time 131 | i := 0 132 | w.Precipitation = []int{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} 133 | w.HourlyIcon = make([]int, 8) 134 | for _i := hourlyStart; _i < 8; _i++ { 135 | w.Precipitation[_i] = int(jsonData[index+i].(map[string]any)["PrecipitationProbability"].(float64)) 136 | w.HourlyIcon[_i] = int(jsonData[index+i].(map[string]any)["Icon"].(float64)) 137 | i++ 138 | } 139 | } 140 | 141 | func (w *Weather) Get10DayWeather(locationKey string) { 142 | response, err := http.Get(fmt.Sprintf("%s/forecasts/v1/daily/10day/%s?apikey=%s&details=true", apiURL, locationKey, w.apiKey)) 143 | defer response.Body.Close() 144 | respBytes, _ := io.ReadAll(response.Body) 145 | 146 | jsonData := map[string]any{} 147 | err = json.Unmarshal(respBytes, &jsonData) 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | day := 0 153 | if jsonData["DailyForecasts"].([]any)[0].(map[string]any)["Date"].(string)[:10] != w.LocalTime[:10] { 154 | day++ 155 | } 156 | 157 | // Today Forecast 158 | w.Today.TempFahrenheitMin = jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Temperature"].(map[string]any)["Minimum"].(map[string]any)["Value"].(float64) 159 | w.Today.TempFahrenheitMax = jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Temperature"].(map[string]any)["Maximum"].(map[string]any)["Value"].(float64) 160 | w.Today.TempCelsiusMin = ftoC(w.Today.TempFahrenheitMin) 161 | w.Today.TempCelsiusMax = ftoC(w.Today.TempFahrenheitMax) 162 | w.Today.WeatherIcon = int(jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Day"].(map[string]any)["Icon"].(float64)) 163 | 164 | // Tomorrow Forecast 165 | w.Tomorrow.TempFahrenheitMin = jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Temperature"].(map[string]any)["Minimum"].(map[string]any)["Value"].(float64) 166 | w.Tomorrow.TempFahrenheitMax = jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Temperature"].(map[string]any)["Maximum"].(map[string]any)["Value"].(float64) 167 | w.Tomorrow.TempCelsiusMin = ftoC(w.Tomorrow.TempFahrenheitMin) 168 | w.Tomorrow.TempCelsiusMax = ftoC(w.Tomorrow.TempFahrenheitMax) 169 | w.Tomorrow.WeatherIcon = int(jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Day"].(map[string]any)["Icon"].(float64)) 170 | 171 | // UV Index 172 | w.UVIndex = int(jsonData["DailyForecasts"].([]any)[day].(map[string]any)["AirAndPollen"].([]any)[5].(map[string]any)["Value"].(float64)) 173 | if w.UVIndex > 12 { 174 | w.UVIndex = 12 175 | } 176 | 177 | // Wind Today 178 | w.Wind.WindImperial = jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Speed"].(map[string]any)["Value"].(float64) 179 | w.Wind.WindMetric = mphToKM(jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Speed"].(map[string]any)["Value"].(float64)) 180 | w.Wind.WindDirection = jsonData["DailyForecasts"].([]any)[day].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Direction"].(map[string]any)["English"].(string) 181 | 182 | // Wind Tomorrow 183 | w.Wind.WindImperialTomorrow = jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Speed"].(map[string]any)["Value"].(float64) 184 | w.Wind.WindMetricTomorrow = mphToKM(jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Speed"].(map[string]any)["Value"].(float64)) 185 | w.Wind.WindDirectionTomorrow = jsonData["DailyForecasts"].([]any)[day+1].(map[string]any)["Day"].(map[string]any)["Wind"].(map[string]any)["Direction"].(map[string]any)["English"].(string) 186 | 187 | // Pollen index 188 | // TODO: Properly calculate pollen. Something with force type casting validation 189 | grass := 2 190 | tree := 2 191 | ragweed := 2 192 | w.Pollen = (grass + tree + ragweed) / 3 193 | 194 | // Complete precipitation 195 | for i := 8; i < 15; i++ { 196 | w.Precipitation[i] = int(jsonData["DailyForecasts"].([]any)[i-8+day].(map[string]any)["Day"].(map[string]any)["PrecipitationProbability"].(float64)) 197 | } 198 | 199 | w.Week = make([]Week, 7) 200 | _i := 1 201 | for i := 0; i < 7; i++ { 202 | w.Week[i].TempFahrenheitMin = jsonData["DailyForecasts"].([]any)[day+_i].(map[string]any)["Temperature"].(map[string]any)["Minimum"].(map[string]any)["Value"].(float64) 203 | w.Week[i].TempFahrenheitMax = jsonData["DailyForecasts"].([]any)[day+_i].(map[string]any)["Temperature"].(map[string]any)["Maximum"].(map[string]any)["Value"].(float64) 204 | w.Week[i].TempCelsiusMin = ftoC(w.Week[i].TempFahrenheitMin) 205 | w.Week[i].TempCelsiusMax = ftoC(w.Week[i].TempFahrenheitMax) 206 | w.Week[i].WeatherIcon = int(jsonData["DailyForecasts"].([]any)[day+_i].(map[string]any)["Day"].(map[string]any)["Icon"].(float64)) 207 | _i++ 208 | } 209 | } 210 | 211 | func BlankData() *Weather { 212 | var week []Week 213 | for i := 0; i < 8; i++ { 214 | week = append(week, Week{ 215 | TempFahrenheitMin: -128, 216 | TempFahrenheitMax: -128, 217 | TempCelsiusMin: -128, 218 | TempCelsiusMax: -128, 219 | WeatherIcon: 0, 220 | }) 221 | } 222 | 223 | return &Weather{ 224 | LocalTime: "", 225 | Current: Current{ 226 | TempFahrenheit: -128, 227 | TempCelsius: -128, 228 | WindDirection: "N", 229 | WindImperial: 0, 230 | WindMetric: 0, 231 | WeatherIcon: 0, 232 | }, 233 | Today: Today{ 234 | TempFahrenheitMin: -128, 235 | TempFahrenheitMax: -128, 236 | TempCelsiusMin: -128, 237 | TempCelsiusMax: -128, 238 | WeatherIcon: 0, 239 | }, 240 | Tomorrow: Tomorrow{ 241 | TempFahrenheitMin: -128, 242 | TempFahrenheitMax: -128, 243 | TempCelsiusMin: -128, 244 | TempCelsiusMax: -128, 245 | WeatherIcon: 0, 246 | }, 247 | Week: week, 248 | Wind: Wind{ 249 | WindDirection: "N", 250 | WindImperial: 0, 251 | WindMetric: 0, 252 | WindDirectionTomorrow: "N", 253 | WindImperialTomorrow: 0, 254 | WindMetricTomorrow: 0, 255 | }, 256 | Precipitation: []int{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, 257 | UVIndex: 0, 258 | Pollen: 0, 259 | HourlyIcon: []int{0, 0, 0, 0, 0, 0, 0, 0}, 260 | Globe: Globe{ 261 | Offset: 0, 262 | Time: int(time.Now().Unix()), 263 | }, 264 | } 265 | } 266 | 267 | func ftoC(f float64) float64 { 268 | return (f - 32) * 5 / 9 269 | } 270 | 271 | func mphToKM(mph float64) float64 { 272 | return mph * 1.60934 273 | } 274 | -------------------------------------------------------------------------------- /accuweather/structures.go: -------------------------------------------------------------------------------- 1 | package accuweather 2 | 3 | type Weather struct { 4 | LocalTime string 5 | Current Current 6 | Today Today 7 | Tomorrow Tomorrow 8 | Week []Week 9 | Wind Wind 10 | Precipitation []int 11 | UVIndex int 12 | Pollen int 13 | HourlyIcon []int 14 | Globe Globe 15 | 16 | apiKey string 17 | } 18 | 19 | type Current struct { 20 | TempFahrenheit float64 21 | TempCelsius float64 22 | WindDirection string 23 | WindImperial float64 24 | WindMetric float64 25 | WeatherIcon int 26 | } 27 | 28 | type Today struct { 29 | TempFahrenheitMin float64 30 | TempFahrenheitMax float64 31 | TempCelsiusMin float64 32 | TempCelsiusMax float64 33 | WeatherIcon int 34 | } 35 | 36 | type Tomorrow struct { 37 | TempFahrenheitMin float64 38 | TempFahrenheitMax float64 39 | TempCelsiusMin float64 40 | TempCelsiusMax float64 41 | WeatherIcon int 42 | } 43 | 44 | type Week struct { 45 | TempFahrenheitMin float64 46 | TempFahrenheitMax float64 47 | TempCelsiusMin float64 48 | TempCelsiusMax float64 49 | WeatherIcon int 50 | } 51 | 52 | type Wind struct { 53 | WindDirection string 54 | WindImperial float64 55 | WindMetric float64 56 | WindDirectionTomorrow string 57 | WindImperialTomorrow float64 58 | WindMetricTomorrow float64 59 | } 60 | 61 | type Globe struct { 62 | Offset int 63 | Time int 64 | } 65 | -------------------------------------------------------------------------------- /conditions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | "unicode/utf16" 6 | ) 7 | 8 | type WeatherConditionsTable struct { 9 | Code1 uint16 10 | Code2 uint16 11 | TextOffset uint32 12 | } 13 | 14 | func (f *Forecast) MakeWeatherConditionsTable() { 15 | f.Header.WeatherConditionTableOffset = f.GetCurrentSize() 16 | 17 | for _, condition := range weatherList.Conditions.Conditions { 18 | code1, err := strconv.ParseInt(condition.Code1, 16, 32) 19 | checkError(err) 20 | 21 | code2, err := strconv.ParseInt(condition.Code2, 16, 32) 22 | checkError(err) 23 | 24 | japaneseCode1, err := strconv.ParseInt(condition.JapaneseCode1, 16, 32) 25 | checkError(err) 26 | 27 | japaneseCode2, err := strconv.ParseInt(condition.JapaneseCode2, 16, 32) 28 | checkError(err) 29 | 30 | f.WeatherConditionsTable = append(f.WeatherConditionsTable, WeatherConditionsTable{ 31 | Code1: uint16(code1), 32 | Code2: uint16(code2), 33 | TextOffset: 0, 34 | }) 35 | 36 | f.WeatherConditionsTable = append(f.WeatherConditionsTable, WeatherConditionsTable{ 37 | Code1: uint16(japaneseCode1), 38 | Code2: uint16(japaneseCode2), 39 | TextOffset: 0, 40 | }) 41 | } 42 | 43 | f.Header.NumberOfWeatherConditionTables = uint32(len(f.WeatherConditionsTable)) 44 | } 45 | 46 | func (f *Forecast) MakeWeatherConditionText() { 47 | i := 0 48 | for _, condition := range weatherList.Conditions.Conditions { 49 | f.WeatherConditionsTable[i].TextOffset = f.GetCurrentSize() 50 | f.WeatherConditionsText = append(f.WeatherConditionsText, utf16.Encode([]rune(f.GetLocalizedName(condition.Name)))...) 51 | f.WeatherConditionsText = append(f.WeatherConditionsText, uint16(0)) 52 | for f.GetCurrentSize()&3 != 0 { 53 | f.WeatherConditionsText = append(f.WeatherConditionsText, uint16(0)) 54 | } 55 | 56 | f.WeatherConditionsTable[i+1].TextOffset = f.GetCurrentSize() 57 | f.WeatherConditionsText = append(f.WeatherConditionsText, utf16.Encode([]rune(f.GetLocalizedName(condition.Name)))...) 58 | f.WeatherConditionsText = append(f.WeatherConditionsText, uint16(0)) 59 | for f.GetCurrentSize()&3 != 0 { 60 | f.WeatherConditionsText = append(f.WeatherConditionsText, uint16(0)) 61 | } 62 | i += 2 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config-example.xml: -------------------------------------------------------------------------------- 1 | 2 | AUTH_KEY_HERE 3 | false 4 | forecast-channel 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | ) 7 | 8 | type Config struct { 9 | AccuweatherKey string `xml:"accuweather_key"` 10 | UseS3 bool `xml:"use_s3"` 11 | S3BucketName string `xml:"s3_bucket_name"` 12 | S3AccountID string `xml:"s3_account_id"` 13 | S3ConnectionURL string `xml:"s3_connection_url"` 14 | S3AccessIDKey string `xml:"s3_access_key_id"` 15 | S3SecretAccessKey string `xml:"s3_secret_access_key"` 16 | } 17 | 18 | func GetConfig() Config { 19 | data, err := os.ReadFile("config.xml") 20 | checkError(err) 21 | 22 | var config Config 23 | err = xml.Unmarshal(data, &config) 24 | checkError(err) 25 | 26 | return config 27 | } 28 | -------------------------------------------------------------------------------- /countries.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var countryCodes = map[string]uint8{} 4 | 5 | func PopulateCountryCodes() { 6 | countryCodes["Japan"] = 1 7 | countryCodes["Antarctica"] = 2 8 | countryCodes["Caribbean Netherlands"] = 3 9 | countryCodes["Falkland Islands"] = 4 10 | countryCodes["Sint Maarten"] = 7 11 | countryCodes["Australia"] = 65 12 | countryCodes["Anguilla"] = 8 13 | countryCodes["Antigua and Barbuda"] = 9 14 | countryCodes["Argentina"] = 10 15 | countryCodes["Aruba"] = 11 16 | countryCodes["Bahamas"] = 12 17 | countryCodes["Barbados"] = 13 18 | countryCodes["Belize"] = 14 19 | countryCodes["Bolivia"] = 15 20 | countryCodes["Brazil"] = 16 21 | countryCodes["British Virgin Islands"] = 17 22 | countryCodes["Canada"] = 18 23 | countryCodes["Cayman Islands"] = 19 24 | countryCodes["Chile"] = 20 25 | countryCodes["Colombia"] = 21 26 | countryCodes["Costa Rica"] = 22 27 | countryCodes["Curaçao"] = 38 28 | countryCodes["Dominica"] = 23 29 | countryCodes["Dominican Republic"] = 24 30 | countryCodes["Ecuador"] = 25 31 | countryCodes["El Salvador"] = 26 32 | countryCodes["French Guiana"] = 27 33 | countryCodes["Grenada"] = 28 34 | countryCodes["Guadeloupe"] = 29 35 | countryCodes["Guatemala"] = 30 36 | countryCodes["Guyana"] = 31 37 | countryCodes["Haiti"] = 32 38 | countryCodes["Honduras"] = 33 39 | countryCodes["Jamaica"] = 34 40 | countryCodes["Martinique"] = 35 41 | countryCodes["Mexico"] = 36 42 | countryCodes["Montserrat"] = 37 43 | countryCodes["Nicaragua"] = 39 44 | countryCodes["Panama"] = 40 45 | countryCodes["Paraguay"] = 41 46 | countryCodes["Peru"] = 42 47 | countryCodes["St. Kitts and Nevis"] = 43 48 | countryCodes["St. Lucia"] = 44 49 | countryCodes["St. Vincent and the Grenadines"] = 45 50 | countryCodes["Suriname"] = 46 51 | countryCodes["Trinidad and Tobago"] = 47 52 | countryCodes["Turks and Caicos Islands"] = 48 53 | countryCodes["United States"] = 49 54 | countryCodes["Uruguay"] = 50 55 | countryCodes["US Virgin Islands"] = 51 56 | countryCodes["Venezuela"] = 52 57 | countryCodes["Armenia"] = 53 58 | countryCodes["Belarus"] = 54 59 | countryCodes["Georgia"] = 55 60 | countryCodes["Kosovo"] = 56 61 | countryCodes["Faroe Islands"] = 63 62 | countryCodes["Albania"] = 64 63 | countryCodes["Australia"] = 65 64 | countryCodes["Austria"] = 66 65 | countryCodes["Belgium"] = 67 66 | countryCodes["Bosnia and Herzegovina"] = 68 67 | countryCodes["Botswana"] = 69 68 | countryCodes["Bulgaria"] = 70 69 | countryCodes["Croatia"] = 71 70 | countryCodes["Cyprus"] = 72 71 | countryCodes["Czechia"] = 73 72 | countryCodes["Denmark"] = 74 73 | countryCodes["Estonia"] = 75 74 | countryCodes["Finland"] = 76 75 | countryCodes["France"] = 77 76 | countryCodes["Germany"] = 78 77 | countryCodes["Greece"] = 79 78 | countryCodes["Hungary"] = 80 79 | countryCodes["Iceland"] = 81 80 | countryCodes["Ireland"] = 82 81 | countryCodes["Italy"] = 83 82 | countryCodes["Latvia"] = 84 83 | countryCodes["Lesotho"] = 85 84 | countryCodes["Liechtenstein"] = 86 85 | countryCodes["Lithuania"] = 87 86 | countryCodes["Luxembourg"] = 88 87 | countryCodes["North Macedonia"] = 89 88 | countryCodes["Malta"] = 90 89 | countryCodes["Montenegro"] = 91 90 | countryCodes["Mozambique"] = 92 91 | countryCodes["Namibia"] = 93 92 | countryCodes["Netherlands"] = 94 93 | countryCodes["New Zealand"] = 95 94 | countryCodes["Norway"] = 96 95 | countryCodes["Poland"] = 97 96 | countryCodes["Portugal"] = 98 97 | countryCodes["Romania"] = 99 98 | countryCodes["Russia"] = 100 99 | countryCodes["Serbia"] = 101 100 | countryCodes["Slovakia"] = 102 101 | countryCodes["Slovenia"] = 103 102 | countryCodes["South Africa"] = 104 103 | countryCodes["Spain"] = 105 104 | countryCodes["Eswatini"] = 106 105 | countryCodes["Sweden"] = 107 106 | countryCodes["Switzerland"] = 108 107 | countryCodes["Türkiye"] = 109 108 | countryCodes["United Kingdom"] = 110 109 | countryCodes["Zambia"] = 111 110 | countryCodes["Zimbabwe"] = 112 111 | countryCodes["Azerbaijan"] = 113 112 | countryCodes["Mauritania"] = 114 113 | countryCodes["Mali"] = 115 114 | countryCodes["Niger"] = 116 115 | countryCodes["Chad"] = 117 116 | countryCodes["Sudan"] = 118 117 | countryCodes["Eritrea"] = 119 118 | countryCodes["Dijibouti"] = 120 119 | countryCodes["Somalia"] = 121 120 | countryCodes["Andorra"] = 122 121 | countryCodes["Guernsey"] = 124 122 | countryCodes["Isle of Man"] = 125 123 | countryCodes["Jersey"] = 126 124 | countryCodes["Monaco"] = 127 125 | countryCodes["Taiwan"] = 128 126 | countryCodes["Cambodia"] = 129 127 | countryCodes["Laos"] = 130 128 | countryCodes["Mongolia"] = 131 129 | countryCodes["Myanmar"] = 132 130 | countryCodes["Nepal"] = 133 131 | countryCodes["Vietnam"] = 134 132 | countryCodes["North Korea"] = 135 133 | countryCodes["South Korea"] = 136 134 | countryCodes["Bangladesh"] = 137 135 | countryCodes["Bhutan"] = 138 136 | countryCodes["Brunei"] = 139 137 | countryCodes["Maldives"] = 140 138 | countryCodes["Sri Lanka"] = 141 139 | countryCodes["East Timor"] = 142 140 | countryCodes["British Indian Ocean Territory"] = 143 141 | countryCodes["Hong Kong"] = 144 142 | countryCodes["Macao"] = 145 143 | countryCodes["Cook Islands"] = 146 144 | countryCodes["Niue"] = 147 145 | countryCodes["Northern Mariana Islands"] = 149 146 | countryCodes["American Samoa"] = 150 147 | countryCodes["Guam"] = 151 148 | countryCodes["Indonesia"] = 152 149 | countryCodes["Singapore"] = 153 150 | countryCodes["Thailand"] = 154 151 | countryCodes["Philippines"] = 155 152 | countryCodes["Malaysia"] = 156 153 | countryCodes["Saint Barthélemy"] = 157 154 | countryCodes["Saint Martin"] = 158 155 | countryCodes["Saint Pierre and Miquelon"] = 159 156 | countryCodes["China"] = 160 157 | countryCodes["Afghanistan"] = 161 158 | countryCodes["Kazakhstan"] = 162 159 | countryCodes["Kyrgyzstan"] = 163 160 | countryCodes["Pakistan"] = 164 161 | countryCodes["Tajikistan"] = 165 162 | countryCodes["Turkmenistan"] = 166 163 | countryCodes["Uzbekistan"] = 167 164 | countryCodes["United Arab Emirates"] = 168 165 | countryCodes["India"] = 169 166 | countryCodes["Egypt"] = 170 167 | countryCodes["Oman"] = 171 168 | countryCodes["Qatar"] = 172 169 | countryCodes["Kuwait"] = 173 170 | countryCodes["Saudi Arabia"] = 174 171 | countryCodes["Syria"] = 175 172 | countryCodes["Bahrain"] = 176 173 | countryCodes["Jordan"] = 177 174 | countryCodes["Iran"] = 178 175 | countryCodes["Iraq"] = 179 176 | countryCodes["Israel"] = 180 177 | countryCodes["Moldova"] = 207 178 | countryCodes["Ukraine"] = 208 179 | countryCodes["Libya"] = 218 180 | countryCodes["Morocco"] = 219 181 | countryCodes["South Sudan"] = 220 182 | countryCodes["Cuba"] = 223 183 | countryCodes["Kiribati"] = 240 184 | } 185 | -------------------------------------------------------------------------------- /duplicates_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "github.com/wii-tools/lzx/lz10" 8 | "os" 9 | "testing" 10 | "unicode/utf16" 11 | ) 12 | 13 | func TestDuplicates(t *testing.T) { 14 | // Every country supports English so we will read from that 15 | PopulateCountryCodes() 16 | for c, u := range countryCodes { 17 | data, err := os.ReadFile(fmt.Sprintf("files/1/%s/forecast.bin", ZFill(u, 3))) 18 | if data == nil { 19 | fmt.Println(fmt.Sprintf("Skipping Country %s", c)) 20 | continue 21 | } 22 | if err != nil { 23 | fmt.Println(fmt.Sprintf("Error in Country %s", c)) 24 | t.Error(err) 25 | } 26 | 27 | decompressed, err := lz10.Decompress(data[320:]) 28 | if err != nil { 29 | fmt.Println(fmt.Sprintf("Error in Country %s", c)) 30 | t.Error(err) 31 | } 32 | 33 | var header Header 34 | err = binary.Read(bytes.NewReader(decompressed), binary.BigEndian, &header) 35 | if err != nil { 36 | fmt.Println(fmt.Sprintf("Error in Country %s", c)) 37 | t.Error(err) 38 | } 39 | 40 | shortTableOffset := header.ShortForecastTableOffset 41 | for i := 0; i < int(header.NumberOfShortForecastTables); i++ { 42 | locationCode := binary.BigEndian.Uint32(decompressed[shortTableOffset:]) 43 | locationTableOffset := header.LocationsTableOffset 44 | count := 0 45 | var locationOffsets []uint32 46 | for ii := 0; ii < int(header.NumberOfLocations); ii++ { 47 | currentLocationCode := binary.BigEndian.Uint32(decompressed[locationTableOffset:]) 48 | if currentLocationCode == locationCode { 49 | count++ 50 | locationOffsets = append(locationOffsets, locationTableOffset) 51 | } 52 | 53 | locationTableOffset += 24 54 | } 55 | 56 | if count != 1 { 57 | fmt.Println(fmt.Sprintf("Error in Country %s", c)) 58 | fmt.Println(fmt.Sprintf("Duplicate Detected. Count: %d, Location Code: %d", count, locationCode)) 59 | 60 | for _, offset := range locationOffsets { 61 | theNameOffset := binary.BigEndian.Uint32(decompressed[offset+4:]) 62 | var name []uint16 63 | for { 64 | // Find the name of the city I hope 65 | currBytes := binary.BigEndian.Uint16(decompressed[theNameOffset:]) 66 | if currBytes == 0 { 67 | break 68 | } 69 | 70 | name = append(name, currBytes) 71 | theNameOffset += 2 72 | } 73 | 74 | fmt.Println("City Name: ", string(utf16.Decode(name))) 75 | } 76 | } 77 | 78 | shortTableOffset += 72 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module ForecastChannel 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.25.2 7 | github.com/aws/aws-sdk-go-v2/config v1.27.4 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4 9 | github.com/aws/aws-sdk-go-v2/service/s3 v1.51.1 10 | github.com/wii-tools/lzx v0.0.0-20231015015251-a22af598bf96 11 | github.com/wk8/go-ordered-map/v2 v2.1.8 12 | golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 21 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.2 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect 29 | github.com/aws/smithy-go v1.20.1 // indirect 30 | github.com/bahlo/generic-list-go v0.2.0 // indirect 31 | github.com/buger/jsonparser v1.1.1 // indirect 32 | github.com/kr/pretty v0.3.0 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/rogpeppe/go-internal v1.8.1 // indirect 35 | github.com/stretchr/testify v1.8.4 // indirect 36 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 37 | gopkg.in/yaml.v3 v3.0.1 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= 2 | github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= 3 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU= 4 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1/go.mod h1:sxpLb+nZk7tIfCWChfd+h4QwHNUR57d8hA1cleTkjJo= 5 | github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= 6 | github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g= 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI= 8 | github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0= 9 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= 10 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= 11 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= 12 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= 13 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= 14 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= 15 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= 16 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= 17 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2 h1:en92G0Z7xlksoOylkUhuBSfJgijC7rHVLRdnIlHEs0E= 18 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.2/go.mod h1:HgtQ/wN5G+8QSlK62lbOtNwQ3wTSByJ4wH2rCkPt+AE= 19 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= 21 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.2 h1:zSdTXYLwuXDNPUS+V41i1SFDXG7V0ITp0D9UT9Cvl18= 22 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.2/go.mod h1:v8m8k+qVy95nYi7d56uP1QImleIIY25BPiNJYzPBdFE= 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk= 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0= 25 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2 h1:1oY1AVEisRI4HNuFoLdRUB0hC63ylDAN6Me3MrfclEg= 26 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.2/go.mod h1:KZ03VgvZwSjkT7fOetQ/wF3MZUvYFirlI1H5NklUNsY= 27 | github.com/aws/aws-sdk-go-v2/service/s3 v1.51.1 h1:juZ+uGargZOrQGNxkVHr9HHR/0N+Yu8uekQnV7EAVRs= 28 | github.com/aws/aws-sdk-go-v2/service/s3 v1.51.1/go.mod h1:SoR0c7Jnq8Tpmt0KSLXIavhjmaagRqQpe9r70W3POJg= 29 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ= 30 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8= 31 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw= 32 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= 33 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew= 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= 35 | github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= 36 | github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= 37 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 38 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 39 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 40 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 41 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 42 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 43 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 44 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 45 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 46 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 47 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 48 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 49 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 50 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 51 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 52 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 53 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 54 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 55 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 58 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 59 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 60 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 61 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 62 | github.com/wii-tools/lzx v0.0.0-20231015015251-a22af598bf96 h1:/VIs7AN9f+C1xXD4z5FwMWlhrr05EhyMge7zaMMjcxA= 63 | github.com/wii-tools/lzx v0.0.0-20231015015251-a22af598bf96/go.mod h1:ufCQJfNwbD5fkg6jHpw69vc5opbc42TePvnloMFYbmY= 64 | github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= 65 | github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 66 | golang.org/x/exp v0.0.0-20230118134722-a68e582fa157 h1:fiNkyhJPUvxbRPbCqY/D9qdjmPzfHcpK3P4bM4gioSY= 67 | golang.org/x/exp v0.0.0-20230118134722-a68e582fa157/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 71 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 72 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 73 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 74 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 75 | -------------------------------------------------------------------------------- /header.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Header struct { 4 | Version uint32 5 | Filesize uint32 6 | CRC32 uint32 7 | OpenTimestamp uint32 8 | CloseTimestamp uint32 9 | CountryCode uint8 10 | _ [3]byte 11 | LanguageCode uint8 12 | TemperatureFlag uint8 13 | Unknown uint8 14 | _ uint8 15 | MessageOffset uint32 16 | NumberOfLongForecastTables uint32 17 | LongForecastTableOffset uint32 18 | NumberOfShortForecastTables uint32 19 | ShortForecastTableOffset uint32 20 | NumberOfWeatherConditionTables uint32 21 | WeatherConditionTableOffset uint32 22 | NumberOfUVIndexTables uint32 23 | UVIndexTableOffset uint32 24 | NumberOfLaundryIndexTables uint32 25 | LaundryIndexTableOffset uint32 26 | NumberOfPollenCountTables uint32 27 | PollenCountTableOffset uint32 28 | NumberOfLocations uint32 29 | LocationsTableOffset uint32 30 | } 31 | 32 | func (f *Forecast) MakeHeader() { 33 | f.Header = Header{ 34 | Version: 0, 35 | Filesize: 0, 36 | CRC32: 0, 37 | OpenTimestamp: fixTime(int(currentTime)), 38 | CloseTimestamp: fixTime(int(currentTime)) + 63, 39 | CountryCode: f.currentCountryCode, 40 | LanguageCode: f.currentLanguageCode, 41 | TemperatureFlag: f.GetTemperatureFlag(), 42 | Unknown: 1, 43 | MessageOffset: 0, 44 | NumberOfLongForecastTables: 0, 45 | LongForecastTableOffset: 0, 46 | NumberOfShortForecastTables: 0, 47 | ShortForecastTableOffset: 0, 48 | NumberOfWeatherConditionTables: 0, 49 | WeatherConditionTableOffset: 0, 50 | NumberOfUVIndexTables: 0, 51 | UVIndexTableOffset: 0, 52 | NumberOfLaundryIndexTables: 0, 53 | LaundryIndexTableOffset: 0, 54 | NumberOfPollenCountTables: 0, 55 | PollenCountTableOffset: 0, 56 | NumberOfLocations: 0, 57 | LocationsTableOffset: 0, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /laundry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "unicode/utf16" 4 | 5 | type LaundryTable struct { 6 | Code uint8 7 | _ [3]byte 8 | TextOffset uint32 9 | } 10 | 11 | func (f *Forecast) MakeLaundryTable() { 12 | f.Header.LaundryIndexTableOffset = f.GetCurrentSize() 13 | 14 | for _, laundry := range weatherList.Laundry { 15 | f.LaundryTable = append(f.LaundryTable, LaundryTable{ 16 | Code: uint8(laundry.Code), 17 | TextOffset: 0, 18 | }) 19 | } 20 | 21 | f.Header.NumberOfLaundryIndexTables = uint32(len(f.LaundryTable)) 22 | } 23 | 24 | func (f *Forecast) MakeLaundryText() { 25 | for i, laundry := range weatherList.Laundry { 26 | f.LaundryTable[i].TextOffset = f.GetCurrentSize() 27 | f.LaundryText = append(f.LaundryText, utf16.Encode([]rune(laundry.Name))...) 28 | f.LaundryText = append(f.LaundryText, uint16(0)) 29 | for f.GetCurrentSize()&3 != 0 { 30 | f.LaundryText = append(f.LaundryText, uint16(0)) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /locations.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/wk8/go-ordered-map/v2" 5 | "unicode/utf16" 6 | ) 7 | 8 | type LocationTable struct { 9 | CountryCode uint8 10 | RegionCode uint8 11 | LocationCode uint16 12 | CityTextOffset uint32 13 | RegionTextOffset uint32 14 | CountryTextOffset uint32 15 | Latitude int16 16 | Longitude int16 17 | LocationZoom1 uint8 18 | LocationZoom2 uint8 19 | _ uint16 20 | } 21 | 22 | // Location helps us keep metadata on a specific location 23 | type Location struct { 24 | CountryCode uint8 25 | RegionCode uint8 26 | LocationCode uint16 27 | Latitude int16 28 | Longitude int16 29 | LocationZoom1 uint8 30 | LocationZoom2 uint8 31 | City *City 32 | InternationalCity *InternationalCity 33 | } 34 | 35 | func (f *Forecast) PopulateLocations(cities []InternationalCity) { 36 | locations := orderedmap.New[string, *orderedmap.OrderedMap[string, *orderedmap.OrderedMap[string, Location]]]() 37 | 38 | // First we will populate the national cities. This way we can ignore any duplicates in international. 39 | locations.Set(f.currentCountryList.Name.English, orderedmap.New[string, *orderedmap.OrderedMap[string, Location]]()) 40 | for _, city := range f.currentCountryList.Cities { 41 | // Populate province slice if it doesn't exist 42 | country, _ := locations.Get(f.currentCountryList.Name.English) 43 | if _, ok := country.Get(city.Province.English); !ok { 44 | country.Set(city.Province.English, orderedmap.New[string, Location]()) 45 | } 46 | 47 | province, _ := country.Get(city.Province.English) 48 | if _, ok := province.Get(city.English); ok { 49 | continue 50 | } 51 | 52 | _city := city 53 | province.Set(city.English, Location{ 54 | CountryCode: f.currentCountryCode, 55 | RegionCode: uint8(country.Len()) + 1, 56 | LocationCode: uint16(province.Len()) + 1, 57 | Latitude: CoordinateEncode(city.Latitude), 58 | Longitude: CoordinateEncode(city.Longitude), 59 | LocationZoom1: uint8(city.Zoom1), 60 | LocationZoom2: uint8(city.Zoom2), 61 | City: &_city, 62 | InternationalCity: &InternationalCity{}, 63 | }) 64 | } 65 | 66 | noProvince := 1 67 | for _, city := range cities { 68 | // Check if city was already populated from national cities 69 | if city.Country.English == f.currentCountryList.Name.English { 70 | continue 71 | } 72 | 73 | if _, ok := locations.Get(city.Country.English); !ok { 74 | locations.Set(city.Country.English, orderedmap.New[string, *orderedmap.OrderedMap[string, Location]]()) 75 | } 76 | 77 | country, _ := locations.Get(city.Country.English) 78 | if _, ok := country.Get(city.Province.English); !ok { 79 | country.Set(city.Province.English, orderedmap.New[string, Location]()) 80 | } 81 | 82 | province, _ := country.Get(city.Province.English) 83 | if _, ok := province.Get(city.Name.English); ok { 84 | continue 85 | } 86 | 87 | _city := city 88 | if _, ok := countryCodes[city.Country.English]; !ok && city.Province.English == "" { 89 | province.Set(city.Name.English, Location{ 90 | CountryCode: 0xFE, 91 | RegionCode: 0xFE, 92 | LocationCode: uint16(noProvince), 93 | Latitude: CoordinateEncode(city.Latitude), 94 | Longitude: CoordinateEncode(city.Longitude), 95 | LocationZoom1: uint8(city.Zoom1), 96 | LocationZoom2: uint8(city.Zoom2), 97 | City: &City{}, 98 | InternationalCity: &_city, 99 | }) 100 | noProvince++ 101 | continue 102 | } 103 | 104 | province.Set(city.Name.English, Location{ 105 | CountryCode: countryCodes[city.Country.English], 106 | RegionCode: uint8(country.Len()) + 1, 107 | LocationCode: uint16(province.Len()) + 1, 108 | Latitude: CoordinateEncode(city.Latitude), 109 | Longitude: CoordinateEncode(city.Longitude), 110 | LocationZoom1: uint8(city.Zoom1), 111 | LocationZoom2: uint8(city.Zoom2), 112 | City: &City{}, 113 | InternationalCity: &_city, 114 | }) 115 | } 116 | 117 | f.rawLocations = locations 118 | } 119 | 120 | func (f *Forecast) MakeLocationTable() { 121 | f.Header.LocationsTableOffset = f.GetCurrentSize() 122 | 123 | currentCountry, _ := f.rawLocations.Get(f.currentCountryList.Name.English) 124 | for province := currentCountry.Oldest(); province != nil; province = province.Next() { 125 | for city := province.Value.Oldest(); city != nil; city = city.Next() { 126 | f.LocationTable = append(f.LocationTable, LocationTable{ 127 | CountryCode: city.Value.CountryCode, 128 | RegionCode: city.Value.RegionCode, 129 | LocationCode: city.Value.LocationCode, 130 | CityTextOffset: 0, 131 | RegionTextOffset: 0, 132 | CountryTextOffset: 0, 133 | Latitude: city.Value.Latitude, 134 | Longitude: city.Value.Longitude, 135 | LocationZoom1: city.Value.LocationZoom1, 136 | LocationZoom2: city.Value.LocationZoom2, 137 | }) 138 | 139 | f.cityNames = append(f.cityNames, *city.Value.City) 140 | f.internationalCities = append(f.internationalCities, *city.Value.InternationalCity) 141 | } 142 | } 143 | 144 | for country := f.rawLocations.Oldest(); country != nil; country = country.Next() { 145 | if country.Key == f.currentCountryList.Name.English { 146 | continue 147 | } 148 | 149 | for province := country.Value.Oldest(); province != nil; province = province.Next() { 150 | for city := province.Value.Oldest(); city != nil; city = city.Next() { 151 | f.LocationTable = append(f.LocationTable, LocationTable{ 152 | CountryCode: city.Value.CountryCode, 153 | RegionCode: city.Value.RegionCode, 154 | LocationCode: city.Value.LocationCode, 155 | CityTextOffset: 0, 156 | RegionTextOffset: 0, 157 | CountryTextOffset: 0, 158 | Latitude: city.Value.Latitude, 159 | Longitude: city.Value.Longitude, 160 | LocationZoom1: city.Value.LocationZoom1, 161 | LocationZoom2: city.Value.LocationZoom2, 162 | }) 163 | 164 | f.cityNames = append(f.cityNames, *city.Value.City) 165 | f.internationalCities = append(f.internationalCities, *city.Value.InternationalCity) 166 | } 167 | } 168 | } 169 | 170 | f.Header.NumberOfLocations = uint32(len(f.LocationTable)) 171 | } 172 | 173 | func (f *Forecast) MakeLocationText() { 174 | // Map with text as a key and offset as value. This ensures we only write text once. 175 | writtenText := make(map[string]uint32) 176 | 177 | for i, city := range f.LocationTable { 178 | if city.CountryCode != f.currentCountryCode { 179 | continue 180 | } 181 | 182 | if value, ok := writtenText[f.GetCityName(f.cityNames[i])]; ok { 183 | f.LocationTable[i].CityTextOffset = value 184 | } else { 185 | f.LocationTable[i].CityTextOffset = f.GetCurrentSize() 186 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetCityName(f.cityNames[i])))...) 187 | f.LocationText = append(f.LocationText, uint16(0)) 188 | for f.GetCurrentSize()&3 != 0 { 189 | f.LocationText = append(f.LocationText, uint16(0)) 190 | } 191 | 192 | writtenText[f.GetCityName(f.cityNames[i])] = f.LocationTable[i].CityTextOffset 193 | } 194 | 195 | if value, ok := writtenText[f.GetLocalizedName(f.cityNames[i].Province)]; ok { 196 | f.LocationTable[i].RegionTextOffset = value 197 | } else { 198 | f.LocationTable[i].RegionTextOffset = f.GetCurrentSize() 199 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetLocalizedName(f.cityNames[i].Province)))...) 200 | f.LocationText = append(f.LocationText, uint16(0)) 201 | for f.GetCurrentSize()&3 != 0 { 202 | f.LocationText = append(f.LocationText, uint16(0)) 203 | } 204 | 205 | writtenText[f.GetLocalizedName(f.cityNames[i].Province)] = f.LocationTable[i].RegionTextOffset 206 | } 207 | 208 | if value, ok := writtenText[f.GetLocalizedName(f.currentCountryList.Name)]; ok { 209 | f.LocationTable[i].CountryTextOffset = value 210 | } else { 211 | f.LocationTable[i].CountryTextOffset = f.GetCurrentSize() 212 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetLocalizedName(f.currentCountryList.Name)))...) 213 | f.LocationText = append(f.LocationText, uint16(0)) 214 | for f.GetCurrentSize()&3 != 0 { 215 | f.LocationText = append(f.LocationText, uint16(0)) 216 | } 217 | 218 | writtenText[f.GetLocalizedName(f.currentCountryList.Name)] = f.LocationTable[i].CountryTextOffset 219 | } 220 | } 221 | 222 | // Now do international cities 223 | for i, city := range f.LocationTable { 224 | if city.CountryCode == f.currentCountryCode { 225 | continue 226 | } 227 | 228 | if value, ok := writtenText[f.GetLocalizedName(f.internationalCities[i].Name)]; ok { 229 | f.LocationTable[i].CityTextOffset = value 230 | } else { 231 | f.LocationTable[i].CityTextOffset = f.GetCurrentSize() 232 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetLocalizedName(f.internationalCities[i].Name)))...) 233 | f.LocationText = append(f.LocationText, uint16(0)) 234 | for f.GetCurrentSize()&3 != 0 { 235 | f.LocationText = append(f.LocationText, uint16(0)) 236 | } 237 | 238 | writtenText[f.GetLocalizedName(f.internationalCities[i].Name)] = f.LocationTable[i].CityTextOffset 239 | } 240 | 241 | if f.internationalCities[i].Province.English != "" { 242 | if value, ok := writtenText[f.GetLocalizedName(f.internationalCities[i].Province)]; ok { 243 | f.LocationTable[i].RegionTextOffset = value 244 | } else { 245 | f.LocationTable[i].RegionTextOffset = f.GetCurrentSize() 246 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetLocalizedName(f.internationalCities[i].Province)))...) 247 | f.LocationText = append(f.LocationText, uint16(0)) 248 | for f.GetCurrentSize()&3 != 0 { 249 | f.LocationText = append(f.LocationText, uint16(0)) 250 | } 251 | 252 | writtenText[f.GetLocalizedName(f.internationalCities[i].Province)] = f.LocationTable[i].RegionTextOffset 253 | } 254 | } 255 | 256 | if f.internationalCities[i].Country.English != "" { 257 | if value, ok := writtenText[f.GetLocalizedName(f.internationalCities[i].Country)]; ok { 258 | f.LocationTable[i].CountryTextOffset = value 259 | } else { 260 | f.LocationTable[i].CountryTextOffset = f.GetCurrentSize() 261 | f.LocationText = append(f.LocationText, utf16.Encode([]rune(f.GetLocalizedName(f.internationalCities[i].Country)))...) 262 | f.LocationText = append(f.LocationText, uint16(0)) 263 | for f.GetCurrentSize()&3 != 0 { 264 | f.LocationText = append(f.LocationText, uint16(0)) 265 | } 266 | 267 | writtenText[f.GetLocalizedName(f.internationalCities[i].Country)] = f.LocationTable[i].CountryTextOffset 268 | } 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /long_table.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type LongForecastTable struct { 8 | CountryCode uint8 9 | RegionCode uint8 10 | LocationCode uint16 11 | LocalTimestamp uint32 12 | GlobalTimestamp uint32 13 | Unknown uint32 14 | TodayForecast uint16 15 | Today6Hour12AMTo6AM uint16 16 | Today6Hour6AMTo12PM uint16 17 | Today6Hour12PMTo6PM uint16 18 | Today6Hour6PMTo12AM uint16 19 | TodayHighCelsius int8 20 | TodayHighDifferenceCelsius int8 21 | TodayLowCelsius int8 22 | TodayLowDifferenceCelsius int8 23 | TodayHighFahrenheit int8 24 | TodayHighDifferenceFahrenheit int8 25 | TodayLowFahrenheit int8 26 | TodayLowDifferenceFahrenheit int8 27 | Today6HourPrecipitation12AMTo6AM uint8 28 | Today6HourPrecipitation6AMTo12PM uint8 29 | Today6HourPrecipitation12PMTo6PM uint8 30 | Today6HourPrecipitation6PMTo12AM uint8 31 | TodayWindDirection uint8 32 | TodayWindSpeedMetric uint8 33 | TodayWindSpeedImperial uint8 34 | TodayUVIndex uint8 35 | TodayLaundryIndex uint8 36 | TodayPollenCount uint8 37 | TomorrowForecast uint16 38 | Tomorrow6Hour12AMTo6AM uint16 39 | Tomorrow6Hour6AMTo12PM uint16 40 | Tomorrow6Hour12PMTo6PM uint16 41 | Tomorrow6Hour6PMTo12AM uint16 42 | TomorrowHighCelsius int8 43 | TomorrowHighDifferenceCelsius int8 44 | TomorrowLowCelsius int8 45 | TomorrowLowDifferenceCelsius int8 46 | TomorrowHighFahrenheit int8 47 | TomorrowHighDifferenceFahrenheit int8 48 | TomorrowLowFahrenheit int8 49 | TomorrowLowDifferenceFahrenheit int8 50 | Tomorrow6HourPrecipitation12AMTo6AM uint8 51 | Tomorrow6HourPrecipitation6AMTo12PM uint8 52 | Tomorrow6HourPrecipitation12PMTo6PM uint8 53 | Tomorrow6HourPrecipitation6PMTo12AM uint8 54 | TomorrowWindDirection uint8 55 | TomorrowWindSpeedMetric uint8 56 | TomorrowWindSpeedImperial uint8 57 | TomorrowUVIndex uint8 58 | TomorrowLaundryIndex uint8 59 | TomorrowPollenCount uint8 60 | FiveDayForecastDay1 uint16 61 | FiveDayForecastDay1HighCelsius int8 62 | FiveDayForecastDay1LowCelsius int8 63 | FiveDayForecastDay1HighFahrenheit int8 64 | FiveDayForecastDay1LowFahrenheit int8 65 | FiveDayForecastDay1Precipitation int8 66 | _ uint8 67 | FiveDayForecastDay2 uint16 68 | FiveDayForecastDay2HighCelsius int8 69 | FiveDayForecastDay2LowCelsius int8 70 | FiveDayForecastDay2HighFahrenheit int8 71 | FiveDayForecastDay2LowFahrenheit int8 72 | FiveDayForecastDay2Precipitation int8 73 | _ uint8 74 | FiveDayForecastDay3 uint16 75 | FiveDayForecastDay3HighCelsius int8 76 | FiveDayForecastDay3LowCelsius int8 77 | FiveDayForecastDay3HighFahrenheit int8 78 | FiveDayForecastDay3LowFahrenheit int8 79 | FiveDayForecastDay3Precipitation int8 80 | _ uint8 81 | FiveDayForecastDay4 uint16 82 | FiveDayForecastDay4HighCelsius int8 83 | FiveDayForecastDay4LowCelsius int8 84 | FiveDayForecastDay4HighFahrenheit int8 85 | FiveDayForecastDay4LowFahrenheit int8 86 | FiveDayForecastDay4Precipitation int8 87 | _ uint8 88 | FiveDayForecastDay5 uint16 89 | FiveDayForecastDay5HighCelsius int8 90 | FiveDayForecastDay5LowCelsius int8 91 | FiveDayForecastDay5HighFahrenheit int8 92 | FiveDayForecastDay5LowFahrenheit int8 93 | FiveDayForecastDay5Precipitation int8 94 | _ uint8 95 | FiveDayForecastDay6 uint16 96 | FiveDayForecastDay6HighCelsius int8 97 | FiveDayForecastDay6LowCelsius int8 98 | FiveDayForecastDay6HighFahrenheit int8 99 | FiveDayForecastDay6LowFahrenheit int8 100 | FiveDayForecastDay6Precipitation int8 101 | _ uint8 102 | FiveDayForecastDay7 uint16 103 | FiveDayForecastDay7HighCelsius int8 104 | FiveDayForecastDay7LowCelsius int8 105 | FiveDayForecastDay7HighFahrenheit int8 106 | FiveDayForecastDay7LowFahrenheit int8 107 | FiveDayForecastDay7Precipitation int8 108 | _ uint8 109 | } 110 | 111 | func (f *Forecast) MakeLongForecastTable() { 112 | f.Header.LongForecastTableOffset = f.GetCurrentSize() 113 | 114 | currentCountry, _ := f.rawLocations.Get(f.currentCountryList.Name.English) 115 | for _, city := range f.currentCountryList.Cities { 116 | weather := *weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] 117 | 118 | province, _ := currentCountry.Get(city.Province.English) 119 | currentCity, _ := province.Get(city.English) 120 | countryCode := currentCity.CountryCode 121 | f.LongForecastTable = append(f.LongForecastTable, LongForecastTable{ 122 | CountryCode: countryCode, 123 | RegionCode: currentCity.RegionCode, 124 | LocationCode: currentCity.LocationCode, 125 | LocalTimestamp: fixTime(weather.Globe.Time), 126 | GlobalTimestamp: fixTime(int(currentTime)), 127 | TodayForecast: ConvertIcon(weather.Today.WeatherIcon, countryCode), 128 | Today6Hour12AMTo6AM: ConvertIcon(weather.HourlyIcon[0], countryCode), 129 | Today6Hour6AMTo12PM: ConvertIcon(weather.HourlyIcon[1], countryCode), 130 | Today6Hour12PMTo6PM: ConvertIcon(weather.HourlyIcon[2], countryCode), 131 | Today6Hour6PMTo12AM: ConvertIcon(weather.HourlyIcon[3], countryCode), 132 | TodayHighCelsius: int8(weather.Today.TempCelsiusMax), 133 | TodayHighDifferenceCelsius: -128, 134 | TodayLowCelsius: int8(weather.Today.TempCelsiusMin), 135 | TodayLowDifferenceCelsius: -128, 136 | TodayHighFahrenheit: int8(weather.Today.TempFahrenheitMax), 137 | TodayHighDifferenceFahrenheit: -128, 138 | TodayLowFahrenheit: int8(weather.Today.TempFahrenheitMin), 139 | TodayLowDifferenceFahrenheit: -128, 140 | Today6HourPrecipitation12AMTo6AM: uint8(weather.Precipitation[0]), 141 | Today6HourPrecipitation6AMTo12PM: uint8(weather.Precipitation[1]), 142 | Today6HourPrecipitation12PMTo6PM: uint8(weather.Precipitation[2]), 143 | Today6HourPrecipitation6PMTo12AM: uint8(weather.Precipitation[3]), 144 | TodayWindDirection: GetWind(weather.Wind.WindDirection), 145 | TodayWindSpeedMetric: uint8(weather.Wind.WindMetric), 146 | TodayWindSpeedImperial: uint8(weather.Wind.WindImperial), 147 | TodayUVIndex: uint8(weather.UVIndex), 148 | TodayLaundryIndex: 231, 149 | TodayPollenCount: uint8(weather.Pollen), 150 | TomorrowForecast: ConvertIcon(weather.Tomorrow.WeatherIcon, countryCode), 151 | Tomorrow6Hour12AMTo6AM: ConvertIcon(weather.HourlyIcon[4], countryCode), 152 | Tomorrow6Hour6AMTo12PM: ConvertIcon(weather.HourlyIcon[5], countryCode), 153 | Tomorrow6Hour12PMTo6PM: ConvertIcon(weather.HourlyIcon[6], countryCode), 154 | Tomorrow6Hour6PMTo12AM: ConvertIcon(weather.HourlyIcon[7], countryCode), 155 | TomorrowHighCelsius: int8(weather.Tomorrow.TempCelsiusMax), 156 | TomorrowHighDifferenceCelsius: -128, 157 | TomorrowLowCelsius: int8(weather.Tomorrow.TempCelsiusMin), 158 | TomorrowLowDifferenceCelsius: -128, 159 | TomorrowHighFahrenheit: int8(weather.Tomorrow.TempFahrenheitMax), 160 | TomorrowHighDifferenceFahrenheit: -128, 161 | TomorrowLowFahrenheit: int8(weather.Tomorrow.TempFahrenheitMin), 162 | TomorrowLowDifferenceFahrenheit: -128, 163 | Tomorrow6HourPrecipitation12AMTo6AM: uint8(weather.Precipitation[4]), 164 | Tomorrow6HourPrecipitation6AMTo12PM: uint8(weather.Precipitation[5]), 165 | Tomorrow6HourPrecipitation12PMTo6PM: uint8(weather.Precipitation[6]), 166 | Tomorrow6HourPrecipitation6PMTo12AM: uint8(weather.Precipitation[7]), 167 | TomorrowWindDirection: GetWind(weather.Wind.WindDirectionTomorrow), 168 | TomorrowWindSpeedMetric: uint8(weather.Wind.WindMetricTomorrow), 169 | TomorrowWindSpeedImperial: uint8(weather.Wind.WindImperialTomorrow), 170 | TomorrowUVIndex: uint8(weather.UVIndex), 171 | TomorrowLaundryIndex: 231, 172 | TomorrowPollenCount: uint8(weather.Pollen), 173 | FiveDayForecastDay1: ConvertIcon(weather.Week[0].WeatherIcon, countryCode), 174 | FiveDayForecastDay1HighCelsius: int8(weather.Week[0].TempCelsiusMax), 175 | FiveDayForecastDay1LowCelsius: int8(weather.Week[0].TempCelsiusMin), 176 | FiveDayForecastDay1HighFahrenheit: int8(weather.Week[0].TempFahrenheitMax), 177 | FiveDayForecastDay1LowFahrenheit: int8(weather.Week[0].TempFahrenheitMin), 178 | FiveDayForecastDay1Precipitation: int8(weather.Precipitation[8]), 179 | FiveDayForecastDay2: ConvertIcon(weather.Week[1].WeatherIcon, countryCode), 180 | FiveDayForecastDay2HighCelsius: int8(weather.Week[1].TempCelsiusMax), 181 | FiveDayForecastDay2LowCelsius: int8(weather.Week[1].TempCelsiusMin), 182 | FiveDayForecastDay2HighFahrenheit: int8(weather.Week[1].TempFahrenheitMax), 183 | FiveDayForecastDay2LowFahrenheit: int8(weather.Week[1].TempFahrenheitMin), 184 | FiveDayForecastDay2Precipitation: int8(weather.Precipitation[9]), 185 | FiveDayForecastDay3: ConvertIcon(weather.Week[2].WeatherIcon, countryCode), 186 | FiveDayForecastDay3HighCelsius: int8(weather.Week[2].TempCelsiusMax), 187 | FiveDayForecastDay3LowCelsius: int8(weather.Week[2].TempCelsiusMin), 188 | FiveDayForecastDay3HighFahrenheit: int8(weather.Week[2].TempFahrenheitMax), 189 | FiveDayForecastDay3LowFahrenheit: int8(weather.Week[2].TempFahrenheitMin), 190 | FiveDayForecastDay3Precipitation: int8(weather.Precipitation[10]), 191 | FiveDayForecastDay4: ConvertIcon(weather.Week[3].WeatherIcon, countryCode), 192 | FiveDayForecastDay4HighCelsius: int8(weather.Week[3].TempCelsiusMax), 193 | FiveDayForecastDay4LowCelsius: int8(weather.Week[3].TempCelsiusMin), 194 | FiveDayForecastDay4HighFahrenheit: int8(weather.Week[3].TempFahrenheitMax), 195 | FiveDayForecastDay4LowFahrenheit: int8(weather.Week[3].TempFahrenheitMin), 196 | FiveDayForecastDay4Precipitation: int8(weather.Precipitation[11]), 197 | FiveDayForecastDay5: ConvertIcon(weather.Week[4].WeatherIcon, countryCode), 198 | FiveDayForecastDay5HighCelsius: int8(weather.Week[4].TempCelsiusMax), 199 | FiveDayForecastDay5LowCelsius: int8(weather.Week[4].TempCelsiusMin), 200 | FiveDayForecastDay5HighFahrenheit: int8(weather.Week[4].TempFahrenheitMax), 201 | FiveDayForecastDay5LowFahrenheit: int8(weather.Week[4].TempFahrenheitMin), 202 | FiveDayForecastDay5Precipitation: int8(weather.Precipitation[12]), 203 | FiveDayForecastDay6: ConvertIcon(weather.Week[5].WeatherIcon, countryCode), 204 | FiveDayForecastDay6HighCelsius: int8(weather.Week[5].TempCelsiusMax), 205 | FiveDayForecastDay6LowCelsius: int8(weather.Week[5].TempCelsiusMin), 206 | FiveDayForecastDay6HighFahrenheit: int8(weather.Week[5].TempFahrenheitMax), 207 | FiveDayForecastDay6LowFahrenheit: int8(weather.Week[5].TempFahrenheitMin), 208 | FiveDayForecastDay6Precipitation: int8(weather.Precipitation[13]), 209 | FiveDayForecastDay7: ConvertIcon(weather.Week[6].WeatherIcon, countryCode), 210 | FiveDayForecastDay7HighCelsius: int8(weather.Week[6].TempCelsiusMax), 211 | FiveDayForecastDay7LowCelsius: int8(weather.Week[6].TempCelsiusMin), 212 | FiveDayForecastDay7HighFahrenheit: int8(weather.Week[6].TempFahrenheitMax), 213 | FiveDayForecastDay7LowFahrenheit: int8(weather.Week[6].TempFahrenheitMin), 214 | FiveDayForecastDay7Precipitation: int8(weather.Precipitation[14]), 215 | }) 216 | } 217 | 218 | f.Header.NumberOfLongForecastTables = uint32(len(f.LongForecastTable)) 219 | } 220 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "ForecastChannel/accuweather" 5 | "bytes" 6 | "context" 7 | "encoding/binary" 8 | "fmt" 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/aws/aws-sdk-go-v2/credentials" 12 | "github.com/aws/aws-sdk-go-v2/service/s3" 13 | "github.com/wii-tools/lzx/lz10" 14 | orderedmap "github.com/wk8/go-ordered-map/v2" 15 | "hash/crc32" 16 | "io" 17 | "log" 18 | "os" 19 | "runtime" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | type Forecast struct { 25 | Header Header 26 | LocationTable []LocationTable 27 | LocationText []uint16 28 | LongForecastTable []LongForecastTable 29 | ShortForecastTable []ShortForecastTable 30 | LaundryTable []LaundryTable 31 | LaundryText []uint16 32 | WeatherConditionsTable []WeatherConditionsTable 33 | WeatherConditionsText []uint16 34 | UVTable []UVTable 35 | UVText []uint16 36 | PollenTable []PollenTable 37 | PollenText []uint16 38 | 39 | currentLanguageCode uint8 40 | currentCountryCode uint8 41 | currentCountryList *NationalList 42 | rawLocations *orderedmap.OrderedMap[string, *orderedmap.OrderedMap[string, *orderedmap.OrderedMap[string, Location]]] 43 | cityNames []City 44 | internationalCities []InternationalCity 45 | } 46 | 47 | var ( 48 | currentTime = time.Now().Unix() 49 | weatherMap = map[string]*accuweather.Weather{} 50 | weatherList *WeatherList 51 | mapMutex = sync.RWMutex{} 52 | _config Config 53 | s3Client *s3.Client 54 | ) 55 | 56 | func main() { 57 | // Get all important data we need 58 | weatherList = ParseWeatherXML() 59 | PopulateCountryCodes() 60 | 61 | _config = GetConfig() 62 | 63 | // Load S3 Config 64 | r2Resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { 65 | return aws.Endpoint{ 66 | URL: _config.S3ConnectionURL, 67 | }, nil 68 | }) 69 | 70 | s3Config, err := config.LoadDefaultConfig(context.TODO(), 71 | config.WithEndpointResolverWithOptions(r2Resolver), 72 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(_config.S3AccessIDKey, _config.S3SecretAccessKey, "")), 73 | config.WithRegion("auto"), 74 | ) 75 | checkError(err) 76 | 77 | s3Client = s3.NewFromConfig(s3Config) 78 | 79 | // Async HTTP done safely and fast 80 | wg := sync.WaitGroup{} 81 | runtime.GOMAXPROCS(runtime.NumCPU()) 82 | semaphore := make(chan struct{}, 10) 83 | 84 | // Next retrieve international weather 85 | wg.Add(len(weatherList.International.Cities)) 86 | for _, city := range weatherList.International.Cities { 87 | go func(_city InternationalCity) { 88 | defer wg.Done() 89 | semaphore <- struct{}{} 90 | weather := accuweather.GetWeather(_city.Longitude, _city.Latitude, currentTime, _config.AccuweatherKey) 91 | mapMutex.Lock() 92 | weatherMap[fmt.Sprintf("%f,%f", _city.Longitude, _city.Latitude)] = weather 93 | mapMutex.Unlock() 94 | <-semaphore 95 | }(city) 96 | } 97 | wg.Wait() 98 | 99 | // We must get the number of national cities not yet generated 100 | numberOfCities := 0 101 | for _, cities := range weatherList.National { 102 | for _, city := range cities.Cities { 103 | if weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] == nil { 104 | numberOfCities++ 105 | } 106 | } 107 | } 108 | 109 | wg.Add(numberOfCities) 110 | for _, cities := range weatherList.National { 111 | for _, city := range cities.Cities { 112 | if weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] == nil { 113 | go func(_city City) { 114 | defer wg.Done() 115 | semaphore <- struct{}{} 116 | weather := accuweather.GetWeather(_city.Longitude, _city.Latitude, currentTime, _config.AccuweatherKey) 117 | mapMutex.Lock() 118 | weatherMap[fmt.Sprintf("%f,%f", _city.Longitude, _city.Latitude)] = weather 119 | mapMutex.Unlock() 120 | <-semaphore 121 | }(city) 122 | } 123 | } 124 | } 125 | wg.Wait() 126 | 127 | wg.Add(len(weatherList.National)) 128 | for _, national := range weatherList.National { 129 | countryCode := countryCodes[national.Name.English] 130 | national := national 131 | go func() { 132 | defer wg.Done() 133 | 134 | wg.Add(len(GetSupportedLanguages(countryCode))) 135 | for _, languageCode := range GetSupportedLanguages(countryCode) { 136 | languageCode := languageCode 137 | go func() { 138 | defer wg.Done() 139 | 140 | semaphore <- struct{}{} 141 | forecast := Forecast{} 142 | forecast.currentCountryList = &national 143 | forecast.currentCountryCode = countryCode 144 | forecast.currentLanguageCode = languageCode 145 | forecast.PopulateLocations(weatherList.International.Cities) 146 | 147 | buffer := new(bytes.Buffer) 148 | forecast.MakeHeader() 149 | forecast.MakeLocationTable() 150 | forecast.MakeLocationText() 151 | forecast.MakeLongForecastTable() 152 | forecast.MakeShortForecastTable(weatherList.International.Cities) 153 | forecast.MakeLaundryTable() 154 | forecast.MakeLaundryText() 155 | forecast.MakeWeatherConditionsTable() 156 | forecast.MakeWeatherConditionText() 157 | forecast.MakeUVTable() 158 | forecast.MakeUVText() 159 | forecast.MakePollenTable() 160 | forecast.MakePollenText() 161 | forecast.WriteAll(buffer) 162 | 163 | forecast.Header.Filesize = uint32(buffer.Len()) 164 | buffer.Reset() 165 | forecast.WriteAll(buffer) 166 | 167 | crcTable := crc32.MakeTable(crc32.IEEE) 168 | checksum := crc32.Checksum(buffer.Bytes()[12:], crcTable) 169 | forecast.Header.CRC32 = checksum 170 | 171 | buffer.Reset() 172 | forecast.WriteAll(buffer) 173 | 174 | // Make short.bin 175 | short := forecast.MakeShortBin(weatherList.International.Cities) 176 | 177 | // Make the folder if it doesn't already exist 178 | err := os.Mkdir(fmt.Sprintf("./files/%d/%s", languageCode, ZFill(countryCode, 3)), 0755) 179 | if !os.IsExist(err) { 180 | // If the folder exists we can just continue 181 | checkError(err) 182 | } 183 | 184 | compressed, err := lz10.Compress(buffer.Bytes()) 185 | checkError(err) 186 | 187 | if _config.UseS3 { 188 | _, err = s3Client.PutObject(context.Background(), &s3.PutObjectInput{ 189 | Bucket: aws.String(_config.S3BucketName), 190 | Key: aws.String(fmt.Sprintf("%d/%s/forecast.bin", languageCode, ZFill(countryCode, 3))), 191 | Body: bytes.NewReader(SignFile(compressed)), 192 | }) 193 | checkError(err) 194 | 195 | _, err = s3Client.PutObject(context.Background(), &s3.PutObjectInput{ 196 | Bucket: aws.String(_config.S3BucketName), 197 | Key: aws.String(fmt.Sprintf("%d/%s/short.bin", languageCode, ZFill(countryCode, 3))), 198 | Body: bytes.NewReader(SignFile(short)), 199 | }) 200 | checkError(err) 201 | } else { 202 | err = os.WriteFile(fmt.Sprintf("./files/%d/%s/forecast.bin", languageCode, ZFill(countryCode, 3)), SignFile(compressed), 0666) 203 | checkError(err) 204 | 205 | err = os.WriteFile(fmt.Sprintf("./files/%d/%s/short.bin", languageCode, ZFill(countryCode, 3)), SignFile(short), 0666) 206 | checkError(err) 207 | } 208 | <-semaphore 209 | }() 210 | } 211 | }() 212 | } 213 | 214 | wg.Wait() 215 | } 216 | 217 | func checkError(err error) { 218 | if err != nil { 219 | log.Fatalf("Forecast Channel file generator has encountered a fatal error! Reason: %v\n", err) 220 | } 221 | } 222 | 223 | func Write(writer io.Writer, data any) { 224 | err := binary.Write(writer, binary.BigEndian, data) 225 | checkError(err) 226 | } 227 | 228 | func (f *Forecast) WriteAll(writer io.Writer) { 229 | Write(writer, f.Header) 230 | Write(writer, f.LocationTable) 231 | Write(writer, f.LocationText) 232 | Write(writer, f.LongForecastTable) 233 | Write(writer, f.ShortForecastTable) 234 | Write(writer, f.LaundryTable) 235 | Write(writer, f.LaundryText) 236 | Write(writer, f.WeatherConditionsTable) 237 | Write(writer, f.WeatherConditionsText) 238 | Write(writer, f.UVTable) 239 | Write(writer, f.UVText) 240 | Write(writer, f.PollenTable) 241 | Write(writer, f.PollenText) 242 | } 243 | 244 | func (f *Forecast) GetCurrentSize() uint32 { 245 | buffer := bytes.NewBuffer(nil) 246 | f.WriteAll(buffer) 247 | 248 | return uint32(buffer.Len()) 249 | } 250 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "os" 6 | ) 7 | 8 | type WeatherList struct { 9 | XMLName xml.Name `xml:"root"` 10 | National []NationalList `xml:"country"` 11 | International InternationalList `xml:"international"` 12 | Conditions ConditionsList `xml:"conditions"` 13 | Laundry []Laundry `xml:"laundry"` 14 | Wind []Wind `xml:"wind"` 15 | UV []UV `xml:"uv"` 16 | Pollen []Pollen `xml:"pollen"` 17 | } 18 | 19 | type NationalList struct { 20 | Name LocalizedNames `xml:"name"` 21 | Cities []City `xml:"city"` 22 | } 23 | 24 | type City struct { 25 | XMLName xml.Name `xml:"city"` 26 | Japanese string `xml:"jpn,attr"` 27 | English string `xml:"eng,attr"` 28 | German string `xml:"de,attr"` 29 | French string `xml:"fr,attr"` 30 | Spanish string `xml:"es,attr"` 31 | Italian string `xml:"it,attr"` 32 | Dutch string `xml:"nl,attr"` 33 | Russian string `xml:"rus,attr"` 34 | Province LocalizedNames `xml:"province"` 35 | Longitude float64 `xml:"longitude"` 36 | Latitude float64 `xml:"latitude"` 37 | Zoom1 int `xml:"zoom1"` 38 | Zoom2 int `xml:"zoom2"` 39 | } 40 | 41 | // LocalizedNames exists because I was too lazy to fix my XML for every single country 42 | type LocalizedNames struct { 43 | Japanese string `xml:"jpn,attr"` 44 | English string `xml:"eng,attr"` 45 | German string `xml:"de,attr"` 46 | French string `xml:"fr,attr"` 47 | Spanish string `xml:"es,attr"` 48 | Italian string `xml:"it,attr"` 49 | Dutch string `xml:"nl,attr"` 50 | Russian string `xml:"rus,attr"` 51 | } 52 | 53 | type InternationalList struct { 54 | XMLName xml.Name `xml:"international"` 55 | Cities []InternationalCity `xml:"city"` 56 | } 57 | 58 | type InternationalCity struct { 59 | XMLName xml.Name `xml:"city"` 60 | Name LocalizedNames `xml:"name"` 61 | Province LocalizedNames `xml:"province"` 62 | Country LocalizedNames `xml:"country"` 63 | Longitude float64 `xml:"longitude"` 64 | Latitude float64 `xml:"latitude"` 65 | Zoom1 int `xml:"zoom1"` 66 | Zoom2 int `xml:"zoom2"` 67 | } 68 | 69 | type ConditionsList struct { 70 | XMLName xml.Name `xml:"conditions"` 71 | Conditions []Condition `xml:"condition"` 72 | } 73 | 74 | type Condition struct { 75 | Code int `xml:"code"` 76 | Name LocalizedNames `xml:"name"` 77 | Code1 string `xml:"code_1"` 78 | Code2 string `xml:"code_2"` 79 | JapaneseCode1 string `xml:"japanese_code_1"` 80 | JapaneseCode2 string `xml:"japanese_code_2"` 81 | } 82 | 83 | type Laundry struct { 84 | XMLName xml.Name `xml:"laundry"` 85 | Name string `xml:"name"` 86 | Code int `xml:"code"` 87 | } 88 | 89 | type Wind struct { 90 | XMLName xml.Name `xml:"wind"` 91 | Name string `xml:"name"` 92 | Code int `xml:"code"` 93 | } 94 | 95 | type UV struct { 96 | XMLName xml.Name `xml:"uv"` 97 | Name LocalizedNames `xml:"name"` 98 | } 99 | 100 | type Pollen struct { 101 | XMLName xml.Name `xml:"pollen"` 102 | Name string `xml:"name"` 103 | Code int `xml:"code"` 104 | } 105 | 106 | func ParseWeatherXML() *WeatherList { 107 | var weather WeatherList 108 | data, err := os.ReadFile("weather.xml") 109 | checkError(err) 110 | 111 | err = xml.Unmarshal(data, &weather) 112 | checkError(err) 113 | 114 | return &weather 115 | } 116 | -------------------------------------------------------------------------------- /pollen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "unicode/utf16" 4 | 5 | type PollenTable struct { 6 | Code uint8 7 | _ [3]byte 8 | TextOffset uint32 9 | } 10 | 11 | func (f *Forecast) MakePollenTable() { 12 | f.Header.PollenCountTableOffset = f.GetCurrentSize() 13 | 14 | for _, pollen := range weatherList.Pollen { 15 | f.PollenTable = append(f.PollenTable, PollenTable{ 16 | Code: uint8(pollen.Code), 17 | TextOffset: 0, 18 | }) 19 | } 20 | 21 | f.Header.NumberOfPollenCountTables = uint32(len(f.PollenTable)) 22 | } 23 | 24 | func (f *Forecast) MakePollenText() { 25 | for i, pollen := range weatherList.Pollen { 26 | f.PollenTable[i].TextOffset = f.GetCurrentSize() 27 | f.PollenText = append(f.PollenText, utf16.Encode([]rune(pollen.Name))...) 28 | f.PollenText = append(f.PollenText, uint16(0)) 29 | for f.GetCurrentSize()&3 != 0 { 30 | f.PollenText = append(f.PollenText, uint16(0)) 31 | } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /short_bin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/wii-tools/lzx/lz10" 7 | "hash/crc32" 8 | ) 9 | 10 | type ShortHeader struct { 11 | Version uint32 12 | Filesize uint32 13 | CRC32 uint32 14 | OpenTimestamp uint32 15 | CloseTimestamp uint32 16 | CountryCode uint8 17 | _ [3]byte 18 | LanguageCode uint8 19 | TemperatureFlag uint8 20 | _ uint16 21 | NumberOfCurrentForecastTables uint32 22 | CurrentForecastTableOffset uint32 23 | } 24 | 25 | type CurrentForecastTable struct { 26 | CountryCode uint8 27 | RegionCode uint8 28 | LocationCode uint16 29 | LocalTimestamp uint32 30 | GlobalTimestamp uint32 31 | CurrentForecast uint16 32 | _ uint8 33 | CurrentTemperatureCelsius uint8 34 | CurrentTemperatureFahrenheit uint8 35 | CurrentWindDirection uint8 36 | CurrentWindSpeedMetric uint8 37 | CurrentWindSpeedImperial uint8 38 | _ uint16 39 | Unknown uint16 40 | } 41 | 42 | func (f *Forecast) MakeShortBin(cities []InternationalCity) []byte { 43 | header := ShortHeader{ 44 | Version: 0, 45 | Filesize: 0, 46 | CRC32: 0, 47 | OpenTimestamp: fixTime(int(currentTime)), 48 | CloseTimestamp: fixTime(int(currentTime)) + 63, 49 | CountryCode: f.currentCountryCode, 50 | LanguageCode: f.currentLanguageCode, 51 | TemperatureFlag: 0, 52 | NumberOfCurrentForecastTables: 0, 53 | CurrentForecastTableOffset: 36, 54 | } 55 | var currentForecastTables []CurrentForecastTable 56 | 57 | currentCountry, _ := f.rawLocations.Get(f.currentCountryList.Name.English) 58 | for _, city := range f.currentCountryList.Cities { 59 | weather := *weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] 60 | 61 | province, _ := currentCountry.Get(city.Province.English) 62 | currentCity, _ := province.Get(city.English) 63 | countryCode := currentCity.CountryCode 64 | currentForecastTables = append(currentForecastTables, CurrentForecastTable{ 65 | CountryCode: countryCode, 66 | RegionCode: currentCity.RegionCode, 67 | LocationCode: currentCity.LocationCode, 68 | LocalTimestamp: fixTime(weather.Globe.Time), 69 | GlobalTimestamp: fixTime(int(currentTime)), 70 | CurrentForecast: ConvertIcon(weather.Current.WeatherIcon, countryCode), 71 | CurrentTemperatureCelsius: uint8(weather.Current.TempCelsius), 72 | CurrentTemperatureFahrenheit: uint8(weather.Current.TempFahrenheit), 73 | CurrentWindDirection: GetWind(weather.Current.WindDirection), 74 | CurrentWindSpeedMetric: uint8(weather.Current.WindMetric), 75 | CurrentWindSpeedImperial: uint8(weather.Current.WindImperial), 76 | Unknown: 0xFFFF, 77 | }) 78 | } 79 | 80 | for _, city := range cities { 81 | weather := *weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] 82 | if city.Country.English == f.currentCountryList.Name.English { 83 | continue 84 | } 85 | 86 | country, _ := f.rawLocations.Get(city.Country.English) 87 | province, _ := country.Get(city.Province.English) 88 | currentCity, _ := province.Get(city.Name.English) 89 | 90 | countryCode := currentCity.CountryCode 91 | currentForecastTables = append(currentForecastTables, CurrentForecastTable{ 92 | CountryCode: countryCode, 93 | RegionCode: currentCity.RegionCode, 94 | LocationCode: currentCity.LocationCode, 95 | LocalTimestamp: fixTime(weather.Globe.Time), 96 | GlobalTimestamp: fixTime(int(currentTime)), 97 | CurrentForecast: ConvertIcon(weather.Current.WeatherIcon, countryCode), 98 | CurrentTemperatureCelsius: uint8(weather.Current.TempCelsius), 99 | CurrentTemperatureFahrenheit: uint8(weather.Current.TempFahrenheit), 100 | CurrentWindDirection: GetWind(weather.Current.WindDirection), 101 | CurrentWindSpeedMetric: uint8(weather.Current.WindMetric), 102 | CurrentWindSpeedImperial: uint8(weather.Current.WindImperial), 103 | Unknown: 0xFFFF, 104 | }) 105 | } 106 | 107 | header.NumberOfCurrentForecastTables = uint32(len(currentForecastTables)) 108 | 109 | buffer := new(bytes.Buffer) 110 | Write(buffer, header) 111 | Write(buffer, currentForecastTables) 112 | 113 | header.Filesize = uint32(buffer.Len()) 114 | buffer.Reset() 115 | Write(buffer, header) 116 | Write(buffer, currentForecastTables) 117 | 118 | crcTable := crc32.MakeTable(crc32.IEEE) 119 | checksum := crc32.Checksum(buffer.Bytes()[12:], crcTable) 120 | header.CRC32 = checksum 121 | 122 | buffer.Reset() 123 | Write(buffer, header) 124 | Write(buffer, currentForecastTables) 125 | 126 | compressed, err := lz10.Compress(buffer.Bytes()) 127 | checkError(err) 128 | 129 | return compressed 130 | } 131 | -------------------------------------------------------------------------------- /short_table.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ShortForecastTable struct { 8 | CountryCode uint8 9 | RegionCode uint8 10 | LocationCode uint16 11 | LocalTimestamp uint32 12 | GlobalTimestamp uint32 13 | _ uint32 14 | TodayForecast uint16 15 | Today6Hour12AMTo6AM uint16 16 | Today6Hour6AMTo12PM uint16 17 | Today6Hour12PMTo6PM uint16 18 | Today6Hour6PMTo12AM uint16 19 | TodayHighCelsius int8 20 | TodayHighDifferenceCelsius int8 21 | TodayLowCelsius int8 22 | TodayLowDifferenceCelsius int8 23 | TodayHighFahrenheit int8 24 | TodayHighDifferenceFahrenheit int8 25 | TodayLowFahrenheit int8 26 | TodayLowDifferenceFahrenheit int8 27 | Today6HourPrecipitation12AMTo6AM uint8 28 | Today6HourPrecipitation6AMTo12PM uint8 29 | Today6HourPrecipitation12PMTo6PM uint8 30 | Today6HourPrecipitation6PMTo12AM uint8 31 | TodayWindDirection uint8 32 | TodayWindSpeedMetric uint8 33 | TodayWindSpeedImperial uint8 34 | Unknown [3]byte 35 | TomorrowForecast uint16 36 | Tomorrow6Hour12AMTo6AM uint16 37 | Tomorrow6Hour6AMTo12PM uint16 38 | Tomorrow6Hour12PMTo6PM uint16 39 | Tomorrow6Hour6PMTo12AM uint16 40 | TomorrowHighCelsius int8 41 | TomorrowHighDifferenceCelsius int8 42 | TomorrowLowCelsius int8 43 | TomorrowLowDifferenceCelsius int8 44 | TomorrowHighFahrenheit int8 45 | TomorrowHighDifferenceFahrenheit int8 46 | TomorrowLowFahrenheit int8 47 | TomorrowLowDifferenceFahrenheit int8 48 | Tomorrow6HourPrecipitation12AMTo6AM uint8 49 | Tomorrow6HourPrecipitation6AMTo12PM uint8 50 | Tomorrow6HourPrecipitation12PMTo6PM uint8 51 | Tomorrow6HourPrecipitation6PMTo12AM uint8 52 | TomorrowWindDirection uint8 53 | TomorrowWindSpeedMetric uint8 54 | TomorrowWindSpeedImperial uint8 55 | TodayUVIndex uint8 56 | TodayLaundryIndex uint8 57 | TodayPollenCount uint8 58 | } 59 | 60 | func (f *Forecast) MakeShortForecastTable(cities []InternationalCity) { 61 | f.Header.ShortForecastTableOffset = f.GetCurrentSize() 62 | 63 | for _, city := range cities { 64 | weather := *weatherMap[fmt.Sprintf("%f,%f", city.Longitude, city.Latitude)] 65 | if city.Country.English == f.currentCountryList.Name.English { 66 | continue 67 | } 68 | 69 | country, _ := f.rawLocations.Get(city.Country.English) 70 | province, _ := country.Get(city.Province.English) 71 | currentCity, _ := province.Get(city.Name.English) 72 | countryCode := currentCity.CountryCode 73 | f.ShortForecastTable = append(f.ShortForecastTable, ShortForecastTable{ 74 | CountryCode: countryCode, 75 | RegionCode: currentCity.RegionCode, 76 | LocationCode: currentCity.LocationCode, 77 | LocalTimestamp: fixTime(weather.Globe.Time), 78 | GlobalTimestamp: fixTime(int(currentTime)), 79 | TodayForecast: ConvertIcon(weather.Today.WeatherIcon, countryCode), 80 | Today6Hour12AMTo6AM: ConvertIcon(weather.HourlyIcon[0], countryCode), 81 | Today6Hour6AMTo12PM: ConvertIcon(weather.HourlyIcon[1], countryCode), 82 | Today6Hour12PMTo6PM: ConvertIcon(weather.HourlyIcon[2], countryCode), 83 | Today6Hour6PMTo12AM: ConvertIcon(weather.HourlyIcon[3], countryCode), 84 | TodayHighCelsius: int8(weather.Today.TempCelsiusMax), 85 | TodayHighDifferenceCelsius: -128, 86 | TodayLowCelsius: int8(weather.Today.TempCelsiusMin), 87 | TodayLowDifferenceCelsius: -128, 88 | TodayHighFahrenheit: int8(weather.Today.TempFahrenheitMax), 89 | TodayHighDifferenceFahrenheit: -128, 90 | TodayLowFahrenheit: int8(weather.Today.TempFahrenheitMin), 91 | TodayLowDifferenceFahrenheit: -128, 92 | Today6HourPrecipitation12AMTo6AM: uint8(weather.Precipitation[0]), 93 | Today6HourPrecipitation6AMTo12PM: uint8(weather.Precipitation[1]), 94 | Today6HourPrecipitation12PMTo6PM: uint8(weather.Precipitation[2]), 95 | Today6HourPrecipitation6PMTo12AM: uint8(weather.Precipitation[3]), 96 | TodayWindDirection: GetWind(weather.Wind.WindDirection), 97 | TodayWindSpeedMetric: uint8(weather.Wind.WindMetric), 98 | TodayWindSpeedImperial: uint8(weather.Wind.WindImperial), 99 | Unknown: [3]byte{0xFF, 0xFF, 0xFF}, 100 | TomorrowForecast: ConvertIcon(weather.Tomorrow.WeatherIcon, countryCode), 101 | Tomorrow6Hour12AMTo6AM: ConvertIcon(weather.HourlyIcon[4], countryCode), 102 | Tomorrow6Hour6AMTo12PM: ConvertIcon(weather.HourlyIcon[5], countryCode), 103 | Tomorrow6Hour12PMTo6PM: ConvertIcon(weather.HourlyIcon[6], countryCode), 104 | Tomorrow6Hour6PMTo12AM: ConvertIcon(weather.HourlyIcon[7], countryCode), 105 | TomorrowHighCelsius: int8(weather.Tomorrow.TempCelsiusMax), 106 | TomorrowHighDifferenceCelsius: -128, 107 | TomorrowLowCelsius: int8(weather.Tomorrow.TempCelsiusMin), 108 | TomorrowLowDifferenceCelsius: -128, 109 | TomorrowHighFahrenheit: int8(weather.Tomorrow.TempFahrenheitMax), 110 | TomorrowHighDifferenceFahrenheit: -128, 111 | TomorrowLowFahrenheit: int8(weather.Tomorrow.TempFahrenheitMin), 112 | TomorrowLowDifferenceFahrenheit: -128, 113 | Tomorrow6HourPrecipitation12AMTo6AM: uint8(weather.Precipitation[4]), 114 | Tomorrow6HourPrecipitation6AMTo12PM: uint8(weather.Precipitation[5]), 115 | Tomorrow6HourPrecipitation12PMTo6PM: uint8(weather.Precipitation[6]), 116 | Tomorrow6HourPrecipitation6PMTo12AM: uint8(weather.Precipitation[7]), 117 | TomorrowWindDirection: GetWind(weather.Wind.WindDirectionTomorrow), 118 | TomorrowWindSpeedMetric: uint8(weather.Wind.WindMetricTomorrow), 119 | TomorrowWindSpeedImperial: uint8(weather.Wind.WindImperialTomorrow), 120 | TodayUVIndex: uint8(weather.UVIndex), 121 | TodayLaundryIndex: 255, 122 | TodayPollenCount: uint8(weather.Pollen), 123 | }) 124 | } 125 | 126 | f.Header.NumberOfShortForecastTables = uint32(len(f.ShortForecastTable)) 127 | } 128 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "crypto/rsa" 8 | "crypto/sha1" 9 | "crypto/x509" 10 | "encoding/pem" 11 | "golang.org/x/exp/slices" 12 | "os" 13 | "strconv" 14 | ) 15 | 16 | // fixTime adjusts the timestamp to coincide with the Wii's UTC timestamp. 17 | func fixTime(value int) uint32 { 18 | return uint32((value - 946684800) / 60) 19 | } 20 | 21 | func ConvertIcon(icon int, countryCode uint8) uint16 { 22 | code := "FFFF" 23 | for _, condition := range weatherList.Conditions.Conditions { 24 | if condition.Code == icon { 25 | if countryCode == 1 { 26 | code = condition.JapaneseCode1 27 | } else { 28 | code = condition.Code1 29 | } 30 | } 31 | } 32 | value, err := strconv.ParseInt(code, 16, 32) 33 | checkError(err) 34 | 35 | return uint16(value) 36 | } 37 | 38 | func GetWind(value string) uint8 { 39 | for _, wind := range weatherList.Wind { 40 | if wind.Name == value { 41 | return uint8(wind.Code) 42 | } 43 | } 44 | 45 | return 0xFF 46 | } 47 | 48 | func CoordinateEncode(value float64) int16 { 49 | value /= 0.0054931640625 50 | return int16(value) 51 | } 52 | 53 | func ZFill(value uint8, size int) string { 54 | str := strconv.FormatInt(int64(value), 10) 55 | temp := "" 56 | 57 | for i := 0; i < size-len(str); i++ { 58 | temp += "0" 59 | } 60 | 61 | return temp + str 62 | } 63 | 64 | func (f *Forecast) IsJapan() bool { 65 | return f.currentCountryCode == 1 66 | } 67 | 68 | func (f *Forecast) GetTemperatureFlag() uint8 { 69 | if f.currentCountryCode == 1 { 70 | return 0 71 | } else if slices.Contains([]uint8{8, 9, 12, 14, 17, 19, 37, 43, 48, 49, 51}, f.currentCountryCode) { 72 | return 1 73 | } else { 74 | return 2 75 | } 76 | } 77 | 78 | func GetSupportedLanguages(countryCode uint8) []uint8 { 79 | // TODO: Fill out the Russian supported countries first as those are specific 80 | if countryCode == 100 { 81 | return []uint8{1, 7} 82 | } 83 | 84 | if countryCode == 1 { 85 | return []uint8{0, 1, 2, 3, 4, 5, 6} 86 | } else if 8 <= countryCode && countryCode <= 52 { 87 | return []uint8{1, 3, 4} 88 | } else if 64 <= countryCode && countryCode <= 110 { 89 | return []uint8{1, 2, 3, 4, 5, 6} 90 | } 91 | 92 | return []uint8{0, 1, 2, 3, 4, 5, 6} 93 | } 94 | 95 | func (f *Forecast) GetLocalizedName(names LocalizedNames) string { 96 | switch f.currentLanguageCode { 97 | case 0: 98 | return names.Japanese 99 | case 1: 100 | return names.English 101 | case 2: 102 | return names.German 103 | case 3: 104 | return names.French 105 | case 4: 106 | return names.Spanish 107 | case 5: 108 | return names.Italian 109 | case 6: 110 | return names.Dutch 111 | case 7: 112 | return names.Russian 113 | } 114 | 115 | // Impossible to reach here 116 | return "" 117 | } 118 | 119 | func (f *Forecast) GetCityName(city City) string { 120 | switch f.currentLanguageCode { 121 | case 0: 122 | return city.Japanese 123 | case 1: 124 | return city.English 125 | case 2: 126 | return city.German 127 | case 3: 128 | return city.French 129 | case 4: 130 | return city.Spanish 131 | case 5: 132 | return city.Italian 133 | case 6: 134 | return city.Dutch 135 | case 7: 136 | return city.Russian 137 | } 138 | 139 | // Impossible to reach here 140 | return "" 141 | } 142 | 143 | func SignFile(contents []byte) []byte { 144 | buffer := new(bytes.Buffer) 145 | 146 | // Get RSA key and sign 147 | rsaData, err := os.ReadFile("Private.pem") 148 | if err != nil { 149 | if !os.IsNotExist(err) { 150 | checkError(err) 151 | } 152 | 153 | // Otherwise the file does not exist. Assume this is GitHub Actions and return an empty signature. 154 | buffer.Write(make([]byte, 64)) 155 | buffer.Write(make([]byte, 256)) 156 | buffer.Write(contents) 157 | 158 | return buffer.Bytes() 159 | } 160 | 161 | rsaBlock, _ := pem.Decode(rsaData) 162 | 163 | parsedKey, err := x509.ParsePKCS8PrivateKey(rsaBlock.Bytes) 164 | checkError(err) 165 | 166 | // Hash our data then sign 167 | hash := sha1.New() 168 | _, err = hash.Write(contents) 169 | checkError(err) 170 | 171 | contentsHashSum := hash.Sum(nil) 172 | 173 | reader := rand.Reader 174 | signature, err := rsa.SignPKCS1v15(reader, parsedKey.(*rsa.PrivateKey), crypto.SHA1, contentsHashSum) 175 | checkError(err) 176 | 177 | buffer.Write(make([]byte, 64)) 178 | buffer.Write(signature) 179 | buffer.Write(contents) 180 | 181 | return buffer.Bytes() 182 | } 183 | -------------------------------------------------------------------------------- /uv.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "unicode/utf16" 4 | 5 | type UVTable struct { 6 | Code uint8 7 | _ [3]byte 8 | TextOffset uint32 9 | } 10 | 11 | func (f *Forecast) MakeUVTable() { 12 | f.Header.UVIndexTableOffset = f.GetCurrentSize() 13 | 14 | for i, _ := range weatherList.UV { 15 | f.UVTable = append(f.UVTable, UVTable{ 16 | Code: uint8(i), 17 | TextOffset: 0, 18 | }) 19 | } 20 | 21 | f.Header.NumberOfUVIndexTables = uint32(len(f.UVTable)) 22 | } 23 | 24 | func (f *Forecast) MakeUVText() { 25 | for i, uv := range weatherList.UV { 26 | f.UVTable[i].TextOffset = f.GetCurrentSize() 27 | f.UVText = append(f.UVText, utf16.Encode([]rune(f.GetLocalizedName(uv.Name)))...) 28 | 29 | f.UVText = append(f.UVText, uint16(0)) 30 | for f.GetCurrentSize()&3 != 0 { 31 | f.UVText = append(f.UVText, uint16(0)) 32 | } 33 | } 34 | 35 | } 36 | --------------------------------------------------------------------------------