Как стать автором
Обновить

Программируем свой дом на .NET

Время на прочтение 14 мин
Количество просмотров 9.4K
Недавно я писал сюда статью о проекте системы управления умным домом, в разработке которого я участвую. Это .NET Windows Service, который может управлять домом по сценариям и через веб-интерфейс. В октябре как раз был релиз версии 2.0.

Весь функционал системы находится в плагинах. Если вам чего-то не хватает, вы можете легко написать собственный плагин, который будет взаимодействовать с нужным железом, интернет-сервисами или делать что-нибудь еще. Чтобы облегчить написание собственных плагинов, я создал небольшой проект, который можно использовать как пример.

Этот демо-плагин собирает информацию с датчиков температуры/влажности nooLite и отображает полученную информацию в веб-интерфейсе. Результат выглядит примерно так:

график изменения температуры за последние двое суток


текущая температура в комнатах (точнее, последние значения, полученные с датчиков)


Предлагаю вашему вниманию слегка хардкорную статью о том, как писался этот плагин. Там по шагам объясняется процесс разработки собственного плагина для умного дома и приводятся ссылки на GitHub, по которым можно скачать готовый код и запустить его. В этой статье вы узнаете, как создать заготовку плагина и запускать ее в режиме отладки, как настроить автоматическое создание таблиц в системной БД и сохранять туда данные. И, наконец, вы узнаете, как получать информацию о температуре и влажности с датчиков (если интересно только это, то листайте статью сразу в самый конец).


Настройка окружения и создание заготовки проекта


Итак, штука, которую мы пишем — это плагин для приложения — windows-сервиса. Соответственно, сначала нужно развернуть на компьютере сервис, к которому будет подключаться наш плагин. Сделать это очень просто: нужно скачать инсталлятор, запустить его и несколько раз нажать «Далее». Во время установки не запрашиваются никакие параметры. Сервис устанавливается в папку C:\Program Files (x86)\ThinkingHome\service.

Отлично! Теперь создадим в Visual Studio пустой C# проект Class Library, при создании выбираем .NET Framework 4.5. В принципе, подойдет не только VS, но и любая другая IDE, например, бесплатная Xamarin Studio (ну и, конечно же можно использовать бесплатную Visual Studio Express).



Теперь нужно добавить в проект ссылку на библиотеку ThinkingHome.Core.Plugins, в которой содержатся базовые классы для плагинов. Самый простой способ сделать это — подключить ее через менеджер пакетов NuGet. Просто наберите в консоли менеджера пакетов:

Install-Package ThinkingHome.Core.Plugins

Дальше все просто: создаем класс MicroclimatePlugin, наследуем его от базового класса ThinkingHome.Core.Plugins.PluginBase и помечаем атрибутом [ThinkingHome.Core.Plugins.PluginAttribute]. Класс PluginBase реализует базовый функционал плагина (подробнее об этом — чуть позже), а атрибут нужен для подключения плагина к сервису через MEF. Пробуем скомпилировать проект и получаем ошибку. Ага, забыли добавить в проект ссылку на библиотеку System.ComponentModel.Composition (эта библиотека входит в .NET Framework и как раз в ней-то и содержится реализация MEF). Добавляем ее, проект начал компилироваться без ошибок.

Теперь переопределяем методы базового класса, чтобы добавить собственную логику в плагин.
[Plugin]
public class MicroclimatePlugin : PluginBase
{
	// этот метод вызывается, когда сервис загружает плагины
	public override void InitPlugin()
	{
		Logger.Debug("init");
		base.InitPlugin();
	}

	// вызывается после того, как все плагины инициализированы
	public override void StartPlugin()
	{
		Logger.Debug("start");
		base.StartPlugin();
	}

	// вызывается при остановке сервиса
	public override void StopPlugin()
	{
		Logger.Debug("stop");
		base.StopPlugin();
	}
}

Как видите, мы пока просто добавили запись сообщений в лог.

Теперь попробуем подключить наш плагин к сервису. В папке, куда был установлен сервис (C:\Program Files (x86)\ThinkingHome\service), есть папка Plugins, из которой загружаются плагины при старте сервиса. Файлы каждого из плагинов должны лежать в отдельной папке. Создаем там папку для нашего плагина (например, назовем ее «ThinkingHome.Plugins.Microclimate») и в свойствах проекта устаналиваем для параметра Output Path значение «C:\Program Files (x86)\ThinkingHome\service\Plugins\ThinkingHome.Plugins.Microclimate».

Внимание! Для всех ссылок на сторонние библиотеки нужно установить параметр Copy Local = False!

Теперь компилируем проекти и видим, что в указанной нами папке появилась DLL с нашим плагином.



Т.к. в процессе разработки и отладки мы будем часто запускать и останавливать сервис, имеет смысл отключить его автоматический запуск и запускать его как консольное приложение. В папке с сервисом есть файл ThinkingHome.TestConsole.exe. Запускаем его (с правами администратора!) и видим:



Закрываем консоль, идем в папку с логами (C:\Program Files (x86)\ThinkingHome\service\Logs) и смотрим файл
<% дата %>-ThinkingHome.Plugins.Microclimate.MicroclimatePlugin.log.
Видим примерно такие строки:

2014-10-12 16:40:07.0981, Info, init
2014-10-12 16:40:07.1132, Info, start
2014-10-12 16:40:52.1292, Info, stop

Таким образом, мы установили сервис, создали заготовку плагина и проверили, что он успешно подключается к сервису.

Код, который мы написали, лежит на GitHub по адресу: github.com/dima117/thinking-home-plugins-microclimate/tree/6799b7e2f0fb7fc30d0d3d2a4b5cec45cb97fa10. Вы можете скачать и запустить его.

Таблицы в БД для хранения данных


Любой плагин, подключенный к системе, может хранить свои данные в системной БД (MS SQL Server CE 4). Средства для работы с БД предоставляет базовый класс PluginBase.

Структура БД

Итак, мы планируем получать информацию с датчиков температуры/влажности nooLite. Каждый датчик будет привязан к какому-то каналу USB-адаптера nooLite RX2164 (приемник) и будет периодически отправлять ему данные о текущей температуре/влажности.
Таким образом, у нас в системе должен быть список датчиков, для каждого из которых должен быть указан канал адаптера, на который отправляются данные и, как минимум, название датчика для отображения его в веб-интерфейсе.
Также нам нужна еще одна сущность для хранения информации с датчика в заданный момент времени. Соответственно, она будет иметь поля: «значение температуры», «значение влажности», «текущее время», «ID датчика».

В результате получаем примерно такую структуру БД:


Кроме перечисленных полей, в модель датчика добавлено еще одно поле «ShowHumidity», значение которого определяет, нужно ли отображать в интерфейсе параметр «влажность». Дело в том, что производитель системы nooLite предлагает две модели датчиков: PT111, измеряющий температуру/влажность, и PT112, измеряющий только температуру, без влажности. Формат передаваемых данных у них одинаковый, но PT112 в поле «влажность» всегда передает значение «0». Чтобы пустое значение не отображалось в интерфейсе, и была добавлена эта настройка.

Создание таблиц

Для автоматического создания нужной структуры БД в проекте используется инструмент ECM7.Migrator (подробнее). Плагины могут содержать миграции — небольшие классы на C# описывающие порции изменений БД. Каждая миграция имеет номер версии БД и мигратор может обновить БД до последней версии из любого предыдущего состояния. Просто добавьте миграции в проект с плагином и они будут автоматически выполнены при старте сервиса.

Для начала нужно добавить в проект ссылку на библиотеку ECM7.Migrator.Framework. Опять же, самый простой способ это сделать — через NuGet. Наберите в консоли менеджера пакетов:
Install-Package ECM7Migrator

Не забываем устанавливать для всех сборок, добавляемых в проект, параметр Copy Local = False!

Далее необходимо пометить всю сборку атрибутом MigrationAssembly (например, в файле AssemblyInfo.cs):
[assembly: MigrationAssembly("ThinkingHome.Plugins.Microclimate")]

Этот атрибут нужен для того, чтобы учет версий БД для нашего плагина выполнялся независимо от остальных плагинов. В качестве параметра необходимо передать некоторую строку — уникальный ключ, который не будет повторяться в других плагинах. Рекомендуется использовать для этого полное название плагина.

После этого добавляем миграции, описывающие изменения БД. Как уже было сказано, каждая миграция — это отдельный класс. Он должен быть унаследован от специального базового класса Migration + он должен быть помечен специальным атрибутом MigrationAttribute, которому в качестве параметра передан уникальный номер версии БД.

Миграция для таблицы датчиков:
using ECM7.Migrator.Framework;
. . .

namespace ThinkingHome.Plugins.Microclimate.Migrations
{
	[Migration(1)]
	public class Migration01_TemperatureSensor : Migration
	{
		public override void Apply()
		{
			Database.AddTable("Microclimate_TemperatureSensor",
				new Column("Id", DbType.Guid, ColumnProperty.PrimaryKey, "newid()"),
				new Column("Channel", DbType.Int32, ColumnProperty.NotNull),
				new Column("DisplayName", DbType.String.WithSize(255), ColumnProperty.NotNull),
				new Column("ShowHumidity", DbType.Boolean, ColumnProperty.NotNull, false)
			);
		}

		public override void Revert()
		{
			Database.RemoveTable("Microclimate_TemperatureSensor");
		}
	}
}

Тут все просто. Как видите, здесь переопределяются методы Apply и Revert базового класса. Apply — обновление БД до версии, указанной в параметре атрибута [Migration] (в нашем случае версия == 1). Revert — откат изменений. Свойство Database базового класса содержит специальный объект, предоставляющий API для выполнения различных операций над БД. API имеет средства для выполнения всех основных операций с БД + на крайний случай там есть метод ExecuteNonQuery, с помощью которого можно выполнить произвольный SQL запрос.

Миграция для таблицы данных:
. . .
using ECM7.Migrator.Framework;
using ForeignKeyConstraint = ECM7.Migrator.Framework.ForeignKeyConstraint;
. . .

namespace ThinkingHome.Plugins.Microclimate.Migrations
{
	[Migration(2)]
	public class Migration02_TemperatureData : Migration
	{
		public override void Apply()
		{
			Database.AddTable("Microclimate_TemperatureData",
				new Column("Id", DbType.Guid, ColumnProperty.PrimaryKey, "newid()"),
				new Column("Temperature", DbType.Int32, ColumnProperty.NotNull, 0),
				new Column("Humidity", DbType.Int32, ColumnProperty.NotNull, 0),
				new Column("SensorId", DbType.Guid, ColumnProperty.NotNull)
			);

			Database.AddForeignKey("FK_Microclimate_TemperatureData_SensorId",
				"Microclimate_TemperatureData", "SensorId",
				"Microclimate_TemperatureSensor", "Id", ForeignKeyConstraint.Cascade);
		}

		public override void Revert()
		{
			Database.RemoveTable("Microclimate_TemperatureData");
		}
	}
}

Эта миграция, кроме таблицы, добавляет еще и внешний ключ. При откате удалять его не обязательно — мигратор умный и при удалении таблицы сам удалит ее внешние ключи.

Теперь проверим все это. Скомпилируем сборку и запустим нашу тестовую консоль.
В папке с логами смотрим содержимое файла <% дата %>-ecm7-migrator-logger.log и видим строчки:

2014-10-12 16:40:07.0981, Info, SELECT [Version] FROM [SchemaInfo] WHERE [Key] = 'ThinkingHome.Plugins.Microclimate'
2014-10-12 16:40:07.1132, Info, Latest version applied : 0.  Target version : 2
2014-10-12 16:40:07.1292, Info, Applying 1: Migration01 temperature sensor
2014-10-12 16:40:07.1612, Info, CREATE TABLE [Microclimate_TemperatureSensor] ([Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT newid(),[Channel] INT NOT NULL,[DisplayName] NVARCHAR(255) NOT NULL,[ShowHumidity] BIT NOT NULL DEFAULT 0)
2014-10-12 16:40:07.1612, Info, INSERT INTO [SchemaInfo] ([Version],[Key]) VALUES ('1','ThinkingHome.Plugins.Microclimate')
2014-10-12 16:40:07.1822, Info, Applying 2: Migration02 temperature data
2014-10-12 16:40:07.1822, Info, CREATE TABLE [Microclimate_TemperatureData] ([Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT newid(),[Temperature] INT NOT NULL DEFAULT 0,[Humidity] INT NOT NULL DEFAULT 0,[SensorId] UNIQUEIDENTIFIER NOT NULL)
2014-10-12 16:40:07.1822, Info, ALTER TABLE [Microclimate_TemperatureData] ADD CONSTRAINT [FK_Microclimate_TemperatureData_SensorId] FOREIGN KEY ([SensorId]) REFERENCES [Microclimate_TemperatureSensor] ([Id]) ON UPDATE NO ACTION ON DELETE CASCADE
2014-10-12 16:40:07.1912, Info, INSERT INTO [SchemaInfo] ([Version],[Key]) VALUES ('2','ThinkingHome.Plugins.Microclimate')

Упс, кажется, мы забыли во второй таблице сделать поле для текущей даты. Уже готовую миграцию лучше не изменять, т.к. в общем случае, у кого-то она может быть уже выполнена и состояние БД не будет соответствовать миграциям в DLL (т.е. будет некорректным). Создадим еще одну миграцию, добавляющую нужное поле.

[Migration(3)]
public class Migration03_TemperatureDataCurrentDate : Migration
{
	public override void Apply()
	{
		Database.AddColumn("Microclimate_TemperatureData", 
			new Column("CurrentDate", DbType.DateTime, ColumnProperty.NotNull, "getdate()"));
	}
	
	public override void Revert()
	{
		Database.RemoveColumn("Microclimate_TemperatureData", "CurrentDate");
	}
}

После запуска тестовой консоли видим в логе следующие записи:
2014-10-12 16:53:03.6288, Info, Latest version applied : 2.  Target version : 3
2014-10-12 16:53:03.6498, Info, Applying 3: Migration03 temperature data current date
2014-10-12 16:53:03.6668, Info, ALTER TABLE [Microclimate_TemperatureData] ADD [CurrentDate] DATETIME NOT NULL DEFAULT getdate()
2014-10-12 16:53:03.6768, Info, INSERT INTO [SchemaInfo] ([Version],[Key]) VALUES ('3','ThinkingHome.Plugins.Microclimate')

Как видите, мигратор сам определил, какие миграции уже выполнены и запустил только те, которых не хватает (т.е. в нашем случае — только третью миграцию).

Код, который мы сейчас написали, лежит на GitHub по адресу:
github.com/dima117/thinking-home-plugins-microclimate/tree/cbb180bf627ef6d7c07ca4eef43e7bbebf510bd7

Модель данных

Теперь давайте опишем классы нашей модели данных. Тут никаких особенностей нет, кроме того, что все члены классов должны быть виртуальными (это связано с особенностями реализации ORM NHibernate, который используется для доступа к данным).

// датчик
public class TemperatureSensor
{
	public virtual Guid Id { get; set; }

	public virtual int Channel { get; set; }

	public virtual string DisplayName { get; set; }

	public virtual bool ShowHumidity { get; set; }
}

// данные
public class TemperatureData
{
	public virtual Guid Id { get; set; }

	public virtual DateTime CurrentDate { get; set; }

	public virtual TemperatureSensor Sensor { get; set; }

	public virtual int Temperature { get; set; }

	public virtual int Humidity { get; set; }
}


Также обратите внимение, что названия свойств модели совпадают с названиями полей таблиц, но для поля SensorId, являющегося ссылкой на таблицу датчиков, описано свойство Sensor (без окончания «Id»), тип которого соответствует типу модели для связанной таблицы.

Мэппинг модели на таблицы БД

Теперь определим, как наша модель соответствует таблицам БД. Для этого переопределите в своем плагине метод InitDbModel из базового класса.

public override void InitDbModel(ModelMapper mapper)
{
	mapper.Class<TemperatureSensor>(cfg => cfg.Table("Microclimate_TemperatureSensor"));
	mapper.Class<TemperatureData>(cfg => cfg.Table("Microclimate_TemperatureData"));
}


В качестве входного параметра сюда передается экземпляр класса NHibernate.Mapping.ByCode.ModelMapper. По умолчанию, мэппер считает, что названия полей таблиц соответствуют названиям свойств классов, а названия полей-ссылок на другие таблицы соответствуют одноименным свойствам, но без окончания Id. Таким образом, в нашем случае достаточно задать только соответствие классов таблицам в БД — остальное NHibernate настроит самостоятельно. Естественно, ModelMapper предоставляет средства, с помощью которых можно настроить любой другой, более сложный мэппинг модели на таблицы БД.

Добавление датчиков и получение информации из БД


Чтобы добавить в БД запись о датчике или получить из БД его данные необходимо создать специальынй объект — сессию NHibernate (это что-то похожее на DbConnection в ADO.NET). Базовый класс плагина имеет свойство Context, содержащий контекст приложения — объект, реализующий интерфейс IServiceContext. Создать сессию NHibernate можно с помощью его метода OpenSession.

// добавление датчика
var sensorId = Guid.NewGuid();

using (var session = Context.OpenSession())
{
	// создаем объект - датчик
	var sensor = new TemperatureSensor
		{
			Id = sensorId,
			DisplayName = "Тестовый датчик",
			Channel = 1,
			ShowHumidity = true
		};

	// добавляем объект в сессию
	session.Save(sensor);

	// сохраняем изменения в БД
	session.Flush();
}

// получение информации для заданного датчика
using (var session = Context.OpenSession())
{
	var data = session.Query<TemperatureData>()
		.Where(d => d.Sensor.Id == sensorId)
		.ToList();
}


Но как нам проверить работоспособность нашего кода?

Вызов методов плагинов по HTTP


Один из самых простых способов — разрешить обращение к нужным методам плагина по протоколу HTTP и вызвать их из адресной строки браузера. Кроме того, это нам понадобится в будущем, когда будем делать UI.

Для обращения к методам плагина по HTTP нужно добавить в проект ссылку на плагин ThinkingHome.Plugins.Listener, немного изменить сигнатуру методов (набор параметров и тип возвращаемого значения) и пометить методы специальным атрибутом.

Теперь по порядку:

1. Как обычно, самый легкий способ подключить в свой проект другой плагин — через NuGet. Для подключения плагина ThinkingHome.Plugins.Listener наберите в консоли менеджера пакетов:
Install-Package ThinkingHome.Plugins.Listener

Не забудьте указать для добавленной ссылки параметр Copy Local = False.

2. Методы, которые нужно вызывать по HTTP, должны принимать один параметр типа ThinkingHome.Plugins.Listener.Api.HttpRequestParams и возвращать значение типа object. Через HttpRequestParams можно получить значения параметров запроса, а возвращаемое значение будет сериализовано в JSON и передано на клиент.

using ThinkingHome.Plugins.Listener.Api;
. . .

public class MicroclimatePlugin : PluginBase
{
	public object AddSensor(HttpRequestParams request)
	{
		string displayName = request.GetRequiredString("displayName");
		int channel = request.GetRequiredInt32("channel");
		bool showHumidity = request.GetRequiredBool("showHumidity");
		. . .
	}

	. . .
}


3. Необходимо пометить метод специальным атрибутом [ThinkingHome.Plugins.Listener.Attributes.HttpCommand], которому в качестве параметра нужно передать URL (относительно корня сайта), с помощью которого будет происходить обращение к этому методу.

using ThinkingHome.Plugins.Listener.Attributes;
. . .

[HttpCommand("/api/microclimate/sensors/add")]
public object AddSensor(HttpRequestParams request)
{
	. . .
}


Теперь мы можем добавить датчик, набрав в браузере адрес (обратите внимание, все запросы нужно отправлять на порт 41831):
httр://localhost:41831/api/microclimate/sensors/add?channel=1&displayName=Тестовый+датчик&showHumidity=false

Полный код методов добавления датчика и получения данных из БД смотрите на GitHub (методы «AddSensor» и «GetSensorData»):
github.com/dima117/thinking-home-plugins-microclimate/blob/7f16f81090d70a6b60d0a6d664fe74abfa1922fa/ThinkingHome.Plugins.Microclimate/MicroclimatePlugin.cs

Получение информации с датчиков


И, наконец, самое интересное (но не самое сложное) — получение информации с датчиков и сохранение ее в БД.

Как я уже писал, в система «из коробки» может работать с беспроводными датчиками температуры/влажности nooLite. Чтобы получать на компьютере информацию с датчиков потребуется также USB-адаптер nooLite RX2164 (приемник).

Для работы с устрйоствами nooLite имеется специальный плагин. Подключаем его через NuGet. Как обычно, не забываем ставить Copy Local = False;

Install-Package ThinkingHome.Plugins.NooLite


Плагин, который мы только-что добавили, при старте начинает «слушать» команды, поступающие на приемник nooLite (при запуске сервиса адаптер должен быть подключен к компьютеру). При получении команды плагин генерирует внутри системы событие специального типа, в обраобтчики которого передаются данные, полученные с датчика. Мы можем подписаться на это событие и сохранить полученные данные в БД (в таблицах, которые мы недавно создали).

Чтобы добавить обработчик для события «получена информация с датчика», нужно описать в нашем плагине метод и пометить его специальным атрибутом [ThinkingHome.Plugins.NooLite.OnMicroclimateDataReceived]. Метод должен принимать на вход 3 параметра
  • int channel — канал адаптера, для которого пришла команда
  • decimal temperature- текущее значение температуры (датчик передает значение температуры каждый час или при изменении температуры более, чем на 0,5 °C)
  • int humidity — текущее значение влажности, в % (если датчик не поддерживает измерение влажности, передается значение 0)

Внутри обработчика мы открываем сессию NHibernate, получаем список датчиков, проходим по списку и для всех датчиков, у которых номер канала совпадает с параметрмо channel, добавляем запись в таблицу с данными.

using ThinkingHome.Plugins.NooLite;
. . .

[Plugin]
public class MicroclimatePlugin : PluginBase
{
	. . .

	[OnMicroclimateDataReceived]	// подписываемся на событие получения информации с датчика
	public void MicroclimateDataReceived(int channel, decimal temperature, int humidity)
	{
		var now = DateTime.Now;

		// открываем сессию NHibernate
		using (var session = Context.OpenSession())
		{
			// получаем датчики с нужным номером канала
			var sensors = session
				.Query<TemperatureSensor>()
				.Where(s => s.Channel == channel)
				.ToList();

			// проходим по списку датчиков
			foreach (var sensor in sensors)
			{
				// создаем модель для наших данных
				var data = new TemperatureData
				{
					Id = Guid.NewGuid(),
					CurrentDate = now,
					Temperature = Convert.ToInt32(temperature),
					Humidity = humidity,
					Sensor = sensor
				};

				// добавляем в сессию
				session.Save(data);
			}

			// сохраняем изменения в БД
			session.Flush();
		}
	}
}


Теперь запускаем сервис, дышим на датчик (или кладем его на батарею) и через пару минут видим, что в БД появились новые записи.

Код, который мы сейчас написали Вы можете скачать с GitHub (проект целиком), скомпилировать и запустить его.
github.com/dima117/thinking-home-plugins-microclimate/commit/7f16f81090d70a6b60d0a6d664fe74abfa1922fa

Заключение


Итак, в этой статье мы узнали:
  • Как создать заготовку плагина, подключить плагин к сервису умного дома и как запустить его в режиме отладки
  • Как сделать, чтобы плагин сам создал себе нужную структуру БД
  • Как работать с БД
  • Как подписываться на события других плагинов (на примере получения температуры с датчиков nooLite)

Пожалуйста, напишите в комментариях, было ли Вам интересно. Если эта статья вам понравится, то будет еще вторая часть, в которой вы узнаете:
  • Как добавить для плагина отдельный раздел в веб-интерфейс системы умного дома
  • Как рисовать красивые графики температуры и влажности
  • Как дать пользователю возможность вынести значения температуры/влажности на стартовый экран веб-интерфейса системы


Весь проект (в текущем состоянии, написан код и для второй части статьи) лежит по адресу: github.com/dima117/thinking-home-plugins-microclimate
Исходный код системы умного дома: github.com/dima117/thinking-home
Документация thinking-home.ru/system
Добавляйтесь в нашу группу ВКонтакте, чтобы быть в курсе последних новостей: vk.com/thinking_home
Теги:
Хабы:
+9
Комментарии 4
Комментарии Комментарии 4

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн