Metadata-Version: 2.4
Name: clmprotoclient
Version: 1.0.3
Summary: Client to communicate with the CANlink mobile 10000 protobuf API.
License-Expression: Apache-2.0
License-File: LICENSE.txt
Keywords: CANlink,CLM10k,protobuf,websocket,CAN
Author: Proemion GmbH
Author-email: info@proemion.com
Requires-Python: >=3.12
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
Requires-Dist: clmprotowrapper (>=3.3.0)
Requires-Dist: prompt-toolkit (>=3.0.24)
Requires-Dist: pyparsing (>=2.4.7)
Requires-Dist: python-dateutil (>=2.8.1)
Requires-Dist: requests (>=2.28.1)
Requires-Dist: tabulate (>=0.8.9)
Requires-Dist: websocket-client (>=1.2.3)
Project-URL: Issues, https://github.com/Proemion/CANlink-Mobile-API-Clients/issues
Project-URL: Repository, https://github.com/Proemion/CANlink-Mobile-API-Clients
Description-Content-Type: text/markdown

# clmprotoclient - Protomolecule Python Module

The module simplifies interaction with the CANlink mobile via the protocol buffers
based websocket API. It provides convenience methods which will create the
actual proto messages and it also wraps around websockets in order to take
care of sending and receiving the messages. It has a dependency to the 
[clmprotowrapper](https://pypi.org/project/clmprotowrapper/) which contains 
the Python bindings generated by proto compiler which can be used directly if one would
want to implement the whole handling without using the provided module.

The package also contains a reference application `clmshell` which uses the
module and which allows to configure the device from a command line prompt.

--------
# Installation

The latest built version can be installed from PyPI.
Install from PyPI using `pip`:

```
pip install clmprotoclient
```

# Building
We use [poetry](https://python-poetry.org) as build system. 


# Contributing
We encourage you to contribute. In case you can not find a pull request reviewer please do not hesitate to reach 
out to Proemion support.
------------------

# Usage

## clmprotoclient.clmlib

The clmlib submodule provides the `clm10k` class which is basically a
convenience wrapper around proto generation and websocket communication.

The `clm10k` class represents a connection instance to the CLM10k protobuf API.

The `clm10k` class provides easy to use methods to generate requests to the
device via the websocket, using the protocol buffers based API.

The responses from the unit are forwarded as is in proto messages, these
messages already present the data in a well structured way, so it does not make
much sense to repack it.

This client requires the  `clmprotowrapper` as wrapper around the protocol buffers API 
of the device which will be automatically installed due to defined dependencies.

### Instantiation

```
    clm10k(ip, password, cb_connected = None, cb_firmware_update = None,
           cb_config_update = None, cb_disconnected = None)
```


Parameters:

* **`ip`** : _`string`_

  IP address of the CLM10k unit.

* **`password`** : _`string`_

  Admin password of the CLM10k unit.

* **`cb_connected`** : _`callback(device_info_data)`_

  This callback function which will be called once a connection to  the device
  is established. Device information will be passed to this function as the
  only parameter.

* **`cb_firmware_update`** : _`callback(firmware_update_info_data)`_

  This callback function will be called when a firmware update has been
  performed or attempted on the device. The update notification is sent out to
  all connected clients, regardless of who started the update.

  Locally the update can be started by HTTPS `POST`'ing the firmware image to
  the https://..device-ip../firmware URL.

  Example:
```
   curl -u admin:password -k -v -F 'file=@./firmware.swu' \
                                   https://192.168.1.43/firmware
```

* **`cb_config_update`** : _`callback(config_update_info_data)`_

  This callback function will be called when a configuration update has been
  performed or attempted on the device. The update notification is sent out to
  all connected clients, regardless of who started the update.

  Locally the update can be started by HTTPS `POST`'ing the configuration file
  to the https://..device-ip../config URL.

  Example:
```
  curl -u admin:password -k -v -F 'file=@./myconf.clm' \
                                   https://192.168.1.43/config`
```

* **`cb_disconnected`** : _`callback()`_

  This callback function will be called when a connection to the device has
  been closed. The callback is not bound to any specific logic, i.e. the
  callback will be triggered regardless if the connection has been closed due
  to user action like a call to terminate() or due to an error (for instance
  when an incorrect password has been supplied). It is up to the application to
  build up a logic in order to interpret the reason of the callback.

  Important: once disconnected a new instance needs to be instantiated in order
  to open a new connection.

### Termination

```
    terminate()
```

Terminate the websocket thread and close the websocket. The class can not be
used afterwards. If you would like to connect again, a new instance needs to be
created.


### Retrieving Device Configuration

```
    get_config(self, options = None, groups = None, get_defaults = False,
               with_description = True, cb_response = None)
```

Retrieve one or more configuration settings.

This function allows to retrieve configuration values of one or more settings.
Either pass the 
option/group elements which should be read, or do not pass any at all - in this
case all available settings will be returned.

If the get_defaults parameter is set to true, then factory defaults of the 
given options will be retrieved instead of the currently set values.

The logic for requesting groups works as follows:

* no groups + no options

    return all options

* groups + no options

    return options found in requested groups

* no groups + options

    return requested options

* groups + options

    return options found in requested groups + return individually specified
    options

Note: requesting a group that is a parent to other subgroups will return all
child options, including the ones found in subgroups.

Parameters:

* **`options`** : _`[ (option_type, group_name), (option_type, ), ... ]`_
  Each list element must be a tuple where the first member of the tuple is the
  option type, while the second member is optional and is the associated group
  name which is required for options which belong to dynamic groups.

* **`groups`**: _`[ (group_type, group_name), (group_type, ), ... ]`_

  Each list element must be a tuple where the first member of the tuple is the
  group type, while the second member is optional and is the group name, which
  is required for dynamic groups.

* **`get_defaults`**: _`bool`_

  Set this parameter to True in order to retrieve configuration defaults
  instead of actually set values.

* **`with_description`**: _`bool`_

  Set this parameter to True in order to enable human readable option
  descriptions in the response.

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Applying Device Configuration

```
   apply_config(options, cb_response = None)
```

Apply one or more application settings.

Zero config elements are not allowed, i.e. if there is nothing to change, then
don't call this function, otherwise the application will answer with an error
response.

Options that belong to the same group will be automatically handled as
"transactions", i.e. all options from the group must succeed, if one fails,
then the whole group won't get applied.

Parameters:

* **`options`** : _`[ (option_type, value, group_name), (option_type, value,),.. ]`_

  Each list element must be a tuple where the first member of the tuple is the
  option type, while the second member is optional and belong to dynamic groups.

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Validate Configuration

```
   validate_config(self, options, cb_response = None)
```

Validate if configuration settings can be applied, without actually applying
them. This call is also helpful to user interfaces which need to figure out
what fields become disabled and thus also need to be disabled in the UI if a
certain configuration is applied.

Zero config elements are not allowed, i.e. if there is nothing to validate,
then don't call this function, otherwise the application will answer with an
error response.

Parameters:

* **`options`** : _`[ (option_type, value, group_name), (option_type, value,),.. ]`_

  Each list element must be a tuple where the first member of the tuple is the
  option type, while the second member is optional and is the associated group
  name which is required for options which belong to dynamic groups.

* **`cb_response``: _`callable(response)`_

  Callback which will provide the response to this request.

### Getting Configuration Group Information

```
    get_config_group_info(groups = [], with_description = True,
                          recursive = True, cb_response = None)
```

Request information about configuration groups and their parent-child
relationship.

Parameters:

* **`groups`** : _`[ group_type, group_type, ...]`_

  List of group types for which to request the information.

* **`with_description`** : _`bool`_

  Controls presence of human readable group descriptions in the response.

* **`recursive`** : _`bool`_

  Parameter which controls if the group information should be returned
  recursively or if only the top level groups are returned.

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Create a New Configuration Group

```
    create_config_group(group, name, cb_response = None)
```

This function creates a configuration group for the requested group type, this
is how network profiles can be created. A unique name must be provided, names
span across all dynamic groups regardless of the group type.

Only dynamic group types are allowed.

Parameters:

* **`group`** : _`int`_

 Type of the group to create.

* **`name`** : _`str`_

  Name of the new group.

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Remove a Configuration Group

```
    remove_config_group(name, cb_response = None)
```

This function removes a configuration group with the given name, this is how
network connection profiles can be removed. Note, that the last profile for
each interface can not be deleted, attempting to do so will result in an error.

Only dynamic groups can be removed, since group names are unique, it is not
necessary to supply the group type.

Parameters:
     
* **`name`**: _`str`_

  Name of the group which needs to be deleted.

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Export Configuration

```
      config_export(options = None, groups = None,
                    factory_reset = None, cb_response = None):
```

Export device configuration to a file that can later be reimported.

If all fields are left out, then all writable config options will be exported.

If the factory_reset parameter is true, then the exported file will contain
only the reset to factory defaults instruction. Options and groups parameters
must not be set.

Parameters:

* **`options`**: _`[ (option_type, group_name), (option_type, ), ...]`_

  Options to export, list of tuples containing the option type as the first
  tuple member and an optional group name as the second tuple member. The group
  name is required for options that belong to a dynamic group.
  
* **`groups`**: _`[ (group_type, group_name), (group_type, ), ... ]`_

  Groups to export, list of tuples containing the group type as the first
  member and an optional group name as the second tuple member. The group name
  is required for dynamic groups.

* **`factory_reset`**: _`bool`_

  If this parameter is set, the configuration export will only contain the
  reset instruction, options and groups parameters must not be set.

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Reset Device to Factory Settings

```
    reset_to_factory_settings(cb_response = None)
```

Reset device to factory settings.

The factory reset deletes all data on the device and restores the original
factory settings.
Note, that the device will be rebooted, meaning that the websocket connection
will be interrupted.

Parameters:

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Reset Configuration to Factory Defaults

```
    reset_configuration(cb_response = None)
```

Reset device configuration to factory defaults.

Note, that the configuration reset only restores the application settings
to factory defaults and does not affect credentials nor removes created users.
The application will be restarted, meaning that the websocket connection
will be interrupted.

Parameters:

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Abort Firmware Update

```
    firmware_update_abort(cb_response = None)
```

Abort an ongoing firmware update. This command can only cancel a pending or
ongoing download, the flashing process can not be interrupted.

Parameters:

* **`cb_response`**: _`callable(response)`_

  Callback which will provide the response to this request.

### Retrieve Device Information

```
    get_device_info()
```

This will return the same data as the cb_connected callback, providing
information about the current firmware and hardware version and alike.

### Set API/Web-UI Access Password

```
    set_password(username, admin_pass, new_pass, cb_response = None)
```

Change the password for the web server of the device, this affects access to
the web UI and to this protocol buffers API.

The current admin password is always required, the new password will only be
set if the current one is correct.

Currently only two users are supported: "admin" has access to everything while
"upload" can only POST files for further transfer to the DataPortal, but has no
access to the protocol buffers API.

Parameters:

* **`username`** : _`str`_

  Name of the user for which the password needs to be set. Currently either
  "admin" or "upload.

* **`admin_pass`** : _`str`_

  Current admin password which is required for the validation of this call.

* **`new_pass`** : _`str`_

  New password for the selected user.
    
* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Remove "Upload" User

```
    remove_upload_user(cb_response = None)
```

The "upload" user is a special account which has no access to the API/UI, but
one that is allowed to `POST` files to the device. The files will then be
automatically uploaded to the DataPortal.

This user has a separate password which can be set via the `set_password()`
call.

In order to disable the "upload" account, simply remove the user.

Parameters:

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Get List Of User Accounts

```
    get_users(self, cb_response = None)
```

Get a list of currently available user accounts.

Parameters:

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Set Date / Time

```
    set_datetime(self, timestamp = None, cb_response = None)
```

Set system date and time.

Parameters:

* **`timestamp`**: _`datetime.datetime()`_

  Datetime object containing a timestamp which should be applied on the device.

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

### Get Data / Time

```
    get_datetime(self, cb_response = None)
```

Get date and time from the device.

Parameters:

* **`cb_response`** : _`callable(response)`_

  Callback which will provide the response to this request.

## Examples

Here is a basic example of how this module could be used. Also have a
look at `clmshell` sources (part of this distribution). Let's assume that we
want to read out and print the communication unit ID, we omit most
of the error checking, however we still implement a minimum amount of
synchronization.

```
#!/usr/bin/env python3

from clmprotoclient import clmlib
from clmprotoclient import clmapi_pb2
import threading

evt = threading.Event() # we will use the event to wait for certain states
connected = False # global flag to indicate if we managed to connect

# response callback to the get_config call:
# look for the expected option in the response and print it
def cb_cu_id_response(data):
    if (isinstance(data, clmapi_pb2.get_config_response)):
        # we are expecting one element
        if (len(data.element) > 0):
            el = data.element[0]
            if (el.option == clmapi_pb2.CFG_OPTION_STR_DP_CU_ID):
                print(f"Current CU ID is \"{el.value.str}\".")

    elif (isinstance(data, clmapi_pb2.standard_response)):
        print(clmapi_pb2.standard_response) # will contain an error message

    evt.set()

# callback which will be triggered upon connection
def cb_connected(data):
    global connected
    connected = True
    evt.set()

evt.clear()

# make sure to edit the next line and use the IP and password of your device
inst = clmlib.clm10k("192.168.2.61", "mypassword", cb_connected = cb_connected)

if (evt.wait(timeout=5) != True) or (not connected):
    print("Timeout: failed to establish a connection!")
    inst.terminate()
    exit(1)

evt.clear()
inst.get_config(options = [ clmapi_pb2.CFG_OPTION_STR_DP_CU_ID ],
                cb_response = cb_cu_id_response)

evt.wait(timeout=5)
inst.terminate()
```

