wiki:Examples/PA-API OAuth2 Python Example

PA-API OAuth2 Python Example

The following is an example of accessing the Telldus PA-API using Python and OAuth 2.0.

This example requires the following 3rd party Python packages: Flask, requests

You can install them using pip:

pip install -U flask requests

The Example Project

Running the Project

You can run the project's main application using Python from the root of the project folder:

python app.py

Then open your browser at http://127.0.0.1:5000.

### Project Directory

You can organise your project directory as follows:

telldus_oauth2_flask_app/
│
├── app.py
├── config.py
├── requirements.txt
└── templates/
    └── index.html

App & Configurations

Configuration

We define some commonly used constants here.

config.py

# config.py
import os

# Obtain the client ID and client secret from https://pa-api.telldus.com/keys/showToken
CLIENT_ID = 'Your client ID (Public key)'
CLIENT_SECRET = 'Your client secret (Private key)'

# Default Telldus Live! account credentials
USERNAME = 'your@email.com'
PASSWORD = 'your_password'

# OAuth2 and API endpoints
TOKEN_URL = 'https://pa-api.telldus.com/oauth2/accessToken'
API_BASE_URL = 'https://pa-api.telldus.com/oauth2'

# Scope requested during token exchange
SCOPE = 'live-app'

# Flask secret key for session management
SECRET_KEY = os.urandom(24)

# User-Agent sent with all API requests
USER_AGENT = 'MyTelldusApp/1.0'

requirements.txt

Flask
requests

The Flask Application

The main application.

app.py

# app.py

from flask import Flask, session, redirect, url_for, request, render_template, flash
import requests
import config
import json

app = Flask(__name__)
app.config.from_object('config')

# ---------------------------------------------------------------------------
# OAuth2 helpers
# ---------------------------------------------------------------------------

def obtain_tokens(username, password):
    """Obtain OAuth2 access and refresh tokens using the password grant."""
    payload = {
        'grant_type': 'password',
        'username': username,
        'password': password,
        'scope': config.SCOPE,
        'client_id': config.CLIENT_ID,
        'client_secret': config.CLIENT_SECRET
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': config.USER_AGENT
    }
    response = requests.post(config.TOKEN_URL, data=payload, headers=headers)
    response.raise_for_status()
    data = response.json()
    access_token = data.get('access_token')
    refresh_token = data.get('refresh_token')
    if not access_token:
        raise ValueError(data.get('error_description', 'Failed to obtain access token.'))
    return access_token, refresh_token


def refresh_tokens(refresh_token):
    """Obtain a new access token using a refresh token."""
    payload = {
        'grant_type': 'refresh_token',
        'refresh_token': refresh_token,
        'client_id': config.CLIENT_ID,
        'client_secret': config.CLIENT_SECRET
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': config.USER_AGENT
    }
    response = requests.post(config.TOKEN_URL, data=payload, headers=headers)
    response.raise_for_status()
    data = response.json()
    return data.get('access_token'), data.get('refresh_token')


def api_get(path, access_token, params=None):
    """Perform an authenticated GET request to the Telldus OAuth2 API."""
    headers = {
        'Authorization': f'Bearer {access_token}',
        'User-Agent': config.USER_AGENT
    }
    url = f'{config.API_BASE_URL}/{path}'
    response = requests.get(url, headers=headers, params=params or {})
    response.raise_for_status()
    return response.json()


def print_debug_message(message):
    print(f'[DEBUG] {message}')


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------

@app.route('/')
def index():
    access_token = session.get('access_token')
    print_debug_message('Accessing home page.')

    if not access_token:
        print_debug_message('No access token found. User not authenticated.')
        return render_template('index.html', authenticated=False)

    # Example API call: list devices
    params = {
        'supportedMethods': 65535,
        'format': 'json'
    }
    print_debug_message(f'Making API call to {config.API_BASE_URL}/devices/list')

    try:
        result = api_get('devices/list', access_token, params)
        devices = result.get('device', [])
        print_debug_message(f'Devices fetched: {json.dumps(devices, indent=2)}')
    except Exception as e:
        flash('An error occurred while fetching devices.', 'danger')
        print_debug_message(f'Exception during API call: {str(e)}')
        devices = []

    return render_template('index.html',
                           authenticated=True,
                           devices=devices,
                           access_token=access_token)


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username', '').strip()
        password = request.form.get('password', '').strip()

        if not username or not password:
            flash('Username and password are required.', 'warning')
            return redirect(url_for('login'))

        print_debug_message(f'Attempting OAuth2 login for user: {username}')
        try:
            access_token, refresh_token = obtain_tokens(username, password)
            print_debug_message('OAuth2 tokens obtained successfully.')
        except Exception as e:
            flash(f'Login failed: {e}', 'danger')
            print_debug_message(f'Login failed: {e}')
            return redirect(url_for('login'))

        session['access_token'] = access_token
        session['refresh_token'] = refresh_token
        flash('You have successfully logged in.', 'success')
        return redirect(url_for('index'))

    return render_template('index.html', authenticated=False, show_login_form=True)


@app.route('/logout')
def logout():
    print_debug_message('Logging out user.')
    session.clear()
    flash('You have been logged out.', 'success')
    return redirect(url_for('index'))


@app.route('/devices')
def devices():
    access_token = session.get('access_token')
    print_debug_message('Accessing devices route.')

    if not access_token:
        flash('You need to log in first.', 'warning')
        return redirect(url_for('index'))

    params = {'supportedMethods': 65535, 'format': 'json'}
    try:
        result = api_get('devices/list', access_token, params)
        devices = result.get('device', [])
    except Exception as e:
        flash('Failed to fetch devices.', 'danger')
        print_debug_message(f'Exception: {e}')
        devices = []

    return render_template('index.html',
                           authenticated=True,
                           devices=devices,
                           show_devices=True)


@app.route('/clients')
def clients():
    access_token = session.get('access_token')
    print_debug_message('Accessing clients route.')

    if not access_token:
        flash('You need to log in first.', 'warning')
        return redirect(url_for('index'))

    params = {'format': 'json'}
    try:
        result = api_get('clients/list', access_token, params)
        clients = result.get('client', [])
    except Exception as e:
        flash('Failed to fetch clients.', 'danger')
        print_debug_message(f'Exception: {e}')
        clients = []

    return render_template('index.html',
                           authenticated=True,
                           clients=clients,
                           show_clients=True)


if __name__ == '__main__':
    app.secret_key = config.SECRET_KEY
    app.run(debug=True)

The HTML Template

The home page also serves as the login form and results view.

templates/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Telldus OAuth2 Flask App</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-5">

    {% with messages = get_flashed_messages(with_categories=true) %}
      {% if messages %}
        {% for category, message in messages %}
          <div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
            {{ message }}
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
          </div>
        {% endfor %}
      {% endif %}
    {% endwith %}

    <h1 class="mb-4">Telldus Live! OAuth2 Integration</h1>

    {% if not authenticated %}

        {% if show_login_form %}
            <p>Enter your Telldus Live! credentials to connect.</p>
            <form method="POST" action="{{ url_for('login') }}">
                <div class="mb-3">
                    <label for="username" class="form-label">Username (email)</label>
                    <input type="email" class="form-control" id="username" name="username" required>
                </div>
                <div class="mb-3">
                    <label for="password" class="form-label">Password</label>
                    <input type="password" class="form-control" id="password" name="password" required>
                </div>
                <button type="submit" class="btn btn-primary">Connect with Telldus</button>
            </form>
        {% else %}
            <p>You are not connected to Telldus Live!.</p>
            <a href="{{ url_for('login') }}" class="btn btn-primary">Connect with Telldus</a>
        {% endif %}

    {% else %}
        <p>You are connected to Telldus Live!</p>
        <a href="{{ url_for('logout') }}" class="btn btn-danger">Logout</a>

        <hr>

        {% if show_devices or devices is defined %}
            <h2>Devices</h2>
            {% if devices %}
                <ul class="list-group">
                    {% for device in devices %}
                        <li class="list-group-item">
                            <strong>{{ device.name }}</strong> (ID: {{ device.id }})
                        </li>
                    {% endfor %}
                </ul>
            {% else %}
                <p>No devices found.</p>
            {% endif %}
        {% endif %}

        {% if show_clients and clients is defined %}
            <h2>Clients</h2>
            {% if clients %}
                <ul class="list-group">
                    {% for client in clients %}
                        <li class="list-group-item">
                            <strong>{{ client.name }}</strong> (ID: {{ client.id }})
                        </li>
                    {% endfor %}
                </ul>
            {% else %}
                <p>No clients found.</p>
            {% endif %}
        {% endif %}

        {% if not show_devices and not show_clients %}
            <h2>Quick Links</h2>
            <a href="{{ url_for('devices') }}" class="btn btn-info me-2">View Devices</a>
            <a href="{{ url_for('clients') }}" class="btn btn-info">View Clients</a>
        {% endif %}

    {% endif %}
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Command-Line Alternative

If you only need to obtain tokens without a web interface, you can use a standalone script. The repository includes oauth2.py for this purpose.

Create a config.ini file:

[oauth2]
client_id = Your client ID
client_secret = Your client secret
username = your@email.com
password = your_password

[server]
user_agent = MyTelldusApp/1.0

Run the script:

python oauth2.py

On success, the access token and refresh token are printed to the console.

Testing an API Call from the Command Line

Once you have an access token, you can test an API call directly with curl:

curl -X GET "https://pa-api.telldus.com/oauth2/devices/list?supportedMethods=65535&format=json" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "User-Agent: MyTelldusApp/1.0"

Replace YOUR_ACCESS_TOKEN with the token obtained from the login flow or the command-line script.

Last modified 8 days ago Last modified on Aug 12, 2026, 1:13:51 PM
Note: See TracWiki for help on using the wiki.