Skip to content

Commit 2507052

Browse files
committed
refactored
1 parent d592076 commit 2507052

9 files changed

+125
-92
lines changed

.github/workflows/ci.yml

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414

1515
strategy:
1616
matrix:
17-
python-version: [3.9, "3.10", 3.11]
17+
python-version: ["3.10", 3.11, 3.12]
1818
platform: [ubuntu-latest, macos-latest, windows-latest]
1919
runs-on: ${{ matrix.platform }}
2020

@@ -28,9 +28,9 @@ jobs:
2828
contents: write
2929

3030
steps:
31-
- uses: actions/checkout@v2
31+
- uses: actions/checkout@v4
3232
- name: Set up Python ${{ matrix.python-version }}
33-
uses: actions/setup-python@v1
33+
uses: actions/setup-python@v5
3434
with:
3535
python-version: ${{ matrix.python-version }}
3636
- name: Install dependencies

README.md

+5-23
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,17 @@
22
[![Coverage](https://raw.githubusercontent.com/andgineer/ambientweather_livedata/python-coverage-comment-action-data/badge.svg)](https://htmlpreview.github.io/?https://github.com/andgineer/ambientweather_livedata/blob/python-coverage-comment-action-data/htmlcov/index.html)
33
# Extract data from Ambient Weather stations
44

5-
Python3 library that extracts information from [Ambient Weather stations](https://www.ambientweather.com/).
5+
Python library that extracts information from [Ambient Weather stations](https://www.ambientweather.com/).
6+
7+
So if you have an Ambient Weather station and you want to get the data from it, this library is for you.
68

79
It collects sensor's data (temperature and humidity) from the [IPObserver](https://www.ambientweather.com/amobserverip.html) `LiveData` tab of the IPObserver web-page.
810
You can get this page from the IPObserver - just open the IPObserver IP adddress in your web-brawser to see it.
911

1012
The library uses xpath and [lxml](http://lxml.de/).
1113

12-
Example:
13-
14-
import ambientweather_livedata
15-
16-
inSensor, outSensor = ambientweather_livedata.get('http://10.0.0.176/LiveData.html')
17-
print('Time: {}\n'.format(inSensor.time))
18-
print('''Indoor\n{delimiter}\nTemperature: {temp}\nHumidity: {humidity}
19-
Absolute preassure: {abs_press}\nRelative preassure: {rel_press}\nBattery status: {battery}\n'''.format(
20-
delimiter='='*20,
21-
temp=inSensor.temp,
22-
humidity=inSensor.humidity,
23-
abs_press=inSensor.abs_press,
24-
rel_press=inSensor.rel_press,
25-
battery=inSensor.battery
26-
))
27-
print('''Outdoor\n{delimiter}\nTemperature: {temp}\nHumidity: {humidity}
28-
Battery status: {battery}\n'''.format(
29-
delimiter='='*20,
30-
temp=inSensor.temp,
31-
humidity=inSensor.humidity,
32-
battery=inSensor.battery
33-
))
14+
[Example](src/example.py)
15+
3416

3517
## Coverage report
3618
* [Codecov](https://app.codecov.io/gh/andgineer/ambientweather_livedata/tree/master/src)

activate.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55

66
VENV_FOLDER="venv"
7-
PYTHON="python3.11"
7+
PYTHON="python3.12"
88

99
RED='\033[1;31m'
1010
GREEN='\033[1;32m'

src/ambientweather.py

+33-26
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,68 @@
11
"""Extract data from Ambient Weather stations."""
2-
from typing import List, Tuple
3-
2+
from typing import Tuple
43
from datetime import datetime
54
import requests
65
from lxml import html
76

8-
97
TITLE = "LiveData" # HTML live data page title
108
TIMEOUT = 5 # seconds
119

1210

13-
class SensorData:
14-
"""Sensor data object"""
11+
class BaseSensorData:
12+
"""Base class for sensor data"""
13+
def __init__(self):
14+
self.time: datetime = None
15+
self.temp: float = None
16+
self.humidity: float = None
17+
self.battery: str = None
1518

16-
time: datetime
17-
temp: float
18-
humidity: float
19-
abs_press: float
20-
rel_press: float
21-
battery: List[str] # ('Normal')
2219

23-
def parse(self, live_data_html: bytes) -> Tuple["SensorData", "SensorData"]:
24-
"""Extract sensor's data from html (LiveData.html from your ObserverIP).
20+
class IndoorSensorData(BaseSensorData):
21+
"""Indoor sensor data"""
22+
def __init__(self):
23+
super().__init__()
24+
self.abs_press: float = None
25+
self.rel_press: float = None
26+
27+
28+
class OutdoorSensorData(BaseSensorData):
29+
"""Outdoor sensor data"""
30+
pass
2531

26-
Returns touple with (sensor1, sensor2 -> SensorData)
27-
"""
2832

33+
class AmbientWeather:
34+
"""Class to handle Ambient Weather data retrieval and parsing"""
35+
36+
@staticmethod
37+
def parse(live_data_html: bytes) -> Tuple[IndoorSensorData, OutdoorSensorData]:
38+
"""Extract sensor data from html (LiveData.html from your ObserverIP)."""
2939
tree = html.fromstring(live_data_html)
3040
title = tree.xpath("//title/text()")
3141
if title[0] != TITLE:
3242
raise ValueError(f"Wrong html page. Good one have to have title {TITLE}")
3343

34-
in_sensor = SensorData()
44+
in_sensor = IndoorSensorData()
45+
out_sensor = OutdoorSensorData()
46+
3547
time_str = tree.xpath('//input[@name="CurrTime"]/@value')[0]
36-
in_sensor.time = datetime.strptime(time_str, "%H:%M %m/%d/%Y")
48+
in_sensor.time = out_sensor.time = datetime.strptime(time_str, "%H:%M %m/%d/%Y")
49+
3750
in_sensor.temp = float(tree.xpath('//input[@name="inTemp"]/@value')[0])
3851
in_sensor.humidity = float(tree.xpath('//input[@name="inHumi"]/@value')[0])
3952
in_sensor.abs_press = float(tree.xpath('//input[@name="AbsPress"]/@value')[0])
4053
in_sensor.rel_press = float(tree.xpath('//input[@name="RelPress"]/@value')[0])
4154
in_sensor.battery = tree.xpath('//input[@name="inBattSta"]/@value')[0]
4255

43-
out_sensor = SensorData()
44-
out_sensor.time = in_sensor.time
4556
out_sensor.temp = float(tree.xpath('//input[@name="outTemp"]/@value')[0])
4657
out_sensor.humidity = float(tree.xpath('//input[@name="outHumi"]/@value')[0])
47-
out_sensor.abs_press = in_sensor.abs_press
48-
out_sensor.rel_press = in_sensor.rel_press
4958
out_sensor.battery = tree.xpath('//input[@name="outBattSta2"]/@value')[0]
5059

5160
return in_sensor, out_sensor
5261

53-
def get(self, url: str) -> Tuple["SensorData", "SensorData"]:
62+
@staticmethod
63+
def get(url: str) -> Tuple[IndoorSensorData, OutdoorSensorData]:
5464
"""
5565
Load ObserverIP live data page from the URL and parse it
56-
57-
Returns touple with (sensor1, sensor2 -> SensorData)
5866
"""
59-
6067
page = requests.get(url, timeout=TIMEOUT).content
61-
return self.parse(page)
68+
return AmbientWeather.parse(page)

src/example.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Example script to demonstrate the usage of the AmbientWeather class."""
2+
from ambientweather import AmbientWeather
3+
4+
5+
def show_weather() -> None:
6+
"""Retrieve and display weather data from an Ambient Weather station."""
7+
in_sensor, out_sensor = AmbientWeather.get('http://10.0.0.176/LiveData.html')
8+
delimiter = '=' * 20
9+
10+
print(f'Time: {in_sensor.time}\n')
11+
12+
print(f"Indoor\n{delimiter}")
13+
print(f"Temperature: {in_sensor.temp}")
14+
print(f"Humidity: {in_sensor.humidity}")
15+
print(f"Absolute pressure: {in_sensor.abs_press}")
16+
print(f"Relative pressure: {in_sensor.rel_press}")
17+
print(f"Battery status: {in_sensor.battery}\n")
18+
19+
print(f"Outdoor\n{delimiter}")
20+
print(f"Temperature: {out_sensor.temp}")
21+
print(f"Humidity: {out_sensor.humidity}")
22+
print(f"Battery status: {out_sensor.battery}")
23+
24+
25+
if __name__ == '__main__':
26+
show_weather()

tests/conftest.py

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def _get_repo_root_dir() -> str:
1313
ROOT_DIR = _get_repo_root_dir()
1414
RESOURCES = Path(f"{ROOT_DIR}/tests/resources")
1515

16+
1617
@pytest.fixture(
1718
scope="function",
1819
params=[

tests/test_ambientweather.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from ambientweather import AmbientWeather
2+
from datetime import datetime
3+
4+
5+
def test_ambientweather_data(ambientweather_data: str):
6+
in_sensor, out_sensor = AmbientWeather.parse(ambientweather_data.encode('utf-8'))
7+
8+
assert in_sensor.time == datetime.strptime('2017/10/06 13:55:00', '%Y/%m/%d %H:%M:%S')
9+
assert in_sensor.temp == 21.9
10+
assert in_sensor.humidity == 50.0
11+
assert in_sensor.abs_press == 744.29
12+
assert in_sensor.rel_press == 728.31
13+
assert in_sensor.battery == 'Normal'
14+
assert out_sensor.temp == 10.1
15+
assert out_sensor.humidity == 71.0
16+
assert out_sensor.battery == 'Normal'

tests/test_ambientweather_data.py

-39
This file was deleted.

tests/test_example.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import pytest
2+
from unittest.mock import patch
3+
import sys
4+
from io import StringIO
5+
6+
from example import show_weather
7+
from ambientweather import AmbientWeather, IndoorSensorData, OutdoorSensorData
8+
9+
10+
def test_show_weather(ambientweather_data):
11+
"""Test the show_weather function from example.py."""
12+
in_sensor, out_sensor = AmbientWeather.parse(ambientweather_data.encode('utf-8'))
13+
14+
# Mock the AmbientWeather.get method to return our parsed data
15+
with patch('ambientweather.AmbientWeather.get', return_value=(in_sensor, out_sensor)):
16+
# Capture stdout
17+
captured_output = StringIO()
18+
sys.stdout = captured_output
19+
20+
# Call the function
21+
show_weather()
22+
23+
# Restore stdout
24+
sys.stdout = sys.__stdout__
25+
26+
# Get the captured output
27+
output = captured_output.getvalue()
28+
29+
# Assert that the output contains expected information
30+
assert f"Time: {in_sensor.time}" in output
31+
assert "Indoor" in output
32+
assert f"Temperature: {in_sensor.temp}" in output
33+
assert f"Humidity: {in_sensor.humidity}" in output
34+
assert f"Absolute pressure: {in_sensor.abs_press}" in output
35+
assert f"Relative pressure: {in_sensor.rel_press}" in output
36+
assert f"Battery status: {in_sensor.battery}" in output
37+
assert "Outdoor" in output
38+
assert f"Temperature: {out_sensor.temp}" in output
39+
assert f"Humidity: {out_sensor.humidity}" in output
40+
assert f"Battery status: {out_sensor.battery}" in output

0 commit comments

Comments
 (0)