From 414f766716f5e4e1403d16cb494908bcb0cbf772 Mon Sep 17 00:00:00 2001 From: jklarson Date: Wed, 18 Mar 2020 14:08:36 -0700 Subject: [PATCH 1/8] Add Ambient Weather service agent to services/core --- services/core/Ambient/README.rst | 147 ++++++ services/core/Ambient/ambient/__init__.py | 0 services/core/Ambient/ambient/agent.py | 442 ++++++++++++++++++ .../Ambient/ambient/data/name_mapping.csv | 31 ++ services/core/Ambient/config | 12 + services/core/Ambient/conftest.py | 6 + services/core/Ambient/requirements.txt | 1 + services/core/Ambient/setup.py | 76 +++ .../core/Ambient/tests/test_ambient_agent.py | 339 ++++++++++++++ 9 files changed, 1054 insertions(+) create mode 100644 services/core/Ambient/README.rst create mode 100644 services/core/Ambient/ambient/__init__.py create mode 100644 services/core/Ambient/ambient/agent.py create mode 100644 services/core/Ambient/ambient/data/name_mapping.csv create mode 100644 services/core/Ambient/config create mode 100644 services/core/Ambient/conftest.py create mode 100644 services/core/Ambient/requirements.txt create mode 100644 services/core/Ambient/setup.py create mode 100644 services/core/Ambient/tests/test_ambient_agent.py diff --git a/services/core/Ambient/README.rst b/services/core/Ambient/README.rst new file mode 100644 index 0000000000..c0a403a3b6 --- /dev/null +++ b/services/core/Ambient/README.rst @@ -0,0 +1,147 @@ +.. _Ambient Weather Agent: + +===================== +Ambient Weather Agent +===================== + +The Ambient weather agent provides the ability to query for current weather data from Ambient weather stations via the +Ambient weather API. The agent inherits features of the Volttron BaseWeatherAgent which provides caching of recently +recieved data, as well as point name mapping and unit conversion using the standardized CF-conventions scheme. + +The values from the Ambient weather station can be accessed through the cloud platform which can be accessed at +https://dashboard.ambientweather.net/dashboard + +Two API Keys are required for all REST API requests: + + applicationKey - identifies the developer / application. To request an application key please email + support@ambientweather.com + + apiKey - grants access to past/present data for a given user's devices. A typical consumer-facing application will + + initially ask the user to create an apiKey on thier AmbientWeather.net account page + (https://dashboard.ambientweather.net/account) and paste it into the app. Developers for personal or in-house apps + will also need to create an apiKey on their own account page. + +API requests are capped at 1 request/second for each user's apiKey and 3 requests/second per applicationKey. When this +limit is exceeded, the API will return a 429 response code. This will result in a response from the Ambient agent +containing "weather_error" and no weather data. + +----------------- +Ambient Endpoints +----------------- + +The Ambient Weather agent provides only current weather data (all other base weather endpoints are unimplemented, and +will return a record containing "weather_error" if used). + +The location format for the Ambient agent is as follows: + + {"location": "", + "api_key":"", + "poll_locations": [ + {"location": "Lab Home A"}, + {"location": "Lab Home B"} + ], + "poll_interval": 60, + "identity": "platform.ambient" + } + +Registry Configuration +---------------------- +The registry configuration file for this agent can be found in agent's data +directory. This configuration provides the point name mapping from the Ambient +API's point scheme to the CF-conventions scheme by default. Points that do not +specify "Standard_Point_Name" were found to not have a logical match to any +point found in the CF-Conventions. For these points Ambient point name +convention (Service_Point_Name) will be used. + +.. csv-table:: Registry Configuration + :header: Service_Point_Name,Standard_Point_Name,Service_Units,Standard_Units + + feelsLike,apparent_temperature,degF, + dewPoint,dew_point_temperature,degF, + dewPointin,dew_point_temperature_indoor,degF, + soiltempf,,degF, + soilhum,,, + uv,ultraviolet_index,, + +--------------------------- +Running Ambient Agent Tests +--------------------------- + +The following instructions can be used to run PyTests for the Ambient agent. + +1. Set up the test file - test_ambient_agent.py is the PyTest file for the ambient agent. The test file features a few +variables at the top of the tests will will need to be filled in by the runner of the Ambient agent tests. The LOCATIONS +variable specifies a list of "locations" of Ambient devices. The required format is a list of dictionaries of the form +{"location": }. Locations are determined by the user when configuring a weather +station for the Ambient service using the Ambient app. For more information about these variables, please view the +README.rst file. For more information about the Ambient API, visit https://www.ambientweather.com/api.html + +2. Set up the test environment - The tests are intended to be run from the Volttron root directory using the Volttron +environment. it is also recommended to use the -s option. In PyCharm, setting the DEBUG_MODE environment variable to +True can be useful for debugging purposes. The tests should target the Ambient agent's directory. + +Example command line: + +.. code-block:: + + (volttron) @:~/volttron$ pytest -s ~/house-deployment/Ambient + + diff --git a/services/core/Ambient/ambient/__init__.py b/services/core/Ambient/ambient/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/core/Ambient/ambient/agent.py b/services/core/Ambient/ambient/agent.py new file mode 100644 index 0000000000..c537305276 --- /dev/null +++ b/services/core/Ambient/ambient/agent.py @@ -0,0 +1,442 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright (c) 2017, Battelle Memorial Institute +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +# FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# The views and conclusions contained in the software and documentation are +# those of the authors and should not be interpreted as representing official, +# policies either expressed or implied, of the FreeBSD Project. +# + +# This material was prepared as an account of work sponsored by an +# agency of the United States Government. Neither the United States +# Government nor the United States Department of Energy, nor Battelle, +# nor any of their employees, nor any jurisdiction or organization +# that has cooperated in the development of these materials, makes +# any warranty, express or implied, or assumes any legal liability +# or responsibility for the accuracy, completeness, or usefulness or +# any information, apparatus, product, software, or process disclosed, +# or represents that its use would not infringe privately owned rights. +# +# Reference herein to any specific commercial product, process, or +# service by trade name, trademark, manufacturer, or otherwise does +# not necessarily constitute or imply its endorsement, recommendation, +# r favoring by the United States Government or any agency thereof, +# or Battelle Memorial Institute. The views and opinions of authors +# expressed herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY +# operated by BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 + +# }}} + +__docformat__ = 'reStructuredText' + +import logging +import datetime +import pytz +import sys +import re + +import grequests +# requests should be imported after grequests as +# requests imports ssl and grequests patches ssl +import requests + +import pkg_resources +from volttron.platform.agent import utils +from volttron.platform.vip.agent import RPC +from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now +from volttron.platform.agent.base_weather import BaseWeatherAgent +from volttron.platform.agent.base_weather import get_forecast_start_stop +from volttron.platform import jsonapi# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2019, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +__docformat__ = 'reStructuredText' + +import logging +import datetime +import pytz +import sys +import re + +import grequests +# requests should be imported after grequests as +# requests imports ssl and grequests patches ssl +import requests + +import pkg_resources +from volttron.platform.agent import utils +from volttron.platform.vip.agent import RPC +from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now +from volttron.platform.agent.base_weather import BaseWeatherAgent +from volttron.platform import jsonapi + +_log = logging.getLogger(__name__) +utils.setup_logging() +__version__ = "0.1" + +WEATHER_WARN = "weather_warnings" +WEATHER_ERROR = "weather_error" +WEATHER_RESULTS = "weather_results" + + +def ambient(config_path, **kwargs): + """Parses the Agent configuration and returns an instance of + the agent created using that configuration. + + :param config_path: Path to a configuration file. + + :type config_path: str + :returns: Ambient + :rtype: Ambient + """ + try: + config = utils.load_config(config_path) + except Exception: + config = {} + if not config: + _log.error("Ambient agent configuration: ".format(config)) + if "api_key" not in config: + raise RuntimeError("Ambient agent must be configured with an api key.") + if "application_key" not in config: + raise RuntimeError("Ambient agent must be configured with an " + "application key.") + _log.debug("config_dict before init: {}".format(config)) + utils.update_kwargs_with_config(kwargs, config) + + return Ambient(**kwargs) + + +class Ambient(BaseWeatherAgent): + """ + The Ambient agent requires having an API key to interact with the remote + API. The agent offers a performance_mode configuration option which + allows users to limit the amount of data returned by the API. + + ***Powered by Dark Sky*** + """ + + def __init__(self, application_key="", **kwargs): + super(Ambient, self).__init__(**kwargs) + _log.debug("vip_identity: " + self.core.identity) + self.headers = {"Accept": "application/json", + "Accept-Language": "en-US" + } + self.remove_service("get_hourly_historical") + self.remove_service("get_hourly_forecast") + + self.app_key = application_key + + self.last_service_call_timestamp = None + + @RPC.export + def get_version(self): + """ + Provides the current version of the agent. + :return: current version number in string format. + """ + return __version__ + + def validate_location(self, service_name, location): + """ + Indicates whether the location dictionary provided matches the format + required by the remote weather API + :param service_name: name of the remote API service + :param location: location dictionary to provide in the remote API url + :return: True if the location matches the required format else False + """ + return isinstance(location.get("location", None), str) + + def get_update_interval(self, service_name): + """ + Indicates the interval between remote API updates + :param service_name: requested service endpoint + :return: datetime timedelta representing the time interval + """ + if service_name == "get_current_weather": + return datetime.timedelta(seconds=3) + else: + return None + + def get_api_description(self, service_name): + """ + Provides a human-readable description of the various endpoints provided + by the agent + :param service_name: requested service endpoint + :return: Human-readable description string + """ + if service_name is "get_current_weather": + "Provides current weather observations by Ambient weather" \ + " station location name via RPC (Requires " \ + "{'location': })" + else: + raise RuntimeError( + "Service {} is not implemented by Ambient.".format(service_name)) + + def get_point_name_defs_file(self): + """ + Constructs the point name mapping dict from the + mapping csv. + :return: dictionary containing a mapping of service point + names to standard point names with optional + """ + # returning resource file instead of stream, as csv.DictReader require file path or file like object opened in + # text mode. + return pkg_resources.resource_filename(__name__, "data/name_mapping.csv") + + def query_current_weather(self, location): + """ + Retrieve data from the Ambient API, return formatted current data and + store forecast data in cache + :param location: location dictionary requested by the user + :return: Timestamp and data for current data from the Ambient API + """ + ambient_response = self.make_request() + location_response = None + current_time = None + cache_records = [] + for record in ambient_response: + record_location = None + record_info = record.pop("info") + if record_info: + record_location = record_info.get("location", "") + if record_location: + weather_data = record.get("lastData", {}) + weather_data["macAddress"] = record.pop("macAddress", "") + weather_data["name"] = record_info.get("name", "") + # "date": "2019-04-25T17:09:00.000Z" + weather_tz_string = weather_data.get('tz', None) + if weather_tz_string: + weather_tz = pytz.timezone(weather_tz_string) + else: + weather_tz = pytz.utc + weather_date = datetime.datetime.strptime( + weather_data.pop("date"), "%Y-%m-%dT%H:%M:%S.%fZ").astimezone(weather_tz) + if location["location"] == record_location: + current_time = format_timestamp(weather_date) + location_response = weather_data + else: + weather_data = self.apply_mapping(weather_data) + self.store_weather_records("get_current_weather", + [jsonapi.dumps({"location": record_location}), + weather_date, + jsonapi.dumps(weather_data)]) + else: + raise RuntimeError("API record contained improper 'info' format") + return current_time, location_response + + def query_forecast_service(self, service, location, quantity, forecast_start): + """ + Unimplemented method stub + :param service: forecast service type of weather data to return + :param location: location dictionary requested during the RPC call + :param quantity: number of records to return, used to generate + Time Machine requests after the forecast request + :param forecast_start: forecast results that are prior to this + timestamp will be filtered by base weather agent + :return: Timestamp and data returned by the Ambient weather API response + """ + raise NotImplementedError + + def make_request(self): + """ Request data from the Ambient Weather API + + An example of the return value is as follows + + [ + { + "macAddress": "18:93:D7:3B:89:0C", + "lastData": { + "dateutc": 1556212140000, + "tempinf": 71.9, + "humidityin": 31, + "battout": "1", + "temp1f": 68.7, + "humidity1": 36, + "batt1": "1", + "date": "2019-04-25T17:09:00.000Z" + }, + "info": { + "name": "Home B WS", + "location": "Lab Home B" + } + }, + { + "macAddress": "50:F1:4A:F7:3C:C4", + "lastData": { + "dateutc": 1556211960000, + "tempinf": 82.5, + "humidityin": 27, + "battout": "1", + "temp1f": 68.5, + "humidity1": 42, + "batt1": "1", + "date": "2019-04-25T17:06:00.000Z" + }, + "info": { + "name": "Home A WS", + "location": "Lab Home A" + } + } + ] + :return: + """ + + # AuthenticationTwo API Keys are required for all REST API requests:applicationKey - identifies the + # developer / application. To request an application key please email support@ambient.comapiKey - + # grants access to past/present data for a given user's devices. A typical consumer-facing application will + # initially ask the user to create an apiKey on thier Ambient.net account page + # (https://dashboard.ambientweather.net/account) and paste it into the app. Developers for personal or + # in-house apps will also need to create an apiKey on their own account page. + # Rate LimitingAPI requests are capped at 1 request/second for each user's apiKey and 3 requests/second + # per applicationKey. When this limit is exceeded, the API will return a 429 response code. + # Please be kind to our servers :) + + # If the previous call to the API was at least 3 seconds ago - this is a constraint set by Ambient + if not self.last_service_call_timestamp or ( + datetime.datetime.now() - self.last_service_call_timestamp).total_seconds() > 3: + + url = 'https://api.ambientweather.net/v1/devices?applicationKey=' + self.app_key + '&apiKey=' + self._api_key + + _log.info("requesting url: {}".format(url)) + grequest = [grequests.get(url, verify=requests.certs.where(), + headers=self.headers, timeout=3)] + gresponse = grequests.map(grequest)[0] + if gresponse is None: + raise RuntimeError("get request did not return any response") + try: + response = jsonapi.loads(gresponse.content) + self.last_service_call_timestamp = datetime.datetime.now() + return response + except ValueError: + self.last_service_call_timestamp = datetime.datetime.now() + self.generate_response_error(url, gresponse.status_code) + else: + raise RuntimeError("Previous API call to Ambient service is too recent, please wait at least 3 seconds " + "between API calls.") + + def query_hourly_forecast(self, location): + """ + Unimplemented method stub + :param location: currently accepts lat/long location dictionary + format only + :return: time of forecast prediction as a timestamp string, + and a list of + """ + raise NotImplementedError + + def query_hourly_historical(self, location, start_date, end_date): + """ + Unimplemented method stub + :param location: no format currently determined for history. + :param start_date: Starting date for historical weather period. + :param end_date: Ending date for historical weather period. + :return: NotImplementedError + """ + raise NotImplementedError + + def generate_response_error(self, url, response_code): + """ + raises a descriptive runtime error based on the response code + returned by a service. + :param url: actual url used for requesting data from Ambient + :param response_code: Http response code returned by a service + following a request + """ + code_x100 = int(response_code / 100) + if code_x100 == 2: + raise RuntimeError( + "Remote API returned no data(code:{}, url:{})".format( + response_code, url)) + elif code_x100 == 3: + raise RuntimeError( + "Remote API redirected request, " + "but redirect failed (code:{}, url:{})".format(response_code, + url)) + elif code_x100 == 4: + raise RuntimeError( + "Request ({}) rejected by remote API: Remote API returned " + "Code {}".format(url, response_code)) + elif code_x100 == 5: + raise RuntimeError( + "Remote API returned invalid response " + "(code:{}, url:{})".format(response_code, url)) + else: + raise RuntimeError( + "API request failed with unexpected response " + "code (code:{}, url:{})".format(response_code, url)) + + +def main(): + """Main method called to start the agent.""" + utils.vip_main(ambient, + version=__version__) + + +if __name__ == '__main__': + # Entry point for script + try: + sys.exit(main()) + except KeyboardInterrupt: + pass diff --git a/services/core/Ambient/ambient/data/name_mapping.csv b/services/core/Ambient/ambient/data/name_mapping.csv new file mode 100644 index 0000000000..ae53b773fa --- /dev/null +++ b/services/core/Ambient/ambient/data/name_mapping.csv @@ -0,0 +1,31 @@ +Service_Point_Name,Standard_Point_Name,Service_Units,Standard_Units +tempf,surface_temperature,degF, +humidity,relative_humidity,, +windspeedmph,wind_speed,mph, +winddir,wind_from_direction,, +windgustmph,wind_speed_of_gust,mph, +maxdailygust,,mph, +windgustdir,,, +winddir_avg2m,,, +windspdmph_avg2m,,mph, +winddir_avg10m,,, +windspdmph_avg10m,,mph, +baromrelin,,Hg, +baromabsin,,Hg, +tempinf,surface_temperature_indoor,degF, +humidityin,humidity_indoor,, +hourlyrainin,,inch, +dailyrainin,,inch, +monthlyrainin,,inch, +yearlyrainin,,inch, +eventrainin,,inch, +totalrainin,,inch, +feelsLike,apparent_temperature,degF, +feelsLikein,apparent_temperature_indoor,degF, +dewPoint,dew_point_temperature,degF, +dewPointin,dew_point_temperature_indoor,degF, +soiltempf,,degF, +soilhum,,, +uv,ultraviolet_index,, +solarradiation,solar_irradiance,W / m ** 2, +co2,,, diff --git a/services/core/Ambient/config b/services/core/Ambient/config new file mode 100644 index 0000000000..7095d5ab3d --- /dev/null +++ b/services/core/Ambient/config @@ -0,0 +1,12 @@ +{ + "application_key" : "", + "api_key": "", + "poll_locations": [ + {"location": "Lab Home A"}, + {"location": "Lab Home B"} + ], + "max_size_gb": 1, + "database_file": "weather.sqlite", + "poll_interval": 60, + "identity": "platform.ambient" +} \ No newline at end of file diff --git a/services/core/Ambient/conftest.py b/services/core/Ambient/conftest.py new file mode 100644 index 0000000000..68e5e611b1 --- /dev/null +++ b/services/core/Ambient/conftest.py @@ -0,0 +1,6 @@ +import sys + +from volttrontesting.fixtures.volttron_platform_fixtures import * + +# Add system path of the agent's directory +sys.path.insert(0, os.path.abspath(os.path.dirname(__file__))) diff --git a/services/core/Ambient/requirements.txt b/services/core/Ambient/requirements.txt new file mode 100644 index 0000000000..45f523a5a6 --- /dev/null +++ b/services/core/Ambient/requirements.txt @@ -0,0 +1 @@ +pint diff --git a/services/core/Ambient/setup.py b/services/core/Ambient/setup.py new file mode 100644 index 0000000000..f29523e7ca --- /dev/null +++ b/services/core/Ambient/setup.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2019, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +from os import path +from setuptools import setup, find_packages + +MAIN_MODULE = 'agent' + +# Find the agent package that contains the main module +packages = find_packages('.') +agent_package = '' + +for package in find_packages(): + # Because there could be other packages such as tests + if path.isfile(package + '/' + MAIN_MODULE + '.py') is True: + agent_package = package +if not agent_package: + raise RuntimeError('None of the packages under {dir} contain the file ' + '{main_module}'.format(main_module=MAIN_MODULE + '.py', + dir=path.abspath('.'))) + +# Find the version number from the main module +agent_module = agent_package + '.' + MAIN_MODULE +_temp = __import__(agent_module, globals(), locals(), ['__version__'], 0) +__version__ = _temp.__version__ + +# Setup +setup( + name=agent_package + 'agent', + version=__version__, + description='Agent for interfacing with the Ambient weather station API service', + install_requires=['volttron'], + packages=packages, + package_data={'ambient': ['data/name_mapping.csv']}, + entry_points={ + 'setuptools.installation': [ + 'eggsecutable = ' + agent_module + ':main', + ] + } +) + diff --git a/services/core/Ambient/tests/test_ambient_agent.py b/services/core/Ambient/tests/test_ambient_agent.py new file mode 100644 index 0000000000..21068cb194 --- /dev/null +++ b/services/core/Ambient/tests/test_ambient_agent.py @@ -0,0 +1,339 @@ +# -*- coding: utf-8 -*- {{{ +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: +# +# Copyright 2017, Battelle Memorial Institute. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 +# }}} + +import pytest +import sqlite3 +import copy +import os +import json +import gevent +import logging +from mock import MagicMock +from volttron.platform.agent import utils +from volttron.platform.messaging.health import STATUS_GOOD + +__version__ = "5.0.1" + +utils.setup_logging() +_log = logging.getLogger(__name__) + +# Ambient agent tests rely upon the configuration of devices operated +# by the owner/operater of Ambient devices - To run the Ambient tests +# the test_data/test_ambient_data.json file should be populated: +# api_key should be filled in with a valid Ambient API key, app_key +# with an Ambient application key, and locations with a list of device +# "locations" corresponding to devices owned/operated by the runner of +# the test suite. + +# Replace get services core with something that will point us to our local directory +ambient_agent_path = os.path.abspath( + os.path.normpath( + os.path.expanduser("~/house-deployment/Ambient"))) + +API_KEY = "" +APP_KEY = "" +LOCATIONS = [ + {"location": " 1: + assert isinstance(results1, dict) + assert results1['observation_time'] + assert results1['weather_results'] + else: + assert isinstance(results1, list) + assert len(results1) == len(config["poll_locations"]) + i = i + 1 + assert query_agent.vip.rpc.call( + "poll.weather", "health.get_status").get(timeout=10).get( + 'status') == STATUS_GOOD + finally: + if agent_uuid: + volttron_instance.stop_agent(agent_uuid) + volttron_instance.remove_agent(agent_uuid) + + From c4b888cf708925b9826d9417aaa503b55735d050 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 26 Mar 2020 15:08:29 -0700 Subject: [PATCH 2/8] Update README.rst minor updates --- services/core/Ambient/README.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/core/Ambient/README.rst b/services/core/Ambient/README.rst index c0a403a3b6..6d20b34d93 100644 --- a/services/core/Ambient/README.rst +++ b/services/core/Ambient/README.rst @@ -16,7 +16,7 @@ Two API Keys are required for all REST API requests: applicationKey - identifies the developer / application. To request an application key please email support@ambientweather.com - apiKey - grants access to past/present data for a given user's devices. A typical consumer-facing application will + + apiKey - grants access to past/present data for a given user's devices. A typical consumer-facing application will initially ask the user to create an apiKey on thier AmbientWeather.net account page (https://dashboard.ambientweather.net/account) and paste it into the app. Developers for personal or in-house apps will also need to create an apiKey on their own account page. @@ -34,12 +34,14 @@ will return a record containing "weather_error" if used). The location format for the Ambient agent is as follows: - {"location": ""} Ambient locations are Arbitrary string identifiers given to a weather station by the weather station owner/operator. This is an example response: +:: + 2019-12-17 15:35:56,395 (listeneragent-3.3 3103) listener.agent INFO: Peer: pubsub, Sender: platform.ambient:, Bus: , Topic: weather/poll/current/all, Headers: {'Date': '2019-12-17T23:35:56.392709+00:00', 'Content-Type': 'Content-Type', 'min_compatible_version': '3.0', 'max_compatible_version': ''}, Message: [{'location': 'Lab Home A', 'observation_time': '2019-12-18T07:33:00.000000+00:00', @@ -91,6 +93,8 @@ and "app_key" parameters are required while all others are optional. Example configuration: +.. code-block:: json + { "application_key" : "", "api_key":"", @@ -128,15 +132,13 @@ Running Ambient Agent Tests The following instructions can be used to run PyTests for the Ambient agent. 1. Set up the test file - test_ambient_agent.py is the PyTest file for the ambient agent. The test file features a few -variables at the top of the tests will will need to be filled in by the runner of the Ambient agent tests. The LOCATIONS +variables at the top of the tests. These will need to be filled in by the runner of the Ambient agent tests. The LOCATIONS variable specifies a list of "locations" of Ambient devices. The required format is a list of dictionaries of the form {"location": }. Locations are determined by the user when configuring a weather -station for the Ambient service using the Ambient app. For more information about these variables, please view the -README.rst file. For more information about the Ambient API, visit https://www.ambientweather.com/api.html +station for the Ambient service using the Ambient app. For more information about the Ambient API, visit https://www.ambientweather.com/api.html 2. Set up the test environment - The tests are intended to be run from the Volttron root directory using the Volttron -environment. it is also recommended to use the -s option. In PyCharm, setting the DEBUG_MODE environment variable to -True can be useful for debugging purposes. The tests should target the Ambient agent's directory. +environment. Setting the environment variable, DEBUG_MODE=True or DEBUG=1 will preserve the test setup and can be useful for debugging purposes. When testing from pycharm set the Working Directory value to be the root of volttron source/checkout directory. Example command line: From 96202205ea9ad802b418953a4d856873befdff83 Mon Sep 17 00:00:00 2001 From: Chandrika Date: Thu, 26 Mar 2020 15:09:23 -0700 Subject: [PATCH 3/8] Update config --- services/core/Ambient/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/Ambient/config b/services/core/Ambient/config index 7095d5ab3d..2f1b8001c2 100644 --- a/services/core/Ambient/config +++ b/services/core/Ambient/config @@ -9,4 +9,4 @@ "database_file": "weather.sqlite", "poll_interval": 60, "identity": "platform.ambient" -} \ No newline at end of file +} From 0fadb464b4dfd02674d1e049c3b831c400e5e5da Mon Sep 17 00:00:00 2001 From: jklarson Date: Thu, 26 Mar 2020 16:44:51 -0700 Subject: [PATCH 4/8] fixed Ambient agent source path in tests --- services/core/Ambient/tests/test_ambient_agent.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/core/Ambient/tests/test_ambient_agent.py b/services/core/Ambient/tests/test_ambient_agent.py index 21068cb194..a4d7269a81 100644 --- a/services/core/Ambient/tests/test_ambient_agent.py +++ b/services/core/Ambient/tests/test_ambient_agent.py @@ -40,10 +40,11 @@ import sqlite3 import copy import os -import json import gevent import logging from mock import MagicMock + +from volttron.platform import get_services_core from volttron.platform.agent import utils from volttron.platform.messaging.health import STATUS_GOOD @@ -61,9 +62,7 @@ # the test suite. # Replace get services core with something that will point us to our local directory -ambient_agent_path = os.path.abspath( - os.path.normpath( - os.path.expanduser("~/house-deployment/Ambient"))) +ambient_agent_path = get_services_core("Ambient") API_KEY = "" APP_KEY = "" From a157ebbc2e983f55647058354dc36d0baffc8aa7 Mon Sep 17 00:00:00 2001 From: jklarson Date: Fri, 27 Mar 2020 12:52:57 -0700 Subject: [PATCH 5/8] Removed duplicate license, unused imports, duplicate globals, cleaned up docs and update interval --- services/core/Ambient/ambient/agent.py | 156 ++++++------------------- 1 file changed, 34 insertions(+), 122 deletions(-) diff --git a/services/core/Ambient/ambient/agent.py b/services/core/Ambient/ambient/agent.py index c537305276..16b71c66cd 100644 --- a/services/core/Ambient/ambient/agent.py +++ b/services/core/Ambient/ambient/agent.py @@ -61,74 +61,15 @@ import datetime import pytz import sys -import re import grequests -# requests should be imported after grequests as -# requests imports ssl and grequests patches ssl +# requests should be imported after grequests as requests imports ssl and grequests patches ssl import requests import pkg_resources from volttron.platform.agent import utils from volttron.platform.vip.agent import RPC -from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now -from volttron.platform.agent.base_weather import BaseWeatherAgent -from volttron.platform.agent.base_weather import get_forecast_start_stop -from volttron.platform import jsonapi# -*- coding: utf-8 -*- {{{ -# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: -# -# Copyright 2019, Battelle Memorial Institute. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# This material was prepared as an account of work sponsored by an agency of -# the United States Government. Neither the United States Government nor the -# United States Department of Energy, nor Battelle, nor any of their -# employees, nor any jurisdiction or organization that has cooperated in the -# development of these materials, makes any warranty, express or -# implied, or assumes any legal liability or responsibility for the accuracy, -# completeness, or usefulness or any information, apparatus, product, -# software, or process disclosed, or represents that its use would not infringe -# privately owned rights. Reference herein to any specific commercial product, -# process, or service by trade name, trademark, manufacturer, or otherwise -# does not necessarily constitute or imply its endorsement, recommendation, or -# favoring by the United States Government or any agency thereof, or -# Battelle Memorial Institute. The views and opinions of authors expressed -# herein do not necessarily state or reflect those of the -# United States Government or any agency thereof. -# -# PACIFIC NORTHWEST NATIONAL LABORATORY operated by -# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY -# under Contract DE-AC05-76RL01830 -# }}} - -__docformat__ = 'reStructuredText' - -import logging -import datetime -import pytz -import sys -import re - -import grequests -# requests should be imported after grequests as -# requests imports ssl and grequests patches ssl -import requests - -import pkg_resources -from volttron.platform.agent import utils -from volttron.platform.vip.agent import RPC -from volttron.platform.agent.utils import format_timestamp, get_aware_utc_now +from volttron.platform.agent.utils import format_timestamp from volttron.platform.agent.base_weather import BaseWeatherAgent from volttron.platform import jsonapi @@ -136,17 +77,11 @@ utils.setup_logging() __version__ = "0.1" -WEATHER_WARN = "weather_warnings" -WEATHER_ERROR = "weather_error" -WEATHER_RESULTS = "weather_results" - def ambient(config_path, **kwargs): - """Parses the Agent configuration and returns an instance of - the agent created using that configuration. - + """ + Parses the Agent configuration and returns an instance of the agent created using that configuration. :param config_path: Path to a configuration file. - :type config_path: str :returns: Ambient :rtype: Ambient @@ -157,11 +92,11 @@ def ambient(config_path, **kwargs): config = {} if not config: _log.error("Ambient agent configuration: ".format(config)) - if "api_key" not in config: + api_key = config.get("api_key") + if not api_key or isinstance(api_key, str): raise RuntimeError("Ambient agent must be configured with an api key.") if "application_key" not in config: - raise RuntimeError("Ambient agent must be configured with an " - "application key.") + raise RuntimeError("Ambient agent must be configured with an application key.") _log.debug("config_dict before init: {}".format(config)) utils.update_kwargs_with_config(kwargs, config) @@ -170,11 +105,8 @@ def ambient(config_path, **kwargs): class Ambient(BaseWeatherAgent): """ - The Ambient agent requires having an API key to interact with the remote - API. The agent offers a performance_mode configuration option which - allows users to limit the amount of data returned by the API. - - ***Powered by Dark Sky*** + The Ambient agent requires having an API key to interact with the remote API. The agent offers a performance_mode + configuration option which allows users to limit the amount of data returned by the API. """ def __init__(self, application_key="", **kwargs): @@ -200,8 +132,7 @@ def get_version(self): def validate_location(self, service_name, location): """ - Indicates whether the location dictionary provided matches the format - required by the remote weather API + Indicates whether the location dictionary provided matches the format required by the remote weather API :param service_name: name of the remote API service :param location: location dictionary to provide in the remote API url :return: True if the location matches the required format else False @@ -215,31 +146,27 @@ def get_update_interval(self, service_name): :return: datetime timedelta representing the time interval """ if service_name == "get_current_weather": - return datetime.timedelta(seconds=3) + return datetime.timedelta(minutes=5) else: return None def get_api_description(self, service_name): """ - Provides a human-readable description of the various endpoints provided - by the agent + Provides a human-readable description of the various endpoints provided by the agent :param service_name: requested service endpoint :return: Human-readable description string """ if service_name is "get_current_weather": - "Provides current weather observations by Ambient weather" \ - " station location name via RPC (Requires " \ - "{'location': })" + "Provides current weather observations for locations by their corresponding Ambient weather station name " \ + "via RPC (Requires {'location': })" else: raise RuntimeError( "Service {} is not implemented by Ambient.".format(service_name)) def get_point_name_defs_file(self): """ - Constructs the point name mapping dict from the - mapping csv. - :return: dictionary containing a mapping of service point - names to standard point names with optional + Constructs the point name mapping dict from the mapping csv. + :return: dictionary containing a mapping of service point names to standard point names with optional """ # returning resource file instead of stream, as csv.DictReader require file path or file like object opened in # text mode. @@ -247,15 +174,13 @@ def get_point_name_defs_file(self): def query_current_weather(self, location): """ - Retrieve data from the Ambient API, return formatted current data and - store forecast data in cache + Retrieve data from the Ambient API, return formatted current data and store forecast data in cache :param location: location dictionary requested by the user :return: Timestamp and data for current data from the Ambient API """ ambient_response = self.make_request() location_response = None current_time = None - cache_records = [] for record in ambient_response: record_location = None record_info = record.pop("info") @@ -291,17 +216,15 @@ def query_forecast_service(self, service, location, quantity, forecast_start): Unimplemented method stub :param service: forecast service type of weather data to return :param location: location dictionary requested during the RPC call - :param quantity: number of records to return, used to generate - Time Machine requests after the forecast request - :param forecast_start: forecast results that are prior to this - timestamp will be filtered by base weather agent + :param quantity: number of records to return, used to generate Time Machine requests after the forecast request + :param forecast_start: forecast results that are prior to this timestamp will be filtered by base weather agent :return: Timestamp and data returned by the Ambient weather API response """ raise NotImplementedError def make_request(self): - """ Request data from the Ambient Weather API - + """ + Request data from the Ambient Weather API An example of the return value is as follows [ @@ -346,7 +269,7 @@ def make_request(self): # AuthenticationTwo API Keys are required for all REST API requests:applicationKey - identifies the # developer / application. To request an application key please email support@ambient.comapiKey - # grants access to past/present data for a given user's devices. A typical consumer-facing application will - # initially ask the user to create an apiKey on thier Ambient.net account page + # initially ask the user to create an apiKey on their Ambient.net account page # (https://dashboard.ambientweather.net/account) and paste it into the app. Developers for personal or # in-house apps will also need to create an apiKey on their own account page. # Rate LimitingAPI requests are capped at 1 request/second for each user's apiKey and 3 requests/second @@ -357,11 +280,11 @@ def make_request(self): if not self.last_service_call_timestamp or ( datetime.datetime.now() - self.last_service_call_timestamp).total_seconds() > 3: - url = 'https://api.ambientweather.net/v1/devices?applicationKey=' + self.app_key + '&apiKey=' + self._api_key + url = 'https://api.ambientweather.net/v1/devices?applicationKey=' + self.app_key + '&apiKey=' + \ + self._api_key _log.info("requesting url: {}".format(url)) - grequest = [grequests.get(url, verify=requests.certs.where(), - headers=self.headers, timeout=3)] + grequest = [grequests.get(url, verify=requests.certs.where(), headers=self.headers, timeout=3)] gresponse = grequests.map(grequest)[0] if gresponse is None: raise RuntimeError("get request did not return any response") @@ -379,10 +302,8 @@ def make_request(self): def query_hourly_forecast(self, location): """ Unimplemented method stub - :param location: currently accepts lat/long location dictionary - format only - :return: time of forecast prediction as a timestamp string, - and a list of + :param location: currently accepts lat/long location dictionary format only + :return: time of forecast prediction as a timestamp string, and a list of """ raise NotImplementedError @@ -398,34 +319,25 @@ def query_hourly_historical(self, location, start_date, end_date): def generate_response_error(self, url, response_code): """ - raises a descriptive runtime error based on the response code - returned by a service. + Raises a descriptive runtime error based on the response code returned by a service. :param url: actual url used for requesting data from Ambient - :param response_code: Http response code returned by a service - following a request + :param response_code: Http response code returned by a service following a request """ code_x100 = int(response_code / 100) if code_x100 == 2: - raise RuntimeError( - "Remote API returned no data(code:{}, url:{})".format( - response_code, url)) + raise RuntimeError("Remote API returned no data(code:{}, url:{})".format(response_code, url)) elif code_x100 == 3: raise RuntimeError( - "Remote API redirected request, " - "but redirect failed (code:{}, url:{})".format(response_code, - url)) + "Remote API redirected request, but redirect failed (code:{}, url:{})".format(response_code, url)) elif code_x100 == 4: raise RuntimeError( - "Request ({}) rejected by remote API: Remote API returned " - "Code {}".format(url, response_code)) + "Request ({}) rejected by remote API: Remote API returned Code {}".format(url, response_code)) elif code_x100 == 5: raise RuntimeError( - "Remote API returned invalid response " - "(code:{}, url:{})".format(response_code, url)) + "Remote API returned invalid response (code:{}, url:{})".format(response_code, url)) else: raise RuntimeError( - "API request failed with unexpected response " - "code (code:{}, url:{})".format(response_code, url)) + "API request failed with unexpected response code (code:{}, url:{})".format(response_code, url)) def main(): From 86190e6961406e04f95b547009b8d07fe089ead2 Mon Sep 17 00:00:00 2001 From: jklarson Date: Fri, 27 Mar 2020 13:13:51 -0700 Subject: [PATCH 6/8] Converted Ambient test keys to environment vars --- services/core/Ambient/tests/test_ambient_agent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/core/Ambient/tests/test_ambient_agent.py b/services/core/Ambient/tests/test_ambient_agent.py index a4d7269a81..22f3df200d 100644 --- a/services/core/Ambient/tests/test_ambient_agent.py +++ b/services/core/Ambient/tests/test_ambient_agent.py @@ -64,8 +64,8 @@ # Replace get services core with something that will point us to our local directory ambient_agent_path = get_services_core("Ambient") -API_KEY = "" -APP_KEY = "" +API_KEY = os.environ.get('AMBIENT_API_KEY') +APP_KEY = os.environ.get('AMBIENT_APP_KEY') LOCATIONS = [ {"location": " Date: Fri, 27 Mar 2020 15:46:04 -0700 Subject: [PATCH 7/8] updated Ambient agent config validation, adjusted timeout durations --- services/core/Ambient/ambient/agent.py | 10 ++++------ services/core/Ambient/tests/test_ambient_agent.py | 9 ++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/services/core/Ambient/ambient/agent.py b/services/core/Ambient/ambient/agent.py index 16b71c66cd..1de617306e 100644 --- a/services/core/Ambient/ambient/agent.py +++ b/services/core/Ambient/ambient/agent.py @@ -92,11 +92,9 @@ def ambient(config_path, **kwargs): config = {} if not config: _log.error("Ambient agent configuration: ".format(config)) - api_key = config.get("api_key") - if not api_key or isinstance(api_key, str): - raise RuntimeError("Ambient agent must be configured with an api key.") - if "application_key" not in config: - raise RuntimeError("Ambient agent must be configured with an application key.") + for key in ["api_key", "application_key"]: + if not config.get(key) or not isinstance(config.get(key), str): + raise RuntimeError("Ambient agent must be configured with '{}' key.".format(key)) _log.debug("config_dict before init: {}".format(config)) utils.update_kwargs_with_config(kwargs, config) @@ -284,7 +282,7 @@ def make_request(self): self._api_key _log.info("requesting url: {}".format(url)) - grequest = [grequests.get(url, verify=requests.certs.where(), headers=self.headers, timeout=3)] + grequest = [grequests.get(url, verify=requests.certs.where(), headers=self.headers, timeout=30)] gresponse = grequests.map(grequest)[0] if gresponse is None: raise RuntimeError("get request did not return any response") diff --git a/services/core/Ambient/tests/test_ambient_agent.py b/services/core/Ambient/tests/test_ambient_agent.py index 22f3df200d..35ff110e86 100644 --- a/services/core/Ambient/tests/test_ambient_agent.py +++ b/services/core/Ambient/tests/test_ambient_agent.py @@ -214,7 +214,7 @@ def test_success_current(volttron_instance, cleanup_cache, weather, cursor = sqlite_connection.cursor() query_data = query_agent.vip.rpc.call(identity, 'get_current_weather', - locations).get(timeout=30) + locations).get(timeout=33) if query_data[0].get("weather_error"): error = query_data[0].get("weather_error") @@ -243,7 +243,7 @@ def test_success_current(volttron_instance, cleanup_cache, weather, assert False cache_data = query_agent.vip.rpc.call(identity, 'get_current_weather', - locations).get(timeout=30) + locations).get(timeout=3) # check names returned are valid assert len(query_data) == len(cache_data) @@ -266,7 +266,7 @@ def test_current_fail(weather, query_agent, locations, api_wait): """ identity = weather[1] query_data = query_agent.vip.rpc.call(identity, 'get_current_weather', - locations).get(timeout=30) + locations).get(timeout=33) for record in query_data: error = record.get("weather_error") assert error.startswith("Invalid location format.") or error.startswith( @@ -328,8 +328,7 @@ def test_polling_locations_valid_config(volttron_instance, query_agent, config, assert len(results1) == len(config["poll_locations"]) i = i + 1 assert query_agent.vip.rpc.call( - "poll.weather", "health.get_status").get(timeout=10).get( - 'status') == STATUS_GOOD + "poll.weather", "health.get_status").get(timeout=10).get('status') == STATUS_GOOD finally: if agent_uuid: volttron_instance.stop_agent(agent_uuid) From 390ab23881ab1284c7ffe639c3e84c5222c422f3 Mon Sep 17 00:00:00 2001 From: jklarson Date: Mon, 27 Apr 2020 14:12:49 -0700 Subject: [PATCH 8/8] Fixed inconsistent behavior in ambient tests that can occur depending upon test configuration --- services/core/Ambient/tests/test_ambient_agent.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/services/core/Ambient/tests/test_ambient_agent.py b/services/core/Ambient/tests/test_ambient_agent.py index 35ff110e86..a16d27d87e 100644 --- a/services/core/Ambient/tests/test_ambient_agent.py +++ b/services/core/Ambient/tests/test_ambient_agent.py @@ -66,10 +66,17 @@ API_KEY = os.environ.get('AMBIENT_API_KEY') APP_KEY = os.environ.get('AMBIENT_APP_KEY') + +# Locations should be a list of location objects with the value string matching the name of the location as configured +# in Ambient LOCATIONS = [ - {"location": ""}, + {"location": ""} ] +# Poll test topics should be a list of weather topic strings with number of entries == length of locations +POLL_TEST_TOPICS = ['weather/poll/current/test_a', 'weather/poll/current/test_b'] + ambient_service = { 'weather_service': ambient_agent_path, 'identity': 'platform.ambient', @@ -285,9 +292,9 @@ def test_current_fail(weather, query_agent, locations, api_wait): 'poll_interval': 5, 'api_key': API_KEY, 'application_key': APP_KEY, - 'poll_topic_suffixes': ['test1', 'test2'] + 'poll_topic_suffixes': ['test_a', 'test_b'] }, - ['weather/poll/current/test1', 'weather/poll/current/test2']) + POLL_TEST_TOPICS) ]) def test_polling_locations_valid_config(volttron_instance, query_agent, config, result_topics, api_wait):