diff --git a/Configuration/AppConfigurationManager.py b/Configuration/AppConfigurationManager.py new file mode 100644 index 0000000..37bf950 --- /dev/null +++ b/Configuration/AppConfigurationManager.py @@ -0,0 +1,11 @@ +from Models.AppConfiguration import AppConfiguration +import json + +class AppConfigurationManager: + appConfiguration: AppConfiguration = None + + def __init__(self): + with open('./appconfig.json', 'rb') as appConfigurationFile: + + appConfiguration = json.load(appConfigurationFile) + self.appConfiguration = AppConfiguration(appConfiguration['deviceConfigurationLocalFilePath']) diff --git a/Configuration/DeviceConfigurationManager.py b/Configuration/DeviceConfigurationManager.py new file mode 100644 index 0000000..02ed56b --- /dev/null +++ b/Configuration/DeviceConfigurationManager.py @@ -0,0 +1,70 @@ +import os.path +from Models.DeviceConfiguration import * +from trashtech_api import * +from Configuration.AppConfigurationManager import AppConfigurationManager + +class DeviceConfigurationManager: + _configFilePath: str = '' + _deviceConfiguration: DeviceConfiguration = None + _trashtechApi: TrashtechApi = None + + def __init__(self, trashtechApi: TrashtechApi, appConfigurationManager: AppConfigurationManager): + + if trashtechApi is None: + raise ValueError('ConfigurationManager: Class initialization with not initialized trashtechApi') + + if appConfigurationManager is None: + raise ValueError('ConfigurationManager: Class initialization with not initialized appConfigurationManager') + + self._configFilePath = appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath + self._trashtechApi = trashtechApi + + def LoadFromFile(self) -> DeviceConfiguration: + try: + if os.path.isfile(self._configFilePath): + with open(self._configFilePath, "rb") as configFile: + data = json.load(configFile) + + self._deviceConfiguration = DeviceConfiguration(data['photo_interval'], + data['photo_rotation'], + data['photo_width'], + data['photo_height'], + data['custom_resolution']) + return self._deviceConfiguration + else: + return None + except IOError as e: + return None + + def TryGetDeviceConfiguration(self) -> DeviceConfiguration: + + if self._deviceConfiguration is not None: + return self._deviceConfiguration + else: + localConfig = self.LoadFromFile() + + if localConfig is not None: + self._deviceConfiguration = localConfig + return self._deviceConfiguration + else: + self._deviceConfiguration = self._trashtechApi.GetConfiguration() + + if self._deviceConfiguration is None: + return None + + self.SaveConfiguration() + return self._deviceConfiguration + + def SaveConfiguration(self): + + if self._deviceConfiguration is None: + return False + + try: + + with open(self._configFilePath, 'w') as outfile: + json.dump(self._deviceConfiguration, outfile, default=lambda o: o.__dict__) + return True + + except Exception as e: + return False diff --git a/Models/AppConfiguration.py b/Models/AppConfiguration.py new file mode 100644 index 0000000..92825ee --- /dev/null +++ b/Models/AppConfiguration.py @@ -0,0 +1,8 @@ + + +class AppConfiguration: + + deviceConfigurationLocalFilePath: str = '' + + def __init__(self, deviceConfigurationLocalFilePath: str): + self.deviceConfigurationLocalFilePath = deviceConfigurationLocalFilePath \ No newline at end of file diff --git a/Models/DeviceConfiguration.py b/Models/DeviceConfiguration.py new file mode 100644 index 0000000..137ff8d --- /dev/null +++ b/Models/DeviceConfiguration.py @@ -0,0 +1,22 @@ + + +class DeviceConfiguration: + + photo_interval: int = -1 + photo_rotation: int = -1 + photo_width: int = -1 + photo_height: int = -1 + custom_resolution: bool = False + + def __init__(self, + photo_interval, + photo_rotation, + photo_width, + photo_height, + custom_resolution): + + self.photo_interval = photo_interval + self.photo_rotation = photo_rotation + self.photo_width = photo_width + self.photo_height = photo_height + self.custom_resolution = custom_resolution \ No newline at end of file diff --git a/Tests/Resources/DeviceConfigurationSnapshot.json b/Tests/Resources/DeviceConfigurationSnapshot.json new file mode 100644 index 0000000..3f83d9a --- /dev/null +++ b/Tests/Resources/DeviceConfigurationSnapshot.json @@ -0,0 +1 @@ +{"photo_interval": 3600, "photo_rotation": 0, "photo_width": 640, "photo_height": 480, "custom_resolution": true} \ No newline at end of file diff --git a/Tests/manual b/Tests/manual new file mode 100644 index 0000000..89f9a41 --- /dev/null +++ b/Tests/manual @@ -0,0 +1,5 @@ +Crate file with unit tests cases: +-file name should follow this convention test_*.py + + +python -m pytest tests/ - run this command to run all tests from tests directory \ No newline at end of file diff --git a/Tests/test_DeviceConfigurationManager.py b/Tests/test_DeviceConfigurationManager.py new file mode 100644 index 0000000..729bbca --- /dev/null +++ b/Tests/test_DeviceConfigurationManager.py @@ -0,0 +1,136 @@ +import pytest +from Configuration.DeviceConfigurationManager import * +from Configuration.AppConfigurationManager import * +from trashtech_api import * + + +class TestConfigurationManager: + + def test_LoadFromFile_NoneTrashtechApiParameter_Invalid(self): + with pytest.raises(Exception): + # Arange + trashtechApi = None + + # Act + Assert + assert DeviceConfigurationManager(trashtechApi, None) + + def test_LoadFromFile_NoneAppConfigurationManagerParameter_Invalid(self): + with pytest.raises(Exception): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = None + + # Act + Assert + assert DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + def test_LoadFromFile_FileDoesNotExsist_Valid(self): + + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = 'blabla.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + # Act + deviceConfiguration = deviceConfigurationManager.LoadFromFile() + + # assert + assert deviceConfiguration is None + + def test_LoadFromFile_FileExsist_Valid(self): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + # Act + deviceConfiguration = deviceConfigurationManager.LoadFromFile() + + # assert + assert deviceConfiguration is not None + assert isinstance(deviceConfiguration, DeviceConfiguration) + + def test_SaveConfiguration_DeviceConfigLoaded_Valid(self): + + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + deviceConfiguration = deviceConfigurationManager.LoadFromFile() + + # Act + saveResult = deviceConfigurationManager.SaveConfiguration() + + # assert + assert saveResult is True + + def test_SaveConfiguration_NoneDeviceConfig_Invalid(self): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + # Act + saveResult = deviceConfigurationManager.SaveConfiguration() + + # assert + assert saveResult is False + + def test_TryGetDeviceConfiguration_DeviceConfigLoadedAlready_Valid(self): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + deviceConfiguration = deviceConfigurationManager.LoadFromFile() + + # Act + deviceConfiguration = deviceConfigurationManager.TryGetDeviceConfiguration() + + # assert + assert deviceConfiguration is not None + + def test_TryGetDeviceConfiguration_DeviceConfigLoadedFromDisk_Valid(self): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + # Act + deviceConfiguration = deviceConfigurationManager.TryGetDeviceConfiguration() + + # assert + assert deviceConfiguration is not None + + def test_TryGetDeviceConfiguration_DeviceConfigLoadedFromNetwork_Valid(self): + # Arange + trashtechApi = TrashtechApi() + appConfigurationManager = AppConfigurationManager() + + appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath = './Tests/Resources/DeviceConfigurationSnapshot1.json' + + deviceConfigurationManager = DeviceConfigurationManager(trashtechApi, appConfigurationManager) + + # Act + deviceConfiguration = deviceConfigurationManager.TryGetDeviceConfiguration() + + # assert + assert deviceConfiguration is not None + + os.remove(appConfigurationManager.appConfiguration.deviceConfigurationLocalFilePath) diff --git a/Tests/test_TrashtechApi.py b/Tests/test_TrashtechApi.py new file mode 100644 index 0000000..7f38238 --- /dev/null +++ b/Tests/test_TrashtechApi.py @@ -0,0 +1,15 @@ +import pytest +from trashtech_api import * + + +class TestTrashTechApi: + + def test_GetConfiguration(self): + # Arrange + trashtechApi = TrashtechApi() + + # Act + configuration = trashtechApi.GetConfiguration() + + # Assert + assert configuration is not None diff --git a/Tests/test_foo.py b/Tests/test_foo.py new file mode 100644 index 0000000..2db00a2 --- /dev/null +++ b/Tests/test_foo.py @@ -0,0 +1,7 @@ +import pytest + +def func(x): + return x + 1 + +def test_answer(): + assert func(3) == 4 \ No newline at end of file diff --git a/appconfig.json b/appconfig.json new file mode 100644 index 0000000..11cfd13 --- /dev/null +++ b/appconfig.json @@ -0,0 +1,3 @@ +{ + "deviceConfigurationLocalFilePath": "./DeviceConfigurationSnapshot.json" +} \ No newline at end of file diff --git a/main.py b/main.py index dc410ca..1ea1b2b 100644 --- a/main.py +++ b/main.py @@ -5,80 +5,92 @@ import logging from snap import Snapper -from trashtech_api import TrashtechApi from gsm_controller import GsmController +from Configuration.DeviceConfigurationManager import * +from Configuration.AppConfigurationManager import * DEVICE_REFERENCE = '000006' FILE_FORMAT = "%s/TT_%s.jpg" TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S' + class TrashtechApp: - def initialize(self): - self.trashtech_client = TrashtechApi() - self.snapper = Snapper() - self.gsm_controller = GsmController() + _trashtech_client: TrashtechApi = None + _deviceConfigurationManager: DeviceConfigurationManager = None + _snapper: Snapper = None + _gsm_controller: GsmController = None + _configuration: DeviceConfiguration = None + + def initialize(self): + self._trashtech_client = TrashtechApi() + appManager = AppConfigurationManager() + + self._deviceConfigurationManager = DeviceConfigurationManager(self._trashtech_client, appManager) + self._snapper = Snapper() + self._gsm_controller = GsmController() + + def retrive_configuration(self): + self._configuration = self._deviceConfigurationManager.TryGetDeviceConfiguration() + self._snapper.set_resolution(self._configuration.photo_width, self._configuration.photo_height) - def retrive_configuration(self): - self.configuration = self.trashtech_client.configuration() - self.snapper.set_resolution(self.configuration['photo_width'], self.configuration['photo_height']) + def interval(self): + return self._configuration.photo_interval - def interval(self): - return self.configuration['photo_interval'] + def call_snap(self, filename): + self._snapper.snap(filename) - def call_snap(self, filename): - self.snapper.snap(filename) + def init_gsm(self): + if self._gsm_controller.is_ppp_interface_present() is not True: + self._gsm_controller.enable() - def init_gsm(self): - if self.gsm_controller.is_ppp_interface_present() is not True: - self.gsm_controller.enable() + def wait_for_gsm(self): + while self._gsm_controller.is_ppp_interface_present() is not True: + logging.info("[INFO] Waiting for GSM..") + time.sleep(2) - def wait_for_gsm(self): - while self.gsm_controller.is_ppp_interface_present() is not True: - logging.info("[INFO] Waiting for GSM..") - time.sleep(2) + if self._gsm_controller.is_ppp_interface_present(): + logging.info("[INFO] GSM module enabled") - if self.gsm_controller.is_ppp_interface_present(): - logging.info("[INFO] GSM module enabled") + time.sleep(5) - time.sleep(5) + def run(self): + start_time = time.time() - def run(self): - start_time = time.time() + self.init_gsm() - self.init_gsm() + image_created_at_timestamp = time.time() + complete_file_path = FILE_FORMAT % ('images', image_created_at_timestamp) - image_created_at_timestamp = time.time() - complete_file_path = FILE_FORMAT % ('images', image_created_at_timestamp) + self.call_snap(complete_file_path) + self.wait_for_gsm() - self.call_snap(complete_file_path) - self.wait_for_gsm() + image_created_at = datetime.datetime.fromtimestamp(image_created_at_timestamp).strftime(TIMESTAMP_FORMAT) + self._trashtech_client.create_status(DEVICE_REFERENCE, complete_file_path, image_created_at) - image_created_at = datetime.datetime.fromtimestamp(image_created_at_timestamp).strftime(TIMESTAMP_FORMAT) - self.trashtech_client.create_status(DEVICE_REFERENCE, complete_file_path, image_created_at) + self._gsm_controller.disable() + logging.info("[INFO] GSM module disabled") - self.gsm_controller.disable() - logging.info("[INFO] GSM module disabled") + execution_time = time.time() - start_time + logging.info("[INFO] Execution time: %s" % execution_time) - execution_time = time.time() - start_time - logging.info("[INFO] Execution time: %s" % execution_time) + wait_iterval_time = int(float(self.interval()) - execution_time) + logging.info("[INFO] Wait interval time: %s" % wait_iterval_time) - wait_iterval_time = int(float(self.interval()) - execution_time) - logging.info("[INFO] Wait interval time: %s" % wait_iterval_time) + thread = threading.Timer(wait_iterval_time, trashtech_app.run) + thread.start() - thread = threading.Timer(wait_iterval_time, trashtech_app.run) - thread.start() if __name__ == '__main__': - logging.basicConfig( - format='[INFO] %(asctime)s - %(message)s', - level=logging.INFO - ) - - logging.info("Hi. We are TRASHTECH. Let's play.") - - trashtech_app = TrashtechApp() - trashtech_app.initialize() - trashtech_app.init_gsm() - trashtech_app.wait_for_gsm() - trashtech_app.retrive_configuration() - trashtech_app.run() + logging.basicConfig( + format='[INFO] %(asctime)s - %(message)s', + level=logging.INFO + ) + + logging.info("Hi. We are TRASHTECH. Let's play.") + + trashtech_app = TrashtechApp() + trashtech_app.initialize() + trashtech_app.init_gsm() + trashtech_app.wait_for_gsm() + trashtech_app.retrive_configuration() + trashtech_app.run() diff --git a/trashtech_api.py b/trashtech_api.py index 70598b4..c1a1652 100644 --- a/trashtech_api.py +++ b/trashtech_api.py @@ -8,7 +8,7 @@ class TrashtechApi: api_base = 'http://trashtech.herokuapp.com/api' - def configuration(self): + def GetConfiguration(self): url = "%s/device_configurations/1" % self.api_base logging.info('[INFO] request call: %s' % url)