-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Jimmy Tanagra <[email protected]>
- Loading branch information
Showing
4 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
Feature: semantics | ||
Custom semantic tag creation | ||
|
||
Background: | ||
Given Clean OpenHAB with latest Ruby Libraries | ||
|
||
Scenario: Semantic tag created | ||
Given a rule | ||
""" | ||
Semantics.create(SecretRoom: Semantics::Room) | ||
logger.info Semantics::SecretRoom.java_class.to_s | ||
""" | ||
When I deploy the rule | ||
Then It should log 'org.openhab.core.semantics.model.location.SecretRoom' within 5 seconds | ||
|
||
Scenario: Support creating multiple tags | ||
Given a rule | ||
""" | ||
Semantics.create(SecretRoom: Semantics::Room, Color: Semantics::Property) | ||
logger.info Semantics::SecretRoom.java_class.to_s | ||
logger.info Semantics::Color.java_class.to_s | ||
""" | ||
When I deploy the rule | ||
Then It should log 'org.openhab.core.semantics.model.location.SecretRoom' within 5 seconds | ||
And It should log 'org.openhab.core.semantics.model.property.Color' within 5 seconds | ||
|
||
Scenario: Support custom label | ||
Given a rule | ||
""" | ||
Semantics.create(Room1: Semantics::Room, label: "My Custom Label") | ||
logger.info org.openhab.core.semantics.SemanticTags.get_label(Semantics::Room1, java.util.Locale.default) | ||
""" | ||
When I deploy the rule | ||
Then It should log 'My Custom Label' within 5 seconds | ||
|
||
# There's a bug in openhab that doesn't load the synonyms other than from the resource bundle | ||
# @wip | ||
# Scenario: Support synonyms | ||
# Given a rule | ||
# """ | ||
# Semantics.create(Room2: Semantics::Room, synonyms: "Alias1") | ||
# logger.info org.openhab.core.semantics.SemanticTags.get_by_label_or_synonym("Alias1", java.util.Locale.default) | ||
# """ | ||
# When I deploy the rule | ||
# Then It should log 'org.openhab.core.semantics.model.location.Room2' within 5 seconds | ||
|
||
Scenario: Semantic tag usable by items | ||
Given a rule | ||
""" | ||
Semantics.create(<tag>: Semantics::<parent>) | ||
items.build { <item_type>_item "<item_name>", tags: Semantics::<tag> } | ||
logger.info "<item_name> is a <semantic_type>? #{<item_name>.<semantic_type>?}" | ||
""" | ||
When I deploy the rule | ||
Then It should log '<item_name> is a <semantic_type>? <result>' within 5 seconds | ||
Examples: | ||
| tag | parent | semantic_type | item_type | item_name | result | | ||
| SecretRoom | Room | location | group | Bunker | true | | ||
| LprCamera | Camera | equipment | group | LPR1 | true | | ||
| HeatingElement | Control | point | dimmer | Heater | true | | ||
| Color | Light | point | color | LightColor | true | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
# frozen_string_literal: true | ||
|
||
java_import org.objectweb.asm.ClassWriter | ||
java_import org.objectweb.asm.Opcodes | ||
|
||
module OpenHAB | ||
module Core | ||
module Items | ||
module Semantics | ||
# | ||
# Utility to create custom Semantic tags | ||
# | ||
# @!visibility private | ||
class CustomSemantic | ||
class << self | ||
# | ||
# Creates a custom semantic tag. | ||
# | ||
# @param [Symbol,String] name The name of the new semantic tag to add | ||
# @param [String, Semantics::Tag] parent The semantic class of the parent tag | ||
# @param [String] label An optional label. If not provided, it will be generated from name by | ||
# splitting up the CamelCase with a space | ||
# @param [String] synonyms A comma separated list of synonyms. | ||
# @param [String] description A longer description. | ||
# | ||
# @return [Semantics::Tag] The created semantic tag class | ||
# | ||
def create(name, parent, label: nil, synonyms: "", description: "") | ||
return Semantics.const_get(name) if Semantics.constants.include?(name) | ||
|
||
name = name.to_s | ||
unless /^[A-Z][_a-zA-Z0-9]+$/.match?(name) | ||
raise "Name must start with a capital letter and not contain any spaces" | ||
end | ||
|
||
parent = Semantics.const_get(parent) if parent.is_a?(String) | ||
valid_types = [Semantics::Location, Semantics::Equipment, Semantics::Point, Semantics::Property] | ||
type = valid_types.find { |t| parent == t || parent < t } | ||
raise "Parent must be one of #{valid_types} or their descendants" unless type | ||
|
||
type = type.java_class.simple_name.downcase | ||
class_name = "org.openhab.core.semantics.model.#{type}.#{name}" | ||
|
||
internal_parent_name = parent.java_class.name.tr(".", "/") | ||
internal_class_name = class_name.tr(".", "/") | ||
|
||
# Create the class/interface | ||
class_writer = ClassWriter.new(0) | ||
class_writer.visit(Opcodes::V11, Opcodes::ACC_PUBLIC + Opcodes::ACC_ABSTRACT + Opcodes::ACC_INTERFACE, | ||
internal_class_name, nil, "java/lang/Object", [internal_parent_name]) | ||
|
||
# Add TagInfo Annotation | ||
class_writer.visit_source("Status.java", nil) | ||
parent.java_class.get_annotation(org.openhab.core.semantics.TagInfo.java_class).id.then do |parent_id| | ||
# CamelCase -> Camel Case, Snake_case->Snake Case | ||
label ||= name.scan(/[a-z]+|[A-Z][a-z]+/).map(&:capitalize).join(" ") | ||
|
||
# Correct a bug in openhab, Semantics::Property's id is `MeasurementProperty` instead of Property | ||
parent_id = "Property" if parent_id == "MeasurementProperty" | ||
|
||
annotation_visitor = class_writer.visit_annotation("Lorg/openhab/core/semantics/TagInfo;", true) | ||
annotation_visitor.visit("id", "#{parent_id}_#{name}") | ||
annotation_visitor.visit("label", label) | ||
annotation_visitor.visit("synonyms", synonyms) | ||
annotation_visitor.visit("description", description) | ||
annotation_visitor.visit_end | ||
end | ||
|
||
class_writer.visit_end | ||
byte_code = class_writer.to_byte_array | ||
|
||
java_klass = org.openhab.core.semantics.SemanticTags.java_class.class_loader | ||
.define_class(class_name, byte_code, 0, byte_code.length) | ||
|
||
register_tag(java_klass) | ||
add_ruby_constant(java_klass) | ||
|
||
java_klass.ruby_class | ||
end | ||
|
||
private | ||
|
||
def register_tag(java_klass) | ||
id = java_klass.get_annotation(org.openhab.core.semantics.TagInfo.java_class).id | ||
type = id.split("_").first | ||
|
||
# Add to org.openhab.core.semantics.model.location.Locations.LOCATIONS | ||
type_plural = "#{type.sub(/y$/, "ie")}s" # pluralize, Property -> Properties, Location -> Locations | ||
field_name = type_plural.upcase.to_sym | ||
type_aggregator = java_import("org.openhab.core.semantics.model.#{type.downcase}.#{type_plural}").first | ||
type_aggregator.field_reader field_name | ||
members_list = type_aggregator.send(field_name) | ||
members_list.add(java_klass) | ||
|
||
# Add to org.openhab.core.semantics.SemanticTags.TAGS | ||
# by calling the private method SemanticTags.addTagSet | ||
semantic_tags = org.openhab.core.semantics.SemanticTags.java_class | ||
add_tag_set = semantic_tags.declared_method(:addTagSet, java.lang.Class.java_class) | ||
add_tag_set.accessible = true | ||
add_tag_set.invoke(semantic_tags, java_klass) | ||
add_tag_set.accessible = false | ||
end | ||
|
||
def add_ruby_constant(java_klass) | ||
OpenHAB::Core::Items::Semantics.const_set(java_klass.simple_name.to_sym, java_klass.ruby_class) | ||
end | ||
end | ||
end | ||
|
||
# | ||
# Create custom semantic tags. | ||
# | ||
# @return [Semantics::Tag] The created semantic tag class | ||
# | ||
# @overload self.create(**tags) | ||
# Quickly create one or more semantic tags using the default label, empty synonyms and descriptions. | ||
# | ||
# @param [kwargs] **tags One or more `tag` => `parent` pairs | ||
# @return [Semantics::Tag] The created semantic tag class | ||
# | ||
# @example Create one semantic tag `Balcony` whose parent is `Semantics::Outdoor` (Location) | ||
# Semantics.create(Balcony: Semantics::Outdoor) | ||
# | ||
# @example Create multiple semantic tags | ||
# Semantics.create(Balcony: Semantics::Outdoor, | ||
# SecretRoom: Semantics::Room, | ||
# Motion: Semantics::Property) | ||
# | ||
# @overload self.create(label: nil, synonyms: "", description: "", **tags) | ||
# Create a custom semantic tag with custom details. | ||
# | ||
# @example | ||
# Semantics.create(SecretRoom: Semantics::Room, label: "My Secret Room", | ||
# synonyms: "HidingPlace", description: "A room that requires a special trick to enter") | ||
# | ||
# @param [String,nil] label Optional label. When nil, infer the label from the tag name, | ||
# converting `CamelCase` to `Camel Case` | ||
# @param [String] synonyms A comma separated list of synonyms for this tag. | ||
# @param [String] description A longer description of the tag. | ||
# @param [kwargs] **tags Exactly one pair of `tag` => `parent` | ||
# @return [Semantics::Tag] The created semantic tag class | ||
# | ||
def self.create(label: nil, synonyms: "", description: "", **tags) | ||
raise "Tags must be specified" if tags.empty? | ||
if (tags.length > 1) && !(label.nil? && synonyms.empty? && description.empty?) | ||
raise "Additional options can only be specified when creating one tag" | ||
end | ||
|
||
tags.each do |name, parent| | ||
CustomSemantic.create(name, parent, label: label, synonyms: synonyms, description: description) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |