Skip to content

Commit

Permalink
Merge branch 'develop' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-slx committed Jun 19, 2021
2 parents 1135fc5 + 02d1b6f commit 0655f1f
Show file tree
Hide file tree
Showing 26 changed files with 398 additions and 143 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# GIT IGNORE LIST

# Python cache
__pycache__

# IDE: Eclipse
.project
.pydevproject
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright © 2020 Michael Schantl
Copyright © 2020-2021 Michael Schantl

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

Expand Down
103 changes: 92 additions & 11 deletions bin/user/weatherlink_live/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -34,7 +34,7 @@
from weewx.engine import InitializationError

DRIVER_NAME = "WeatherLinkLive"
DRIVER_VERSION = "1.0.5"
DRIVER_VERSION = "1.0.6"

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -100,6 +100,10 @@ def loader(config_dict, engine):
return WeatherlinkLiveDriver(config_dict, engine)


def confeditor_loader():
return WeatherlinkLiveConfEditor()


class WeatherlinkLiveDriver(AbstractDevice):
"""
Main driver class
Expand All @@ -120,6 +124,7 @@ def __init__(self, conf_dict, engine):

self.is_running = False
self.scheduler = None
self.no_data_count = 0
self.data_event = None
self.poll_host = None
self.push_host = None
Expand All @@ -138,36 +143,62 @@ def genLoopPackets(self):
except Exception as e:
raise InitializationError("Error while starting driver: %s" % str(e)) from e

# Either it's the first iteration of the driver
# or we've just returned a packet and are now resuming the driver.
self._reset_data_count()

self._log_success("Entering driver loop")
while True:
self._check_no_data_count()

try:
self.scheduler.raise_error()
self.poll_host.raise_error()
self.push_host.raise_error()
except Exception as e:
raise WeeWxIOError("Error while receiving or processing packets: %s" % str(e)) from e

log.debug("Waiting for new packet")
self.data_event.wait(5) # do a check every 5 secs
self.data_event.clear()

if self.poll_host.packets:
self._log_success("Emitting poll packet")
self._reset_data_count()
yield self.poll_host.packets.popleft()

if self.push_host.packets:
elif self.push_host.packets:
self._log_success("Emitting push (broadcast) packet")
self._reset_data_count()
yield self.push_host.packets.popleft()

log.debug("Waiting for new packet")
self.data_event.wait(5) # do a check every 5 secs
self.data_event.clear()
else:
self._increase_no_data_count()

def start(self):
if self.is_running:
return

self.is_running = True
self.data_event = threading.Event()
self.poll_host = data_host.WllPollHost(self.configuration.host, self.mappers, self.data_event)
self.push_host = data_host.WLLBroadcastHost(self.configuration.host, self.mappers, self.data_event)
self.scheduler = scheduler.Scheduler(self.configuration.polling_interval, self.poll_host.poll,
self.push_host.refresh_broadcast, self.data_event)
self.poll_host = data_host.WllPollHost(
self.configuration.host,
self.mappers,
self.data_event,
self.configuration.socket_timeout
)
self.push_host = data_host.WLLBroadcastHost(
self.configuration.host,
self.mappers,
self.data_event,
self.configuration.socket_timeout
)
self.scheduler = scheduler.Scheduler(
self.configuration.polling_interval,
self.poll_host.poll,
self.push_host.refresh_broadcast,
self.data_event
)

def closePort(self):
"""Close connection"""
Expand All @@ -180,7 +211,57 @@ def closePort(self):
if self.push_host is not None:
self.push_host.close()

def _log_success(self, msg: str, level: int = logging.INFO) -> None:
def _increase_no_data_count(self):
self.no_data_count += 1
self._log_failure("No data since %d iterations" % self.no_data_count, logging.WARNING)

def _reset_data_count(self):
self.no_data_count = 0

def _check_no_data_count(self):
max_iterations = self.configuration.max_no_data_iterations
if max_iterations < 1:
raise ValueError("Max iterations without data must not be less than 1 (got: %d)" % max_iterations)

if self.no_data_count >= max_iterations:
raise WeeWxIOError("Received no data for %d iterations" % max_iterations)

def _log_success(self, msg: str, level: int = logging.DEBUG) -> None:
if not self.configuration.log_success:
return
log.log(level, msg)

def _log_failure(self, msg: str, level: int = logging.DEBUG) -> None:
if not self.configuration.log_error:
return
log.log(level, msg)


class WeatherlinkLiveConfEditor(weewx.drivers.AbstractConfEditor):
@property
def default_stanza(self):
return """
# This section configures the WeatherLink Live driver
[WeatherLinkLive]
# Driver module
driver = user.weatherlink_live
# Host name or IP address of WeatherLink Live
host = weatherlink
# Mapping of transmitter ids to WeeWX records
# Default for Vantage Pro2
mapping = th:1, th_indoor, baro, rain:1, wind:1, thw:1, windchill:1
"""

def modify_config(self, config_dict):
print("""
Configuring accumulators for custom types.""")
config_dict.setdefault('Accumulator', {})

config_dict['Accumulator'].setdefault('rainCount', {})
config_dict['Accumulator']['rainCount']['extractor'] = 'sum'

config_dict['Accumulator'].setdefault('rainSize', {})
config_dict['Accumulator']['rainSize']['extractor'] = 'last'
2 changes: 1 addition & 1 deletion bin/user/weatherlink_live/callback.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down
33 changes: 26 additions & 7 deletions bin/user/weatherlink_live/configuration.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand All @@ -24,9 +24,10 @@
from user.weatherlink_live.mappers import TMapping, THMapping, WindMapping, RainMapping, SolarMapping, UvMapping, \
WindChillMapping, ThwMapping, ThswMapping, SoilTempMapping, SoilMoistureMapping, LeafWetnessMapping, \
THIndoorMapping, BaroMapping, AbstractMapping
from user.weatherlink_live.static.config import KEY_DRIVER_POLLING_INTERVAL, KEY_DRIVER_HOST, KEY_DRIVER_MAPPING
from user.weatherlink_live.static.config import KEY_DRIVER_POLLING_INTERVAL, KEY_DRIVER_HOST, KEY_DRIVER_MAPPING, \
KEY_MAX_NO_DATA_ITERATIONS
from user.weatherlink_live.utils import to_list
from weeutil.weeutil import to_bool
from weeutil.weeutil import to_bool, to_float, to_int

MAPPERS = {
't': TMapping,
Expand Down Expand Up @@ -55,13 +56,23 @@ def create_configuration(config: dict, driver_name: str):

host = driver_dict[KEY_DRIVER_HOST]
polling_interval = float(driver_dict.get(KEY_DRIVER_POLLING_INTERVAL, 10))
max_no_data_iterations = to_int(driver_dict.get(KEY_MAX_NO_DATA_ITERATIONS, 5))
mapping_list = to_list(driver_dict[KEY_DRIVER_MAPPING])
mappings = _parse_mappings(mapping_list)

log_success = to_bool(config.get('log_success', False))
log_error = to_bool(config.get('log_failure', True))

config_obj = Configuration(host, mappings, polling_interval, log_success, log_error)
socket_timeout = to_float(config.get('socket_timeout', 20))

config_obj = Configuration(
host=host,
mappings=mappings,
polling_interval=polling_interval,
max_no_data_iterations=max_no_data_iterations,
log_success=log_success,
log_error=log_error,
socket_timeout=socket_timeout
)
return config_obj


Expand All @@ -77,14 +88,22 @@ def _parse_mappings(mappings_list: List[str]) -> List[List[str]]:
class Configuration(object):
"""Configuration of driver"""

def __init__(self, host: str, mappings: List[List[str]], polling_interval: float, log_success: bool,
log_error: bool):
def __init__(self,
host: str,
mappings: List[List[str]],
polling_interval: float,
max_no_data_iterations: int,
log_success: bool,
log_error: bool,
socket_timeout: float):
self.host = host
self.mappings = mappings
self.polling_interval = polling_interval
self.max_no_data_iterations = max_no_data_iterations

self.log_success = log_success
self.log_error = log_error
self.socket_timeout = socket_timeout

def __repr__(self):
return str(self.__dict__)
Expand Down
20 changes: 15 additions & 5 deletions bin/user/weatherlink_live/data_host.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -71,12 +71,17 @@ def notify_error(self, e):
class WllPollHost(DataHost):
"""Host object for polling data from WLL"""

def __init__(self, host: str, mappers: List[AbstractMapping], data_event: threading.Event):
def __init__(self,
host: str,
mappers: List[AbstractMapping],
data_event: threading.Event,
http_timeout: float = 20):
super().__init__(mappers, data_event)
self.host = host
self.http_timeout = http_timeout

def poll(self):
packet = request_current(self.host)
packet = request_current(self.host, timeout=self.http_timeout)
log.debug("Polled current conditions")

self._create_record(packet)
Expand All @@ -88,16 +93,21 @@ def close(self):
class WLLBroadcastHost(DataHost, PacketCallback):
"""Class for triggering UDP broadcasts and receiving them"""

def __init__(self, host: str, mappers: List[AbstractMapping], data_event: threading.Event):
def __init__(self,
host: str,
mappers: List[AbstractMapping],
data_event: threading.Event,
http_timeout: float = 20):
super().__init__(mappers, data_event)
self.host = host
self.http_timeout = http_timeout

self._receiver = None
self._port = 22222

def refresh_broadcast(self, request_duration: float):
log.debug("Re-requesting UDP broadcast")
packet = start_broadcast(self.host, request_duration)
packet = start_broadcast(self.host, request_duration, timeout=self.http_timeout)
port = packet.broadcast_port

if self._port != port:
Expand Down
3 changes: 2 additions & 1 deletion bin/user/weatherlink_live/davis_broadcast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -63,6 +63,7 @@ def _reception(self):
continue

data, source_addr = self.sock.recvfrom(2048)
log.debug("Received %d bytes from %s" % (len(data), source_addr))
try:
json_data = json.loads(data.decode("utf-8"))
except JSONDecodeError as e:
Expand Down
52 changes: 43 additions & 9 deletions bin/user/weatherlink_live/davis_http.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright © 2020 Michael Schantl and contributors
# Copyright © 2020-2021 Michael Schantl and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
Expand All @@ -17,19 +17,53 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import logging
import time
from typing import Optional

import requests

from user.weatherlink_live.packets import WlHttpBroadcastStartRequestPacket, WlHttpConditionsRequestPacket
from weewx import WeeWxIOError

log = logging.getLogger(__name__)

def start_broadcast(host: str, duration):
r = requests.get("http://%s:80/v1/real_time?duration=%d" % (host, duration))
json = r.json()
return WlHttpBroadcastStartRequestPacket.try_create(json, host)

def start_broadcast(host: str, duration, timeout: float = 5):
error: Optional[Exception] = None

def request_current(host: str):
r = requests.get("http://%s:80/v1/current_conditions" % host)
json = r.json()
return WlHttpConditionsRequestPacket.try_create(json, host)
for i in range(3):
try:
r = requests.get("http://%s:80/v1/real_time?duration=%d" % (host, duration), timeout=timeout)
json = r.json()
return WlHttpBroadcastStartRequestPacket.try_create(json, host)
except Exception as e:
error = e
log.error(e)
log.error("HTTP broadcast start request failed. Retry #%d follows shortly" % i)
time.sleep(2.5)

if error is not None:
raise error

raise WeeWxIOError("HTTP broadcast start request failed without setting an error")


def request_current(host: str, timeout: float = 5):
error: Optional[Exception] = None

for i in range(3):
try:
r = requests.get("http://%s:80/v1/current_conditions" % host, timeout=timeout)
json = r.json()
return WlHttpConditionsRequestPacket.try_create(json, host)
except Exception as e:
error = e
log.error(e)
log.error("HTTP conditions request failed. Retry #%d follows shortly" % i)
time.sleep(2.5)

if error is not None:
raise error

raise WeeWxIOError("HTTP conditions request failed without setting an error")
Loading

0 comments on commit 0655f1f

Please sign in to comment.