def _apply_fake_settings(self): PVOutputAPISettings.get_solo().update( auth_token='XXXXX', system_identifier=12345, ) PVOutputAddStatusSettings.get_solo().update(export=True, upload_delay=1)
def test_export_okay(self, send_robust_mock, now_mock, export_data_mock, should_export_mock, requests_post_mock): """ Test export() as designed. """ now_mock.return_value = timezone.make_aware( timezone.datetime(2017, 10, 1, hour=15)) export_data_mock.return_value = { 'x': 'y' } # Unimportant for this test. should_export_mock.return_value = True requests_post_mock.return_value = mock.MagicMock(status_code=200, text='Fake accept') self._apply_fake_settings() self.assertFalse(requests_post_mock.called) self.assertFalse(send_robust_mock.called) dsmr_pvoutput.services.export() self.assertIsNotNone(PVOutputAddStatusSettings.get_solo().next_export) self.assertTrue(requests_post_mock.called) self.assertTrue(send_robust_mock.called) # Check API parameters. api_settings = PVOutputAPISettings.get_solo() requests_post_mock.assert_called_once_with( PVOutputAddStatusSettings.API_URL, headers={ 'X-Pvoutput-Apikey': api_settings.auth_token, 'X-Pvoutput-SystemId': api_settings.system_identifier, }, data={'x': 'y'}, ) # With processing delay as well. requests_post_mock.reset_mock() send_robust_mock.reset_mock() PVOutputAddStatusSettings.objects.update(processing_delay=5, next_export=None) dsmr_pvoutput.services.export() status_settings = PVOutputAddStatusSettings.get_solo() self.assertIsNotNone(status_settings.next_export) self.assertEqual(status_settings.latest_sync, timezone.now()) self.assertTrue(requests_post_mock.called) self.assertTrue(send_robust_mock.called) api_settings = PVOutputAPISettings.get_solo() requests_post_mock.assert_called_once_with( PVOutputAddStatusSettings.API_URL, headers={ 'X-Pvoutput-Apikey': api_settings.auth_token, 'X-Pvoutput-SystemId': api_settings.system_identifier, }, data={ 'x': 'y', 'delay': 5, }, )
def test_get_next_export(self, now_mock): PVOutputAddStatusSettings.get_solo() PVOutputAddStatusSettings.objects.update( upload_interval=PVOutputAddStatusSettings.INTERVAL_5_MINUTES) now_mock.return_value = timezone.make_aware( timezone.datetime(2018, 2, 1, hour=12, minute=13, second=15)) result = dsmr_pvoutput.services.get_next_export() self.assertEqual(result.hour, 12) self.assertEqual(result.minute, 15) self.assertEqual(result.second, 0) PVOutputAddStatusSettings.objects.update( upload_interval=PVOutputAddStatusSettings.INTERVAL_15_MINUTES) now_mock.return_value = timezone.make_aware( timezone.datetime(2018, 2, 1, hour=12, minute=25, second=50)) result = dsmr_pvoutput.services.get_next_export() self.assertEqual(result.hour, 12) self.assertEqual(result.minute, 30) self.assertEqual(result.second, 0) # Pass hour mark. now_mock.return_value = timezone.make_aware( timezone.datetime(2018, 2, 1, hour=12, minute=59, second=30)) result = dsmr_pvoutput.services.get_next_export() self.assertEqual(result.hour, 13) self.assertEqual(result.minute, 0) self.assertEqual(result.second, 0)
def test_export_not_allowed(self, post_mock): """ Test export() blocking behaviour. """ self._apply_fake_settings() PVOutputAddStatusSettings.get_solo().update(export=False) dsmr_pvoutput.services.run(scheduled_process=self.schedule_process) self.assertFalse(post_mock.called) self._apply_fake_settings() PVOutputAPISettings.get_solo().update(auth_token='') dsmr_pvoutput.services.run(scheduled_process=self.schedule_process) self.assertFalse(post_mock.called) self._apply_fake_settings() PVOutputAPISettings.get_solo().update(system_identifier='') dsmr_pvoutput.services.run(scheduled_process=self.schedule_process) self.assertFalse(post_mock.called)
def status_info(): """ Returns the status info of the application. """ capabilities = get_capabilities() status = { 'capabilities': capabilities, 'electricity': get_electricity_status(capabilities), 'gas': get_gas_status(capabilities), 'readings': get_reading_status(), 'statistics': get_statistics_status(), 'tools': { 'backup': { 'enabled': False, 'latest_backup': None, }, 'dropbox': { 'enabled': False, 'latest_sync': None, }, 'pvoutput': { 'enabled': False, 'latest_sync': None, }, 'mindergas': { 'enabled': False, 'latest_sync': None, }, 'mqtt': get_mqtt_status(), } } # (External) tools below. backup_settings = BackupSettings.get_solo() if backup_settings.daily_backup: status['tools']['backup']['enabled'] = True status['tools']['backup'][ 'latest_backup'] = backup_settings.latest_backup dropbox_settings = DropboxSettings.get_solo() if dropbox_settings.access_token: status['tools']['dropbox']['enabled'] = True status['tools']['dropbox'][ 'latest_sync'] = dropbox_settings.latest_sync pvoutput_settings = PVOutputAddStatusSettings.get_solo() if pvoutput_settings.export: status['tools']['pvoutput']['enabled'] = True status['tools']['pvoutput'][ 'latest_sync'] = pvoutput_settings.latest_sync mindergas_settings = MinderGasSettings.get_solo() if mindergas_settings.export: status['tools']['mindergas']['enabled'] = True status['tools']['mindergas'][ 'latest_sync'] = mindergas_settings.latest_sync return status
def schedule_next_export(): """ Schedules the next export, according to user preference. """ next_export = get_next_export() logger.debug(' - PVOutput | Delaying the next export until: %s', timezone.localtime(next_export)) status_settings = PVOutputAddStatusSettings.get_solo() status_settings.next_export = next_export status_settings.save()
def export(): """ Exports data to PVOutput, calling Add Status. """ if not should_export(): return api_settings = PVOutputAPISettings.get_solo() status_settings = PVOutputAddStatusSettings.get_solo() # Find the first and last consumption of today, taking any delay into account. local_now = timezone.localtime(timezone.now()) start = local_now.replace(hour=0, minute=0, second=0) # Midnight end = local_now - timezone.timedelta(minutes=status_settings.upload_delay) ecs = ElectricityConsumption.objects.filter(read_at__gte=start, read_at__lte=end) if not ecs.exists(): print(' [!] PVOutput: No data found for {}'.format(local_now)) return schedule_next_export() first = ecs[0] last = ecs.order_by('-read_at')[0] diff = last - first # Custom operator total_consumption = diff['delivered_1'] + diff['delivered_2'] net_power = last.currently_delivered - last.currently_returned # Negative when returning more Watt than requested. consumption_timestamp = timezone.localtime(last.read_at) data = { 'd': consumption_timestamp.date().strftime('%Y%m%d'), 't': consumption_timestamp.time().strftime('%H:%M'), 'v3': int(total_consumption * 1000), # Energy Consumption (Wh) 'v4': int(net_power * 1000), # Power Consumption (W) 'n': 1, # Net Flag, always enabled for smart meters } # Optional, paid PVOutput feature. if status_settings.processing_delay: data.update({'delay': status_settings.processing_delay}) print(' - PVOutput | Uploading data @ {}'.format(data)) pvoutput_upload.send_robust(None, data=data) response = requests.post(PVOutputAddStatusSettings.API_URL, headers={ 'X-Pvoutput-Apikey': api_settings.auth_token, 'X-Pvoutput-SystemId': api_settings.system_identifier, }, data=data) if response.status_code != 200: print(' [!] PVOutput upload failed (HTTP {}): {}'.format( response.status_code, response.text)) schedule_next_export()
def schedule_next_export(): """ Schedules the next export, according to user preference. """ next_export = get_next_export() print( ' - PVOutput | Delaying the next export until: {}'.format(next_export)) status_settings = PVOutputAddStatusSettings.get_solo() status_settings.next_export = next_export status_settings.save()
def _apply_fake_settings(self): api_settings = PVOutputAPISettings.get_solo() api_settings.auth_token = 'XXXXX' api_settings.system_identifier = 12345 api_settings.save() status_settings = PVOutputAddStatusSettings.get_solo() status_settings.export = True status_settings.upload_delay = 1 status_settings.save()
def should_export(): """ Checks whether we should export data yet, for Add Status calls. """ api_settings = PVOutputAPISettings.get_solo() status_settings = PVOutputAddStatusSettings.get_solo() # Only when enabled and credentials set. if not status_settings.export or not api_settings.auth_token or not api_settings.system_identifier: return False return dsmr_backend.services.is_timestamp_passed(timestamp=status_settings.next_export)
def schedule_next_export(): """ Schedules the next export, according to user preference. """ status_settings = PVOutputAddStatusSettings.get_solo() next_export = timezone.now() + timezone.timedelta( minutes=status_settings.upload_interval) print( ' - PVOutput | Delaying the next export until: {}'.format(next_export)) status_settings.next_export = next_export status_settings.save()
def get_next_export(): """ Rounds the timestamp to the nearest upload interval, preventing the uploads to shift forward. """ status_settings = PVOutputAddStatusSettings.get_solo() next_export = timezone.now() + timezone.timedelta(minutes=status_settings.upload_interval) # Make sure it shifts back to the closest interval point possible. minute_marker = next_export.minute minute_marker = minute_marker - (minute_marker % status_settings.upload_interval) return next_export.replace(minute=minute_marker, second=0, microsecond=0)
def _on_pvoutput_setting_update(): api_settings = PVOutputAPISettings.get_solo() add_status_settings = PVOutputAddStatusSettings.get_solo() ScheduledProcess.objects.filter( module=settings.DSMRREADER_MODULE_PVOUTPUT_EXPORT).update( planned=timezone.now(), active=all([ api_settings.auth_token, api_settings.system_identifier, add_status_settings.export ]))
def test_export_fail(self, now_mock, should_export_mock, requests_post_mock): """ Test export() failing by denied API call. """ now_mock.return_value = timezone.make_aware(timezone.datetime(2017, 10, 1, hour=15)) should_export_mock.return_value = True self._apply_fake_settings() requests_post_mock.return_value = mock.MagicMock(status_code=400, text='Error message') dsmr_pvoutput.services.export() settings = PVOutputAddStatusSettings.get_solo() self.assertEqual(settings.next_export, timezone.now() + timezone.timedelta(minutes=5)) self.assertTrue(requests_post_mock.called)
def test_export_not_allowed(self, now_mock, should_export_mock): """ Test export() blocking behaviour. """ now_mock.return_value = timezone.make_aware(timezone.datetime(2017, 10, 1, hour=15)) should_export_mock.return_value = False self._apply_fake_settings() # Nothing should happen. dsmr_pvoutput.services.export() self.assertTrue(should_export_mock.called) self.assertIsNone(PVOutputAddStatusSettings.get_solo().next_export)
def test_should_export_default(self, now_mock): """ Test should_export() default behaviour. """ now_mock.return_value = timezone.make_aware(timezone.datetime(2017, 10, 1, hour=15)) api_settings = PVOutputAPISettings.get_solo() status_settings = PVOutputAddStatusSettings.get_solo() self.assertIsNone(api_settings.auth_token) self.assertFalse(api_settings.system_identifier) self.assertFalse(status_settings.export) self.assertIsNone(status_settings.next_export) self.assertFalse(dsmr_pvoutput.services.should_export())
def check_pvoutput_sync(**kwargs): from dsmr_pvoutput.models.settings import PVOutputAddStatusSettings pvoutput_settings = PVOutputAddStatusSettings.get_solo() if not pvoutput_settings.export: return offset = timezone.now() - timezone.timedelta( minutes=settings. DSMRREADER_STATUS_ALLOWED_SCHEDULED_PROCESS_LAGG_IN_MINUTES) if pvoutput_settings.next_export > offset: return return MonitoringStatusIssue( __name__, _('Waiting for the next PVOutput export to be executed'), pvoutput_settings.next_export)
def export(): """ Exports data to PVOutput, calling Add Status. """ if not should_export(): return api_settings = PVOutputAPISettings.get_solo() status_settings = PVOutputAddStatusSettings.get_solo() try: data = get_export_data(next_export=status_settings.next_export, upload_delay=status_settings.upload_delay) except LookupError: return if not data: logger.warning(' [!] PVOutput: No data found (yet)') return schedule_next_export() # Optional, paid PVOutput feature. if status_settings.processing_delay: data.update({'delay': status_settings.processing_delay}) logger.debug('PVOutput: Uploading data: %s', data) pvoutput_upload.send_robust(None, data=data) response = requests.post(PVOutputAddStatusSettings.API_URL, headers={ 'User-Agent': settings.DSMRREADER_USER_AGENT, 'X-Pvoutput-Apikey': api_settings.auth_token, 'X-Pvoutput-SystemId': api_settings.system_identifier, }, data=data) if response.status_code != 200: logger.error(' [!] PVOutput upload failed (HTTP %s): %s', response.status_code, response.text) else: status_settings.latest_sync = timezone.now() status_settings.save() schedule_next_export()
def should_export(): """ Checks whether we should export data yet, for Add Status calls. """ api_settings = PVOutputAPISettings.get_solo() status_settings = PVOutputAddStatusSettings.get_solo() # Only when enabled and credentials set. if not status_settings.export or not api_settings.auth_token or not api_settings.system_identifier: return False # Nonsense when having no data. capabilities = dsmr_backend.services.get_capabilities() if not capabilities['electricity_returned']: print( ' - [!] PVOutput | No electricity return recorded by application!') return False return dsmr_backend.services.is_timestamp_passed( timestamp=status_settings.next_export)
def get_context_data(self, **kwargs): context_data = super(Configuration, self).get_context_data(**kwargs) # 20+ queries, we should cache this at some point. context_data.update( dict( api_settings=APISettings.get_solo(), backend_settings=BackendSettings.get_solo(), backup_settings=BackupSettings.get_solo(), consumption_settings=ConsumptionSettings.get_solo(), datalogger_settings=DataloggerSettings.get_solo(), dropbox_settings=DropboxSettings.get_solo(), email_settings=EmailSettings.get_solo(), frontend_settings=FrontendSettings.get_solo(), mindergas_settings=MinderGasSettings.get_solo(), mqtt_broker_settings=MQTTBrokerSettings.get_solo(), mqtt_jsondaytotals_settings=JSONDayTotalsMQTTSettings.get_solo( ), mqtt_splittopicdaytotals_settings= SplitTopicDayTotalsMQTTSettings.get_solo(), mqtt_jsoncurrentperiodtotals_settings= JSONCurrentPeriodTotalsMQTTSettings.get_solo(), mqtt_splittopiccurrentperiodtotals_settings= SplitTopicCurrentPeriodTotalsMQTTSettings.get_solo(), mqtt_jsongasconsumption_settings=JSONGasConsumptionMQTTSettings .get_solo(), mqtt_splittopicgasconsumption_settings= SplitTopicGasConsumptionMQTTSettings.get_solo(), mqtt_splittopicmeterstatistics_settings= SplitTopicMeterStatisticsMQTTSettings.get_solo(), mqtt_jsontelegram_settings=JSONTelegramMQTTSettings.get_solo(), mqtt_rawtelegram_settings=RawTelegramMQTTSettings.get_solo(), mqtt_splittopictelegram_settings=SplitTopicTelegramMQTTSettings .get_solo(), notification_settings=NotificationSetting.get_solo(), pvoutput_api_settings=PVOutputAPISettings.get_solo(), pvoutput_addstatus_settings=PVOutputAddStatusSettings.get_solo( ), retention_settings=RetentionSettings.get_solo(), weather_settings=WeatherSettings.get_solo(), influxdb_settings=InfluxdbIntegrationSettings.get_solo(), )) return context_data
def test_check_pvoutput_sync_disabled(self): PVOutputAddStatusSettings.get_solo().update( export=False, next_export=timezone.now() - timezone.timedelta(minutes=1)) self.assertIsNone(check_pvoutput_sync())
def setUp(self): self.instance = PVOutputAddStatusSettings().get_solo()
def setUp(self): PVOutputAddStatusSettings.get_solo().update(export=True, next_export=timezone.now())