diff --git a/mobile/build.gradle b/mobile/build.gradle
index 2b840ae35ee5cc8b55bf2bbd5a92781059461bb2..c7cae1ac0de681f3aec0fb9e95fa12c4d328b2ca 100644
--- a/mobile/build.gradle
+++ b/mobile/build.gradle
@@ -106,6 +106,7 @@ dependencies {
     implementation 'com.android.support:multidex:1.0.3'
     implementation 'org.jmdns:jmdns:3.5.4'
     implementation 'com.squareup.okhttp3:okhttp:3.11.0'
+    implementation 'com.github.heremaps:oksse:c92d0556f01e769d7c06c650941107642ce98fb5'
     implementation 'com.larswerkman:HoloColorPicker:1.5'
     implementation 'com.github.BigBadaboom:androidsvg:3511e136498da94018ef9fa438895984ea9b99db'
     implementation 'com.github.apl-devs:appintro:v4.2.3'
diff --git a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java
index a25f65e1056191a88dffd7f76c7fcb9ba8925007..aa80d0aa46f7c3280c9b992de1f4be2a1474ff9d 100644
--- a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java
+++ b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABItem.java
@@ -221,11 +221,27 @@ public abstract class OpenHABItem implements Parcelable {
                 .build();
     }
 
+    public static OpenHABItem updateFromEvent(OpenHABItem item, JSONObject jsonObject)
+            throws JSONException {
+        if (jsonObject == null) {
+            return item;
+        }
+        Builder builder = parseFromJson(jsonObject);
+        // Events don't contain the link property, so preserve that if previously present
+        if (item != null) {
+            builder.link(item.link());
+        }
+        return builder.build();
+    }
+
     public static OpenHABItem fromJson(JSONObject jsonObject) throws JSONException {
         if (jsonObject == null) {
             return null;
         }
+        return parseFromJson(jsonObject).build();
+    }
 
+    private static OpenHABItem.Builder parseFromJson(JSONObject jsonObject) throws JSONException {
         String name = jsonObject.getString("name");
         String state = jsonObject.optString("state", "");
         if ("NULL".equals(state) || "UNDEF".equals(state) || "undefined".equalsIgnoreCase(state)) {
@@ -267,7 +283,6 @@ public abstract class OpenHABItem implements Parcelable {
                 .members(members)
                 .options(options)
                 .state(state)
-                .readOnly(readOnly)
-                .build();
+                .readOnly(readOnly);
     }
 }
diff --git a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java
index aa6a915c225c5e3d0662c73e9d14430cbb56ea4c..4bc4f3f27c4f05b701fd0851a4021f4e1bd06673 100644
--- a/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java
+++ b/mobile/src/main/java/org/openhab/habdroid/model/OpenHABWidget.java
@@ -295,6 +295,19 @@ public abstract class OpenHABWidget implements Parcelable {
         }
     }
 
+    public static OpenHABWidget updateFromEvent(OpenHABWidget source, JSONObject eventPayload,
+            String iconFormat) throws JSONException {
+        OpenHABItem item = OpenHABItem.updateFromEvent(
+                source.item(), eventPayload.getJSONObject("item"));
+        String iconPath = determineOH2IconPath(item, source.type(),
+                source.icon(), iconFormat, !source.mappings().isEmpty());
+        return source.toBuilder()
+                .label(eventPayload.optString("label", source.label()))
+                .item(item)
+                .iconPath(iconPath)
+                .build();
+    }
+
     private static String determineOH2IconPath(OpenHABItem item, Type type, String icon,
             String iconFormat, boolean hasMappings) {
         String itemState = item != null ? item.state() : null;
diff --git a/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java b/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..e7bcd878306b8cb7edfe48c9909774f7d0ddaf0e
--- /dev/null
+++ b/mobile/src/main/java/org/openhab/habdroid/model/ServerProperties.java
@@ -0,0 +1,178 @@
+package org.openhab.habdroid.model;
+
+import android.os.Parcelable;
+import android.util.Log;
+
+import com.google.auto.value.AutoValue;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.openhab.habdroid.core.connection.Connection;
+import org.openhab.habdroid.util.AsyncHttpClient;
+import org.openhab.habdroid.util.Util;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import okhttp3.Call;
+import okhttp3.Headers;
+import okhttp3.Request;
+
+@AutoValue
+public abstract class ServerProperties implements Parcelable {
+    private static final String TAG = ServerProperties.class.getSimpleName();
+
+    public static final int SERVER_FLAG_JSON_REST_API         = 1 << 0;
+    public static final int SERVER_FLAG_SSE_SUPPORT           = 1 << 1;
+    public static final int SERVER_FLAG_ICON_FORMAT_SUPPORT   = 1 << 2;
+    public static final int SERVER_FLAG_CHART_SCALING_SUPPORT = 1 << 3;
+
+    public static class UpdateHandle {
+        public void cancel() {
+            if (call != null) {
+                call.cancel();
+                call = null;
+            }
+        }
+
+        private Call call;
+        private Builder builder;
+    }
+
+    public interface UpdateSuccessCallback {
+        void handleServerPropertyUpdate(ServerProperties props);
+    }
+    public interface UpdateFailureCallback {
+        void handleUpdateFailure(Request request, int statusCode, Throwable error);
+    }
+
+    public abstract int flags();
+    public abstract List<OpenHABSitemap> sitemaps();
+
+    public boolean hasJsonApi() {
+        return (flags() & SERVER_FLAG_JSON_REST_API) != 0;
+    }
+
+    public boolean hasSseSupport() {
+        return (flags() & SERVER_FLAG_SSE_SUPPORT) != 0;
+    }
+
+    abstract Builder toBuilder();
+
+    @AutoValue.Builder
+    static abstract class Builder {
+        abstract Builder flags(int flags);
+        abstract Builder sitemaps(List<OpenHABSitemap> sitemaps);
+
+        abstract ServerProperties build();
+        abstract int flags();
+    }
+
+    public static UpdateHandle updateSitemaps(ServerProperties props, Connection connection,
+            UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) {
+        UpdateHandle handle = new UpdateHandle();
+        handle.builder = props.toBuilder();
+        fetchSitemaps(connection.getAsyncHttpClient(), handle, successCb, failureCb);
+        return handle;
+    }
+
+    public static UpdateHandle fetch(Connection connection,
+            UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) {
+        final UpdateHandle handle = new UpdateHandle();
+        handle.builder = new AutoValue_ServerProperties.Builder();
+        fetchFlags(connection.getAsyncHttpClient(), handle, successCb, failureCb);
+        return handle;
+    }
+
+    private static void fetchFlags(AsyncHttpClient client, UpdateHandle handle,
+            UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) {
+        handle.call = client.get("rest", new AsyncHttpClient.StringResponseHandler() {
+            @Override
+            public void onFailure(Request request, int statusCode, Throwable error) {
+                failureCb.handleUpdateFailure(request, statusCode, error);
+            }
+
+            @Override
+            public void onSuccess(String response, Headers headers) {
+                try {
+                    JSONObject result = new JSONObject(response);
+                    // If this succeeded, we're talking to OH2
+                    int flags = SERVER_FLAG_JSON_REST_API
+                            | SERVER_FLAG_ICON_FORMAT_SUPPORT
+                            | SERVER_FLAG_CHART_SCALING_SUPPORT;
+                    try {
+                        String versionString = result.getString("version");
+                        int versionNumber = Integer.parseInt(versionString);
+                        // all versions that return a number here have full SSE support
+                        flags |= SERVER_FLAG_SSE_SUPPORT;
+                    } catch (NumberFormatException nfe) {
+                        // ignored: older versions without SSE support didn't return a number
+                    }
+                    handle.builder.flags(flags);
+                    fetchSitemaps(client, handle, successCb, failureCb);
+                } catch (JSONException e) {
+                    if (response.startsWith("<?xml")) {
+                        // We're talking to an OH1 instance
+                        handle.builder.flags(0);
+                        fetchSitemaps(client, handle, successCb, failureCb);
+                    } else {
+                        failureCb.handleUpdateFailure(handle.call.request(), 200, e);
+                    }
+                }
+            }
+        });
+    }
+
+    private static void fetchSitemaps(AsyncHttpClient client, UpdateHandle handle,
+            UpdateSuccessCallback successCb, UpdateFailureCallback failureCb) {
+        handle.call = client.get("rest/sitemaps", new AsyncHttpClient.StringResponseHandler() {
+            @Override
+            public void onFailure(Request request, int statusCode, Throwable error) {
+                failureCb.handleUpdateFailure(request, statusCode, error);
+            }
+
+            @Override
+            public void onSuccess(String response, Headers headers) {
+                // OH1 returns XML, later versions return JSON
+                List<OpenHABSitemap> result = (handle.builder.flags() & SERVER_FLAG_JSON_REST_API) != 0
+                        ? loadSitemapsFromJson(response)
+                        : loadSitemapsFromXml(response);
+                Log.d(TAG, "Server returned sitemaps: " + result);
+                handle.builder.sitemaps(result != null ? result : new ArrayList<>());
+                successCb.handleServerPropertyUpdate(handle.builder.build());
+            }
+        });
+    }
+
+    private static List<OpenHABSitemap> loadSitemapsFromXml(String response) {
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        try {
+            DocumentBuilder builder = dbf.newDocumentBuilder();
+            Document sitemapsXml = builder.parse(new InputSource(new StringReader(response)));
+            return Util.parseSitemapList(sitemapsXml);
+        } catch (ParserConfigurationException | SAXException | IOException e) {
+            Log.e(TAG, "Failed parsing sitemap XML", e);
+            return null;
+        }
+    }
+
+    private static List<OpenHABSitemap> loadSitemapsFromJson(String response) {
+        try {
+            JSONArray jsonArray = new JSONArray(response);
+            return Util.parseSitemapList(jsonArray);
+        } catch (JSONException e) {
+            Log.e(TAG, "Failed parsing sitemap JSON", e);
+            return null;
+        }
+    }
+}
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java
index 8327ffe0e5148df5196e51fa0ae1fc2345daa13a..20749038c4c8d2cb7d2c7d7038f8122c970f336d 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/AboutActivity.java
@@ -33,6 +33,7 @@ import org.openhab.habdroid.core.CloudMessagingHelper;
 import org.openhab.habdroid.core.connection.Connection;
 import org.openhab.habdroid.core.connection.ConnectionFactory;
 import org.openhab.habdroid.core.connection.exception.ConnectionException;
+import org.openhab.habdroid.model.ServerProperties;
 import org.openhab.habdroid.util.SyncHttpClient;
 import org.openhab.habdroid.util.Util;
 
@@ -104,14 +105,14 @@ public class AboutActivity extends AppCompatActivity implements
     public static class AboutMainFragment extends MaterialAboutFragment {
         private final static String TAG = AboutMainFragment.class.getSimpleName();
         private final static String URL_TO_GITHUB = "https://github.com/openhab/openhab-android";
-        private int mOpenHABVersion;
+        private ServerProperties mServerProperties;
         private Connection mConnection;
 
         @Nullable
         @Override
         public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                 @Nullable Bundle savedInstanceState) {
-            mOpenHABVersion = getArguments().getInt("openHABVersion", 0);
+            mServerProperties = getArguments().getParcelable("serverProperties");
             try {
                 mConnection = ConnectionFactory.getUsableConnection();
             } catch (ConnectionException ignored) {}
@@ -187,7 +188,7 @@ public class AboutActivity extends AppCompatActivity implements
 
             MaterialAboutCard.Builder ohServerCard = new MaterialAboutCard.Builder();
             ohServerCard.title(R.string.about_server);
-            if (mConnection == null || mOpenHABVersion == 0) {
+            if (mConnection == null || mServerProperties == null) {
                 ohServerCard.addItem(new MaterialAboutActionItem.Builder()
                         .text(R.string.error_about_no_conn)
                         .icon(R.drawable.ic_info_outline)
@@ -213,7 +214,7 @@ public class AboutActivity extends AppCompatActivity implements
                         .icon(R.drawable.ic_info_outline)
                         .build());
 
-                if (mOpenHABVersion == 1) {
+                if (!useJsonApi()) {
                     String secret = getServerSecret();
                     if (!TextUtils.isEmpty(secret)) {
                         ohServerCard.addItem(new MaterialAboutActionItem.Builder()
@@ -289,8 +290,12 @@ public class AboutActivity extends AppCompatActivity implements
             }
         }
 
+        private boolean useJsonApi() {
+            return mServerProperties != null && mServerProperties.hasJsonApi();
+        }
+
         private String getServerUuid() {
-            final String uuidUrl = mOpenHABVersion == 1 ? "static/uuid" : "rest/uuid";
+            final String uuidUrl = useJsonApi() ? "rest/uuid" : "static/uuid";
             SyncHttpClient.HttpTextResult result =
                     mConnection.getSyncHttpClient().get(uuidUrl).asText();
             if (result.isSuccessful()) {
@@ -303,14 +308,14 @@ public class AboutActivity extends AppCompatActivity implements
         }
 
         private String getApiVersion() {
-            String versionUrl = mOpenHABVersion == 1 ? "static/version" : "rest";
+            String versionUrl = useJsonApi() ? "rest" : "static/version";
             Log.d(TAG, "url = " + versionUrl);
             SyncHttpClient.HttpTextResult result =
                     mConnection.getSyncHttpClient().get(versionUrl).asText();
             if (!result.isSuccessful()) {
                 Log.e(TAG, "Could not fetch rest API version " + result.error);
             } else {
-                if (mOpenHABVersion == 1) {
+                if (!useJsonApi()) {
                     return result.response;
                 } else {
                     try {
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java
index b2c6c8d334b188544120e91ca18de689979f8f7d..ce293a6b33f35f38d362c12db7582e627f92c67c 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABMainActivity.java
@@ -62,8 +62,6 @@ import android.view.WindowManager;
 import android.widget.ProgressBar;
 import android.widget.Toast;
 
-import org.json.JSONArray;
-import org.json.JSONException;
 import org.openhab.habdroid.R;
 import org.openhab.habdroid.core.CloudMessagingHelper;
 import org.openhab.habdroid.core.OnUpdateBroadcastReceiver;
@@ -77,17 +75,14 @@ import org.openhab.habdroid.core.connection.exception.NetworkNotSupportedExcepti
 import org.openhab.habdroid.core.connection.exception.NoUrlInformationException;
 import org.openhab.habdroid.model.OpenHABLinkedPage;
 import org.openhab.habdroid.model.OpenHABSitemap;
+import org.openhab.habdroid.model.ServerProperties;
 import org.openhab.habdroid.ui.activity.ContentController;
 import org.openhab.habdroid.util.AsyncServiceResolver;
 import org.openhab.habdroid.util.Constants;
 import org.openhab.habdroid.util.AsyncHttpClient;
 import org.openhab.habdroid.util.Util;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
 
 import java.io.IOException;
-import java.io.StringReader;
 import java.lang.reflect.Constructor;
 import java.net.ConnectException;
 import java.net.SocketTimeoutException;
@@ -97,23 +92,18 @@ import java.security.cert.CertPathValidatorException;
 import java.security.cert.CertificateExpiredException;
 import java.security.cert.CertificateNotYetValidException;
 import java.security.cert.CertificateRevokedException;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
 
 import javax.jmdns.ServiceInfo;
 import javax.net.ssl.SSLException;
 import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
 
 import es.dmoral.toasty.Toasty;
 import okhttp3.Call;
 import okhttp3.Headers;
 import okhttp3.Request;
 
-import static org.openhab.habdroid.ui.OpenHABPreferencesActivity.START_EXTRA_OPENHAB_VERSION;
 import static org.openhab.habdroid.util.Util.exceptionHasCause;
 import static org.openhab.habdroid.util.Util.getHostFromUrl;
 
@@ -133,12 +123,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements
     // Drawer item codes
     private static final int GROUP_ID_SITEMAPS = 1;
 
-    private enum InitState {
-        QUERY_SERVER_PROPS,
-        LOAD_SITEMAPS,
-        DONE
-    }
-
     // preferences
     private SharedPreferences mSettings;
     private AsyncServiceResolver mServiceResolver;
@@ -151,8 +135,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements
     private Menu mDrawerMenu;
     private ColorStateList mDrawerIconTintList;
     private RecyclerView.RecycledViewPool mViewPool;
-    private ArrayList<OpenHABSitemap> mSitemapList;
-    private int mOpenHABVersion;
     private ProgressBar mProgressBar;
     // select sitemap dialog
     private Dialog mSelectSitemapDialog;
@@ -163,8 +145,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements
     private String mPendingOpenedNotificationId;
     private OpenHABSitemap mSelectedSitemap;
     private ContentController mController;
-    private InitState mInitState = InitState.QUERY_SERVER_PROPS;
-    private Call mPendingCall;
+    private ServerProperties mServerProperties;
+    private ServerProperties.UpdateHandle mPropsUpdateHandle;
     private boolean mStarted;
 
     /**
@@ -229,10 +211,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements
 
         // Check if we have openHAB page url in saved instance state?
         if (savedInstanceState != null) {
-            mOpenHABVersion = savedInstanceState.getInt("openHABVersion");
-            mSitemapList = savedInstanceState.getParcelableArrayList("sitemapList");
+            mServerProperties = savedInstanceState.getParcelable("serverProperties");
             mSelectedSitemap = savedInstanceState.getParcelable("sitemap");
-            mInitState = InitState.values()[savedInstanceState.getInt("initState")];
             int lastConnectionHash = savedInstanceState.getInt("connectionHash");
             if (lastConnectionHash != -1) {
                 try {
@@ -257,7 +237,6 @@ public class OpenHABMainActivity extends AppCompatActivity implements
                 showSitemapSelectionDialog();
             }
         } else {
-            mSitemapList = new ArrayList<>();
         }
 
         processIntent(getIntent());
@@ -304,35 +283,31 @@ public class OpenHABMainActivity extends AppCompatActivity implements
 
     public void retryServerPropertyQuery() {
         mController.clearServerCommunicationFailure();
-        if (mPendingCall != null) {
-            mPendingCall.cancel();
-        }
         queryServerProperties();
     }
 
     private void queryServerProperties() {
-        final String url = "rest/bindings";
-        mInitState = InitState.QUERY_SERVER_PROPS;
-        mPendingCall = mConnection.getAsyncHttpClient().get(url, new AsyncHttpClient.StringResponseHandler() {
-            @Override
-            public void onFailure(Request request, int statusCode, Throwable error) {
-                if (statusCode == 404 && mConnection != null) {
-                    // no bindings endpoint; we're likely talking to an OH1 instance
-                    mOpenHABVersion = 1;
-                    loadSitemapList(true);
+        if (mPropsUpdateHandle != null) {
+            mPropsUpdateHandle.cancel();
+        }
+        ServerProperties.UpdateSuccessCallback successCb = props -> {
+            mServerProperties = props;
+            updateSitemapDrawerItems();
+            if (props.sitemaps().isEmpty()) {
+                Log.e(TAG, "openHAB returned empty sitemap list");
+                mController.indicateServerCommunicationFailure(
+                        getString(R.string.error_empty_sitemap_list));
+            } else {
+                OpenHABSitemap sitemap = selectConfiguredSitemapFromList();
+                if (sitemap != null) {
+                    openSitemap(sitemap);
                 } else {
-                    // other error -> use default handling
-                    handleServerCommunicationFailure(request, statusCode, error);
+                    showSitemapSelectionDialog();
                 }
             }
-
-            @Override
-            public void onSuccess(String response, Headers headers) {
-                mOpenHABVersion = 2;
-                Log.d(TAG, "openHAB version 2");
-                loadSitemapList(true);
-            }
-        });
+        };
+        mPropsUpdateHandle = ServerProperties.fetch(mConnection,
+                successCb, this::handlePropertyFetchFailure);
     }
 
     @Override
@@ -435,7 +410,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements
 
         mConnection = newConnection;
         hideSnackbar();
-        mSitemapList.clear();
+        mServerProperties = null;
         mSelectedSitemap = null;
 
         // Handle pending NFC tag if initial connection determination finished
@@ -495,14 +470,9 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         onAvailableConnectionChanged();
         updateNotificationDrawerItem();
 
-        if (mConnection != null) {
-            if (mInitState == InitState.QUERY_SERVER_PROPS) {
-                mController.clearServerCommunicationFailure();
-                queryServerProperties();
-            } else if (mInitState == InitState.LOAD_SITEMAPS) {
-                mController.clearServerCommunicationFailure();
-                loadSitemapList(true);
-            }
+        if (mConnection != null && mServerProperties == null) {
+            mController.clearServerCommunicationFailure();
+            queryServerProperties();
         }
         openPendingNfcPageIfNeeded();
         openNotificationsPageIfNeeded();
@@ -521,8 +491,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         if (mSelectSitemapDialog != null && mSelectSitemapDialog.isShowing()) {
             mSelectSitemapDialog.dismiss();
         }
-        if (mPendingCall != null) {
-            mPendingCall.cancel();
+        if (mPropsUpdateHandle != null) {
+            mPropsUpdateHandle.cancel();
         }
     }
 
@@ -553,8 +523,10 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         mDrawerLayout.addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
             @Override
             public void onDrawerOpened(View drawerView) {
-                if (mInitState == InitState.DONE) {
-                    loadSitemapList(false);
+                if (mServerProperties != null && mPropsUpdateHandle == null) {
+                    mPropsUpdateHandle = ServerProperties.updateSitemaps(mServerProperties, mConnection,
+                            props -> { mServerProperties = props; updateSitemapDrawerItems(); },
+                            OpenHABMainActivity.this::handlePropertyFetchFailure);
                 }
             }
         });
@@ -585,7 +557,8 @@ public class OpenHABMainActivity extends AppCompatActivity implements
                     case R.id.settings:
                         Intent settingsIntent = new Intent(OpenHABMainActivity.this,
                                 OpenHABPreferencesActivity.class);
-                        settingsIntent.putExtra(START_EXTRA_OPENHAB_VERSION, getOpenHABVersion());
+                        settingsIntent.putExtra(OpenHABPreferencesActivity.START_EXTRA_SERVER_PROPERTIES,
+                                mServerProperties);
                         startActivityForResult(settingsIntent, SETTINGS_REQUEST_CODE);
                         return true;
                     case R.id.about:
@@ -593,7 +566,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements
                         return true;
                 }
                 if (item.getGroupId() == GROUP_ID_SITEMAPS) {
-                    OpenHABSitemap sitemap = mSitemapList.get(item.getItemId());
+                    OpenHABSitemap sitemap = mServerProperties.sitemaps().get(item.getItemId());
                     openSitemap(sitemap);
                     return true;
                 }
@@ -609,18 +582,26 @@ public class OpenHABMainActivity extends AppCompatActivity implements
 
     private void updateSitemapDrawerItems() {
         MenuItem sitemapItem = mDrawerMenu.findItem(R.id.sitemaps);
-
-        if (mSitemapList.isEmpty()) {
+        if (mServerProperties == null) {
             sitemapItem.setVisible(false);
         } else {
-            sitemapItem.setVisible(true);
-            SubMenu menu = sitemapItem.getSubMenu();
-            menu.clear();
-
-            for (int i = 0; i < mSitemapList.size(); i++) {
-                OpenHABSitemap sitemap = mSitemapList.get(i);
-                MenuItem item = menu.add(GROUP_ID_SITEMAPS, i, i, sitemap.label());
-                loadSitemapIcon(sitemap, item);
+            final String defaultSitemapName =
+                    mSettings.getString(Constants.PREFERENCE_SITEMAP_NAME, "");
+            final List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps();
+            Util.sortSitemapList(sitemaps, defaultSitemapName);
+
+            if (sitemaps.isEmpty()) {
+                sitemapItem.setVisible(false);
+            } else {
+                sitemapItem.setVisible(true);
+                SubMenu menu = sitemapItem.getSubMenu();
+                menu.clear();
+
+                for (int i = 0; i < sitemaps.size(); i++) {
+                    OpenHABSitemap sitemap = sitemaps.get(i);
+                    MenuItem item = menu.add(GROUP_ID_SITEMAPS, i, i, sitemap.label());
+                    loadSitemapIcon(sitemap, item);
+                }
             }
         }
     }
@@ -693,103 +674,25 @@ public class OpenHABMainActivity extends AppCompatActivity implements
 
     private void openAbout() {
         Intent aboutIntent = new Intent(this, AboutActivity.class);
-        aboutIntent.putExtra("openHABVersion", mOpenHABVersion);
+        aboutIntent.putExtra("serverProperties", mServerProperties);
 
         startActivityForResult(aboutIntent, INFO_REQUEST_CODE);
         Util.overridePendingTransition(this, false);
     }
 
-    /**
-     * Get sitemaps from openHAB. If user already configured preferred sitemap
-     * just open it. If no preferred sitemap is configured let user select one.
-     */
-
-    private void loadSitemapList(final boolean selectSitemapAfterLoad) {
-        if (mConnection == null) {
-            return;
-        }
-
-        Log.d(TAG, "Loading sitemap list from /rest/sitemaps");
-
-        mInitState = InitState.LOAD_SITEMAPS;
-        mPendingCall = mConnection.getAsyncHttpClient().get("rest/sitemaps", new AsyncHttpClient.StringResponseHandler() {
-            @Override
-            public void onFailure(Request request, int statusCode, Throwable error) {
-                handleServerCommunicationFailure(request, statusCode, error);
-            }
-
-            @Override
-            public void onSuccess(String response, Headers headers) {
-                mPendingCall = null;
-                mInitState = InitState.DONE;
-
-                // OH1 returns XML, later versions return JSON
-                List<OpenHABSitemap> result = mOpenHABVersion == 1
-                        ? loadSitemapsFromXml(response)
-                        : loadSitemapsFromJson(response);
-                Log.d(TAG, "Server returned sitemaps: " + result);
-                mSitemapList.clear();
-                if (result != null) {
-                    String defaultSitemapName =
-                            mSettings.getString(Constants.PREFERENCE_SITEMAP_NAME, "");
-                    mSitemapList.addAll(Util.sortSitemapList(result, defaultSitemapName));
-                }
-                updateSitemapDrawerItems();
-
-                if (!selectSitemapAfterLoad) {
-                    return;
-                }
-
-                if (mSitemapList.isEmpty()) {
-                    Log.e(TAG, "openHAB returned empty sitemap list");
-                    mController.indicateServerCommunicationFailure(
-                            getString(R.string.error_empty_sitemap_list));
-                } else {
-                    OpenHABSitemap sitemap = selectConfiguredSitemapFromList();
-                    if (sitemap != null) {
-                        openSitemap(sitemap);
-                    } else {
-                        showSitemapSelectionDialog();
-                    }
-                }
-            }
-        });
-    }
-
-    private static List<OpenHABSitemap> loadSitemapsFromXml(String response) {
-        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-        try {
-            DocumentBuilder builder = dbf.newDocumentBuilder();
-            Document sitemapsXml = builder.parse(new InputSource(new StringReader(response)));
-            return Util.parseSitemapList(sitemapsXml);
-        } catch (ParserConfigurationException | SAXException | IOException e) {
-            Log.e(TAG, "Failed parsing sitemap XML", e);
-            return null;
-        }
-    }
-
-    private static List<OpenHABSitemap> loadSitemapsFromJson(String response) {
-        try {
-            JSONArray jsonArray = new JSONArray(response);
-            return Util.parseSitemapList(jsonArray);
-        } catch (JSONException e) {
-            Log.e(TAG, "Failed parsing sitemap JSON", e);
-            return null;
-        }
-    }
-
     private OpenHABSitemap selectConfiguredSitemapFromList() {
         SharedPreferences settings =
                 PreferenceManager.getDefaultSharedPreferences(this);
         String configuredSitemap = settings.getString(Constants.PREFERENCE_SITEMAP_NAME, "");
+        List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps();
         final OpenHABSitemap result;
 
-        if (mSitemapList.size() == 1) {
+        if (sitemaps.size() == 1) {
             // We only have one sitemap, use it
-            result = mSitemapList.get(0);
+            result = sitemaps.get(0);
         } else if (!configuredSitemap.isEmpty()) {
             // Select configured sitemap if still present, nothing otherwise
-            result = Util.getSitemapByName(mSitemapList, configuredSitemap);
+            result = Util.getSitemapByName(sitemaps, configuredSitemap);
         } else {
             // Nothing configured -> can't auto-select anything
             result = null;
@@ -824,16 +727,17 @@ public class OpenHABMainActivity extends AppCompatActivity implements
             return;
         }
 
-        final String[] sitemapLabels = new String[mSitemapList.size()];
-        for (int i = 0; i < mSitemapList.size(); i++) {
-            sitemapLabels[i] = mSitemapList.get(i).label();
+        List<OpenHABSitemap> sitemaps = mServerProperties.sitemaps();
+        final String[] sitemapLabels = new String[sitemaps.size()];
+        for (int i = 0; i < sitemaps.size(); i++) {
+            sitemapLabels[i] = sitemaps.get(i).label();
         }
         mSelectSitemapDialog = new AlertDialog.Builder(this)
                 .setTitle(R.string.mainmenu_openhab_selectsitemap)
                 .setItems(sitemapLabels, new DialogInterface.OnClickListener() {
                     @Override
                     public void onClick(DialogInterface dialog, int item) {
-                        OpenHABSitemap sitemap = mSitemapList.get(item);
+                        OpenHABSitemap sitemap = sitemaps.get(item);
                         Log.d(TAG, "Selected sitemap " + sitemap);
                         PreferenceManager.getDefaultSharedPreferences(OpenHABMainActivity.this)
                                 .edit()
@@ -937,15 +841,13 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         // Save UI state changes to the savedInstanceState.
         // This bundle will be passed to onCreate if the process is
         // killed and restarted.
-        savedInstanceState.putInt("openHABVersion", mOpenHABVersion);
-        savedInstanceState.putParcelableArrayList("sitemapList", mSitemapList);
+        savedInstanceState.putParcelable("serverProperties", mServerProperties);
         savedInstanceState.putParcelable("sitemap", mSelectedSitemap);
         savedInstanceState.putBoolean("isSitemapSelectionDialogShown", mSelectSitemapDialog != null &&
                 mSelectSitemapDialog.isShowing());
         savedInstanceState.putString("controller", mController.getClass().getCanonicalName());
         savedInstanceState.putInt("connectionHash",
                 mConnection != null ? mConnection.hashCode() : -1);
-        savedInstanceState.putInt("initState", mInitState.ordinal());
         mController.onSaveInstanceState(savedInstanceState);
         super.onSaveInstanceState(savedInstanceState);
     }
@@ -1048,7 +950,7 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         }
     }
 
-    private void handleServerCommunicationFailure(Request request, int statusCode, Throwable error) {
+    private void handlePropertyFetchFailure(Request request, int statusCode, Throwable error) {
         Log.e(TAG, "Error: " + error.toString());
         Log.e(TAG, "HTTP status code: " + statusCode);
         CharSequence message;
@@ -1125,16 +1027,15 @@ public class OpenHABMainActivity extends AppCompatActivity implements
         }
 
         mController.indicateServerCommunicationFailure(message);
-        mPendingCall = null;
-        mInitState = InitState.DONE;
+        mPropsUpdateHandle = null;
     }
 
     public boolean isStarted() {
         return mStarted;
     }
 
-    public int getOpenHABVersion() {
-        return mOpenHABVersion;
+    public ServerProperties getServerProperties() {
+        return mServerProperties;
     }
 
     public Connection getConnection() {
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java
index f1ee9c03c324d5345abe4cba181e686952846149..3ef3091ee760d53c45efad60832148341a83aaf3 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABPreferencesActivity.java
@@ -38,6 +38,7 @@ import android.view.MenuItem;
 
 import org.openhab.habdroid.R;
 import org.openhab.habdroid.core.CloudMessagingHelper;
+import org.openhab.habdroid.model.ServerProperties;
 import org.openhab.habdroid.util.CacheManager;
 import org.openhab.habdroid.util.Constants;
 import org.openhab.habdroid.util.Util;
@@ -52,12 +53,11 @@ import static org.openhab.habdroid.util.Util.getHostFromUrl;
 public class OpenHABPreferencesActivity extends AppCompatActivity {
     public static final String RESULT_EXTRA_THEME_CHANGED = "theme_changed";
     public static final String RESULT_EXTRA_SITEMAP_CLEARED = "sitemap_cleared";
-    public static final String START_EXTRA_OPENHAB_VERSION = "openhab_version";
+    public static final String START_EXTRA_SERVER_PROPERTIES = "server_properties";
     private static final String STATE_KEY_RESULT = "result";
 
     private static final String TAG = OpenHABPreferencesActivity.class.getSimpleName();
     private Intent mResultIntent;
-    private static int mOpenhabVersion;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -80,8 +80,6 @@ public class OpenHABPreferencesActivity extends AppCompatActivity {
             mResultIntent = savedInstanceState.getParcelable(STATE_KEY_RESULT);
         }
         setResult(RESULT_OK, mResultIntent);
-
-        mOpenhabVersion = getIntent().getIntExtra(START_EXTRA_OPENHAB_VERSION, 0);
     }
 
     @Override
@@ -343,11 +341,14 @@ public class OpenHABPreferencesActivity extends AppCompatActivity {
                 getParent(vibrationPreference).removePreference(vibrationPreference);
             }
 
-            if (mOpenhabVersion == 1) {
-                Log.d(TAG, "Removing prefs that aren't available in openHAB 1");
+            ServerProperties props =
+                    getActivity().getIntent().getParcelableExtra(START_EXTRA_SERVER_PROPERTIES);
+            if (props != null && (props.flags() & ServerProperties.SERVER_FLAG_ICON_FORMAT_SUPPORT) == 0) {
                 Preference iconFormatPreference =
                         ps.findPreference(Constants.PREFERENCE_ICON_FORMAT);
                 getParent(iconFormatPreference).removePreference(iconFormatPreference);
+            }
+            if (props != null && (props.flags() & ServerProperties.SERVER_FLAG_CHART_SCALING_SUPPORT) == 0) {
                 Preference chartScalingPreference =
                         ps.findPreference(Constants.PREFERENCE_CHART_SCALING);
                 getParent(chartScalingPreference).removePreference(chartScalingPreference);
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java
index 992038a3176c5e70ff53ff47981d32f1521320fb..b514380b8d106e70e1bc5f3aecf61576a0e0b22d 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetAdapter.java
@@ -148,7 +148,8 @@ public class OpenHABWidgetAdapter extends RecyclerView.Adapter<OpenHABWidgetAdap
         if (compatibleUpdate) {
             for (int i = 0; i < widgets.size(); i++) {
                 if (!mItems.get(i).equals(widgets.get(i))) {
-                    updateAtPosition(i, widgets.get(i));
+                    mItems.set(i, widgets.get(i));
+                    notifyItemChanged(i);
                 }
             }
         } else {
@@ -158,12 +159,14 @@ public class OpenHABWidgetAdapter extends RecyclerView.Adapter<OpenHABWidgetAdap
         }
     }
 
-    public void updateAtPosition(int position, OpenHABWidget widget) {
-        if (position >= mItems.size()) {
-            return;
+    public void updateWidget(OpenHABWidget widget) {
+        for (int i = 0; i < mItems.size(); i++) {
+            if (mItems.get(i).id().equals(widget.id())) {
+                mItems.set(i, widget);
+                notifyItemChanged(i);
+                break;
+            }
         }
-        mItems.set(position, widget);
-        notifyItemChanged(position);
     }
 
     @Override
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java
index 8a3c0f48de85637195c6d481ab97ba1c3bb594ed..a2e51ed4562c27fd1db3cd524e58af41fd2d2802 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/OpenHABWidgetListFragment.java
@@ -58,7 +58,6 @@ public class OpenHABWidgetListFragment extends Fragment
     // Am I visible?
     private boolean mIsVisible = false;
     private String mTitle;
-    private List<OpenHABWidget> mWidgets;
     private SwipeRefreshLayout refreshLayout;
     private String mHighlightedPageLink;
 
@@ -100,10 +99,6 @@ public class OpenHABWidgetListFragment extends Fragment
         mRecyclerView.addItemDecoration(new OpenHABWidgetAdapter.WidgetItemDecoration(mActivity));
         mRecyclerView.setLayoutManager(mLayoutManager);
         mRecyclerView.setAdapter(openHABWidgetAdapter);
-
-        if (mWidgets != null) {
-            update(mTitle, mWidgets);
-        }
     }
 
     @Override
@@ -290,7 +285,6 @@ public class OpenHABWidgetListFragment extends Fragment
 
     public void update(String pageTitle, List<OpenHABWidget> widgets) {
         mTitle = pageTitle;
-        mWidgets = widgets;
 
         if (openHABWidgetAdapter != null) {
             openHABWidgetAdapter.update(widgets, refreshLayout.isRefreshing());
@@ -302,19 +296,10 @@ public class OpenHABWidgetListFragment extends Fragment
         }
     }
 
-    public boolean onWidgetUpdated(OpenHABWidget widget) {
-        if (mWidgets != null) {
-            for (int i = 0; i < mWidgets.size(); i++) {
-                if (mWidgets.get(i).id().equals(widget.id())) {
-                    mWidgets.set(i, widget);
-                    if (openHABWidgetAdapter != null) {
-                        openHABWidgetAdapter.updateAtPosition(i, widget);
-                    }
-                    return true;
-                }
-            }
+    public void updateWidget(OpenHABWidget widget) {
+        if (openHABWidgetAdapter != null) {
+            openHABWidgetAdapter.updateWidget(widget);
         }
-        return false;
     }
 
     public String getDisplayPageUrl() {
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java
index 8bb84ad3f214ef1fd2af7d3b492f31fd4c031b16..d32db36625e71cd8638cc4bb41aac03ddc286820 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/ContentController.java
@@ -42,6 +42,7 @@ import org.openhab.habdroid.core.connection.ConnectionFactory;
 import org.openhab.habdroid.model.OpenHABLinkedPage;
 import org.openhab.habdroid.model.OpenHABSitemap;
 import org.openhab.habdroid.model.OpenHABWidget;
+import org.openhab.habdroid.model.ServerProperties;
 import org.openhab.habdroid.ui.OpenHABMainActivity;
 import org.openhab.habdroid.ui.OpenHABNotificationFragment;
 import org.openhab.habdroid.ui.OpenHABPreferencesActivity;
@@ -386,7 +387,14 @@ public abstract class ContentController implements PageConnectionHolderFragment.
 
     @Override
     public boolean serverReturnsJson() {
-        return mActivity.getOpenHABVersion() != 1;
+        ServerProperties props = mActivity.getServerProperties();
+        return props != null && props.hasJsonApi();
+    }
+
+    @Override
+    public boolean serverSupportsSse() {
+        ServerProperties props = mActivity.getServerProperties();
+        return props != null && props.hasSseSupport();
     }
 
     @Override
@@ -413,9 +421,10 @@ public abstract class ContentController implements PageConnectionHolderFragment.
     }
 
     @Override
-    public void onWidgetUpdated(OpenHABWidget widget) {
+    public void onWidgetUpdated(String pageUrl, OpenHABWidget widget) {
         for (OpenHABWidgetListFragment f : collectWidgetFragments()) {
-            if (f.onWidgetUpdated(widget)) {
+            if (pageUrl.equals(f.getDisplayPageUrl())) {
+                f.updateWidget(widget);
                 break;
             }
         }
diff --git a/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java b/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java
index db5ccbbd2efa5c37a1f88fb133e2ad241d3a294d..f6577ec5b6a635bc1747b0aa833958eff3824e50 100644
--- a/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java
+++ b/mobile/src/main/java/org/openhab/habdroid/ui/activity/PageConnectionHolderFragment.java
@@ -1,11 +1,16 @@
 package org.openhab.habdroid.ui.activity;
 
+import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.here.oksse.ServerSentEvent;
+
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.openhab.habdroid.core.connection.Connection;
@@ -31,7 +36,9 @@ import javax.xml.parsers.ParserConfigurationException;
 
 import okhttp3.Call;
 import okhttp3.Headers;
+import okhttp3.HttpUrl;
 import okhttp3.Request;
+import okhttp3.Response;
 
 /**
  * Fragment that manages connections for active instances of
@@ -46,29 +53,41 @@ public class PageConnectionHolderFragment extends Fragment {
     public interface ParentCallback {
         /**
          * Ask parent whether server returns JSON or XML
+         *
          * @return true if server returns JSON, false if it returns XML
          */
         boolean serverReturnsJson();
 
+        /**
+         * Ask parent whether server has support for Server Sent Events
+         *
+         * @return true if server supports SSE, false otherwise
+         */
+        boolean serverSupportsSse();
+
         /**
          * Ask parent for the icon format to use
+         *
          * @return Icon format ('PNG' or 'SVG')
          */
         String getIconFormat();
 
         /**
          * Let parent know about an update to the widget list for a given URL.
-         * @param pageUrl URL of the updated page
+         *
+         * @param pageUrl   URL of the updated page
          * @param pageTitle Updated page title
-         * @param widgets Updated list of widgets for the given page
+         * @param widgets   Updated list of widgets for the given page
          */
         void onPageUpdated(String pageUrl, String pageTitle, List<OpenHABWidget> widgets);
 
         /**
          * Let parent know about an update to the contents of a single widget.
-         * @param widget Updated widget
+         *
+         * @param pageUrl  URL of the page the updated widget belongs to
+         * @param widget   Updated widget
          */
-        void onWidgetUpdated(OpenHABWidget widget);
+        void onWidgetUpdated(String pageUrl, OpenHABWidget widget);
     }
 
     private Map<String, ConnectionHandler> mConnections = new HashMap<>();
@@ -110,7 +129,7 @@ public class PageConnectionHolderFragment extends Fragment {
 
     /**
      * Assign parent callback
-     *
+     * <p>
      * To be called by the parent as early as possible,
      * as it's expected to be non-null at all times
      *
@@ -126,7 +145,7 @@ public class PageConnectionHolderFragment extends Fragment {
     /**
      * Update list of page URLs to track
      *
-     * @param urls New list of URLs to track
+     * @param urls       New list of URLs to track
      * @param connection Connection to use, or null if none is available
      */
     public void updateActiveConnections(List<String> urls, Connection connection) {
@@ -166,7 +185,7 @@ public class PageConnectionHolderFragment extends Fragment {
     /**
      * Ask for new data to be delivered for a given page
      *
-     * @param pageUrl URL of page to trigger update for
+     * @param pageUrl     URL of page to trigger update for
      * @param forceReload true if existing data should be discarded and new data be loaded,
      *                    false if only existing data should be delivered, if it exists
      */
@@ -192,11 +211,22 @@ public class PageConnectionHolderFragment extends Fragment {
         private String mAtmosphereTrackingId;
         private String mLastPageTitle;
         private List<OpenHABWidget> mLastWidgetList;
+        private EventHelper mEventHelper;
 
         public ConnectionHandler(String pageUrl, Connection connection, ParentCallback cb) {
             mUrl = pageUrl;
             mHttpClient = connection.getAsyncHttpClient();
             mCallback = cb;
+            if (cb.serverSupportsSse()) {
+                Uri uri = Uri.parse(mUrl);
+                List<String> segments = uri.getPathSegments();
+                if (segments.size() > 2) {
+                    String sitemap = segments.get(segments.size() - 2);
+                    String pageId = segments.get(segments.size() - 1);
+                    mEventHelper = new EventHelper(mHttpClient, sitemap, pageId,
+                            this::handleUpdateEvent, this::handleSseSubscriptionFailure);
+                }
+            }
         }
 
         public boolean updateFromConnection(Connection c) {
@@ -211,6 +241,9 @@ public class PageConnectionHolderFragment extends Fragment {
                 mRequestHandle.cancel();
                 mRequestHandle = null;
             }
+            if (mEventHelper != null) {
+                mEventHelper.shutdown();
+            }
             mLongPolling = false;
         }
 
@@ -225,6 +258,11 @@ public class PageConnectionHolderFragment extends Fragment {
         }
 
         private void load() {
+            if (mEventHelper != null && mLongPolling) {
+                // We update via events
+                return;
+            }
+
             Log.d(TAG, "Loading data for " + mUrl);
             Map<String, String> headers = new HashMap<String, String>();
             if (!mCallback.serverReturnsJson()) {
@@ -246,6 +284,9 @@ public class PageConnectionHolderFragment extends Fragment {
             }
             final long timeoutMillis = mLongPolling ? 300000 : 10000;
             mRequestHandle = mHttpClient.get(mUrl, headers, timeoutMillis, this);
+            if (mEventHelper != null) {
+                mEventHelper.connect();
+            }
         }
 
         @Override
@@ -339,5 +380,155 @@ public class PageConnectionHolderFragment extends Fragment {
                 return false;
             }
         }
+
+        boolean handleUpdateEvent(String payload) {
+            if (mLastWidgetList == null) {
+                return false;
+            }
+            try {
+                JSONObject object = new JSONObject(payload);
+                String widgetId = object.getString("widgetId");
+                for (int i = 0; i < mLastWidgetList.size(); i++) {
+                    OpenHABWidget widget = mLastWidgetList.get(i);
+                    if (widgetId.equals(widget.id())) {
+                        OpenHABWidget updatedWidget = OpenHABWidget.updateFromEvent(widget,
+                                object, mCallback.getIconFormat());
+                        mLastWidgetList.set(i, updatedWidget);
+                        mCallback.onWidgetUpdated(mUrl, updatedWidget);
+                        return true;
+                    }
+                }
+            } catch (JSONException e) {
+                Log.w(TAG, "Could not parse SSE event ('" + payload + "')", e);
+            }
+            return false;
+        }
+
+        void handleSseSubscriptionFailure() {
+            mEventHelper = null;
+            if (mLongPolling) {
+                load();
+            }
+        }
+
+        private static class EventHelper implements ServerSentEvent.Listener {
+            interface FailureCallback {
+                void handleFailure();
+            }
+            interface UpdateCallback {
+                void handleUpdateEvent(String message);
+            }
+
+            private static final int MAX_RETRIES = 10;
+
+            private final AsyncHttpClient mClient;
+            private final UpdateCallback mUpdateCb;
+            private final FailureCallback mFailureCb;
+            private final String mSitemap;
+            private final String mPageId;
+            private final Handler mHandler;
+            private Call mSubscribeHandle;
+            private ServerSentEvent mEventStream;
+            private int mRetries;
+
+            EventHelper(AsyncHttpClient client, String sitemap, String pageId,
+                    UpdateCallback updateCb, FailureCallback failureCb) {
+                mClient = client;
+                mUpdateCb = updateCb;
+                mFailureCb = failureCb;
+                mSitemap = sitemap;
+                mPageId = pageId;
+                mHandler = new Handler(Looper.getMainLooper());
+            }
+
+            void connect() {
+                shutdown();
+
+                mSubscribeHandle = mClient.post("/rest/sitemaps/events/subscribe",
+                        "{}", "application/json", new AsyncHttpClient.StringResponseHandler() {
+                    @Override
+                    public void onFailure(Request request, int statusCode, Throwable error) {
+                        if (statusCode == 404) {
+                            Log.d(TAG, "Server does not have SSE support");
+                        } else {
+                            Log.w(TAG, "Failed subscribing for SSE", error);
+                        }
+                        mFailureCb.handleFailure();
+                    }
+
+                    @Override
+                    public void onSuccess(String body, Headers headers) {
+                        try {
+                            JSONObject result = new JSONObject(body);
+                            String status = result.getString("status");
+                            if (!status.equals("CREATED")) {
+                                throw new JSONException("Unexpected status " + status);
+                            }
+                            JSONObject headerObject = result.getJSONObject("context").getJSONObject("headers");
+                            String url = headerObject.getJSONArray("Location").getString(0);
+                            HttpUrl u = HttpUrl.parse(url).newBuilder()
+                                    .addQueryParameter("sitemap", mSitemap)
+                                    .addQueryParameter("pageid", mPageId)
+                                    .build();
+                            Request request = new Request.Builder()
+                                    .url(u)
+                                    .build();
+                            mEventStream = mClient.makeSseClient()
+                                    .newServerSentEvent(request, EventHelper.this);
+                        } catch (JSONException e) {
+                            Log.w(TAG, "Failed parsing SSE subscription", e);
+                            mFailureCb.handleFailure();
+                        }
+                    }
+                });
+            }
+
+            void shutdown() {
+                if (mEventStream != null) {
+                    mEventStream.close();
+                    mEventStream = null;
+                }
+                if (mSubscribeHandle != null) {
+                    mSubscribeHandle.cancel();
+                    mSubscribeHandle = null;
+                }
+            }
+
+
+            @Override
+            public void onOpen(ServerSentEvent sse, Response response) {
+                mRetries = 0;
+            }
+
+            @Override
+            public void onMessage(ServerSentEvent sse, String id, String event, String message) {
+                mHandler.post(() -> mUpdateCb.handleUpdateEvent(message));
+            }
+
+            @Override
+            public void onComment(ServerSentEvent sse, String comment) {
+            }
+
+            @Override
+            public boolean onRetryTime(ServerSentEvent sse, long milliseconds) {
+                return true;
+            }
+
+            @Override
+            public boolean onRetryError(ServerSentEvent sse, Throwable throwable, Response response) {
+                // Stop retrying after maximum amount of subsequent retries is reached
+                return ++mRetries < MAX_RETRIES;
+            }
+
+            @Override
+            public void onClosed(ServerSentEvent sse) {
+                mFailureCb.handleFailure();
+            }
+
+            @Override
+            public Request onPreRetry(ServerSentEvent sse, Request originalRequest) {
+                return originalRequest;
+            }
+        }
     }
 }
diff --git a/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java b/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java
index 1e4d85828be6f4ccc9c3acdc737191f2825369d2..1075078dd60a0af8efd016454d4dd51eaa33f52b 100644
--- a/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java
+++ b/mobile/src/main/java/org/openhab/habdroid/util/HttpClient.java
@@ -11,6 +11,8 @@ package org.openhab.habdroid.util;
 
 import android.support.annotation.VisibleForTesting;
 
+import com.here.oksse.OkSse;
+
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -44,6 +46,10 @@ public abstract class HttpClient {
         mClient = client;
     }
 
+    public OkSse makeSseClient() {
+        return new OkSse(mClient);
+    }
+
     public HttpUrl buildUrl(String url) {
         HttpUrl absoluteUrl = HttpUrl.parse(url);
         if (absoluteUrl == null && mBaseUrl != null) {
diff --git a/mobile/src/main/java/org/openhab/habdroid/util/Util.java b/mobile/src/main/java/org/openhab/habdroid/util/Util.java
index f71ed882e488c550cb14374cbb4008df9f6adc46..e56f53271d3581398b36e5479a14afc0954e1b84 100644
--- a/mobile/src/main/java/org/openhab/habdroid/util/Util.java
+++ b/mobile/src/main/java/org/openhab/habdroid/util/Util.java
@@ -110,7 +110,7 @@ public class Util {
         return sitemapList;
     }
 
-    public static List<OpenHABSitemap> sortSitemapList(List<OpenHABSitemap> sitemapList, String defaultSitemapName) {
+    public static void sortSitemapList(List<OpenHABSitemap> sitemapList, String defaultSitemapName) {
         // Sort by sitename label, the default sitemap should be the first one
         Collections.sort(sitemapList, new Comparator<OpenHABSitemap>() {
             @Override
@@ -124,8 +124,6 @@ public class Util {
                 return sitemap1.label().compareToIgnoreCase(sitemap2.label());
             }
         });
-
-        return sitemapList;
     }
 
     public static boolean sitemapExists(List<OpenHABSitemap> sitemapList, String sitemapName) {
diff --git a/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java b/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java
index 6d49ff92600d03cc3e9127b378c6339b7816df5a..f87d60480253253793e64549323548c5a4623581 100644
--- a/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java
+++ b/mobile/src/test/java/org/openhab/habdroid/util/UtilTest.java
@@ -94,7 +94,7 @@ public class UtilTest {
     public void testSortSitemapList() throws IOException, SAXException, ParserConfigurationException {
         List<OpenHABSitemap> sitemapList = Util.parseSitemapList(getSitemapOH1Document());
 
-        sitemapList = Util.sortSitemapList(Util.parseSitemapList(getSitemapOH1Document()), "");
+        Util.sortSitemapList(sitemapList, "");
         // Should be sorted
         assertEquals("Garden", sitemapList.get(0).label());
         assertEquals("Heating", sitemapList.get(1).label());
@@ -105,7 +105,7 @@ public class UtilTest {
         assertEquals("Scenes", sitemapList.get(6).label());
         assertEquals("Schedule", sitemapList.get(7).label());
 
-        sitemapList = Util.sortSitemapList(Util.parseSitemapList(getSitemapOH1Document()), "schedule");
+        Util.sortSitemapList(sitemapList, "schedule");
         // Should be sorted, but "Schedule" should be the first one
         assertEquals("Schedule", sitemapList.get(0).label());
         assertEquals("Garden", sitemapList.get(1).label());