Skip to content

Commit 57c73b0

Browse files
committed
Initial commit
1 parent 1c9fb44 commit 57c73b0

20 files changed

+2455
-7
lines changed

.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

+8-3
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@ import PackageDescription
55

66
let package = Package(
77
name: "SwiftyWeatherKit",
8+
platforms: [
9+
.macOS(.v10_12),
10+
.iOS(.v12),
11+
.tvOS(.v12)
12+
],
813
products: [
914
// Products define the executables and libraries produced by a package, and make them visible to other packages.
1015
.library(
1116
name: "SwiftyWeatherKit",
1217
targets: ["SwiftyWeatherKit"]),
1318
],
1419
dependencies: [
15-
// Dependencies declare other packages that this package depends on.
16-
// .package(url: /* package url */, from: "1.0.0"),
20+
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "4.0.0"),
21+
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0"),
1722
],
1823
targets: [
1924
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2025
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2126
.target(
2227
name: "SwiftyWeatherKit",
23-
dependencies: []),
28+
dependencies: ["SwiftyJSON", "Alamofire"]),
2429
.testTarget(
2530
name: "SwiftyWeatherKitTests",
2631
dependencies: ["SwiftyWeatherKit"]),

README.md

+248-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,250 @@
11
# SwiftyWeatherKit
22

3-
A description of this package.
3+
[![Status](https://travis-ci.org/MikeManzo/SwiftyWeatherKit.svg?branch=master)](https://travis-ci.org/MikeManzo/SwiftyWeatherKit)
4+
![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/mikemanzo/SwiftyWeatherKit.svg)
5+
![GitHub last commit](https://img.shields.io/github/last-commit/MikeManzo/SwiftyWeatherKit.svg)
6+
![GitHub All Releases](https://img.shields.io/github/downloads/MikeManzo/SwiftyWeatherKit/total.svg)
7+
![Swift](https://img.shields.io/badge/%20in-swift%205.1-orange.svg)
8+
9+
A swift weather package to support multiple weather APIs.
10+
11+
# Supported APIs
12+
| Weather API | Documentation | Supported Format | API Support | Notes
13+
| :----: | :----- | :---- | :----: | :---- |
14+
| [Ambient Weather](https://github.com/ambient-weather/api-docs) | * [API Docs](https://ambientweather.docs.apiary.io/)<br>* [API Wiki](https://github.com/ambient-weather/api-docs/wiki)<br>* [Device Specifications](https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs) | - [x] JSON<br> - [ ] Real Time | - [x] All Paramaters | Supports error conditions and<br>rate limits as deffined in the API
15+
|
16+
17+
# Installation
18+
19+
## Swift Package Manager
20+
For [Package Manager](https://swift.org/package-manager/) add the following package to your Package.swift file. Swift 4 & 5 are supported for macOS 10.12, iOS 12 and tvOS 12.
21+
22+
``` Swift
23+
.package(url: "https://github.com/MikeManzo/SwiftyWeatherKit", .branch("master")),
24+
```
25+
26+
# Getting Started
27+
SwiftyWeatherKit uses a factory pattern to create individual weather services. For example:
28+
```swift
29+
/// Weather Factory
30+
public class SwiftyWeatherKit {
31+
public static var WeatherFactory = SwiftyWeatherKit()
32+
33+
open class func shared() -> SwiftyWeatherKit {
34+
return WeatherFactory
35+
}
36+
37+
/// Initialize the desired service using a platform model
38+
/// - Parameters:
39+
/// - weatherType: desired weather service
40+
/// - apiKeys: key(s) that are required to initialize the service; it's up to the resulting service to handle the order and # of keys.
41+
/// - Returns: a fully formed weather platform
42+
public func getService(WeatherServiceType weatherType: WeatherServiceType, WeatherAPI api: [String]) -> WeatherPlatform? {
43+
switch weatherType {
44+
case .Ambient:
45+
return AmbientWeather(apiKeys: api)
46+
case .Undefined:
47+
return nil
48+
}
49+
}
50+
}
51+
```
52+
To initialize the service, all you you need to do is pass it the required API key(s). Some APIs require a single key; some require multiple keys. For now, the only supported service is AmbientWeather.net. AmbientWeather requires two keys. To initialze the service, you need call only one function. For example:
53+
```swift
54+
guard let service = SwiftyWeatherKit.shared().getService(WeatherServiceType: .Ambient,
55+
WeatherAPI: ["*** YOUR API Key ***",
56+
"*** YOUR Application Key***"]) else { return }
57+
```
58+
Once you have successfully initialized the service, it needs to be setup for one of two possible use cases.
59+
## Use Case #1: Latest Data Reported
60+
Retrieve the last good data reported by the weather station. For example:
61+
```swift
62+
service.setupService(completionHandler: { stationStatus in
63+
switch stationStatus {
64+
case .NotReporting:
65+
print("According to the weather service, you do not have any devices reporting data")
66+
break
67+
case .Reporting:
68+
sleep(1) // <-- DO NOT DO THIS!! This is for the test stub ONLY: It just prevents the API from throwing us out w/ back-to-back calls within 1 second (e.g, rate exceeded)
69+
for (_, device) in service.reportingDevices {
70+
switch device {
71+
case is AWDevice:
72+
print(device.prettyString)
73+
service.getLastMeasurement(uniqueID: device.deviceID, completionHandler: { stationData in
74+
guard let data = stationData else { return }
75+
print(data.prettyString)
76+
})
77+
default:
78+
print("Unknown device type detected")
79+
break
80+
}
81+
}
82+
case .Error:
83+
print("There was an error retrieving weather information from the weather service")
84+
break
85+
}
86+
})
87+
```
88+
For this use case we focus on *getLastMeasurement*:
89+
```swift
90+
service.getLastMeasurement(uniqueID: device.deviceID, completionHandler: { stationData in
91+
guard let data = stationData else { return }
92+
print(data.prettyString)
93+
})
94+
```
95+
The code snippet above produces the following output for a weather station connected to AmbientWeather.net. Each sensor can be interrogated for its intrinsic JSON value.
96+
```
97+
MAC Address: A1:B2:C3:D4:E5:F6
98+
Info:
99+
Name: My Station
100+
Location: My Roof
101+
GeoLocation:
102+
Location: Somewhere
103+
Address: Some Address
104+
Elevation: Some Elevation
105+
Coordiates:
106+
Type: Point
107+
Latitude: YOUR LATITUDE
108+
Longitude: YOUR LONGITUDE
109+
110+
Outdoor Battery Status: 0
111+
Date: 1590605700000 ms
112+
Relative Pressure: 29.62 inHg
113+
Absolute Pressure: 29.58 inHg
114+
Hourly Rain: 0.0 in/hr
115+
Rain Today: 0.0 in
116+
Monthly Rain: 1.54 in
117+
Yearly Rain: 13.85 in
118+
Last Time it Rained: 2020-05-23T08:32:00.000Z
119+
Indoor Temperature: 90.7 ºF
120+
Outdoor Temperature: 77.2 ºF
121+
Outdoor Dew Point: 63.61 ºF
122+
Outdoor Temperature Feels Like: 77.58 ºF
123+
Wind Direction: 116 º
124+
Wind Speed: 6.5 MPH
125+
Wind Gust: 15.0 MPH
126+
Max Wind Gust Today: 17.4 MPH
127+
Wind Gust Direction: 190 º
128+
2 Minute Wind Speed Avg: 5.2 MPH
129+
10 Minute Wind Speed Avg: 5.2 MPH
130+
10 Minute Wind Direction Avg: 133 º
131+
UV Index: 5
132+
Solar Radiation: 358.0 W/m^2
133+
Outdoor Humidity: 63 %
134+
Indoor Humidity: 30 %
135+
```
136+
## Use Case #2: Retrieve "n" Historical Measurements
137+
The second use case is to retrieve historical data. By changing *count* in the function below, you can retrieve mulitple historical measurements for the desired station. For example:
138+
```swift
139+
service.setupService(completionHandler: { stationStatus in
140+
switch stationStatus {
141+
case .NotReporting:
142+
print("According to the selected service, you do not have any devices reporting weather data")
143+
break
144+
case .Reporting:
145+
sleep(1) // <-- Test Sub: Just to prevent the API from throwing us out w/ back-to-back calls
146+
for (_, device) in service.reportingDevices {
147+
switch device {
148+
case is AWDevice:
149+
print(device.prettyString)
150+
service.getHistoricalMeasurements(uniqueID: device.deviceID, count: 288, completionHandler: { stationData in
151+
guard let historcalData = stationData else { return }
152+
print("Successfully returned:\(historcalData.count) Measurements from AmbientWeather")
153+
})
154+
default:
155+
print("Unknown device type detected")
156+
break
157+
}
158+
}
159+
case .Error:
160+
print("There was an error retrieving weather information from the selected service")
161+
break
162+
}
163+
})
164+
```
165+
For this use case we focus on *getHistoricalMeasurements*:
166+
```swift
167+
service.getHistoricalMeasurements(uniqueID: device.deviceID, count: 288, completionHandler: { stationData in
168+
guard let historcalData = stationData else { return }
169+
print("Successfully returned\(historcalData.count) Measurements")
170+
})
171+
```
172+
The code snippet above produces the following output for a weather station connected to AmbientWeather.net. As above, each sensor can be interrogated for its intrinsic JSON value.
173+
```
174+
MAC Address: A1:B2:C3:D4:E5:F6
175+
Info:
176+
Name: My Station
177+
Location: My Roof
178+
GeoLocation:
179+
Location: Somewhere
180+
Address: Some Address
181+
Elevation: Some Elevation
182+
Coordiates:
183+
Type: Point
184+
Latitude: YOUR LATITUDE
185+
Longitude: YOUR LONGITUDE
186+
187+
Successfully returned 288 Measurements
188+
```
189+
## Access to Device (station) Data
190+
Each weather service consists of a Device
191+
```swift
192+
public protocol SWKDevice {
193+
var prettyString: String { get }
194+
var deviceID: String? { get }
195+
}
196+
```
197+
that contains data:
198+
```swift
199+
public protocol SWKDeviceData {
200+
var prettyString: String { get }
201+
var availableSensors: [SWKSensor] { get }
202+
}
203+
```
204+
where SWKSensor is defined as:
205+
```swift
206+
/// Supported Service Types
207+
public enum SWKSensorType {
208+
case Temperature
209+
case AirQuality
210+
case Pressure
211+
case Battery
212+
case Humidity
213+
case General
214+
case Rain
215+
case Wind
216+
}
217+
218+
///
219+
/// Base sensor descriptor for SwiftyWeatherKit
220+
/// Generic descriptors:
221+
/// - _description: What the sensor does
222+
/// - _sensorID: Uniqe Identifier of of the sensor
223+
/// - _unit: What the measurements are in (e.g., in, W, F, etc.)
224+
/// - _name: What do you want to call the sensor
225+
/// - _value: Current measurement for the sensor
226+
///
227+
open class SWKSensor {
228+
private let _type: SWKSensorType
229+
private let _description: String
230+
private let _sensorID: String
231+
private let _unit: String
232+
private let _name: String
233+
private var _value: Any
234+
235+
.
236+
.
237+
.
238+
}
239+
```
240+
Once you have successfully called either *getLastMeasurement* or *getHistoricalMeasurements* you will have access to every piece of data that the station is reporting back to the API service. Have a look in the package for details on exposed Properties to use in your application.
241+
242+
## To Do
243+
- [ ] Add support for [Cocoapods](https://cocoapods.org)
244+
- [ ] Add Real time Ambient Weather API support
245+
- [ ] Add support for another weather API - suggestions are welcome
246+
- [ ] Extensive testing for multiple reporting devices per service
247+
- [ ] Localization - I will need help with this
248+
249+
### License
250+
Copyright 2020 Mike Manzo. Licensed under the Apache License, Version 2.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// AWDevice.swift
3+
//
4+
//
5+
// Created by Mike Manzo on 5/17/20.
6+
//
7+
8+
import Foundation
9+
10+
///
11+
/// [Ambient Weather Device Specification](https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs)
12+
///
13+
open class AWDevice: SWKDevice, Codable {
14+
private let info: AWStationInfo?
15+
private let macAddress: String?
16+
let lastData: AWStationData?
17+
18+
enum CodingKeys: String, CodingKey {
19+
case macAddress = "macAddress"
20+
case lastData = "lastData"
21+
case info = "info"
22+
}
23+
24+
/// Return the MAC Address of the device as reported by AmbientWeather.net
25+
public var deviceID: String? {
26+
return macAddress
27+
}
28+
29+
/// Return the station info. Note, this returns all info the *user* has entered for teh device
30+
public var information: AWStationInfo? {
31+
return info
32+
}
33+
34+
///
35+
/// Provides a simple way to "see" what ths device is reporting
36+
///
37+
public var prettyString: String {
38+
let debugInfo = """
39+
MAC Address: \(macAddress!)
40+
\(info?.prettyString ?? "INFO: Error")
41+
"""
42+
return debugInfo
43+
}
44+
45+
///
46+
/// Public & Codeable Initializer ... this creates the object and populates it w/ the JSON-derived decoer
47+
/// - Parameter decoder: JSON_Derived decoder
48+
/// - Throws: a decoding error if something has gone wrong
49+
///
50+
required public init(from decoder: Decoder) throws {
51+
let container = try decoder.container(keyedBy: CodingKeys.self)
52+
53+
do {
54+
macAddress = try container.decodeIfPresent(String.self, forKey: .macAddress) ?? "XXX"
55+
lastData = try container.decode(AWStationData.self, forKey: .lastData)
56+
info = try container.decode(AWStationInfo.self, forKey: .info)
57+
} catch let error as DecodingError {
58+
throw error
59+
}
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// AWStationCoordinates.swift
3+
//
4+
//
5+
// Created by Mike Manzo on 5/10/20.
6+
//
7+
8+
import Foundation
9+
import CoreLocation
10+
11+
public class AWStationCoordinates: Codable {
12+
let latitude: Double?
13+
let longitude: Double?
14+
15+
enum CodingKeys: String, CodingKey {
16+
case longitude = "lon"
17+
case latitude = "lat"
18+
}
19+
20+
/// If the data is present, return a CLLocation object from the reporting lat/lon
21+
var latLon: CLLocation? {
22+
if latitude != nil && longitude != nil {
23+
return CLLocation(latitude: latitude!, longitude: longitude!)
24+
} else {
25+
return nil
26+
}
27+
}
28+
29+
///
30+
/// Public & Codeable Initializer ... this creates the object and populates it w/ the JSON-derived decoer
31+
/// - Parameter decoder: JSON-Derived decoder
32+
/// - Throws: a decoding error if something has gone wrong
33+
///
34+
required public init(from decoder: Decoder) throws {
35+
let container = try decoder.container(keyedBy: CodingKeys.self)
36+
37+
do {
38+
latitude = try container.decodeIfPresent(Double.self, forKey: .latitude)
39+
longitude = try container.decodeIfPresent(Double.self, forKey: .longitude)
40+
} catch let error as DecodingError {
41+
throw error
42+
}
43+
}
44+
}

0 commit comments

Comments
 (0)