class WeatherStation(WoTThing): def __init__(self, config): super(WeatherStation, self).__init__(config, "my weatherstation", "thing", "my weather station") # initialize the weather station seed(100) async def get_weather_data(self): # do whatever it takes to get current temperature, pressure and wind speed self.temperature = randint(40, 80) self.barometic_pressure = randint(290, 310) / 10.0 self.wind_speed = randint(0, 25) logging.debug('new values fetched: %s, %s, %s', self.temperature, self.barometric_pressure, self.wind_speed) temperature = WoTThing.wot_property(name='temperature', initial_value=0.0, description='the temperature in ℉', value_source_fn=get_weather_data, units='℉') barometric_pressure = WoTThing.wot_property( name='barometric_pressure', initial_value=30.0, description='the air pressure in inches', units='in') wind_speed = WoTThing.wot_property(name='wind_speed', initial_value=30.0, description='the wind speed in mph', units='mph')
class TwilioSMS(WoTThing): required_config = Namespace() required_config.add_option( 'twilio_account_sid', doc='the Twilio Account SID', default="NOT A REAL SID", ) required_config.add_option( 'twilio_auth_token', doc='the Twilio Auth Token', default="NOT A REAL AUTH TOKEN", ) required_config.add_option( 'from_number', doc="the user's Twilio phone number (+1XXXYYYZZZZ)", default="+1XXXYYYZZZZ", ) def __init__(self, config): super(TwilioSMS, self).__init__(config, "Twilio SMS", "thing", "a gateway for sending SMS") def set_to_number(self, to_number): self.send_twilio_sms(to_number, None) def set_message(self, message): self.send_twilio_sms(None, message) def send_twilio_sms(self, to_number, message): # logging.debug('args %s, kwargs: %s', args, kwargs) if to_number is not None: self.to_number = to_number if message is not None: self.message = message if self.to_number is None or self.message is None: logging.debug('NOT READY: to_number: %s, message: %s', self.to_number, self.message) return logging.debug('sending SMS') client = Client(self.config.twilio_account_sid, self.config.twilio_auth_token) message = client.messages.create(body=self.message, from_=self.config.from_number, to=self.to_number) self.to_number = None self.message = None to_number = WoTThing.wot_property(name='to_number', initial_value=None, type='string', description='the number to send to', value_forwarder=set_to_number) message = WoTThing.wot_property(name='message', initial_value=None, type='string', description='the body of the SMS message', value_forwarder=set_message)
class ExampleDimmableLight(WoTThing): def __init__(self, config, lamp_hardware): super(ExampleDimmableLight, self).__init__( config, 'My Lamp', 'dimmableLight', 'A web connected lamp' ) self._lamp_hardware = lamp_hardware async def _get_illumination_state(self): """this method will be run at a configurable interval to poll for changes of state of the lamp. For example, to detect if a meddlesome child were to be randomly turning the light on/off and adjusting the brightness independently of this program.""" self.on = self._lamp_hardware.get_lamp_state() self.level = self._lamp_hardware.get_lamp_level() def _set_hardware_illumination_state(self, boolean_value): # do whatever it takes to set the state of the lamp by # talking to the hardware self._lamp_hardware.set_lamp_state(boolean_value) def _set_hardware_level(self, new_level): # do whatever it takes to change the level of the lamp by # talking to the hardware self._lamp_hardware.set_lamp_level(new_level) on = WoTThing.wot_property( name='on', initial_value=True, description="is the light illuminated?", value_source_fn=_get_illumination_state, value_forwarder=_set_hardware_illumination_state ) level = WoTThing.wot_property( name='level', initial_value=0, description="lamp brightness level", value_forwarder=_set_hardware_level, minimum=0, maximum=100 )
class BitcoinTrend(WoTThing): required_config = Namespace() required_config.add_option( 'name', doc='the name of this Bitcoin Trend Monitor', default="bitcoin trend", ) required_config.add_option( 'target_url', doc='the URL for json data', default="https://api.coindesk.com/v1/bpi/currentprice/USD.json") required_config.add_option( 'seconds_for_timeout', doc='the number of seconds to allow for fetching bitcoin data', default=10) @staticmethod def sign(x): if x < 0: return -1 if x > 0: return 1 return 0 def __init__(self, config): super(BitcoinTrend, self).__init__(config, config.name, "thing", "my bitcoin trend monitor") self.previous_value = 0 async def get_bitcoin_value(self): async with aiohttp.ClientSession() as session: async with async_timeout.timeout(self.config.seconds_for_timeout): async with session.get(self.config.target_url) as response: self.bitcoin_data = json.loads(await response.text()) current_observation = self.bitcoin_data['bpi']['USD']['rate_float'] self.trend = self.sign(current_observation - self.previous_value) self.previous_value = current_observation logging.debug( 'new value fetched: %s, trend: %s', current_observation, self.trend, ) trend = WoTThing.wot_property( name='trend', initial_value=0, description='the trend positive or negative', value_source_fn=get_bitcoin_value, )
class Thumper(WoTThing): def __init__(self, config): super(Thumper, self).__init__(config, "the thumper", "thing", "a thumper") async def get_next_value(self): self.thump = not self.thump logging.debug('fetched new value: %s', self.thump) thump = WoTThing.wot_property( name='thump', initial_value=True, description='thump', value_source_fn=get_next_value, )
class SceneThing(WoTThing): required_config = Namespace() required_config.add_option( 'all_things_url', doc='a URL for fetching all things data', default="http://gateway.local/things", ) required_config.add_option( 'thing_state_url_template', doc='a URL for fetching the current state of a thing', default="http://gateway.local/things/{}/properties/{}", ) required_config.add_option( 'seconds_for_timeout', doc='the number of seconds to allow for fetching enphase data', default=10) def __init__(self, config): super(SceneThing, self).__init__(config, "Scene Thing", "thing", "A controller for scenes") self.state_file_name = '{}.json'.format(self.name) try: with open(self.state_file_name) as state_file: self.participants = json.load(state_file) except FileNotFoundError: logging.info('no scene state file found for %s', self.state_file_name) self.participants = {} except json.decoder.JSONDecodeError: logging.info('bad file format for %s', self.state_file_name) self.participants = {} self.listeners = [] self.preserved_state = {} on_off = WoTThing.wot_property( name='on', initial_value=False, description='on/off status', value_forwarder=scene_on_off, ) learn = WoTThing.wot_property( name='learn', initial_value=False, description='learn mode', value_forwarder=learn_on_off, ) async def get_all_things(self): async with aiohttp.ClientSession() as session: async with async_timeout.timeout(self.config.seconds_for_timeout): async with session.get( self.config.all_things_url, headers={ 'Accept': 'application/json', 'Authorization': 'Bearer {}'.format( self.config.things_gateway_auth_key), 'Content-Type': 'application/json' }) as response: all_things = json.loads(await response.text()) print(json.dumps(all_things)) return all_things @staticmethod def quote_strings(a_value): if isinstance(a_value, str): return '"{}"'.format(a_value) return a_value async def change_property(self, a_thing_id, a_property, a_value): while True: try: async with aiohttp.ClientSession() as session: async with async_timeout.timeout( self.config.seconds_for_timeout): async with session.put( "http://gateway.local/things/{}/properties/{}/" .format(a_thing_id, a_property), headers={ 'Accept': 'application/json', 'Authorization': 'Bearer {}'.format( self.config.things_gateway_auth_key), 'Content-Type': 'application/json' }, data='{{"{}": {}}}'.format( a_property, str(self.quote_strings( a_value)).lower())) as response: logging.debug( 'change_property: sent %s to %s', '{{"{}": {}}}'.format( a_property, str(self.quote_strings(a_value)).lower()), a_thing_id) return await response.text() except aiohttp.client_exceptions.ClientConnectorError as e: logging.error( 'change_property: problem contacting http:/gateway.local: {}' .format(e)) logging.info('change_property: retrying after 20 second pause') await asyncio.sleep(20.0) async def change_properties_from_a_change_set(self, a_thing_id, a_change_set): # it seems that some devices cannot have properties changed if they are # on in their 'on' state. If the change_set contains a property to turn # the thing on, do that first. if 'on' in a_change_set and a_change_set['on'] is True: await self.change_property(a_thing_id, 'on', True) await asyncio.gather( *(self.change_property(a_thing_id, a_property, a_value) for a_property, a_value in a_change_set.items() if a_property != 'on')) # if the change_set has an 'on' property to turn something off, ensure that # turning the thing off is the last thing done. if 'on' in a_change_set and a_change_set['on'] is False: await self.change_property(a_thing_id, 'on', False) async def monitor_state(self, a_thing_id): async with websockets.connect( 'ws://gateway.local/things/{}?jwt={}'.format( a_thing_id, self.config.things_gateway_auth_key), ) as websocket: async for message in websocket: raw = json.loads(message) if raw['messageType'] == 'propertyStatus': if a_thing_id in self.participants: self.participants[a_thing_id].update(raw["data"]) else: self.participants[a_thing_id] = raw["data"] async def learn_changes(self): # connect to Things Gateway and create listener for every object and property all_things = await self.get_all_things() for a_thing in all_things: if a_thing['name'] == self.name: logging.debug('skipping thing %s', a_thing['name']) continue a_thing_id = a_thing['href'].replace('/things/', '') self.listeners.append( asyncio.ensure_future(self.monitor_state(a_thing_id))) async def stop_learning(self): # close all listeners asyncio.gather(*self.listeners, return_exceptions=True).cancel() logging.info('stop_learing: this is what I learned:') for a_thing_id, a_change_set in self.participants.items(): logging.info(' {}: {}'.format(a_thing_id, a_change_set)) with open(self.state_file_name, 'w') as state_file: json.dump(self.participants, state_file) async def capture_current_state(self, a_thing_id, a_change_set): self.preserved_state[a_thing_id] = {} for a_property in a_change_set.keys(): # as of 0.4, the 'properties' resource has not been implemented in the Things API # this means that rather than fetching all the properties for a thing in one call # we've got to do it one property at a time. async with aiohttp.ClientSession() as session: async with async_timeout.timeout( self.config.seconds_for_timeout): logging.debug( self.config.thing_state_url_template.format( a_thing_id, a_property)) async with session.get( self.config.thing_state_url_template.format( a_thing_id, a_property), headers={ 'Accept': 'application/json', 'Authorization': 'Bearer {}'.format( self.config.things_gateway_auth_key), 'Content-Type': 'application/json' }) as response: state_snapshot = json.loads(await response.text()) self.preserved_state[a_thing_id].update(state_snapshot) async def turn_on_participants(self): if self.learn == ON: self.learn = OFF # go through participants, capturing their current state self.preserved_state = {} await asyncio.gather( *(self.capture_current_state(a_thing, a_change_set) for a_thing, a_change_set in self.participants.items())) # go through participtants setting their state logging.debug('start turn_on_participants') await asyncio.gather( *(self.change_properties_from_a_change_set(a_thing, change_set) for a_thing, change_set in self.participants.items())) async def restore_participants(self): # go through participants and turn off if self.learn == ON: self.learn = OFF await asyncio.gather( *(self.change_properties_from_a_change_set(a_thing, change_set) for a_thing, change_set in self.preserved_state.items()))
class WeatherStation(WoTThing): required_config = Namespace() required_config.add_option( 'weather_underground_api_key', doc='the api key to access Weather Underground data', short_form="K", default="not a real key") required_config.add_option( 'state_code', doc='the two letter state code', default="OR", ) required_config.add_option( 'city_name', doc='the name of the city', default="Corvallis", ) required_config.add_option( 'name', doc='the name of this weather station', default="my weather station", ) required_config.add_aggregation('target_url', function=create_url) required_config.add_option( 'seconds_for_timeout', doc='the number of seconds to allow for fetching weather data', default=10) def __init__(self, config): super(WeatherStation, self).__init__( config, config.name, "thing", "my weather station with data for {}, {}".format( config.city_name, config.state_code)) self.weather_data = { 'current_observation': { 'temp_f': self.temperature, 'pressure_in': self.barometric_pressure, 'wind_mph': self.wind_speed, } } async def get_weather_data(self): async with aiohttp.ClientSession() as session: async with async_timeout.timeout(self.config.seconds_for_timeout): async with session.get(self.config.target_url) as response: self.weather_data = json.loads(await response.text()) current_observation = self.weather_data['current_observation'] self.temperature = current_observation['temp_f'] self.barometric_pressure = current_observation['pressure_in'] self.wind_speed = current_observation['wind_mph'] logging.debug('new values fetched: %s, %s, %s', self.temperature, self.barometric_pressure, self.wind_speed) temperature = WoTThing.wot_property(name='temperature', initial_value=0.0, description='the temperature in ℉', value_source_fn=get_weather_data, units='℉') barometric_pressure = WoTThing.wot_property( name='barometric_pressure', initial_value=30.0, description='the air pressure in inches', units='in') wind_speed = WoTThing.wot_property(name='wind_speed', initial_value=30.0, description='the wind speed in mph', units='mph')
class EnphaseEnergyMonitor(WoTThing): required_config = Namespace() required_config.add_option( 'enphase_address', doc='local area network address ', default="10.0.0.101", ) required_config.add_aggregation('target_url', function=create_url) required_config.add_option( 'seconds_for_timeout', doc='the number of seconds to allow for fetching enphase data', default=10) _LIFETIME_GENERATION = 1 _CURRENTLY_GENERATING = 3 _MICROINVERTER_TOTAL = 7 _MICROINVERTERS_ONLINE = 9 def __init__(self, config): super(EnphaseEnergyMonitor, self).__init__(config, "Enphase Solar Panels", "thing", "Data for my solar panels") _multiplicative_factor = { 'Wh': 0.001, 'W': 0.001, 'kWh': 1.0, 'kW': 1.0, 'MWh': 1000.0, 'GWh': 1000000.0 } @staticmethod def _scale_based_on_units(raw_string): number_as_str, units = raw_string.split() return float( number_as_str) * EnphaseEnergyMonitor._multiplicative_factor[ units.strip()] async def get_enphase_data(self): async with aiohttp.ClientSession() as session: async with async_timeout.timeout(self.config.seconds_for_timeout): async with session.get(self.config.target_url) as response: enphase_home_page_raw = await response.text() enphase_page = BeautifulSoup(enphase_home_page_raw, 'html.parser') # this is stupidly fragile - we're assuming this page format never # changes from fetch to fetch - observation has shown this to be ok # but don't know if that will hold over Enphase software updates. td_elements = enphase_page.find_all('table')[2].find_all('td') self.lifetime_generation = self._scale_based_on_units( td_elements[self._LIFETIME_GENERATION].contents[0]) self.generating_now = self._scale_based_on_units( td_elements[self._CURRENTLY_GENERATING].contents[0]) self.microinverter_total = td_elements[ self._MICROINVERTER_TOTAL].contents[0] self.microinverters_online = td_elements[ self._MICROINVERTERS_ONLINE].contents[0] logging.debug('new values fetched: %s, %s, %s, %s', self.lifetime_generation, self.generating_now, self.microinverter_total, self.microinverters_online) lifetime_generation = WoTThing.wot_property( name='lifetime_generation', initial_value=0.0, description='Total lifetime generation in KWh', value_source_fn=get_enphase_data, units='KWh') generating_now = WoTThing.wot_property( name='generating_now', initial_value=0.0, description='currently generating in KWh', units='KW') microinverter_total = WoTThing.wot_property( name='microinverter_total', initial_value=0, description='the number of microinverters installed') microinverters_online = WoTThing.wot_property( name='microinverters_online', initial_value=0, description='the number of micro inverters online', )
class PelletStove(WoTThing): required_config = Namespace() required_config.add_option( name='startup_level', doc='the stove intensity level used on startup', default='high', # low, medium, high ) required_config.add_option( name='medium_linger_time_in_minutes', doc='the time in minutes of lingering on medium during shutdown', default=5.0, ) required_config.add_option( name='low_linger_time_in_minutes', doc='the time in minutes of lingering on low during shutdown', default=5.0, ) required_config.add_option( 'controller_implementation_class', doc='a fully qualified name of a class that can control the stove', default='stove_controller.StoveControllerImplementation', from_string_converter=class_converter) def __init__(self, config): super(PelletStove, self).__init__(config, "Pellet Stove Controller", "thing", "pellet stove automation") self.set_medium_linger(config.medium_linger_time_in_minutes) self.set_low_linger(config.low_linger_time_in_minutes) self._controller = config.controller_implementation_class() self.lingering_shutdown_task = None self.logging_count = 0 async def get_thermostat_state(self): previous_thermostat_state = self.thermostat_state self.thermostat_state = self._controller.get_thermostat_state() self.logging_count += 1 if self.logging_count % 300 == 0: logging.debug('still monitoring thermostat') self.logging_count = 0 if previous_thermostat_state != self.thermostat_state: if self.thermostat_state: logging.info('start heating') await self.set_stove_mode_to_heating() else: logging.info('start lingering shutdown') self.lingering_shutdown_task = asyncio.get_event_loop( ).create_task(self.set_stove_mode_to_lingering()) def set_medium_linger(self, value_in_minutes): self.medium_linger_time_in_seconds = value_in_minutes * 60 logging.debug('medium_linger_time set to %s seconds', self.medium_linger_time_in_seconds) def set_low_linger(self, value_in_minutes): self.low_linger_time_in_seconds = value_in_minutes * 60 logging.debug('low_linger_time set to %s seconds', self.low_linger_time_in_seconds) thermostat_state = WoTThing.wot_property( name='thermostat_state', description='the on/off state of the thermostat', initial_value=False, value_source_fn=get_thermostat_state, ) stove_state = WoTThing.wot_property( name='stove_state', description='the stove intensity level', initial_value='off', # off, low, medium, high ) stove_automation_mode = WoTThing.wot_property( name='stove_automation_mode', description='the current operating mode of the stove', initial_value= 'off', # off, heating, lingering_in_medium, lingering_in_low, overridden ) medium_linger_minutes = WoTThing.wot_property( name='medium_linger_minutes', description= 'how long should the medium level last during lingering shutdown', initial_value=5.0, value_forwarder=set_medium_linger) low_linger_minutes = WoTThing.wot_property( name='low_linger_minutes', description= 'how long should the low level last during lingering shutdown', initial_value=5.0, value_forwarder=set_low_linger) async def set_stove_mode_to_heating(self): if self.lingering_shutdown_task: logging.debug( 'canceling lingering shutdown to turn stove back to high') self.lingering_shutdown_task.cancel() with contextlib.suppress(asyncio.CancelledError): await self.lingering_shutdown_task self.lingering_shutdown_task = None self._controller.set_on_high() self.stove_state = 'high' self.stove_automation_mode = 'heating' async def set_stove_mode_to_lingering(self): self._controller.set_on_medium() self.stove_state = 'medium' self.stove_automation_mode = 'lingering_in_medium' logging.debug('stove set to medium for %s seconds', self.medium_linger_time_in_seconds) await asyncio.sleep(self.medium_linger_time_in_seconds) self._controller.set_on_low() self.stove_state = 'low' self.stove_automation_mode = 'lingering_in_low' logging.debug('stove set to low for %s seconds', self.low_linger_time_in_seconds) await asyncio.sleep(self.low_linger_time_in_seconds) self._controller.set_off() self.stove_state = 'off' self.stove_automation_mode = 'off' logging.info('stove turned off') self.lingering_shutdown_task = None def shutdown(self): if self.lingering_shutdown_task: logging.debug('lingering_shutdown_task is pending - canceling') self.lingering_shutdown_task.cancel() with contextlib.suppress(asyncio.CancelledError): asyncio.get_event_loop().run_until_complete( self.lingering_shutdown_task) self._controller.shutdown()